diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1813401ee5b40..8b376c45ea49f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2873,6 +2873,7 @@ src/platform/packages/private/shared-ux/page/kibana_no_data/types @elastic/appex src/platform/packages/private/shared-ux/prompt/no_data_views/impl @elastic/appex-sharedux src/platform/packages/private/shared-ux/prompt/no_data_views/mocks @elastic/appex-sharedux src/platform/packages/private/shared-ux/table_persist @elastic/appex-sharedux +src/platform/packages/shared/chart_expressions/common @elastic/kibana-visualizations src/platform/packages/shared/cloud @elastic/kibana-core src/platform/packages/shared/content-management/content_editor @elastic/appex-sharedux src/platform/packages/shared/content-management/content_insights/content_insights_public @elastic/appex-sharedux @@ -3055,8 +3056,6 @@ src/platform/packages/shared/shared-ux/prompt/no_data_views/types @elastic/appex src/platform/packages/shared/shared-ux/prompt/not_found @elastic/appex-sharedux src/platform/packages/shared/shared-ux/router/impl @elastic/appex-sharedux src/platform/packages/shared/shared-ux/storybook/mock @elastic/appex-sharedux -src/platform/packages/shared/Users/gsoldevila/Work/kibana-secondary/Users/gsoldevila/Work/kibana-secondary/src/core @elastic/kibana-core -src/platform/packages/shared/Users/gsoldevila/Work/kibana-secondary/Users/gsoldevila/Work/kibana-secondary/src/plugins/chart_expressions/common @elastic/kibana-visualizations src/platform/plugins/private/advanced_settings @elastic/appex-sharedux @elastic/kibana-management src/platform/plugins/private/event_annotation @elastic/kibana-visualizations src/platform/plugins/private/event_annotation_listing @elastic/kibana-visualizations diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 51064981fab85..e6923d95fad7f 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -565,7 +565,9 @@ all http requests to https over the port configured as <>). . Select *Create space* and provide a name, description, and URL identifier. + diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 9945bce1322a3..f12014443bb0b 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -4521,724 +4521,6 @@ paths: summary: Get information about rules tags: - alerting - /api/alerts/alert/{alertId}: - delete: - deprecated: true - description: | - Deprecated in 7.13.0. Use the delete rule API instead. WARNING: After you delete an alert, you cannot recover it. - operationId: legaryDeleteAlert - parameters: - - $ref: '#/components/parameters/Alerting_kbn_xsrf' - - description: The identifier for the alert. - in: path - name: alertId - required: true - schema: - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - type: string - responses: - '204': - description: Indicates a successful call. - '401': - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - $ref: '#/components/schemas/Alerting_401_response' - description: Authorization information is missing or invalid. - summary: Delete an alert - tags: - - alerting - get: - deprecated: true - description: Deprecated in 7.13.0. Use the get rule API instead. - operationId: legacyGetAlert - parameters: - - description: The identifier for the alert. - in: path - name: alertId - required: true - schema: - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - type: string - responses: - '200': - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - $ref: '#/components/schemas/Alerting_alert_response_properties' - description: Indicates a successful call. - '401': - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - $ref: '#/components/schemas/Alerting_401_response' - description: Authorization information is missing or invalid. - summary: Get an alert by identifier - tags: - - alerting - post: - deprecated: true - description: Deprecated in 7.13.0. Use the create rule API instead. - operationId: legacyCreateAlert - parameters: - - $ref: '#/components/parameters/Alerting_kbn_xsrf' - - description: An UUID v1 or v4 identifier for the alert. If this parameter is omitted, the identifier is randomly generated. - in: path - name: alertId - required: true - schema: - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - type: string - requestBody: - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - title: Legacy create alert request properties - type: object - properties: - actions: - items: - type: object - properties: - actionTypeId: - description: The identifier for the action type. - type: string - group: - description: | - Grouping actions is recommended for escalations for different types of alert instances. If you don't need this functionality, set it to `default`. - type: string - id: - description: The ID of the action saved object. - type: string - params: - description: | - The map to the `params` that the action type will receive. `params` are handled as Mustache templates and passed a default set of context. - type: object - required: - - actionTypeId - - group - - id - - params - type: array - alertTypeId: - description: The ID of the alert type that you want to call when the alert is scheduled to run. - type: string - consumer: - description: The name of the application that owns the alert. This name has to match the Kibana feature name, as that dictates the required role-based access control privileges. - type: string - enabled: - description: Indicates if you want to run the alert on an interval basis after it is created. - type: boolean - name: - description: A name to reference and search. - type: string - notifyWhen: - description: The condition for throttling the notification. - enum: - - onActionGroupChange - - onActiveAlert - - onThrottleInterval - type: string - params: - description: The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined. - type: object - schedule: - description: | - The schedule specifying when this alert should be run. A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule. - type: object - properties: - interval: - description: The interval format specifies the interval in seconds, minutes, hours or days at which the alert should run. - example: 10s - type: string - tags: - description: A list of keywords to reference and search. - items: - type: string - type: array - throttle: - description: | - How often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, setting a throttle of `10m` or `1h` will prevent it from sending 90 notifications during this period. - type: string - required: - - alertTypeId - - consumer - - name - - notifyWhen - - params - - schedule - required: true - responses: - '200': - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - $ref: '#/components/schemas/Alerting_alert_response_properties' - description: Indicates a successful call. - '401': - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - $ref: '#/components/schemas/Alerting_401_response' - description: Authorization information is missing or invalid. - summary: Create an alert - tags: - - alerting - put: - deprecated: true - description: Deprecated in 7.13.0. Use the update rule API instead. - operationId: legacyUpdateAlert - parameters: - - $ref: '#/components/parameters/Alerting_kbn_xsrf' - - description: The identifier for the alert. - in: path - name: alertId - required: true - schema: - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - type: string - requestBody: - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - title: Legacy update alert request properties - type: object - properties: - actions: - items: - type: object - properties: - actionTypeId: - description: The identifier for the action type. - type: string - group: - description: | - Grouping actions is recommended for escalations for different types of alert instances. If you don't need this functionality, set it to `default`. - type: string - id: - description: The ID of the action saved object. - type: string - params: - description: | - The map to the `params` that the action type will receive. `params` are handled as Mustache templates and passed a default set of context. - type: object - required: - - actionTypeId - - group - - id - - params - type: array - name: - description: A name to reference and search. - type: string - notifyWhen: - description: The condition for throttling the notification. - enum: - - onActionGroupChange - - onActiveAlert - - onThrottleInterval - type: string - params: - description: The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined. - type: object - schedule: - description: | - The schedule specifying when this alert should be run. A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule. - type: object - properties: - interval: - description: The interval format specifies the interval in seconds, minutes, hours or days at which the alert should run. - example: 1d - type: string - tags: - description: A list of keywords to reference and search. - items: - type: string - type: array - throttle: - description: | - How often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, setting a throttle of `10m` or `1h` will prevent it from sending 90 notifications during this period. - type: string - required: - - name - - notifyWhen - - params - - schedule - required: true - responses: - '200': - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - $ref: '#/components/schemas/Alerting_alert_response_properties' - description: Indicates a successful call. - '401': - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - $ref: '#/components/schemas/Alerting_401_response' - description: Authorization information is missing or invalid. - summary: Update an alert - tags: - - alerting - /api/alerts/alert/{alertId}/_disable: - post: - deprecated: true - description: Deprecated in 7.13.0. Use the disable rule API instead. - operationId: legacyDisableAlert - parameters: - - $ref: '#/components/parameters/Alerting_kbn_xsrf' - - description: The identifier for the alert. - in: path - name: alertId - required: true - schema: - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - type: string - responses: - '204': - description: Indicates a successful call. - '401': - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - $ref: '#/components/schemas/Alerting_401_response' - description: Authorization information is missing or invalid. - summary: Disable an alert - tags: - - alerting - /api/alerts/alert/{alertId}/_enable: - post: - deprecated: true - description: Deprecated in 7.13.0. Use the enable rule API instead. - operationId: legacyEnableAlert - parameters: - - $ref: '#/components/parameters/Alerting_kbn_xsrf' - - description: The identifier for the alert. - in: path - name: alertId - required: true - schema: - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - type: string - responses: - '204': - description: Indicates a successful call. - '401': - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - $ref: '#/components/schemas/Alerting_401_response' - description: Authorization information is missing or invalid. - summary: Enable an alert - tags: - - alerting - /api/alerts/alert/{alertId}/_mute_all: - post: - deprecated: true - description: Deprecated in 7.13.0. Use the mute all alerts API instead. - operationId: legacyMuteAllAlertInstances - parameters: - - $ref: '#/components/parameters/Alerting_kbn_xsrf' - - description: The identifier for the alert. - in: path - name: alertId - required: true - schema: - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - type: string - responses: - '204': - description: Indicates a successful call. - '401': - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - $ref: '#/components/schemas/Alerting_401_response' - description: Authorization information is missing or invalid. - summary: Mute all alert instances - tags: - - alerting - /api/alerts/alert/{alertId}/_unmute_all: - post: - deprecated: true - description: Deprecated in 7.13.0. Use the unmute all alerts API instead. - operationId: legacyUnmuteAllAlertInstances - parameters: - - $ref: '#/components/parameters/Alerting_kbn_xsrf' - - description: The identifier for the alert. - in: path - name: alertId - required: true - schema: - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - type: string - responses: - '204': - description: Indicates a successful call. - '401': - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - $ref: '#/components/schemas/Alerting_401_response' - description: Authorization information is missing or invalid. - summary: Unmute all alert instances - tags: - - alerting - /api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_mute: - post: - deprecated: true - description: Deprecated in 7.13.0. Use the mute alert API instead. - operationId: legacyMuteAlertInstance - parameters: - - $ref: '#/components/parameters/Alerting_kbn_xsrf' - - description: An identifier for the alert. - in: path - name: alertId - required: true - schema: - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - type: string - - description: An identifier for the alert instance. - in: path - name: alertInstanceId - required: true - schema: - example: dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2 - type: string - responses: - '204': - description: Indicates a successful call. - '401': - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - $ref: '#/components/schemas/Alerting_401_response' - description: Authorization information is missing or invalid. - summary: Mute an alert instance - tags: - - alerting - /api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute: - post: - deprecated: true - description: Deprecated in 7.13.0. Use the unmute alert API instead. - operationId: legacyUnmuteAlertInstance - parameters: - - $ref: '#/components/parameters/Alerting_kbn_xsrf' - - description: An identifier for the alert. - in: path - name: alertId - required: true - schema: - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - type: string - - description: An identifier for the alert instance. - in: path - name: alertInstanceId - required: true - schema: - example: dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2 - type: string - responses: - '204': - description: Indicates a successful call. - '401': - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - $ref: '#/components/schemas/Alerting_401_response' - description: Authorization information is missing or invalid. - summary: Unmute an alert instance - tags: - - alerting - /api/alerts/alerts/_find: - get: - deprecated: true - description: | - Deprecated in 7.13.0. Use the find rules API instead. NOTE: Alert `params` are stored as a flattened field type and analyzed as keywords. As alerts change in Kibana, the results on each page of the response also change. Use the find API for traditional paginated results, but avoid using it to export large amounts of data. - operationId: legacyFindAlerts - parameters: - - description: The default operator to use for the `simple_query_string`. - example: OR - in: query - name: default_search_operator - schema: - default: OR - type: string - - description: The fields to return in the `attributes` key of the response. - in: query - name: fields - schema: - items: - type: string - type: array - - description: | - A KQL string that you filter with an attribute from your saved object. It should look like `savedObjectType.attributes.title: "myTitle"`. However, if you used a direct attribute of a saved object, such as `updatedAt`, you must define your filter, for example, `savedObjectType.updatedAt > 2018-12-22`. - in: query - name: filter - schema: - type: string - - description: Filters the rules that have a relation with the reference objects with a specific type and identifier. - in: query - name: has_reference - schema: - type: object - properties: - id: - type: string - type: - type: string - - description: The page number to return. - example: 1 - in: query - name: page - schema: - default: 1 - type: integer - - description: The number of alerts to return per page. - example: 20 - in: query - name: per_page - schema: - default: 20 - type: integer - - description: An Elasticsearch `simple_query_string` query that filters the alerts in the response. - in: query - name: search - schema: - type: string - - description: The fields to perform the `simple_query_string` parsed query against. - in: query - name: search_fields - schema: - oneOf: - - type: string - - items: - type: string - type: array - - description: | - Determines which field is used to sort the results. The field must exist in the `attributes` key of the response. - in: query - name: sort_field - schema: - type: string - - description: Determines the sort order. - example: asc - in: query - name: sort_order - schema: - default: desc - enum: - - asc - - desc - type: string - responses: - '200': - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - type: object - properties: - data: - items: - $ref: '#/components/schemas/Alerting_alert_response_properties' - type: array - page: - type: integer - perPage: - type: integer - total: - type: integer - description: Indicates a successful call. - '401': - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - $ref: '#/components/schemas/Alerting_401_response' - description: Authorization information is missing or invalid. - summary: Get a paginated set of alerts - tags: - - alerting - /api/alerts/alerts/_health: - get: - deprecated: true - description: Deprecated in 7.13.0. Use the get alerting framework health API instead. - operationId: legacyGetAlertingHealth - responses: - '200': - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - type: object - properties: - alertingFrameworkHealth: - description: | - Three substates identify the health of the alerting framework: `decryptionHealth`, `executionHealth`, and `readHealth`. - type: object - properties: - decryptionHealth: - description: The timestamp and status of the alert decryption. - type: object - properties: - status: - enum: - - error - - ok - - warn - example: ok - type: string - timestamp: - example: '2023-01-13T01:28:00.280Z' - format: date-time - type: string - executionHealth: - description: The timestamp and status of the alert execution. - type: object - properties: - status: - enum: - - error - - ok - - warn - example: ok - type: string - timestamp: - example: '2023-01-13T01:28:00.280Z' - format: date-time - type: string - readHealth: - description: The timestamp and status of the alert reading events. - type: object - properties: - status: - enum: - - error - - ok - - warn - example: ok - type: string - timestamp: - example: '2023-01-13T01:28:00.280Z' - format: date-time - type: string - hasPermanentEncryptionKey: - description: If `false`, the encrypted saved object plugin does not have a permanent encryption key. - example: true - type: boolean - isSufficientlySecure: - description: If `false`, security is enabled but TLS is not. - example: true - type: boolean - description: Indicates a successful call. - '401': - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - $ref: '#/components/schemas/Alerting_401_response' - description: Authorization information is missing or invalid. - summary: Get the alerting framework health - tags: - - alerting - /api/alerts/alerts/list_alert_types: - get: - deprecated: true - description: Deprecated in 7.13.0. Use the get rule types API instead. - operationId: legacyGetAlertTypes - responses: - '200': - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - items: - type: object - properties: - actionGroups: - description: | - An explicit list of groups for which the alert type can schedule actions, each with the action group's unique ID and human readable name. Alert actions validation uses this configuration to ensure that groups are valid. - items: - type: object - properties: - id: - type: string - name: - type: string - type: array - actionVariables: - description: | - A list of action variables that the alert type makes available via context and state in action parameter templates, and a short human readable description. The Alert UI will use this information to prompt users for these variables in action parameter editors. - type: object - properties: - context: - items: - type: object - properties: - description: - type: string - name: - type: string - type: array - params: - items: - type: object - properties: - description: - type: string - name: - type: string - type: array - state: - items: - type: object - properties: - description: - type: string - name: - type: string - type: array - authorizedConsumers: - description: The list of the plugins IDs that have access to the alert type. - type: object - defaultActionGroupId: - description: The default identifier for the alert type group. - type: string - enabledInLicense: - description: Indicates whether the rule type is enabled based on the subscription. - type: boolean - id: - description: The unique identifier for the alert type. - type: string - isExportable: - description: Indicates whether the alert type is exportable in Saved Objects Management UI. - type: boolean - minimumLicenseRequired: - description: The subscriptions required to use the alert type. - type: string - name: - description: The descriptive name of the alert type. - type: string - producer: - description: An identifier for the application that produces this alert type. - type: string - recoveryActionGroup: - description: | - An action group to use when an alert instance goes from an active state to an inactive one. If it is not specified, the default recovered action group is used. - type: object - properties: - id: - type: string - name: - type: string - type: array - description: Indicates a successful call. - '401': - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - $ref: '#/components/schemas/Alerting_401_response' - description: Authorization information is missing or invalid. - summary: Get the alert types - tags: - - alerting /api/apm/agent_keys: post: description: Create a new agent key for APM. @@ -44044,13 +43326,6 @@ components: disabledFeatures: [] imageUrl: '' parameters: - Alerting_kbn_xsrf: - description: Cross-site request forgery protection - in: header - name: kbn-xsrf - required: true - schema: - type: string APM_UI_elastic_api_version: description: The version of the API to use in: header @@ -44404,89 +43679,6 @@ components: type: integer title: Unsuccessful rule API response type: object - Alerting_alert_response_properties: - title: Legacy alert response properties - type: object - properties: - actions: - items: - type: object - type: array - alertTypeId: - example: .index-threshold - type: string - apiKeyOwner: - example: elastic - nullable: true - type: string - createdAt: - description: The date and time that the alert was created. - example: '2022-12-05T23:36:58.284Z' - format: date-time - type: string - createdBy: - description: The identifier for the user that created the alert. - example: elastic - type: string - enabled: - description: Indicates whether the alert is currently enabled. - example: true - type: boolean - executionStatus: - type: object - properties: - lastExecutionDate: - example: '2022-12-06T00:13:43.890Z' - format: date-time - type: string - status: - example: ok - type: string - id: - description: The identifier for the alert. - example: b530fed0-74f5-11ed-9801-35303b735aef - type: string - muteAll: - example: false - type: boolean - mutedInstanceIds: - items: - type: string - nullable: true - type: array - name: - description: The name of the alert. - example: my alert - type: string - notifyWhen: - example: onActionGroupChange - type: string - params: - additionalProperties: true - type: object - schedule: - type: object - properties: - interval: - type: string - scheduledTaskId: - example: b530fed0-74f5-11ed-9801-35303b735aef - type: string - tags: - items: - type: string - type: array - throttle: - nullable: true - type: string - updatedAt: - example: '2022-12-05T23:36:58.284Z' - type: string - updatedBy: - description: The identifier for the user that updated this alert most recently. - example: elastic - nullable: true - type: string Alerting_fieldmap_properties: title: Field map objects in the get rule types response type: object diff --git a/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_route_handler_context.ts b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_route_handler_context.ts index 702a3ea10941d..b3ab51bf64e34 100644 --- a/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_route_handler_context.ts +++ b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_route_handler_context.ts @@ -13,6 +13,7 @@ import type { DeprecationsRequestHandlerContext, DeprecationsClient, } from '@kbn/core-deprecations-server'; +import type { KibanaRequest } from '@kbn/core-http-server'; import type { InternalDeprecationsServiceStart } from './deprecations_service'; /** @@ -25,14 +26,16 @@ export class CoreDeprecationsRouteHandlerContext implements DeprecationsRequestH constructor( private readonly deprecationsStart: InternalDeprecationsServiceStart, private readonly elasticsearchRouterHandlerContext: CoreElasticsearchRouteHandlerContext, - private readonly savedObjectsRouterHandlerContext: CoreSavedObjectsRouteHandlerContext + private readonly savedObjectsRouterHandlerContext: CoreSavedObjectsRouteHandlerContext, + private readonly request: KibanaRequest ) {} public get client() { if (this.#client == null) { this.#client = this.deprecationsStart.asScopedToClient( this.elasticsearchRouterHandlerContext.client, - this.savedObjectsRouterHandlerContext.client + this.savedObjectsRouterHandlerContext.client, + this.request ); } return this.#client; diff --git a/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_service.test.ts b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_service.test.ts index 0ea283b6eb5d6..a517b8300e935 100644 --- a/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_service.test.ts +++ b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_service.test.ts @@ -12,7 +12,7 @@ import { registerConfigDeprecationsInfoMock, } from './deprecations_service.test.mocks'; import { mockCoreContext } from '@kbn/core-base-server-mocks'; -import { httpServiceMock } from '@kbn/core-http-server-mocks'; +import { httpServerMock, httpServiceMock } from '@kbn/core-http-server-mocks'; import { coreUsageDataServiceMock } from '@kbn/core-usage-data-server-mocks'; import { configServiceMock } from '@kbn/config-mocks'; import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks'; @@ -83,12 +83,13 @@ describe('DeprecationsService', () => { it('returns client with #getAllDeprecations method', async () => { const esClient = elasticsearchServiceMock.createScopedClusterClient(); const savedObjectsClient = savedObjectsClientMock.create(); + const request = httpServerMock.createKibanaRequest(); const deprecationsService = new DeprecationsService(coreContext); await deprecationsService.setup(deprecationsCoreSetupDeps); const start = deprecationsService.start(); - const deprecationsClient = start.asScopedToClient(esClient, savedObjectsClient); + const deprecationsClient = start.asScopedToClient(esClient, savedObjectsClient, request); expect(deprecationsClient.getAllDeprecations).toBeDefined(); }); diff --git a/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_service.ts b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_service.ts index c0a0ef0f88c7b..8189172a5fe12 100644 --- a/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_service.ts +++ b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_service.ts @@ -20,19 +20,25 @@ import type { DeprecationsClient, } from '@kbn/core-deprecations-server'; import { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +import type { KibanaRequest } from '@kbn/core-http-server'; import { DeprecationsFactory } from './deprecations_factory'; import { registerRoutes } from './routes'; import { config as deprecationConfig, DeprecationConfigType } from './deprecation_config'; import { registerApiDeprecationsInfo, registerConfigDeprecationsInfo } from './deprecations'; +/** + * Deprecation Service: Internal Start contract + */ export interface InternalDeprecationsServiceStart { /** * Creates a {@link DeprecationsClient} with provided SO client and ES client. - * + * @param esClient Scoped Elasticsearch client + * @param savedObjectsClient Scoped SO Client */ asScopedToClient( esClient: IScopedClusterClient, - savedObjectsClient: SavedObjectsClientContract + savedObjectsClient: SavedObjectsClientContract, + request: KibanaRequest ): DeprecationsClient; } @@ -113,13 +119,19 @@ export class DeprecationsService private createScopedDeprecations(): ( esClient: IScopedClusterClient, - savedObjectsClient: SavedObjectsClientContract + savedObjectsClient: SavedObjectsClientContract, + request: KibanaRequest ) => DeprecationsClient { - return (esClient: IScopedClusterClient, savedObjectsClient: SavedObjectsClientContract) => { + return ( + esClient: IScopedClusterClient, + savedObjectsClient: SavedObjectsClientContract, + request: KibanaRequest + ) => { return { getAllDeprecations: this.deprecationsFactory!.getAllDeprecations.bind(null, { savedObjectsClient, esClient, + request, }), }; }; diff --git a/packages/core/deprecations/core-deprecations-server-internal/src/mocks/deprecations_registry.mock.ts b/packages/core/deprecations/core-deprecations-server-internal/src/mocks/deprecations_registry.mock.ts index 9280243527207..eca729871bc1e 100644 --- a/packages/core/deprecations/core-deprecations-server-internal/src/mocks/deprecations_registry.mock.ts +++ b/packages/core/deprecations/core-deprecations-server-internal/src/mocks/deprecations_registry.mock.ts @@ -12,6 +12,7 @@ import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-m import type { GetDeprecationsContext } from '@kbn/core-deprecations-server'; import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks'; import type { DeprecationsRegistry } from '../deprecations_registry'; +import { httpServerMock } from '@kbn/core-http-server-mocks'; type DeprecationsRegistryContract = PublicMethodsOf; @@ -28,6 +29,7 @@ const createGetDeprecationsContextMock = () => { const mocked: jest.Mocked = { esClient: elasticsearchClientMock.createScopedClusterClient(), savedObjectsClient: savedObjectsClientMock.create(), + request: httpServerMock.createKibanaRequest(), }; return mocked; diff --git a/packages/core/deprecations/core-deprecations-server/src/contracts.ts b/packages/core/deprecations/core-deprecations-server/src/contracts.ts index eecd0a8c7e1ac..4391ac2c1f68f 100644 --- a/packages/core/deprecations/core-deprecations-server/src/contracts.ts +++ b/packages/core/deprecations/core-deprecations-server/src/contracts.ts @@ -11,6 +11,7 @@ import type { MaybePromise } from '@kbn/utility-types'; import type { DeprecationsDetails } from '@kbn/core-deprecations-common'; import type { IScopedClusterClient } from '@kbn/core-elasticsearch-server'; import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; +import type { KibanaRequest } from '@kbn/core-http-server'; /** * The deprecations service provides a way for the Kibana platform to communicate deprecated @@ -87,27 +88,51 @@ import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-ser * @public */ export interface DeprecationsServiceSetup { + /** + * Registers deprecation messages or notices for a specific feature or functionality + * within the application. This allows developers to flag certain behaviors or APIs + * as deprecated, providing guidance and warnings for future deprecation plans. + * + * @param {RegisterDeprecationsConfig} deprecationContext - The configuration object containing + * information about the deprecated features, including messages, corrective actions, + * and any relevant metadata to inform users or developers about the deprecation. + */ registerDeprecations: (deprecationContext: RegisterDeprecationsConfig) => void; } /** + * Options to provide when registering deprecations via {@link DeprecationsServiceSetup.registerDeprecations}. * @public */ export interface RegisterDeprecationsConfig { + /** + * Method called when the user wants to list any existing deprecations. + * Returns the list of deprecation messages to warn about. + * @param {GetDeprecationsContext} context Scoped clients and helpers to ease fetching the deprecations. + */ getDeprecations: (context: GetDeprecationsContext) => MaybePromise; } /** + * Scoped clients and helpers to ease fetching the deprecations. * @public */ export interface GetDeprecationsContext { + /** Elasticsearch client scoped to the current user */ esClient: IScopedClusterClient; + /** Saved Objects client scoped to the current user and space */ savedObjectsClient: SavedObjectsClientContract; + request: KibanaRequest; } /** + * Provides a method to scope the {@link DeprecationsServiceSetup | Deprecations Service} to a specific domain. * @public */ export interface DeprecationRegistryProvider { + /** + * Returns the {@link DeprecationsServiceSetup | Deprecations Service} scoped to a specific domain. + * @param domainId Domain ID to categorize the deprecations reported under it. + */ getRegistry: (domainId: string) => DeprecationsServiceSetup; } diff --git a/packages/core/deprecations/core-deprecations-server/src/request_handler_context.ts b/packages/core/deprecations/core-deprecations-server/src/request_handler_context.ts index e75df37aa2c6e..fc5990cd65028 100644 --- a/packages/core/deprecations/core-deprecations-server/src/request_handler_context.ts +++ b/packages/core/deprecations/core-deprecations-server/src/request_handler_context.ts @@ -15,6 +15,9 @@ import type { DomainDeprecationDetails } from '@kbn/core-deprecations-common'; * @public */ export interface DeprecationsClient { + /** + * Fetch all Kibana deprecations. + */ getAllDeprecations: () => Promise; } @@ -23,5 +26,8 @@ export interface DeprecationsClient { * @public */ export interface DeprecationsRequestHandlerContext { + /** + * {@link DeprecationsClient | Deprecations client} exposed in the request handler context. + */ client: DeprecationsClient; } diff --git a/packages/core/deprecations/core-deprecations-server/tsconfig.json b/packages/core/deprecations/core-deprecations-server/tsconfig.json index fa09534af0b92..0179bace38bed 100644 --- a/packages/core/deprecations/core-deprecations-server/tsconfig.json +++ b/packages/core/deprecations/core-deprecations-server/tsconfig.json @@ -15,6 +15,7 @@ "@kbn/core-deprecations-common", "@kbn/core-elasticsearch-server", "@kbn/core-saved-objects-api-server", + "@kbn/core-http-server", ], "exclude": [ "target/**/*", diff --git a/packages/core/http/core-http-request-handler-context-server-internal/src/core_route_handler_context.ts b/packages/core/http/core-http-request-handler-context-server-internal/src/core_route_handler_context.ts index 376eb5a2bd24f..3f7136a1a97d5 100644 --- a/packages/core/http/core-http-request-handler-context-server-internal/src/core_route_handler_context.ts +++ b/packages/core/http/core-http-request-handler-context-server-internal/src/core_route_handler_context.ts @@ -111,7 +111,8 @@ export class CoreRouteHandlerContext implements CoreRequestHandlerContext { this.#deprecations = new CoreDeprecationsRouteHandlerContext( this.coreStart.deprecations, this.elasticsearch, - this.savedObjects + this.savedObjects, + this.request ); } return this.#deprecations; diff --git a/packages/core/http/core-http-server-internal/src/__snapshots__/http_config.test.ts.snap b/packages/core/http/core-http-server-internal/src/__snapshots__/http_config.test.ts.snap index aef7f6e70afca..71aedb914b72d 100644 --- a/packages/core/http/core-http-server-internal/src/__snapshots__/http_config.test.ts.snap +++ b/packages/core/http/core-http-server-internal/src/__snapshots__/http_config.test.ts.snap @@ -131,7 +131,6 @@ Object { "enabled": false, "keystore": Object {}, "supportedProtocols": Array [ - "TLSv1.1", "TLSv1.2", "TLSv1.3", ], diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts index bb9ae3dd84698..929db1f171a83 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts @@ -15,24 +15,23 @@ import { import { SavedObjectsRepository } from './repository'; import { loggerMock } from '@kbn/logging-mocks'; -import { SavedObjectsSerializer } from '@kbn/core-saved-objects-base-server-internal'; import { kibanaMigratorMock } from '../mocks'; import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; import { mockTimestamp, - mappings, createRegistry, createDocumentMigrator, - createSpySerializer, } from '../test_helpers/repository.test.common'; +import type { ISavedObjectsRepository } from '@kbn/core-saved-objects-api-server'; +import { ISavedObjectsSpacesExtension } from '@kbn/core-saved-objects-server'; +import { savedObjectsExtensionsMock } from '../mocks/saved_objects_extensions.mock'; describe('SavedObjectsRepository', () => { let client: ReturnType; - let repository: SavedObjectsRepository; + let repository: ISavedObjectsRepository; let migrator: ReturnType; let logger: ReturnType; - let serializer: jest.Mocked; const registry = createRegistry(); const documentMigrator = createDocumentMigrator(registry); @@ -46,23 +45,13 @@ describe('SavedObjectsRepository', () => { migrator.runMigrations = jest.fn().mockResolvedValue([{ status: 'skipped' }]); logger = loggerMock.create(); - // create a mock serializer "shim" so we can track function calls, but use the real serializer's implementation - serializer = createSpySerializer(registry); - - const allTypes = registry.getAllTypes().map((type) => type.name); - const allowedTypes = [...new Set(allTypes.filter((type) => !registry.isHidden(type)))]; - - // @ts-expect-error must use the private constructor to use the mocked serializer - repository = new SavedObjectsRepository({ - index: '.kibana-test', - mappings, - client, + repository = SavedObjectsRepository.createRepository( migrator, - typeRegistry: registry, - serializer, - allowedTypes, - logger, - }); + registry, + '.kibana-test', + client, + logger + ); mockGetCurrentTime.mockReturnValue(mockTimestamp); mockGetSearchDsl.mockClear(); @@ -87,4 +76,60 @@ describe('SavedObjectsRepository', () => { expect(repository.getCurrentNamespace('space-a')).toBe('space-a'); }); }); + + describe('#asScopedToNamespace', () => { + it('returns a new client with undefined spacesExtensions (not available)', () => { + const scopedRepository = repository.asScopedToNamespace('space-a'); + expect(scopedRepository).toBeInstanceOf(SavedObjectsRepository); + expect(scopedRepository).not.toStrictEqual(repository); + + // Checking extensions.spacesExtension are both undefined + // @ts-expect-error type is ISavedObjectsRepository, but in reality is SavedObjectsRepository + expect(repository.extensions.spacesExtension).toBeUndefined(); + // @ts-expect-error type is ISavedObjectsRepository, but in reality is SavedObjectsRepository + expect(scopedRepository.extensions.spacesExtension).toBeUndefined(); + // @ts-expect-error type is ISavedObjectsRepository, but in reality is SavedObjectsRepository + expect(scopedRepository.extensions.spacesExtension).toStrictEqual( + // @ts-expect-error type is ISavedObjectsRepository, but in reality is SavedObjectsRepository + repository.extensions.spacesExtension + ); + }); + }); + + describe('with spacesExtension', () => { + let spacesExtension: jest.Mocked; + + beforeEach(() => { + spacesExtension = savedObjectsExtensionsMock.createSpacesExtension(); + repository = SavedObjectsRepository.createRepository( + migrator, + registry, + '.kibana-test', + client, + logger, + [], + { spacesExtension } + ); + }); + + describe('#asScopedToNamespace', () => { + it('returns a new client with space-scoped spacesExtensions', () => { + const scopedRepository = repository.asScopedToNamespace('space-a'); + expect(scopedRepository).toBeInstanceOf(SavedObjectsRepository); + expect(scopedRepository).not.toStrictEqual(repository); + expect(spacesExtension.asScopedToNamespace).toHaveBeenCalledWith('space-a'); + + // Checking extensions.spacesExtension are both defined but different + // @ts-expect-error type is ISavedObjectsRepository, but in reality is SavedObjectsRepository + expect(repository.extensions.spacesExtension).not.toBeUndefined(); + // @ts-expect-error type is ISavedObjectsRepository, but in reality is SavedObjectsRepository + expect(scopedRepository.extensions.spacesExtension).not.toBeUndefined(); + // @ts-expect-error type is ISavedObjectsRepository, but in reality is SavedObjectsRepository + expect(scopedRepository.extensions.spacesExtension).not.toStrictEqual( + // @ts-expect-error type is ISavedObjectsRepository, but in reality is SavedObjectsRepository + repository.extensions.spacesExtension + ); + }); + }); + }); }); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index 31a3c47e9c8ee..b2b8de1b4192a 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -168,7 +168,7 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { }); } - private constructor(options: SavedObjectsRepositoryOptions) { + private constructor(private readonly options: SavedObjectsRepositoryOptions) { const { index, mappings, @@ -564,4 +564,17 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { getCurrentNamespace(namespace?: string) { return this.helpers.common.getCurrentNamespace(namespace); } + + /** + * {@inheritDoc ISavedObjectsRepository.asScopedToNamespace} + */ + asScopedToNamespace(namespace: string) { + return new SavedObjectsRepository({ + ...this.options, + extensions: { + ...this.options.extensions, + spacesExtension: this.extensions.spacesExtension?.asScopedToNamespace(namespace), + }, + }); + } } diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/repository.mock.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/repository.mock.ts index 7fc01d314abf7..5b2f2041fd340 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/repository.mock.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/repository.mock.ts @@ -34,6 +34,7 @@ const createRepositoryMock = () => { collectMultiNamespaceReferences: jest.fn(), updateObjectsSpaces: jest.fn(), getCurrentNamespace: jest.fn(), + asScopedToNamespace: jest.fn().mockImplementation(createRepositoryMock), }; return mock; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/saved_objects_extensions.mock.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/saved_objects_extensions.mock.ts index 9dc7c0f0133c5..0cde72544bd3f 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/saved_objects_extensions.mock.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/saved_objects_extensions.mock.ts @@ -47,6 +47,7 @@ const createSecurityExtension = (): jest.Mocked const createSpacesExtension = (): jest.Mocked => ({ getCurrentNamespace: jest.fn(), getSearchableNamespaces: jest.fn(), + asScopedToNamespace: jest.fn().mockImplementation(createSpacesExtension), }); const create = () => ({ diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/saved_objects_client.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/saved_objects_client.test.ts index 73cbb826bfdd0..9440b45269145 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/saved_objects_client.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/saved_objects_client.test.ts @@ -335,4 +335,12 @@ describe('SavedObjectsClient', () => { expect(client.getCurrentNamespace()).toEqual('ns'); expect(mockRepository.getCurrentNamespace).toHaveBeenCalledWith(); }); + + test('#asScopedToNamespace', () => { + const client = new SavedObjectsClient(mockRepository); + + const rescopedClient = client.asScopedToNamespace('ns'); + expect(rescopedClient).not.toStrictEqual(client); + expect(mockRepository.asScopedToNamespace).toHaveBeenCalledWith('ns'); + }); }); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/saved_objects_client.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/saved_objects_client.ts index a4d0b44751c7f..addfefde9cfc3 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/saved_objects_client.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/saved_objects_client.ts @@ -216,4 +216,9 @@ export class SavedObjectsClient implements SavedObjectsClientContract { getCurrentNamespace() { return this._repository.getCurrentNamespace(); } + + /** {@inheritDoc SavedObjectsClientContract.asScopedToNamespace} */ + asScopedToNamespace(namespace: string) { + return new SavedObjectsClient(this._repository.asScopedToNamespace(namespace)); + } } diff --git a/packages/core/saved-objects/core-saved-objects-api-server-mocks/src/repository.mock.ts b/packages/core/saved-objects/core-saved-objects-api-server-mocks/src/repository.mock.ts index 5584d130c1091..c9b9ab4fba2a3 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-mocks/src/repository.mock.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-mocks/src/repository.mock.ts @@ -33,6 +33,7 @@ const create = () => { collectMultiNamespaceReferences: jest.fn(), updateObjectsSpaces: jest.fn(), getCurrentNamespace: jest.fn(), + asScopedToNamespace: jest.fn().mockImplementation(create), }; mock.createPointInTimeFinder = savedObjectsPointInTimeFinderMock.create({ diff --git a/packages/core/saved-objects/core-saved-objects-api-server-mocks/src/saved_objects_client.mock.ts b/packages/core/saved-objects/core-saved-objects-api-server-mocks/src/saved_objects_client.mock.ts index c83ef37a1a956..aebeeb43059f4 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-mocks/src/saved_objects_client.mock.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-mocks/src/saved_objects_client.mock.ts @@ -8,12 +8,10 @@ */ import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; -import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; import { savedObjectsPointInTimeFinderMock } from './point_in_time_finder.mock'; const create = () => { - const mock = { - errors: SavedObjectsErrorHelpers, + const mock: jest.Mocked = { create: jest.fn(), bulkCreate: jest.fn(), checkConflicts: jest.fn(), @@ -33,7 +31,8 @@ const create = () => { collectMultiNamespaceReferences: jest.fn(), updateObjectsSpaces: jest.fn(), getCurrentNamespace: jest.fn(), - } as unknown as jest.Mocked; + asScopedToNamespace: jest.fn().mockImplementation(create), + }; mock.createPointInTimeFinder = savedObjectsPointInTimeFinderMock.create({ savedObjectsMock: mock, diff --git a/packages/core/saved-objects/core-saved-objects-api-server-mocks/src/saved_objects_extensions.mock.ts b/packages/core/saved-objects/core-saved-objects-api-server-mocks/src/saved_objects_extensions.mock.ts index 776ecfe3a7385..da9527ec4964f 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-mocks/src/saved_objects_extensions.mock.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-mocks/src/saved_objects_extensions.mock.ts @@ -48,6 +48,7 @@ const createSecurityExtension = (): jest.Mocked const createSpacesExtension = (): jest.Mocked => ({ getCurrentNamespace: jest.fn(), getSearchableNamespaces: jest.fn(), + asScopedToNamespace: jest.fn().mockImplementation(createSpacesExtension), }); const create = (): jest.Mocked => ({ diff --git a/packages/core/saved-objects/core-saved-objects-api-server/src/saved_objects_client.ts b/packages/core/saved-objects/core-saved-objects-api-server/src/saved_objects_client.ts index 495d7a58b0897..ae7b7d48e0d15 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server/src/saved_objects_client.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server/src/saved_objects_client.ts @@ -427,4 +427,10 @@ export interface SavedObjectsClientContract { * Returns the namespace associated with the client. If the namespace is the default one, this method returns `undefined`. */ getCurrentNamespace(): string | undefined; + + /** + * Returns a clone of the current Saved Objects client but scoped to the specified namespace. + * @param namespace Space to which the client should be scoped to. + */ + asScopedToNamespace(namespace: string): SavedObjectsClientContract; } diff --git a/packages/core/saved-objects/core-saved-objects-api-server/src/saved_objects_repository.ts b/packages/core/saved-objects/core-saved-objects-api-server/src/saved_objects_repository.ts index 46ebe1e4ceee6..e2be2a89d2a9b 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server/src/saved_objects_repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server/src/saved_objects_repository.ts @@ -552,4 +552,10 @@ export interface ISavedObjectsRepository { * namespace. */ getCurrentNamespace(namespace?: string): string | undefined; + + /** + * Returns a new Saved Objects repository scoped to the specified namespace. + * @param namespace Space to which the repository should be scoped to. + */ + asScopedToNamespace(namespace: string): ISavedObjectsRepository; } diff --git a/packages/core/saved-objects/core-saved-objects-server/src/extensions/spaces.ts b/packages/core/saved-objects/core-saved-objects-server/src/extensions/spaces.ts index c81235c08e500..eb89e778b6c0e 100644 --- a/packages/core/saved-objects/core-saved-objects-server/src/extensions/spaces.ts +++ b/packages/core/saved-objects/core-saved-objects-server/src/extensions/spaces.ts @@ -25,4 +25,9 @@ export interface ISavedObjectsSpacesExtension { * If a wildcard '*' is used, it is expanded to an explicit list of namespace strings. */ getSearchableNamespaces: (namespaces: string[] | undefined) => Promise; + /** + * Returns a new Saved Objects Spaces Extension scoped to the specified namespace. + * @param namespace Space to which the extension should be scoped to. + */ + asScopedToNamespace(namespace: string): ISavedObjectsSpacesExtension; } diff --git a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.test.ts b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.test.ts index 440a62853c099..f72664ff0c1fa 100644 --- a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.test.ts +++ b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.test.ts @@ -355,7 +355,6 @@ describe('CoreUsageDataService', () => { "keystoreConfigured": false, "redirectHttpFromPortConfigured": false, "supportedProtocols": Array [ - "TLSv1.1", "TLSv1.2", "TLSv1.3", ], diff --git a/packages/kbn-health-gateway-server/src/server/server_config.test.ts b/packages/kbn-health-gateway-server/src/server/server_config.test.ts index 0ef93e6daa3cb..fd04fcc09e6f6 100644 --- a/packages/kbn-health-gateway-server/src/server/server_config.test.ts +++ b/packages/kbn-health-gateway-server/src/server/server_config.test.ts @@ -56,7 +56,6 @@ describe('server config', () => { "enabled": false, "keystore": Object {}, "supportedProtocols": Array [ - "TLSv1.1", "TLSv1.2", "TLSv1.3", ], diff --git a/packages/kbn-relocate/README.md b/packages/kbn-relocate/README.md index 899a5e9b97970..06a04067a26a2 100644 --- a/packages/kbn-relocate/README.md +++ b/packages/kbn-relocate/README.md @@ -8,12 +8,9 @@ You must have `gh` CLI tool installed. You can install it by running: ```sh brew install gh +gh auth login ``` -You must have `elastic/kibana` remote configured under the name `upstream`. - -You must have a remote named `origin` pointing to your fork of the Kibana repo. - ## Usage First of all, you need to decide whether you want to contribute to an existing PR or to create a new one. Use the `--pr` flag to specify the PR you are trying to update: diff --git a/packages/kbn-relocate/constants.ts b/packages/kbn-relocate/constants.ts index 059c32004370e..5b5f4099b782b 100644 --- a/packages/kbn-relocate/constants.ts +++ b/packages/kbn-relocate/constants.ts @@ -13,13 +13,26 @@ export const BASE_FOLDER = process.cwd() + '/'; export const BASE_FOLDER_DEPTH = process.cwd().split('/').length; export const KIBANA_FOLDER = process.cwd().split('/').pop()!; export const EXCLUDED_MODULES = ['@kbn/core']; -export const TARGET_FOLDERS = [ - 'src/platform/plugins/', - 'src/platform/packages/', - 'x-pack/platform/plugins/', - 'x-pack/platform/packages/', - 'x-pack/solutions/', -]; +export const TARGET_FOLDERS: Record = { + 'platform:private': [ + 'src/platform/packages/private/', + 'src/platform/plugins/private/', + 'x-pack/platform/packages/private/', + 'x-pack/platform/plugins/private/', + ], + 'platform:shared': [ + 'src/platform/packages/shared/', + 'src/platform/plugins/shared/', + 'x-pack/platform/packages/shared/', + 'x-pack/platform/plugins/shared/', + ], + 'observability:private': [ + 'x-pack/solutions/observability/packages/', + 'x-pack/solutions/observability/plugins/', + ], + 'search:private': ['x-pack/solutions/search/packages/', 'x-pack/solutions/search/plugins/'], + 'security:private': ['x-pack/solutions/security/packages/', 'x-pack/solutions/security/plugins/'], +}; export const EXTENSIONS = [ 'eslintignore', 'gitignore', diff --git a/packages/kbn-relocate/index.ts b/packages/kbn-relocate/index.ts index cddaa307ab7b6..12f89a29275e6 100644 --- a/packages/kbn-relocate/index.ts +++ b/packages/kbn-relocate/index.ts @@ -46,15 +46,17 @@ export const runKbnRelocateCli = () => { await findAndMoveModule(flags.moveOnly, log); } else { const { pr, team, path, include, exclude, baseBranch } = flags; - await findAndRelocateModules({ - prNumber: toOptString('prNumber', pr), - baseBranch: toOptString('baseBranch', baseBranch, 'main')!, - teams: toStringArray(team), - paths: toStringArray(path), - included: toStringArray(include), - excluded: toStringArray(exclude), - log, - }); + await findAndRelocateModules( + { + prNumber: toOptString('prNumber', pr), + baseBranch: toOptString('baseBranch', baseBranch, 'main')!, + teams: toStringArray(team), + paths: toStringArray(path), + included: toStringArray(include), + excluded: toStringArray(exclude), + }, + log + ); } }, { diff --git a/packages/kbn-relocate/relocate.ts b/packages/kbn-relocate/relocate.ts index dde0c83fc096e..fe2537ddeb040 100644 --- a/packages/kbn-relocate/relocate.ts +++ b/packages/kbn-relocate/relocate.ts @@ -16,22 +16,17 @@ import type { ToolingLog } from '@kbn/tooling-log'; import { getPackages } from '@kbn/repo-packages'; import { REPO_ROOT } from '@kbn/repo-info'; import type { Package } from './types'; -import { - DESCRIPTION, - EXCLUDED_MODULES, - KIBANA_FOLDER, - NEW_BRANCH, - TARGET_FOLDERS, -} from './constants'; +import { DESCRIPTION, EXCLUDED_MODULES, KIBANA_FOLDER, NEW_BRANCH } from './constants'; import { belongsTo, calculateModuleTargetFolder, + isInTargetFolder, replaceReferences, replaceRelativePaths, -} from './utils.relocate'; -import { safeExec } from './utils.exec'; -import { relocatePlan, relocateSummary } from './utils.logging'; -import { checkoutBranch, checkoutResetPr } from './utils.git'; +} from './utils/relocate'; +import { safeExec } from './utils/exec'; +import { relocatePlan, relocateSummary } from './utils/logging'; +import { checkoutBranch, checkoutResetPr, findGithubLogin, findRemoteName } from './utils/git'; const moveModule = async (module: Package, log: ToolingLog) => { const destination = calculateModuleTargetFolder(module); @@ -52,11 +47,6 @@ const relocateModules = async (toMove: Package[], log: ToolingLog): Promise module.directory.includes(folder))) { - log.warning(`The module ${module.id} is already in a "sustainable" folder. Skipping`); - // skip modules that are already moved - continue; - } log.info(''); log.info('--------------------------------------------------------------------------------'); log.info(`\t${module.id} (${i + 1} of ${toMove.length})`); @@ -93,10 +83,9 @@ export interface RelocateModulesParams { paths: string[]; included: string[]; excluded: string[]; - log: ToolingLog; } -const findModules = ({ teams, paths, included, excluded }: FindModulesParams) => { +const findModules = ({ teams, paths, included, excluded }: FindModulesParams, log: ToolingLog) => { // get all modules const modules = getPackages(REPO_ROOT); @@ -123,13 +112,14 @@ const findModules = ({ teams, paths, included, excluded }: FindModulesParams) => paths.some((path) => module.directory.includes(path)) ) // the module is not explicitly excluded - .filter(({ id }) => !excluded.includes(id)), - 'id' + .filter(({ id }) => !excluded.includes(id)) + // exclude modules that are in the correct folder + .filter((module) => !isInTargetFolder(module, log)) ); }; export const findAndMoveModule = async (moduleId: string, log: ToolingLog) => { - const modules = findModules({ teams: [], paths: [], included: [moduleId], excluded: [] }); + const modules = findModules({ teams: [], paths: [], included: [moduleId], excluded: [] }, log); if (!modules.length) { log.warning(`Cannot move ${moduleId}, either not found or not allowed!`); } else { @@ -137,10 +127,24 @@ export const findAndMoveModule = async (moduleId: string, log: ToolingLog) => { } }; -export const findAndRelocateModules = async (params: RelocateModulesParams) => { - const { prNumber, log, baseBranch, ...findParams } = params; +export const findAndRelocateModules = async (params: RelocateModulesParams, log: ToolingLog) => { + const upstream = await findRemoteName('elastic/kibana'); + if (!upstream) { + log.error( + 'This repository does not have a remote pointing to the elastic/kibana repository. Aborting' + ); + return; + } + + const origin = await findRemoteName(`${await findGithubLogin()}/kibana`); + if (!origin) { + log.error('This repository does not have a remote pointing to your Kibana fork. Aborting'); + return; + } + + const { prNumber, baseBranch, ...findParams } = params; - const toMove = findModules(findParams); + const toMove = findModules(findParams, log); if (!toMove.length) { log.info( `No packages match the specified filters. Please tune your '--path' and/or '--team' and/or '--include' flags` @@ -164,7 +168,7 @@ export const findAndRelocateModules = async (params: RelocateModulesParams) => { await safeExec(`git restore --staged .`); await safeExec(`git restore .`); await safeExec(`git clean -f -d`); - await safeExec(`git checkout ${baseBranch} && git pull upstream ${baseBranch}`); + await safeExec(`git checkout ${baseBranch} && git pull ${upstream} ${baseBranch}`); if (prNumber) { // checkout existing PR, reset all commits, rebase from baseBranch @@ -204,7 +208,7 @@ export const findAndRelocateModules = async (params: RelocateModulesParams) => { const pushCmd = prNumber ? `git push --force-with-lease` - : `git push --set-upstream origin ${NEW_BRANCH}`; + : `git push --set-upstream ${origin} ${NEW_BRANCH}`; if (!res2.pushBranch) { log.info(`Remember to push changes with "${pushCmd}"`); diff --git a/packages/kbn-relocate/utils.exec.ts b/packages/kbn-relocate/utils/exec.ts similarity index 100% rename from packages/kbn-relocate/utils.exec.ts rename to packages/kbn-relocate/utils/exec.ts diff --git a/packages/kbn-relocate/utils.git.ts b/packages/kbn-relocate/utils/git.ts similarity index 81% rename from packages/kbn-relocate/utils.git.ts rename to packages/kbn-relocate/utils/git.ts index 4f002772528fd..f2e529bee6d0f 100644 --- a/packages/kbn-relocate/utils.git.ts +++ b/packages/kbn-relocate/utils/git.ts @@ -8,8 +8,26 @@ */ import inquirer from 'inquirer'; -import type { Commit, PullRequest } from './types'; -import { safeExec } from './utils.exec'; +import type { Commit, PullRequest } from '../types'; +import { safeExec } from './exec'; + +export const findRemoteName = async (repo: string) => { + const res = await safeExec('git remote -v'); + const remotes = res.stdout.split('\n').map((line) => line.split(/\t| /).filter(Boolean)); + return remotes.find(([_, url]) => url.includes(`github.com/${repo}`))?.[0]; +}; + +export const findGithubLogin = async () => { + const res = await safeExec('gh auth status'); + // e.g. ✓ Logged in to github.com account gsoldevila (/Users/gsoldevila/.config/gh/hosts.yml) + const loginLine = res.stdout + .split('\n') + .find((line) => line.includes('Logged in')) + ?.split(/\t| /) + .filter(Boolean); + + return loginLine?.[loginLine?.findIndex((fragment) => fragment === 'account') + 1]; +}; export const findPr = async (number: string): Promise => { const res = await safeExec(`gh pr view ${number} --json commits,headRefName`); diff --git a/packages/kbn-relocate/utils.logging.ts b/packages/kbn-relocate/utils/logging.ts similarity index 96% rename from packages/kbn-relocate/utils.logging.ts rename to packages/kbn-relocate/utils/logging.ts index 5b290292dd75e..742610dfe1de6 100644 --- a/packages/kbn-relocate/utils.logging.ts +++ b/packages/kbn-relocate/utils/logging.ts @@ -10,8 +10,8 @@ import type { ToolingLog } from '@kbn/tooling-log'; import { appendFileSync, writeFileSync } from 'fs'; import dedent from 'dedent'; -import type { Package } from './types'; -import { calculateModuleTargetFolder } from './utils.relocate'; +import type { Package } from '../types'; +import { calculateModuleTargetFolder } from './relocate'; import { BASE_FOLDER, DESCRIPTION, @@ -19,7 +19,7 @@ import { SCRIPT_ERRORS, UPDATED_REFERENCES, UPDATED_RELATIVE_PATHS, -} from './constants'; +} from '../constants'; export const relocatePlan = (modules: Package[], log: ToolingLog) => { const plugins = modules.filter((module) => module.manifest.type === 'plugin'); diff --git a/packages/kbn-relocate/utils.relocate.ts b/packages/kbn-relocate/utils/relocate.ts similarity index 84% rename from packages/kbn-relocate/utils.relocate.ts rename to packages/kbn-relocate/utils/relocate.ts index 15121fefd344a..2f2b6a78379e6 100644 --- a/packages/kbn-relocate/utils.relocate.ts +++ b/packages/kbn-relocate/utils/relocate.ts @@ -10,7 +10,7 @@ import { join } from 'path'; import type { ToolingLog } from '@kbn/tooling-log'; import { orderBy } from 'lodash'; -import type { Package } from './types'; +import type { Package } from '../types'; import { applyTransforms } from './transforms'; import { BASE_FOLDER, @@ -22,8 +22,8 @@ import { TARGET_FOLDERS, UPDATED_REFERENCES, UPDATED_RELATIVE_PATHS, -} from './constants'; -import { quietExec, safeExec } from './utils.exec'; +} from '../constants'; +import { quietExec, safeExec } from './exec'; export const belongsTo = (module: Package, owner: string): boolean => { return Array.from(module.manifest.owner)[0] === owner; @@ -40,11 +40,18 @@ export const calculateModuleTargetFolder = (module: Package): string => { const isPlugin = module.manifest.type === 'plugin'; const fullPath = join(BASE_FOLDER, module.directory); let moduleDelimiter = isPlugin ? '/plugins/' : '/packages/'; - if (TARGET_FOLDERS.some((folder) => module.directory.includes(folder)) && group === 'platform') { - // if a platform module has already been relocated, strip the /private/ or /shared/ part too - moduleDelimiter += `${module.visibility}/`; + + // for platform modules that are in a sustainable folder, strip the /private/ or /shared/ part too + if (module.directory.includes(`${moduleDelimiter}private/`)) { + moduleDelimiter += 'private/'; + } else if (module.directory.includes(`${moduleDelimiter}shared/`)) { + moduleDelimiter += 'shared/'; } - const moduleFolder = fullPath.split(moduleDelimiter).pop()!; + + const chunks = fullPath.split(moduleDelimiter); + chunks.shift(); // remove the base path up to '/packages/' or '/plugins/' + const moduleFolder = chunks.join(moduleDelimiter); // in case there's an extra /packages/ or /plugins/ folder + let path: string; if (group === 'platform') { @@ -79,6 +86,26 @@ export const calculateModuleTargetFolder = (module: Package): string => { return applyTransforms(module, path); }; +export const isInTargetFolder = (module: Package, log: ToolingLog): boolean => { + if (!module.group || !module.visibility) { + log.warning(`The module '${module.id}' is missing the group/visibility information`); + return true; + } + + const baseTargetFolders = TARGET_FOLDERS[`${module.group}:${module.visibility}`]; + const baseTargetFolder = baseTargetFolders.find((candidate) => { + return module.directory.includes(candidate); + }); + if (baseTargetFolder) { + log.info( + `The module ${module.id} is already in the correct folder: '${baseTargetFolder}'. Skipping` + ); + return true; + } + + return false; +}; + export const replaceReferences = async (module: Package, destination: string, log: ToolingLog) => { const dir = module.directory; const source = diff --git a/packages/kbn-relocate/transforms.ts b/packages/kbn-relocate/utils/transforms.ts similarity index 97% rename from packages/kbn-relocate/transforms.ts rename to packages/kbn-relocate/utils/transforms.ts index 72f57a24daa00..ed584abeb55ab 100644 --- a/packages/kbn-relocate/transforms.ts +++ b/packages/kbn-relocate/utils/transforms.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { Package } from './types'; +import type { Package } from '../types'; type TransformFunction = (param: string) => string; const TRANSFORMS: Record = { diff --git a/packages/kbn-server-http-tools/src/ssl/ssl_config.ts b/packages/kbn-server-http-tools/src/ssl/ssl_config.ts index 4385f954c3306..bf184f4075625 100644 --- a/packages/kbn-server-http-tools/src/ssl/ssl_config.ts +++ b/packages/kbn-server-http-tools/src/ssl/ssl_config.ts @@ -52,7 +52,7 @@ export const sslSchema = schema.object( schema.literal(TLS_V1_2), schema.literal(TLS_V1_3), ]), - { defaultValue: [TLS_V1_1, TLS_V1_2, TLS_V1_3], minSize: 1 } + { defaultValue: [TLS_V1_2, TLS_V1_3], minSize: 1 } ), clientAuthentication: schema.oneOf( [schema.literal('none'), schema.literal('optional'), schema.literal('required')], diff --git a/packages/kbn-test/src/jest/setup/react_testing_library.js b/packages/kbn-test/src/jest/setup/react_testing_library.js index d8994fdbe00e4..7b184485df1cb 100644 --- a/packages/kbn-test/src/jest/setup/react_testing_library.js +++ b/packages/kbn-test/src/jest/setup/react_testing_library.js @@ -36,6 +36,11 @@ jest.mock('@testing-library/react', () => { }; }); +const consoleFilters = [ + /^The above error occurred in the <.*?> component:/, // error boundary output + /^Error: Uncaught .+/, // jsdom output +]; + // This is a workaround to run tests with React 17 and the latest @testing-library/react // And prevent the act warnings that were supposed to be muted by @testing-library // The testing library mutes the act warnings in some cases by setting IS_REACT_ACT_ENVIRONMENT which is React@18 feature https://github.com/testing-library/react-testing-library/pull/1137/ @@ -44,14 +49,27 @@ jest.mock('@testing-library/react', () => { // NOTE: we're not muting all the act warnings but only those that testing-library wanted to mute const originalConsoleError = console.error; console.error = (...args) => { + const message = args[0]?.toString(); + if (global.IS_REACT_ACT_ENVIRONMENT === false) { - if ( - args[0].toString().includes('Warning: An update to %s inside a test was not wrapped in act') - ) { + if (message.includes('Warning: An update to %s inside a test was not wrapped in act')) { return; } } + // Additionally this is a restoration of the original console.error suppression + // expected by the usage of renderHook from react-hooks-testing-library + // which has been moved into latest react-testing-library but the suppression + // of the console.error was not moved with it. + // + // So adding by example from the original implementation: + // https://github.com/testing-library/react-hooks-testing-library/blob/1e01273374af4e48a0feb1f2233bf6c76d742167/src/core/console.ts + // with a slight modification to catch non-string errors as well + + if (message && consoleFilters.some((filter) => filter.test(message))) { + return; + } + originalConsoleError(...args); }; diff --git a/src/plugins/discover/public/application/view_alert/view_alert_utils.tsx b/src/plugins/discover/public/application/view_alert/view_alert_utils.tsx index a059b6f507259..ed56ee5f07c9c 100644 --- a/src/plugins/discover/public/application/view_alert/view_alert_utils.tsx +++ b/src/plugins/discover/public/application/view_alert/view_alert_utils.tsx @@ -33,7 +33,7 @@ export interface QueryParams { to: string | null; } -const LEGACY_BASE_ALERT_API_PATH = '/api/alerts'; +const BASE_ALERTING_API_PATH = '/api/alerting'; const buildTimeRangeFilter = ( dataView: DataView, @@ -77,7 +77,7 @@ export const getAlertUtils = ( const fetchAlert = async (id: string) => { try { return await core.http.get>( - `${LEGACY_BASE_ALERT_API_PATH}/alert/${id}` + `${BASE_ALERTING_API_PATH}/rule/${id}` ); } catch (error) { const errorTitle = i18n.translate('discover.viewAlert.alertRuleFetchErrorTitle', { diff --git a/src/plugins/field_formats/common/converters/bytes.ts b/src/plugins/field_formats/common/converters/bytes.ts index f56e523fbca7b..86e853f33a8c1 100644 --- a/src/plugins/field_formats/common/converters/bytes.ts +++ b/src/plugins/field_formats/common/converters/bytes.ts @@ -15,7 +15,7 @@ import { FIELD_FORMAT_IDS } from '../types'; export class BytesFormat extends NumeralFormat { static id = FIELD_FORMAT_IDS.BYTES; static title = i18n.translate('fieldFormats.bytes.title', { - defaultMessage: 'Bytes', + defaultMessage: 'Bytes and Bits', }); id = BytesFormat.id; diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.test.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.test.tsx index e633ec4a70be1..430753074a958 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.test.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.test.tsx @@ -8,13 +8,12 @@ */ import React from 'react'; -import { TopNavMenuItem } from './top_nav_menu_item'; -import { TopNavMenuData } from './top_nav_menu_data'; +import { TopNavMenuItem, TopNavMenuItemProps } from './top_nav_menu_item'; import { shallowWithIntl } from '@kbn/test-jest-helpers'; import { EuiButtonIcon } from '@elastic/eui'; describe('TopNavMenu', () => { - const ensureMenuItemDisabled = (data: TopNavMenuData) => { + const ensureMenuItemDisabled = (data: TopNavMenuItemProps) => { const component = shallowWithIntl(); expect(component.prop('isDisabled')).toEqual(true); @@ -24,10 +23,11 @@ describe('TopNavMenu', () => { }; it('Should render and click an item', () => { - const data: TopNavMenuData = { + const data: TopNavMenuItemProps = { id: 'test', label: 'test', run: jest.fn(), + closePopover: jest.fn(), }; const component = shallowWithIntl(); @@ -43,13 +43,14 @@ describe('TopNavMenu', () => { }); it('Should render item with all attributes', () => { - const data: TopNavMenuData = { + const data: TopNavMenuItemProps = { id: 'test', label: 'test', description: 'description', testId: 'test-class-name', disableButton: false, run: jest.fn(), + closePopover: jest.fn(), }; const component = shallowWithIntl(); @@ -61,13 +62,14 @@ describe('TopNavMenu', () => { }); it('Should render emphasized item which should be clickable', () => { - const data: TopNavMenuData = { + const data: TopNavMenuItemProps = { id: 'test', label: 'test', iconType: 'beaker', iconSide: 'right', emphasize: true, run: jest.fn(), + closePopover: jest.fn(), }; const component = shallowWithIntl(); @@ -78,12 +80,13 @@ describe('TopNavMenu', () => { }); it('Should render an icon-only item', () => { - const data: TopNavMenuData = { + const data: TopNavMenuItemProps = { id: 'test', label: 'test', iconType: 'share', iconOnly: true, run: jest.fn(), + closePopover: jest.fn(), }; const component = shallowWithIntl(); @@ -100,6 +103,7 @@ describe('TopNavMenu', () => { label: 'test', disableButton: true, run: jest.fn(), + closePopover: jest.fn(), }); }); @@ -109,6 +113,7 @@ describe('TopNavMenu', () => { label: 'test', disableButton: () => true, run: jest.fn(), + closePopover: jest.fn(), }); }); @@ -121,6 +126,7 @@ describe('TopNavMenu', () => { emphasize: true, disableButton: true, run: jest.fn(), + closePopover: jest.fn(), }); }); @@ -133,6 +139,26 @@ describe('TopNavMenu', () => { emphasize: true, disableButton: () => true, run: jest.fn(), + closePopover: jest.fn(), }); }); + + it('Should render emphasized item in mobile mode, which should be clickable and call closePopover on click', () => { + const data: TopNavMenuItemProps = { + id: 'test', + label: 'test', + iconType: 'beaker', + iconSide: 'right', + emphasize: true, + isMobileMenu: true, + run: jest.fn(), + closePopover: jest.fn(), + }; + + const component = shallowWithIntl(); + const event = { currentTarget: { value: 'a' } }; + component.simulate('click', event); + expect(data.run).toHaveBeenCalledTimes(1); + expect(data.closePopover).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx index 84232d47fd337..f19d7d08d79d9 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx @@ -19,7 +19,12 @@ import { } from '@elastic/eui'; import { TopNavMenuData } from './top_nav_menu_data'; -export function TopNavMenuItem(props: TopNavMenuData & { isMobileMenu?: boolean }) { +export interface TopNavMenuItemProps extends TopNavMenuData { + closePopover: () => void; + isMobileMenu?: boolean; +} + +export function TopNavMenuItem(props: TopNavMenuItemProps) { function isDisabled(): boolean { const val = isFunction(props.disableButton) ? props.disableButton() : props.disableButton; return val!; @@ -46,6 +51,9 @@ export function TopNavMenuItem(props: TopNavMenuData & { isMobileMenu?: boolean function handleClick(e: MouseEvent) { if (isDisabled()) return; props.run(e.currentTarget); + if (props.isMobileMenu) { + props.closePopover(); + } } const commonButtonProps = { diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_items.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_items.tsx index 928749beb4477..e5150679bf1d5 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_items.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_items.tsx @@ -35,9 +35,18 @@ export const TopNavMenuItems = ({ className={className} popoverBreakpoints={popoverBreakpoints} > - {config.map((menuItem: TopNavMenuData, i: number) => { - return ; - })} + {(closePopover) => + config.map((menuItem: TopNavMenuData, i: number) => { + return ( + + ); + }) + } ); }; diff --git a/src/plugins/unified_search/server/autocomplete/value_suggestions_route.ts b/src/plugins/unified_search/server/autocomplete/value_suggestions_route.ts index 1ea0084423ea1..afda8f890e96c 100644 --- a/src/plugins/unified_search/server/autocomplete/value_suggestions_route.ts +++ b/src/plugins/unified_search/server/autocomplete/value_suggestions_route.ts @@ -25,6 +25,13 @@ export function registerValueSuggestionsRoute(router: IRouter, config$: Observab .addVersion( { version: '1', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because uses the current user authorizations.', + }, + }, validate: { request: { params: schema.object( diff --git a/src/plugins/vis_types/timelion/server/routes/functions.ts b/src/plugins/vis_types/timelion/server/routes/functions.ts index 4e989f2ac747c..6f435536ce7d5 100644 --- a/src/plugins/vis_types/timelion/server/routes/functions.ts +++ b/src/plugins/vis_types/timelion/server/routes/functions.ts @@ -15,6 +15,13 @@ export function functionsRoute(router: IRouter, { functions }: { functions: Load router.get( { path: '/internal/timelion/functions', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because it just returns a static list of function ids to use in timelion expression.', + }, + }, validate: false, }, async (context, request, response) => { diff --git a/src/plugins/vis_types/timelion/server/routes/run.ts b/src/plugins/vis_types/timelion/server/routes/run.ts index 1e63004d9895c..3d737890db0f6 100644 --- a/src/plugins/vis_types/timelion/server/routes/run.ts +++ b/src/plugins/vis_types/timelion/server/routes/run.ts @@ -39,6 +39,13 @@ export function runRoute( router.post( { path: '/internal/timelion/run', + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because it executes some server side data processing and uses the data-plugin to query ES following the data-plugin authz rules.', + }, + }, validate: { body: schema.object({ sheet: schema.arrayOf(schema.string()), diff --git a/src/plugins/vis_types/timeseries/server/routes/fields.ts b/src/plugins/vis_types/timeseries/server/routes/fields.ts index 8751e2ad222f7..f5c8c84fdb70c 100644 --- a/src/plugins/vis_types/timeseries/server/routes/fields.ts +++ b/src/plugins/vis_types/timeseries/server/routes/fields.ts @@ -18,6 +18,13 @@ export const fieldsRoutes = (router: VisTypeTimeseriesRouter, framework: Framewo router.get<{}, { index: string }, {}>( { path: ROUTES.FIELDS, + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because it uses the data-plugin to query ES following the data-plugin authz rules.', + }, + }, validate: { query: schema.object({ index: schema.string() }), }, diff --git a/src/plugins/vis_types/timeseries/server/routes/vis.ts b/src/plugins/vis_types/timeseries/server/routes/vis.ts index 89ea60764e8a0..a75eb463e072c 100644 --- a/src/plugins/vis_types/timeseries/server/routes/vis.ts +++ b/src/plugins/vis_types/timeseries/server/routes/vis.ts @@ -21,6 +21,13 @@ export const visDataRoutes = (router: VisTypeTimeseriesRouter, framework: Framew router.post<{}, {}, VisPayload>( { path: ROUTES.VIS_DATA, + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization because it uses the data-plugin to query ES following the data-plugin authz rules.', + }, + }, validate: { body: escapeHatch, }, diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.test.tsx b/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.test.tsx index dffca3addd34e..196553ce55066 100644 --- a/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.test.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.test.tsx @@ -5,12 +5,7 @@ * 2.0. */ import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; -import { - act, - renderHook, - type RenderHookResult, - type WrapperComponent, -} from '@testing-library/react-hooks'; +import { renderHook, act, type RenderHookResult } from '@testing-library/react'; import { merge } from 'lodash'; import React, { PropsWithChildren } from 'react'; import { BehaviorSubject, Observable, of, Subject } from 'rxjs'; @@ -33,7 +28,7 @@ import type { NotificationsStart } from '@kbn/core/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { AssistantScope } from '@kbn/ai-assistant-common'; -let hookResult: RenderHookResult; +let hookResult: RenderHookResult; type MockedService = DeeplyMockedKeys> & { conversations: DeeplyMockedKeys< @@ -81,34 +76,32 @@ const useKibanaMockServices = { }; describe('useConversation', () => { - let wrapper: WrapperComponent>; + const wrapper = ({ children }: PropsWithChildren) => ( + {children} + ); - beforeEach(() => { + afterEach(() => { jest.clearAllMocks(); - wrapper = ({ children }: PropsWithChildren) => ( - {children} - ); }); describe('with initial messages and a conversation id', () => { - beforeEach(() => { - hookResult = renderHook(useConversation, { - initialProps: { - chatService: mockChatService, - connectorId: 'my-connector', - initialMessages: [ - { - '@timestamp': new Date().toISOString(), - message: { content: '', role: MessageRole.User }, - }, - ], - initialConversationId: 'foo', - }, - wrapper, - }); - }); it('throws an error', () => { - expect(hookResult.result.error).toBeTruthy(); + expect(() => + renderHook(useConversation, { + initialProps: { + chatService: mockChatService, + connectorId: 'my-connector', + initialMessages: [ + { + '@timestamp': new Date().toISOString(), + message: { content: '', role: MessageRole.User }, + }, + ], + initialConversationId: 'foo', + }, + wrapper, + }) + ).toThrow(/Cannot set initialMessages if initialConversationId is set/); }); }); @@ -434,25 +427,29 @@ describe('useConversation', () => { describe('when the title is updated', () => { describe('without a stored conversation', () => { - beforeEach(() => { - hookResult = renderHook(useConversation, { - initialProps: { - chatService: mockChatService, - connectorId: 'my-connector', - initialMessages: [ - { - '@timestamp': new Date().toISOString(), - message: { content: '', role: MessageRole.User }, - }, - ], - initialConversationId: 'foo', - }, - wrapper, - }); - }); + it('throws an error', (done) => { + try { + const { result } = renderHook(useConversation, { + initialProps: { + chatService: mockChatService, + connectorId: 'my-connector', + initialMessages: [ + { + '@timestamp': new Date().toISOString(), + message: { content: '', role: MessageRole.User }, + }, + ], + initialConversationId: 'foo', + }, + wrapper, + }); - it('throws an error', () => { - expect(() => hookResult.result.current.saveTitle('my-new-title')).toThrow(); + result.current.saveTitle('my-new-title'); + } catch (e) { + expect(e).toBeInstanceOf(Error); + expect(e.message).toBe('Cannot set initialMessages if initialConversationId is set'); + done(); + } }); }); diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/use_local_storage.test.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_local_storage.test.ts index ab1d00392fdb9..ea4ed05e36b66 100644 --- a/x-pack/packages/kbn-ai-assistant/src/hooks/use_local_storage.test.ts +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_local_storage.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { useLocalStorage } from './use_local_storage'; describe('useLocalStorage', () => { diff --git a/x-pack/platform/packages/private/ml/url_state/index.ts b/x-pack/platform/packages/private/ml/url_state/index.ts index 13443af51bc4d..3bfc7f4ba1fa1 100644 --- a/x-pack/platform/packages/private/ml/url_state/index.ts +++ b/x-pack/platform/packages/private/ml/url_state/index.ts @@ -10,7 +10,7 @@ export { parseUrlState, usePageUrlState, useUrlState, - PageUrlStateService, + UrlStateService, Provider, UrlStateProvider, type Accessor, diff --git a/x-pack/platform/packages/private/ml/url_state/src/url_state.test.tsx b/x-pack/platform/packages/private/ml/url_state/src/url_state.test.tsx index 033ecd77fadf4..ab7726d99f238 100644 --- a/x-pack/platform/packages/private/ml/url_state/src/url_state.test.tsx +++ b/x-pack/platform/packages/private/ml/url_state/src/url_state.test.tsx @@ -6,14 +6,26 @@ */ import React, { useEffect, type FC } from 'react'; -import { render, act } from '@testing-library/react'; +import { render, act, renderHook } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; -import { parseUrlState, useUrlState, UrlStateProvider } from './url_state'; +import { + parseUrlState, + useUrlState, + UrlStateProvider, + usePageUrlState, + useGlobalUrlState, +} from './url_state'; const mockHistoryInitialState = "?_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFieldName:action),query:(query_string:(analyze_wildcard:!t,query:'*')))&_g=(ml:(jobIds:!(dec-2)),refreshInterval:(display:Off,pause:!f,value:0),time:(from:'2019-01-01T00:03:40.000Z',mode:absolute,to:'2019-08-30T11:55:07.000Z'))&savedSearchId=571aaf70-4c88-11e8-b3d7-01146121b73d"; +const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); + describe('getUrlState', () => { test('properly decode url with _g and _a', () => { expect(parseUrlState(mockHistoryInitialState)).toEqual({ @@ -143,3 +155,50 @@ describe('useUrlState', () => { expect(getByTestId('appState').innerHTML).toBe('the updated query'); }); }); + +describe('usePageUrlState', () => { + it('manages page-specific state with default values', () => { + const pageKey = 'testPage'; + const defaultPageState = { + defaultValue: 'initial', + }; + + const updatedPageState = { + defaultValue: 'updated', + }; + + const { result } = renderHook(() => usePageUrlState(pageKey, defaultPageState), { wrapper }); + + expect(result.current[0]).toEqual(defaultPageState); + + act(() => { + result.current[1](updatedPageState); + }); + + expect(result.current[0]).toEqual(updatedPageState); + }); +}); + +describe('useGlobalUrlState', () => { + it('manages global state with ML and time properties', () => { + const defaultState = { + ml: { jobIds: ['initial-job'] }, + time: { from: 'now-15m', to: 'now' }, + }; + + const updatedState = { + ml: { jobIds: ['updated-job'] }, + time: { from: 'now-1h', to: 'now' }, + }; + + const { result } = renderHook(() => useGlobalUrlState(defaultState), { wrapper }); + + expect(result.current[0]).toEqual(defaultState); + + act(() => { + result.current[1](updatedState); + }); + + expect(result.current[0]).toEqual(updatedState); + }); +}); diff --git a/x-pack/platform/packages/private/ml/url_state/src/url_state.tsx b/x-pack/platform/packages/private/ml/url_state/src/url_state.tsx index 7cd6bc1d812e6..e24176321094a 100644 --- a/x-pack/platform/packages/private/ml/url_state/src/url_state.tsx +++ b/x-pack/platform/packages/private/ml/url_state/src/url_state.tsx @@ -26,6 +26,7 @@ import type { Observable } from 'rxjs'; import { BehaviorSubject } from 'rxjs'; import { distinctUntilChanged } from 'rxjs'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import useDeepCompareEffect from 'react-use/lib/useDeepCompareEffect'; export interface Dictionary { [id: string]: TValue; @@ -211,27 +212,26 @@ export const useUrlState = ( /** * Service for managing URL state of particular page. */ -export class PageUrlStateService { - private _pageUrlState$ = new BehaviorSubject(null); - private _pageUrlStateCallback: ((update: Partial, replaceState?: boolean) => void) | null = - null; +export class UrlStateService { + private _urlState$ = new BehaviorSubject(null); + private _urlStateCallback: ((update: Partial, replaceState?: boolean) => void) | null = null; /** * Provides updates for the page URL state. */ - public getPageUrlState$(): Observable { - return this._pageUrlState$.pipe(distinctUntilChanged(isEqual)); + public getUrlState$(): Observable { + return this._urlState$.pipe(distinctUntilChanged(isEqual)); } - public getPageUrlState(): T | null { - return this._pageUrlState$.getValue(); + public getUrlState(): T | null { + return this._urlState$.getValue(); } public updateUrlState(update: Partial, replaceState?: boolean): void { - if (!this._pageUrlStateCallback) { + if (!this._urlStateCallback) { throw new Error('Callback has not been initialized.'); } - this._pageUrlStateCallback(update, replaceState); + this._urlStateCallback(update, replaceState); } /** @@ -239,7 +239,7 @@ export class PageUrlStateService { * @param currentState */ public setCurrentState(currentState: T): void { - this._pageUrlState$.next(currentState); + this._urlState$.next(currentState); } /** @@ -247,7 +247,7 @@ export class PageUrlStateService { * @param callback */ public setUpdateCallback(callback: (update: Partial, replaceState?: boolean) => void): void { - this._pageUrlStateCallback = callback; + this._urlStateCallback = callback; } } @@ -256,32 +256,53 @@ export interface PageUrlState { pageUrlState: object; } -/** - * Hook for managing the URL state of the page. - */ -export const usePageUrlState = ( - pageKey: T['pageKey'], - defaultState?: T['pageUrlState'] -): [ - T['pageUrlState'], - (update: Partial, replaceState?: boolean) => void, - PageUrlStateService -] => { - const [appState, setAppState] = useUrlState('_a'); - const pageState = appState?.[pageKey]; +interface AppStateOptions { + pageKey: string; + defaultState?: T; +} - const setCallback = useRef(); +interface GlobalStateOptions { + defaultState?: T; +} + +type UrlStateOptions = K extends '_a' + ? AppStateOptions + : GlobalStateOptions; + +function isAppStateOptions( + _stateKey: Accessor, + options: Partial> +): options is AppStateOptions { + return 'pageKey' in options; +} + +export const useUrlStateService = ( + stateKey: K, + options: UrlStateOptions +): [T, (update: Partial, replaceState?: boolean) => void, UrlStateService] => { + const optionsRef = useRef(options); + + useDeepCompareEffect(() => { + optionsRef.current = options; + }, [options]); + + const [state, setState] = useUrlState(stateKey); + const urlState = isAppStateOptions(stateKey, optionsRef.current) + ? state?.[optionsRef.current.pageKey] + : state; + + const setCallback = useRef(); useEffect(() => { - setCallback.current = setAppState; - }, [setAppState]); + setCallback.current = setState; + }, [setState]); - const prevPageState = useRef(); + const prevPageState = useRef(); - const resultPageState: T['pageUrlState'] = useMemo(() => { + const resultState: T = useMemo(() => { const result = { - ...(defaultState ?? {}), - ...(pageState ?? {}), + ...(optionsRef.current.defaultState ?? {}), + ...(urlState ?? {}), }; if (isEqual(result, prevPageState.current)) { @@ -300,38 +321,82 @@ export const usePageUrlState = ( prevPageState.current = result; return result; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pageState]); + }, [urlState]); const onStateUpdate = useCallback( - (update: Partial, replaceState?: boolean) => { + (update: Partial, replaceState?: boolean) => { if (!setCallback?.current) { throw new Error('Callback for URL state update has not been initialized.'); } - - setCallback.current( - pageKey, - { - ...resultPageState, - ...update, - }, - replaceState - ); + if (isAppStateOptions(stateKey, optionsRef.current)) { + setCallback.current( + optionsRef.current.pageKey, + { + ...resultState, + ...update, + }, + replaceState + ); + } else { + setCallback.current({ ...resultState, ...update }, replaceState); + } }, - [pageKey, resultPageState] + [stateKey, resultState] ); - const pageUrlStateService = useMemo(() => new PageUrlStateService(), []); + const urlStateService = useMemo(() => new UrlStateService(), []); useEffect( - function updatePageUrlService() { - pageUrlStateService.setCurrentState(resultPageState); - pageUrlStateService.setUpdateCallback(onStateUpdate); + function updateUrlStateService() { + urlStateService.setCurrentState(resultState); + urlStateService.setUpdateCallback(onStateUpdate); }, - [pageUrlStateService, onStateUpdate, resultPageState] + [urlStateService, onStateUpdate, resultState] ); - return useMemo(() => { - return [resultPageState, onStateUpdate, pageUrlStateService]; - }, [resultPageState, onStateUpdate, pageUrlStateService]); + return useMemo( + () => [resultState, onStateUpdate, urlStateService], + [resultState, onStateUpdate, urlStateService] + ); +}; + +/** + * Hook for managing the URL state of the page. + */ +export const usePageUrlState = ( + pageKey: T['pageKey'], + defaultState?: T['pageUrlState'] +): [ + T['pageUrlState'], + (update: Partial, replaceState?: boolean) => void, + UrlStateService +] => { + return useUrlStateService<'_a', T['pageUrlState']>('_a', { pageKey, defaultState }); +}; + +/** + * Global state type, to add more state types, add them here + */ + +export interface GlobalState { + ml: { + jobIds: string[]; + }; + time?: { + from: string; + to: string; + }; +} + +/** + * Hook for managing the global URL state. + */ +export const useGlobalUrlState = ( + defaultState?: GlobalState +): [ + GlobalState, + (update: Partial, replaceState?: boolean) => void, + UrlStateService +] => { + return useUrlStateService<'_g', GlobalState>('_g', { defaultState }); }; diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index 6c6c7f8516d5a..e55adb9fe6df6 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -27320,9 +27320,6 @@ "xpack.logsShared.dataSearch.cancelButtonLabel": "Annuler la demande", "xpack.logsShared.dataSearch.loadingErrorRetryButtonLabel": "Réessayer", "xpack.logsShared.dataSearch.shardFailureErrorMessage": "Index {indexName} : {errorMessage}", - "xpack.logsShared.deprecations.migrateLogViewSettingsToLogSourcesSetting.message": "Les options d'affichage des indices et des données précédemment fournies par le biais de la page des paramètres de l'interface utilisateur des logs sont désormais obsolètes. Veuillez désormais utiliser le paramètre avancé des sources de logs Kibana.", - "xpack.logsShared.deprecations.migrateLogViewSettingsToLogSourcesSetting.message.manualStepMessage": "Mettez à jour le paramètre avancé des sources de logs Kibana (via Gestion > Paramètres avancés) pour qu'il corresponde au paramètre précédemment fourni via la page des paramètres de l'interface utilisateur des logs. Ensuite, via la page des paramètres de l'interface utilisateur des logs, utilisez l'option de paramètre avancé des sources de logs Kibana.", - "xpack.logsShared.deprecations.migrateLogViewSettingsToLogSourcesSetting.title": "Paramétrage des sources de logs", "xpack.logsShared.lobs.logEntryActionsViewInContextButton": "Afficher en contexte", "xpack.logsShared.logEntryActionsMenu.apmActionLabel": "Afficher dans APM", "xpack.logsShared.logEntryActionsMenu.buttonLabel": "Examiner", @@ -29621,10 +29618,8 @@ "xpack.ml.explorer.distributionChart.valueWithoutAnomalyScoreLabel": "valeur", "xpack.ml.explorer.invalidKuerySyntaxErrorMessageFromTable": "Syntaxe non valide dans la barre de requête. L'entrée doit être du code KQL (Kibana Query Language) valide", "xpack.ml.explorer.invalidKuerySyntaxErrorMessageQueryBar": "Requête non valide", - "xpack.ml.explorer.invalidTimeRangeInUrlCallout": "Le filtre de temps a été modifié pour inclure la plage entière en raison d'un filtre de temps par défaut non valide. Vérifiez les paramètres avancés pour {field}.", "xpack.ml.explorer.jobIdLabel": "ID tâche", "xpack.ml.explorer.jobScoreAcrossAllInfluencersLabel": "(score de tâche pour tous les influenceurs)", - "xpack.ml.explorer.kueryBar.filterPlaceholder": "Filtrer par champ d'influenceur… ({queryExample})", "xpack.ml.explorer.mapTitle": "Nombre d'anomalies par emplacement {infoTooltip}", "xpack.ml.explorer.noAnomaliesFoundLabel": "Aucune anomalie n'a été trouvée", "xpack.ml.explorer.noConfiguredInfluencersTooltip": "La liste Principaux influenceurs est masquée, car aucun influenceur n'a été configuré pour les tâches sélectionnées.", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 569228e7d4088..18a4ea7485487 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -27183,9 +27183,6 @@ "xpack.logsShared.dataSearch.cancelButtonLabel": "リクエストのキャンセル", "xpack.logsShared.dataSearch.loadingErrorRetryButtonLabel": "再試行", "xpack.logsShared.dataSearch.shardFailureErrorMessage": "インデックス {indexName}:{errorMessage}", - "xpack.logsShared.deprecations.migrateLogViewSettingsToLogSourcesSetting.message": "以前はログUI設定ページで提供されていたインデックスとデータ表示オプションは、現在では廃止予定です。Kibanaログソースの詳細設定を使用するように移行してください。", - "xpack.logsShared.deprecations.migrateLogViewSettingsToLogSourcesSetting.message.manualStepMessage": "ログソースKibana詳細設定([管理]>[詳細設定])を、以前にLogs UI設定ページで指定した設定と一致するように更新します。次に、ログUI設定ページで、Kibanaログソースの詳細設定オプションを使用します。", - "xpack.logsShared.deprecations.migrateLogViewSettingsToLogSourcesSetting.title": "ログソース設定", "xpack.logsShared.lobs.logEntryActionsViewInContextButton": "コンテキストで表示", "xpack.logsShared.logEntryActionsMenu.apmActionLabel": "APMで表示", "xpack.logsShared.logEntryActionsMenu.buttonLabel": "調査", @@ -29482,10 +29479,8 @@ "xpack.ml.explorer.distributionChart.valueWithoutAnomalyScoreLabel": "値", "xpack.ml.explorer.invalidKuerySyntaxErrorMessageFromTable": "クエリーバーに無効な構文。インプットは有効な Kibana クエリー言語(KQL)でなければなりません", "xpack.ml.explorer.invalidKuerySyntaxErrorMessageQueryBar": "無効なクエリー", - "xpack.ml.explorer.invalidTimeRangeInUrlCallout": "無効なデフォルト時間フィルターのため、時間フィルターが全範囲に変更されました。{field}の詳細設定を確認してください。", "xpack.ml.explorer.jobIdLabel": "ジョブID", "xpack.ml.explorer.jobScoreAcrossAllInfluencersLabel": "(すべての影響因子のジョブスコア)", - "xpack.ml.explorer.kueryBar.filterPlaceholder": "影響因子フィールドでフィルタリング…({queryExample})", "xpack.ml.explorer.mapTitle": "場所別異常件数{infoTooltip}", "xpack.ml.explorer.noAnomaliesFoundLabel": "異常値が見つかりませんでした", "xpack.ml.explorer.noConfiguredInfluencersTooltip": "選択されたジョブに影響因子が構成されていないため、トップインフルエンスリストは非表示になっています。", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index 611dbf21d2a4a..285f889c557b2 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -26741,9 +26741,6 @@ "xpack.logsShared.dataSearch.cancelButtonLabel": "取消请求", "xpack.logsShared.dataSearch.loadingErrorRetryButtonLabel": "重试", "xpack.logsShared.dataSearch.shardFailureErrorMessage": "索引 {indexName}:{errorMessage}", - "xpack.logsShared.deprecations.migrateLogViewSettingsToLogSourcesSetting.message": "以前通过日志 UI 设置页面提供的索引和数据视图选项现已弃用。请进行迁移,以使用 Kibana 日志源高级设置。", - "xpack.logsShared.deprecations.migrateLogViewSettingsToLogSourcesSetting.message.manualStepMessage": "更新日志源 Kibana 高级设置(通过'管理 > 高级设置'),以匹配以前通过日志 UI 设置页面提供的设置。然后,通过日志 UI 设置页面使用 Kibana 日志源高级设置选项。", - "xpack.logsShared.deprecations.migrateLogViewSettingsToLogSourcesSetting.title": "日志源设置", "xpack.logsShared.lobs.logEntryActionsViewInContextButton": "在上下文中查看", "xpack.logsShared.logEntryActionsMenu.apmActionLabel": "在 APM 中查看", "xpack.logsShared.logEntryActionsMenu.buttonLabel": "调查", @@ -29006,10 +29003,8 @@ "xpack.ml.explorer.distributionChart.valueWithoutAnomalyScoreLabel": "值", "xpack.ml.explorer.invalidKuerySyntaxErrorMessageFromTable": "查询栏中的语法无效。输入必须是有效的 Kibana 查询语言 (KQL)", "xpack.ml.explorer.invalidKuerySyntaxErrorMessageQueryBar": "无效查询", - "xpack.ml.explorer.invalidTimeRangeInUrlCallout": "由于默认时间筛选无效,时间筛选已更改为完整范围。检查 {field} 的高级设置。", "xpack.ml.explorer.jobIdLabel": "作业 ID", "xpack.ml.explorer.jobScoreAcrossAllInfluencersLabel": "(所有影响因素的作业分数)", - "xpack.ml.explorer.kueryBar.filterPlaceholder": "按影响因素字段筛选……({queryExample})", "xpack.ml.explorer.mapTitle": "异常计数(按位置){infoTooltip}", "xpack.ml.explorer.noAnomaliesFoundLabel": "找不到异常", "xpack.ml.explorer.noConfiguredInfluencersTooltip": "'排名最前影响因素'列表被隐藏,因为没有为所选作业配置影响因素。", diff --git a/x-pack/platform/plugins/private/watcher/common/constants/index_names.ts b/x-pack/platform/plugins/private/watcher/common/constants/index_names.ts index b854ecd047483..64425387dc138 100644 --- a/x-pack/platform/plugins/private/watcher/common/constants/index_names.ts +++ b/x-pack/platform/plugins/private/watcher/common/constants/index_names.ts @@ -5,7 +5,6 @@ * 2.0. */ -export const INDEX_NAMES: { [key: string]: string } = { - WATCHES: '.watches', +export const INDEX_NAMES = { WATCHER_HISTORY: '.watcher-history-*', }; diff --git a/x-pack/platform/plugins/private/watcher/server/plugin.ts b/x-pack/platform/plugins/private/watcher/server/plugin.ts index 75afac5c4d1f2..7572a63368d47 100644 --- a/x-pack/platform/plugins/private/watcher/server/plugin.ts +++ b/x-pack/platform/plugins/private/watcher/server/plugin.ts @@ -60,7 +60,6 @@ export class WatcherServerPlugin implements Plugin { { requiredClusterPrivileges: ['manage_watcher'], requiredIndexPrivileges: { - [INDEX_NAMES.WATCHES]: ['read'], [INDEX_NAMES.WATCHER_HISTORY]: ['read'], }, ui: [], @@ -68,7 +67,6 @@ export class WatcherServerPlugin implements Plugin { { requiredClusterPrivileges: ['monitor_watcher'], requiredIndexPrivileges: { - [INDEX_NAMES.WATCHES]: ['read'], [INDEX_NAMES.WATCHER_HISTORY]: ['read'], }, ui: [], diff --git a/x-pack/platform/plugins/private/watcher/server/routes/api/watches/register_list_route.ts b/x-pack/platform/plugins/private/watcher/server/routes/api/watches/register_list_route.ts index 9951da819e9e0..56affd6be154e 100644 --- a/x-pack/platform/plugins/private/watcher/server/routes/api/watches/register_list_route.ts +++ b/x-pack/platform/plugins/private/watcher/server/routes/api/watches/register_list_route.ts @@ -5,29 +5,9 @@ * 2.0. */ -import { IScopedClusterClient } from '@kbn/core/server'; -import { get } from 'lodash'; -import { fetchAllFromScroll } from '../../../lib/fetch_all_from_scroll'; -import { INDEX_NAMES, ES_SCROLL_SETTINGS } from '../../../../common/constants'; import { RouteDependencies } from '../../../types'; -// @ts-ignore import { Watch } from '../../../models/watch'; -function fetchWatches(dataClient: IScopedClusterClient) { - return dataClient.asCurrentUser - .search( - { - index: INDEX_NAMES.WATCHES, - scroll: ES_SCROLL_SETTINGS.KEEPALIVE, - body: { - size: ES_SCROLL_SETTINGS.PAGE_SIZE, - }, - }, - { ignore: [404] } - ) - .then((body) => fetchAllFromScroll(body, dataClient)); -} - export function registerListRoute({ router, license, lib: { handleEsError } }: RouteDependencies) { router.get( { @@ -37,17 +17,13 @@ export function registerListRoute({ router, license, lib: { handleEsError } }: R license.guardApiRoute(async (ctx, request, response) => { try { const esClient = (await ctx.core).elasticsearch.client; - const hits = await fetchWatches(esClient); - const watches = hits.map((hit: any) => { - const id = get(hit, '_id'); - const watchJson = get(hit, '_source'); - const watchStatusJson = get(hit, '_source.status'); - + const { watches: hits } = await esClient.asCurrentUser.watcher.queryWatches(); + const watches = hits.map(({ _id, watch, status }) => { return Watch.fromUpstreamJson( { - id, - watchJson, - watchStatusJson, + id: _id, + watchJson: watch, + watchStatusJson: status, }, { throwExceptions: { diff --git a/x-pack/platform/plugins/shared/integration_assistant/server/constants.ts b/x-pack/platform/plugins/shared/integration_assistant/server/constants.ts index 83577961095d7..b7edc9d5e8e7d 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/server/constants.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/server/constants.ts @@ -15,3 +15,6 @@ export enum LogFormat { STRUCTURED = 'structured', UNSTRUCTURED = 'unstructured', } +export const FLEET_ALL_ROLE = 'fleet-all' as const; +export const INTEGRATIONS_ALL_ROLE = 'integrations-all' as const; +export const ACTIONS_AND_CONNECTORS_ALL_ROLE = 'actions:execute-advanced-connectors' as const; diff --git a/x-pack/platform/plugins/shared/integration_assistant/server/integration_builder/build_integration.test.ts b/x-pack/platform/plugins/shared/integration_assistant/server/integration_builder/build_integration.test.ts index eaa50e87d41b4..01d3976b9dd6b 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/server/integration_builder/build_integration.test.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/server/integration_builder/build_integration.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { buildPackage, renderPackageManifestYAML } from './build_integration'; +import { buildPackage, isValidName, renderPackageManifestYAML } from './build_integration'; import { testIntegration } from '../../__jest__/fixtures/build_integration'; import { generateUniqueId, ensureDirSync, createSync } from '../util'; import { createDataStream } from './data_stream'; @@ -39,6 +39,7 @@ jest.mock('adm-zip', () => { return jest.fn().mockImplementation(() => ({ addLocalFolder: jest.fn(), toBuffer: jest.fn(), + addFile: jest.fn(), })); }); @@ -46,8 +47,8 @@ describe('buildPackage', () => { const packagePath = `${mockedDataPath}/integration-assistant-${mockedId}`; const integrationPath = `${packagePath}/integration-1.0.0`; - const firstDatastreamName = 'datastream_1'; - const secondDatastreamName = 'datastream_2'; + const firstDatastreamName = 'datastream_one'; + const secondDatastreamName = 'datastream_two'; const firstDataStreamInputTypes: InputType[] = ['filestream', 'kafka']; const secondDataStreamInputTypes: InputType[] = ['kafka']; @@ -74,8 +75,8 @@ describe('buildPackage', () => { const firstDataStream: DataStream = { name: firstDatastreamName, - title: 'Datastream_1', - description: 'Datastream_1 description', + title: 'datastream_one', + description: 'datastream_one description', inputTypes: firstDataStreamInputTypes, docs: firstDataStreamDocs, rawSamples: ['{"test1": "test1"}'], @@ -85,8 +86,8 @@ describe('buildPackage', () => { const secondDataStream: DataStream = { name: secondDatastreamName, - title: 'Datastream_2', - description: 'Datastream_2 description', + title: 'datastream_two', + description: 'datastream_two description', inputTypes: secondDataStreamInputTypes, docs: secondDataStreamDocs, rawSamples: ['{"test1": "test1"}'], @@ -123,15 +124,6 @@ describe('buildPackage', () => { expect(createSync).toHaveBeenCalledWith(`${integrationPath}/manifest.yml`, expect.any(String)); }); - it('Should create logo files if info is present in the integration', async () => { - testIntegration.logo = 'logo'; - - await buildPackage(testIntegration); - - expect(ensureDirSync).toHaveBeenCalledWith(`${integrationPath}/img`); - expect(createSync).toHaveBeenCalledWith(`${integrationPath}/img/logo.svg`, expect.any(Buffer)); - }); - it('Should not create logo files if info is not present in the integration', async () => { jest.clearAllMocks(); testIntegration.logo = undefined; @@ -186,19 +178,19 @@ describe('buildPackage', () => { it('Should call createReadme once with sorted fields', async () => { jest.clearAllMocks(); - const firstDSFieldsMapping = [{ name: 'name a', description: 'description 1', type: 'type 1' }]; + const firstDSFieldsMapping = [{ name: 'name_a', description: 'description 1', type: 'type 1' }]; const firstDataStreamFields = [ - { name: 'name b', description: 'description 1', type: 'type 1' }, + { name: 'name_b', description: 'description 1', type: 'type 1' }, ]; const secondDSFieldsMapping = [ - { name: 'name c', description: 'description 2', type: 'type 2' }, - { name: 'name e', description: 'description 3', type: 'type 3' }, + { name: 'name_c', description: 'description 2', type: 'type 2' }, + { name: 'name_e', description: 'description 3', type: 'type 3' }, ]; const secondDataStreamFields = [ - { name: 'name d', description: 'description 2', type: 'type 2' }, + { name: 'name_d', description: 'description 2', type: 'type 2' }, ]; (createFieldMapping as jest.Mock).mockReturnValueOnce(firstDSFieldsMapping); @@ -217,17 +209,17 @@ describe('buildPackage', () => { { datastream: firstDatastreamName, fields: [ - { name: 'name a', description: 'description 1', type: 'type 1' }, + { name: 'name_a', description: 'description 1', type: 'type 1' }, - { name: 'name b', description: 'description 1', type: 'type 1' }, + { name: 'name_b', description: 'description 1', type: 'type 1' }, ], }, { datastream: secondDatastreamName, fields: [ - { name: 'name c', description: 'description 2', type: 'type 2' }, - { name: 'name d', description: 'description 2', type: 'type 2' }, - { name: 'name e', description: 'description 3', type: 'type 3' }, + { name: 'name_c', description: 'description 2', type: 'type 2' }, + { name: 'name_d', description: 'description 2', type: 'type 2' }, + { name: 'name_e', description: 'description 3', type: 'type 3' }, ], }, ] @@ -239,13 +231,13 @@ describe('renderPackageManifestYAML', () => { test('generates the package manifest correctly', () => { const integration: Integration = { title: 'Sample Integration', - name: 'sample-integration', + name: 'sample_integration', description: ' This is a sample integration\n\nWith multiple lines and weird spacing. \n\n And more lines ', logo: 'some-logo.png', dataStreams: [ { - name: 'data-stream-1', + name: 'data_stream_one', title: 'Data Stream 1', description: 'This is data stream 1', inputTypes: ['filestream'], @@ -257,7 +249,7 @@ describe('renderPackageManifestYAML', () => { samplesFormat: { name: 'ndjson', multiline: false }, }, { - name: 'data-stream-2', + name: 'data_stream_two', title: 'Data Stream 2', description: 'This is data stream 2\nWith multiple lines of description\nBut otherwise, nothing special', @@ -292,3 +284,59 @@ describe('renderPackageManifestYAML', () => { }); }); }); + +describe('isValidName', () => { + it('should return true for valid names', () => { + expect(isValidName('validName')).toBe(true); + expect(isValidName('Valid_Name')).toBe(true); + expect(isValidName('anotherValidName')).toBe(true); + }); + + it('should return false for names with numbers', () => { + expect(isValidName('invalid123')).toBe(false); + expect(isValidName('123invalid')).toBe(false); + expect(isValidName('invalid_123')).toBe(false); + }); + + it('should return false for empty string', () => { + expect(isValidName('')).toBe(false); + }); + + it('should return false for names with spaces', () => { + expect(isValidName('invalid name')).toBe(false); + expect(isValidName(' invalid')).toBe(false); + expect(isValidName('invalid ')).toBe(false); + expect(isValidName('invalid name with spaces')).toBe(false); + }); + + it('should return false for names with special characters', () => { + expect(isValidName('invalid@name')).toBe(false); + expect(isValidName('invalid#name')).toBe(false); + expect(isValidName('invalid$name')).toBe(false); + expect(isValidName('invalid%name')).toBe(false); + expect(isValidName('invalid^name')).toBe(false); + expect(isValidName('invalid&name')).toBe(false); + expect(isValidName('invalid*name')).toBe(false); + expect(isValidName('invalid(name')).toBe(false); + expect(isValidName('invalid/name')).toBe(false); + }); + + it('should return false for names with dashes', () => { + expect(isValidName('invalid-name')).toBe(false); + expect(isValidName('invalid-name-with-dashes')).toBe(false); + }); + + it('should return false for names with periods', () => { + expect(isValidName('invalid.name')).toBe(false); + expect(isValidName('invalid.name.with.periods')).toBe(false); + }); + + it('should return false for names with mixed invalid characters', () => { + expect(isValidName('invalid@name#with$special%characters')).toBe(false); + expect(isValidName('invalid name with spaces and 123')).toBe(false); + }); + + it('should return false for names with empty string', () => { + expect(isValidName('')).toBe(false); + }); +}); diff --git a/x-pack/platform/plugins/shared/integration_assistant/server/integration_builder/build_integration.ts b/x-pack/platform/plugins/shared/integration_assistant/server/integration_builder/build_integration.ts index 785c11125afd9..e63e7d0648da7 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/server/integration_builder/build_integration.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/server/integration_builder/build_integration.ts @@ -34,6 +34,12 @@ function configureNunjucks() { export async function buildPackage(integration: Integration): Promise { configureNunjucks(); + if (!isValidName(integration.name)) { + throw new Error( + `Invalid integration name: ${integration.name}, Should only contain letters and underscores` + ); + } + const workingDir = joinPath(getDataPath(), `integration-assistant-${generateUniqueId()}`); const packageDirectoryName = `${integration.name}-${initialVersion}`; const packageDir = createDirectories(workingDir, integration, packageDirectoryName); @@ -41,6 +47,11 @@ export async function buildPackage(integration: Integration): Promise { const dataStreamsDir = joinPath(packageDir, 'data_stream'); const fieldsPerDatastream = integration.dataStreams.map((dataStream) => { const dataStreamName = dataStream.name; + if (!isValidName(dataStreamName)) { + throw new Error( + `Invalid datastream name: ${dataStreamName}, Should only contain letters and underscores` + ); + } const specificDataStreamDir = joinPath(dataStreamsDir, dataStreamName); const dataStreamFields = createDataStream(integration.name, specificDataStreamDir, dataStream); @@ -60,12 +71,15 @@ export async function buildPackage(integration: Integration): Promise { }); createReadme(packageDir, integration.name, integration.dataStreams, fieldsPerDatastream); - const zipBuffer = await createZipArchive(workingDir, packageDirectoryName); + const zipBuffer = await createZipArchive(integration, workingDir, packageDirectoryName); removeDirSync(workingDir); return zipBuffer; } - +export function isValidName(input: string): boolean { + const regex = /^[a-zA-Z_]+$/; + return input.length > 0 && regex.test(input); +} function createDirectories( workingDir: string, integration: Integration, @@ -84,17 +98,6 @@ function createPackage(packageDir: string, integration: Integration): void { createPackageManifest(packageDir, integration); // Skipping creation of system tests temporarily for custom package generation // createPackageSystemTests(packageDir, integration); - if (integration?.logo !== undefined) { - createLogo(packageDir, integration.logo); - } -} - -function createLogo(packageDir: string, logo: string): void { - const logoDir = joinPath(packageDir, 'img'); - ensureDirSync(logoDir); - - const buffer = Buffer.from(logo, 'base64'); - createSync(joinPath(logoDir, 'logo.svg'), buffer); } function createBuildFile(packageDir: string): void { @@ -113,10 +116,20 @@ function createChangelog(packageDir: string): void { createSync(joinPath(packageDir, 'changelog.yml'), changelogTemplate); } -async function createZipArchive(workingDir: string, packageDirectoryName: string): Promise { +async function createZipArchive( + integration: Integration, + workingDir: string, + packageDirectoryName: string +): Promise { const tmpPackageDir = joinPath(workingDir, packageDirectoryName); const zip = new AdmZip(); zip.addLocalFolder(tmpPackageDir, packageDirectoryName); + + if (integration.logo) { + const logoDir = joinPath(packageDirectoryName, 'img/logo.svg'); + const logoBuffer = Buffer.from(integration.logo, 'base64'); + zip.addFile(logoDir, logoBuffer); + } const buffer = zip.toBuffer(); return buffer; } diff --git a/x-pack/platform/plugins/shared/integration_assistant/server/routes/analyze_logs_routes.ts b/x-pack/platform/plugins/shared/integration_assistant/server/routes/analyze_logs_routes.ts index 93ac55f6f712c..de6f3fc054dc6 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/server/routes/analyze_logs_routes.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/server/routes/analyze_logs_routes.ts @@ -10,7 +10,12 @@ import { getRequestAbortedSignal } from '@kbn/data-plugin/server'; import { APMTracer } from '@kbn/langchain/server/tracers/apm'; import { getLangSmithTracer } from '@kbn/langchain/server/tracers/langsmith'; import { ANALYZE_LOGS_PATH, AnalyzeLogsRequestBody, AnalyzeLogsResponse } from '../../common'; -import { ROUTE_HANDLER_TIMEOUT } from '../constants'; +import { + ACTIONS_AND_CONNECTORS_ALL_ROLE, + FLEET_ALL_ROLE, + INTEGRATIONS_ALL_ROLE, + ROUTE_HANDLER_TIMEOUT, +} from '../constants'; import { getLogFormatDetectionGraph } from '../graphs/log_type_detection/graph'; import type { IntegrationAssistantRouteHandlerContext } from '../plugin'; import { getLLMClass, getLLMType } from '../util/llm'; @@ -39,9 +44,11 @@ export function registerAnalyzeLogsRoutes( version: '1', security: { authz: { - enabled: false, - reason: - 'This route is opted out from authorization because the privileges are not defined yet.', + requiredPrivileges: [ + FLEET_ALL_ROLE, + INTEGRATIONS_ALL_ROLE, + ACTIONS_AND_CONNECTORS_ALL_ROLE, + ], }, }, validate: { diff --git a/x-pack/platform/plugins/shared/integration_assistant/server/routes/build_integration_routes.ts b/x-pack/platform/plugins/shared/integration_assistant/server/routes/build_integration_routes.ts index f62d6d55f933d..94bcbfaedaebf 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/server/routes/build_integration_routes.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/server/routes/build_integration_routes.ts @@ -14,6 +14,11 @@ import { withAvailability } from './with_availability'; import { isErrorThatHandlesItsOwnResponse } from '../lib/errors'; import { handleCustomErrors } from './routes_util'; import { GenerationErrorCode } from '../../common/constants'; +import { + ACTIONS_AND_CONNECTORS_ALL_ROLE, + FLEET_ALL_ROLE, + INTEGRATIONS_ALL_ROLE, +} from '../constants'; export function registerIntegrationBuilderRoutes( router: IRouter ) { @@ -27,9 +32,11 @@ export function registerIntegrationBuilderRoutes( version: '1', security: { authz: { - enabled: false, - reason: - 'This route is opted out from authorization because the privileges are not defined yet.', + requiredPrivileges: [ + FLEET_ALL_ROLE, + INTEGRATIONS_ALL_ROLE, + ACTIONS_AND_CONNECTORS_ALL_ROLE, + ], }, }, validate: { diff --git a/x-pack/platform/plugins/shared/integration_assistant/server/routes/categorization_routes.ts b/x-pack/platform/plugins/shared/integration_assistant/server/routes/categorization_routes.ts index 5f63ed9c7bf3c..72aaf1d963efb 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/server/routes/categorization_routes.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/server/routes/categorization_routes.ts @@ -14,7 +14,12 @@ import { CategorizationRequestBody, CategorizationResponse, } from '../../common'; -import { ROUTE_HANDLER_TIMEOUT } from '../constants'; +import { + ACTIONS_AND_CONNECTORS_ALL_ROLE, + FLEET_ALL_ROLE, + INTEGRATIONS_ALL_ROLE, + ROUTE_HANDLER_TIMEOUT, +} from '../constants'; import { getCategorizationGraph } from '../graphs/categorization'; import type { IntegrationAssistantRouteHandlerContext } from '../plugin'; import { getLLMClass, getLLMType } from '../util/llm'; @@ -42,9 +47,11 @@ export function registerCategorizationRoutes( version: '1', security: { authz: { - enabled: false, - reason: - 'This route is opted out from authorization because the privileges are not defined yet.', + requiredPrivileges: [ + FLEET_ALL_ROLE, + INTEGRATIONS_ALL_ROLE, + ACTIONS_AND_CONNECTORS_ALL_ROLE, + ], }, }, validate: { diff --git a/x-pack/platform/plugins/shared/integration_assistant/server/routes/cel_routes.ts b/x-pack/platform/plugins/shared/integration_assistant/server/routes/cel_routes.ts index 9ce16c3909119..63faeb6a71f26 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/server/routes/cel_routes.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/server/routes/cel_routes.ts @@ -10,7 +10,12 @@ import { getRequestAbortedSignal } from '@kbn/data-plugin/server'; import { APMTracer } from '@kbn/langchain/server/tracers/apm'; import { getLangSmithTracer } from '@kbn/langchain/server/tracers/langsmith'; import { CEL_INPUT_GRAPH_PATH, CelInputRequestBody, CelInputResponse } from '../../common'; -import { ROUTE_HANDLER_TIMEOUT } from '../constants'; +import { + ACTIONS_AND_CONNECTORS_ALL_ROLE, + FLEET_ALL_ROLE, + INTEGRATIONS_ALL_ROLE, + ROUTE_HANDLER_TIMEOUT, +} from '../constants'; import { getCelGraph } from '../graphs/cel'; import type { IntegrationAssistantRouteHandlerContext } from '../plugin'; import { getLLMClass, getLLMType } from '../util/llm'; @@ -34,9 +39,11 @@ export function registerCelInputRoutes(router: IRouter - - - ); - } else { - continue; - } - } - // Create jobId badges for jobs with no groups or with groups not selected - for (let i = 0; i < selectedIds.length; i++) { - const currentId = selectedIds[i]; - if (maps.groupsMap[currentId] === undefined) { - const jobGroups = maps.jobsMap[currentId] || []; - - if (jobGroups.some((g) => currentGroups.includes(g)) === false) { - badges.push( - - - - ); - } else { - continue; - } - } else { - continue; - } - } - - if (showAllBarBadges || badges.length <= limit) { - if (badges.length > limit) { - badges.push( - - - {i18n.translate('xpack.ml.jobSelector.hideBarBadges', { - defaultMessage: 'Hide', - })} - - - ); - } - - return <>{badges}; - } else { - const overFlow = badges.length - limit; - - badges.splice(limit); - badges.push( - - - {i18n.translate('xpack.ml.jobSelector.showBarBadges', { - defaultMessage: `And {overFlow} more`, - values: { overFlow }, - })} - - - ); - - return <>{badges}; - } -} -IdBadges.propTypes = { - limit: PropTypes.number, - maps: PropTypes.shape({ - jobsMap: PropTypes.object, - groupsMap: PropTypes.object, - }), - onLinkClick: PropTypes.func, - selectedIds: PropTypes.array, - showAllBarBadges: PropTypes.bool, -}; diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/id_badges/id_badges.test.js b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/id_badges/id_badges.test.tsx similarity index 78% rename from x-pack/platform/plugins/shared/ml/public/application/components/job_selector/id_badges/id_badges.test.js rename to x-pack/platform/plugins/shared/ml/public/application/components/job_selector/id_badges/id_badges.test.tsx index cd99398c578a3..424c3d8231863 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/id_badges/id_badges.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/id_badges/id_badges.test.tsx @@ -6,29 +6,28 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; // eslint-disable-line import/no-extraneous-dependencies +import { render } from '@testing-library/react'; +import type { IdBadgesProps } from './id_badges'; import { IdBadges } from './id_badges'; -const props = { +const props: IdBadgesProps = { limit: 2, - maps: { - groupsMap: { - group1: ['job1', 'job2'], - group2: ['job3'], + selectedGroups: [ + { + groupId: 'group1', + jobIds: ['job1', 'job2'], }, - jobsMap: { - job1: ['group1'], - job2: ['group1'], - job3: ['group2'], - job4: [], + { + groupId: 'group2', + jobIds: ['job3'], }, - }, + ], + selectedJobIds: ['job1', 'job2', 'job3'], onLinkClick: jest.fn(), - selectedIds: ['group1', 'job1', 'job3'], showAllBarBadges: false, }; -const overLimitProps = { ...props, selectedIds: ['group1', 'job1', 'job3', 'job4'] }; +const overLimitProps: IdBadgesProps = { ...props, selectedJobIds: ['job4'] }; describe('IdBadges', () => { test('When group selected renders groupId and not corresponding jobIds', () => { @@ -56,10 +55,16 @@ describe('IdBadges', () => { }); describe('showAllBarBadges is true', () => { - const overLimitShowAllProps = { + const overLimitShowAllProps: IdBadgesProps = { ...props, showAllBarBadges: true, - selectedIds: ['group1', 'job1', 'job3', 'job4'], + selectedGroups: [ + { + groupId: 'group1', + jobIds: ['job1', 'job2'], + }, + ], + selectedJobIds: ['job3', 'job4'], }; test('shows all badges when selection is over limit', () => { diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/id_badges/id_badges.tsx b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/id_badges/id_badges.tsx new file mode 100644 index 0000000000000..b03ece5aaac55 --- /dev/null +++ b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/id_badges/id_badges.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 React from 'react'; +import { EuiFlexItem, EuiLink, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { JobSelectorBadge } from '../job_selector_badge'; +import type { GroupObj } from '../job_selector'; + +export interface IdBadgesProps { + limit: number; + selectedGroups: GroupObj[]; + selectedJobIds: string[]; + onLinkClick: () => void; + showAllBarBadges: boolean; +} + +export function IdBadges({ + limit, + selectedGroups, + onLinkClick, + selectedJobIds, + showAllBarBadges, +}: IdBadgesProps) { + const badges = []; + + // Create group badges. Skip job ids here. + for (let i = 0; i < selectedGroups.length; i++) { + const currentGroup = selectedGroups[i]; + badges.push( + + + + ); + } + // Create badges for jobs with no groups + for (let i = 0; i < selectedJobIds.length; i++) { + const currentId = selectedJobIds[i]; + if (selectedGroups.some((g) => g.jobIds.includes(currentId))) { + continue; + } + badges.push( + + + + ); + } + + if (showAllBarBadges || badges.length <= limit) { + if (badges.length > limit) { + badges.push( + + + {i18n.translate('xpack.ml.jobSelector.hideBarBadges', { + defaultMessage: 'Hide', + })} + + + ); + } + + return <>{badges}; + } else { + const overFlow = badges.length - limit; + + badges.splice(limit); + badges.push( + + + {i18n.translate('xpack.ml.jobSelector.showBarBadges', { + defaultMessage: `And {overFlow} more`, + values: { overFlow }, + })} + + + ); + + return <>{badges}; + } +} diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/id_badges/index.js b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/id_badges/index.ts similarity index 100% rename from x-pack/platform/plugins/shared/ml/public/application/components/job_selector/id_badges/index.js rename to x-pack/platform/plugins/shared/ml/public/application/components/job_selector/id_badges/index.ts diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_select_service_utils.ts b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_select_service_utils.ts index 26110819fd1ed..47507a1e760a4 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_select_service_utils.ts +++ b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_select_service_utils.ts @@ -11,9 +11,10 @@ import d3 from 'd3'; import type { Dictionary } from '../../../../common/types/common'; import type { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; +import type { MlJobGroupWithTimeRange } from './job_selector_flyout'; export function getGroupsFromJobs(jobs: MlJobWithTimeRange[]) { - const groups: Dictionary = {}; + const groups: Dictionary = {}; const groupsMap: Dictionary = {}; jobs.forEach((job) => { @@ -86,7 +87,7 @@ export function getTimeRangeFromSelection(jobs: MlJobWithTimeRange[], selection: if (jobs.length > 0) { const times: number[] = []; jobs.forEach((job) => { - if (selection.includes(job.job_id)) { + if (selection.includes(job.job_id) || selection.some((s) => job.groups?.includes(s))) { if (job.timeRange.from !== undefined) { times.push(job.timeRange.from); } diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector.tsx b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector.tsx index 063aa303944be..848da20c66e65 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector.tsx @@ -16,7 +16,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useUrlState } from '@kbn/ml-url-state'; import './_index.scss'; import { useStorage } from '@kbn/ml-local-storage'; @@ -29,7 +28,7 @@ import type { MlJobWithTimeRange } from '../../../../common/types/anomaly_detect import { ML_APPLY_TIME_RANGE_CONFIG } from '../../../../common/types/storage'; import { FeedBackButton } from '../feedback_button'; -interface GroupObj { +export interface GroupObj { groupId: string; jobIds: string[]; } @@ -78,6 +77,15 @@ export interface JobSelectorProps { dateFormatTz: string; singleSelection: boolean; timeseriesOnly: boolean; + onSelectionChange?: ({ + jobIds, + time, + }: { + jobIds: string[]; + time?: { from: string; to: string }; + }) => void; + selectedJobIds?: string[]; + selectedGroups?: GroupObj[]; } export interface JobSelectionMaps { @@ -85,23 +93,23 @@ export interface JobSelectionMaps { groupsMap: Dictionary; } -export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: JobSelectorProps) { - const [globalState, setGlobalState] = useUrlState('_g'); +export function JobSelector({ + dateFormatTz, + singleSelection, + timeseriesOnly, + selectedJobIds = [], + selectedGroups = [], + onSelectionChange, +}: JobSelectorProps) { const [applyTimeRangeConfig, setApplyTimeRangeConfig] = useStorage( ML_APPLY_TIME_RANGE_CONFIG, true ); - const selectedJobIds = globalState?.ml?.jobIds ?? []; - const selectedGroups = globalState?.ml?.groups ?? []; - - const [maps, setMaps] = useState({ - groupsMap: getInitialGroupsMap(selectedGroups), - jobsMap: {}, - }); const [selectedIds, setSelectedIds] = useState( mergeSelection(selectedJobIds, selectedGroups, singleSelection) ); + const [showAllBarBadges, setShowAllBarBadges] = useState(false); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); @@ -124,20 +132,13 @@ export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: J } const applySelection: JobSelectorFlyoutProps['onSelectionConfirmed'] = useCallback( - ({ newSelection, jobIds, groups: newGroups, time }) => { + ({ newSelection, jobIds, time }) => { setSelectedIds(newSelection); - setGlobalState({ - ml: { - jobIds, - groups: newGroups, - }, - ...(time !== undefined ? { time } : {}), - }); - + onSelectionChange?.({ jobIds, time }); closeFlyout(); }, - [setGlobalState, setSelectedIds] + [onSelectionChange] ); function renderJobSelectionBar() { @@ -155,9 +156,9 @@ export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: J > setShowAllBarBadges(!showAllBarBadges)} - selectedIds={selectedIds} + selectedJobIds={selectedJobIds} + selectedGroups={selectedGroups} showAllBarBadges={showAllBarBadges} /> @@ -211,9 +212,7 @@ export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: J singleSelection={singleSelection} selectedIds={selectedIds} onSelectionConfirmed={applySelection} - onJobsFetched={setMaps} onFlyoutClose={closeFlyout} - maps={maps} applyTimeRangeConfig={applyTimeRangeConfig} onTimeRangeConfigChange={setApplyTimeRangeConfig} /> diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector_flyout.tsx b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector_flyout.tsx index 02fb52c120303..4684ef1e63b43 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector_flyout.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector_flyout.tsx @@ -29,7 +29,10 @@ import { getTimeRangeFromSelection, normalizeTimes, } from './job_select_service_utils'; -import type { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; +import type { + MlJobTimeRange, + MlJobWithTimeRange, +} from '../../../../common/types/anomaly_detection_jobs'; import { useMlKibana } from '../../contexts/kibana'; import type { JobSelectionMaps } from './job_selector'; @@ -39,7 +42,6 @@ export const DEFAULT_GANTT_BAR_WIDTH = 299; // pixels export interface JobSelectionResult { newSelection: string[]; jobIds: string[]; - groups: Array<{ groupId: string; jobIds: string[] }>; time: { from: string; to: string } | undefined; } @@ -52,12 +54,17 @@ export interface JobSelectorFlyoutProps { onSelectionConfirmed: (payload: JobSelectionResult) => void; singleSelection: boolean; timeseriesOnly: boolean; - maps: JobSelectionMaps; withTimeRangeSelector?: boolean; applyTimeRangeConfig?: boolean; onTimeRangeConfigChange?: (v: boolean) => void; } +export interface MlJobGroupWithTimeRange { + id: string; + jobIds: string[]; + timeRange: MlJobTimeRange; +} + export const JobSelectorFlyoutContent: FC = ({ dateFormatTz, selectedIds = [], @@ -66,7 +73,6 @@ export const JobSelectorFlyoutContent: FC = ({ onJobsFetched, onSelectionConfirmed, onFlyoutClose, - maps, applyTimeRangeConfig, onTimeRangeConfigChange, withTimeRangeSelector = true, @@ -83,42 +89,37 @@ export const JobSelectorFlyoutContent: FC = ({ const [isLoading, setIsLoading] = useState(true); const [showAllBadges, setShowAllBadges] = useState(false); const [jobs, setJobs] = useState([]); - const [groups, setGroups] = useState([]); + const [groups, setGroups] = useState([]); + const [ganttBarWidth, setGanttBarWidth] = useState(DEFAULT_GANTT_BAR_WIDTH); - const [jobGroupsMaps, setJobGroupsMaps] = useState(maps); const flyoutEl = useRef(null); const applySelection = useCallback(() => { - // allNewSelection will be a list of all job ids (including those from groups) selected from the table - const allNewSelection: string[] = []; - const groupSelection: Array<{ groupId: string; jobIds: string[] }> = []; + const selectedGroupIds = newSelection.filter((id) => groups.some((group) => group.id === id)); - newSelection.forEach((id) => { - if (jobGroupsMaps.groupsMap[id] !== undefined) { - // Push all jobs from selected groups into the newSelection list - allNewSelection.push(...jobGroupsMaps.groupsMap[id]); - // if it's a group - push group obj to set in global state - groupSelection.push({ groupId: id, jobIds: jobGroupsMaps.groupsMap[id] }); - } else { - allNewSelection.push(id); - } - }); - // create a Set to remove duplicate values - const allNewSelectionUnique = Array.from(new Set(allNewSelection)); + const jobsInSelectedGroups = [ + ...new Set( + groups + .filter((group) => selectedGroupIds.includes(group.id)) + .flatMap((group) => group.jobIds) + ), + ]; + + const standaloneJobs = newSelection.filter( + (id) => !selectedGroupIds.includes(id) && !jobsInSelectedGroups.includes(id) + ); - const time = applyTimeRangeConfig - ? getTimeRangeFromSelection(jobs, allNewSelectionUnique) - : undefined; + const finalSelection = [...selectedGroupIds, ...standaloneJobs]; + const time = applyTimeRangeConfig ? getTimeRangeFromSelection(jobs, finalSelection) : undefined; onSelectionConfirmed({ - newSelection: allNewSelectionUnique, - jobIds: allNewSelectionUnique, - groups: groupSelection, + newSelection: finalSelection, + jobIds: finalSelection, time, }); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [onSelectionConfirmed, newSelection, jobGroupsMaps, applyTimeRangeConfig]); + }, [onSelectionConfirmed, newSelection, applyTimeRangeConfig]); function removeId(id: string) { setNewSelection(newSelection.filter((item) => item !== id)); @@ -168,7 +169,6 @@ export const JobSelectorFlyoutContent: FC = ({ const { groups: groupsWithTimerange, groupsMap } = getGroupsFromJobs(normalizedJobs); setJobs(normalizedJobs); setGroups(groupsWithTimerange); - setJobGroupsMaps({ groupsMap, jobsMap: resp.jobsMap }); if (onJobsFetched) { onJobsFetched({ groupsMap, jobsMap: resp.jobsMap }); @@ -215,7 +215,7 @@ export const JobSelectorFlyoutContent: FC = ({ setShowAllBadges(!showAllBadges)} diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.js b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.tsx similarity index 76% rename from x-pack/platform/plugins/shared/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.js rename to x-pack/platform/plugins/shared/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.tsx index 4ff3e992c6e19..101c6f53d33fd 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.js +++ b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.tsx @@ -6,17 +6,40 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; // eslint-disable-line import/no-extraneous-dependencies +import { render } from '@testing-library/react'; +import type { NewSelectionIdBadgesProps } from './new_selection_id_badges'; import { NewSelectionIdBadges } from './new_selection_id_badges'; -const props = { +const props: NewSelectionIdBadgesProps = { limit: 2, - maps: { - groupsMap: { - group1: ['job1', 'job2'], - group2: ['job3'], + groups: [ + { + id: 'group1', + jobIds: ['job1', 'job2'], + timeRange: { + from: 0, + to: 0, + fromPx: 0, + toPx: 0, + fromMoment: null, + toMoment: null, + widthPx: 0, + }, }, - }, + { + id: 'group2', + jobIds: ['job3', 'job4'], + timeRange: { + from: 0, + to: 0, + fromPx: 0, + toPx: 0, + fromMoment: null, + toMoment: null, + widthPx: 0, + }, + }, + ], onLinkClick: jest.fn(), onDeleteClick: jest.fn(), newSelection: ['group1', 'job1', 'job3'], diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.tsx b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.tsx index 71db8bdbbf85a..06c53f427e39a 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.tsx @@ -10,24 +10,24 @@ import React from 'react'; import { EuiFlexItem, EuiLink, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { JobSelectorBadge } from '../job_selector_badge'; -import type { JobSelectionMaps } from '../job_selector'; +import type { MlJobGroupWithTimeRange } from '../job_selector_flyout'; -interface NewSelectionIdBadgesProps { +export interface NewSelectionIdBadgesProps { limit: number; - maps: JobSelectionMaps; newSelection: string[]; onDeleteClick?: Function; onLinkClick?: MouseEventHandler; showAllBadges?: boolean; + groups: MlJobGroupWithTimeRange[]; } export const NewSelectionIdBadges: FC = ({ limit, - maps, newSelection, onDeleteClick, onLinkClick, showAllBadges, + groups, }) => { const badges = []; @@ -41,7 +41,7 @@ export const NewSelectionIdBadges: FC = ({ g.id === newSelection[i])} removeId={onDeleteClick} /> diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/use_job_selection.ts b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/use_job_selection.ts index 51d1882084d3e..87914c25f944b 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/use_job_selection.ts +++ b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/use_job_selection.ts @@ -5,43 +5,13 @@ * 2.0. */ -import { difference } from 'lodash'; import { useEffect, useMemo } from 'react'; - +import useObservable from 'react-use/lib/useObservable'; import { i18n } from '@kbn/i18n'; -import { useUrlState } from '@kbn/ml-url-state'; - import type { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; - import { useNotifications } from '../../contexts/kibana'; import { useJobSelectionFlyout } from '../../contexts/ml/use_job_selection_flyout'; - -// check that the ids read from the url exist by comparing them to the -// jobs loaded via mlJobsService. -function getInvalidJobIds(jobs: MlJobWithTimeRange[], ids: string[]) { - return ids.filter((id) => { - const jobExists = jobs.some((job) => job.job_id === id); - return jobExists === false && id !== '*'; - }); -} - -// This is useful when redirecting from dashboards where groupIds are treated as jobIds -const getJobIdsFromGroups = (jobIds: string[], jobs: MlJobWithTimeRange[]) => { - const result = new Set(); - - jobIds.forEach((id) => { - const jobsInGroup = jobs.filter((job) => job.groups?.includes(id)); - - if (jobsInGroup.length > 0) { - jobsInGroup.forEach((job) => result.add(job.job_id)); - } else { - // If it's not a group ID, keep it (regardless of whether it's valid or not) - result.add(id); - } - }); - - return Array.from(result); -}; +import { useAnomalyExplorerContext } from '../../explorer/anomaly_explorer_context'; export interface JobSelection { jobIds: string[]; @@ -49,67 +19,52 @@ export interface JobSelection { } export const useJobSelection = (jobs: MlJobWithTimeRange[]) => { - const [globalState, setGlobalState] = useUrlState('_g'); const { toasts: toastNotifications } = useNotifications(); + const { anomalyExplorerCommonStateService } = useAnomalyExplorerContext(); - const getJobSelection = useJobSelectionFlyout(); + const selectedJobs = useObservable( + anomalyExplorerCommonStateService.selectedJobs$, + anomalyExplorerCommonStateService.selectedJobs + ); + const invalidJobIds = useObservable( + anomalyExplorerCommonStateService.invalidJobIds$, + anomalyExplorerCommonStateService.invalidJobIds + ); - const tmpIds = useMemo(() => { - const ids = getJobIdsFromGroups(globalState?.ml?.jobIds || [], jobs); - return (typeof ids === 'string' ? [ids] : ids).map((id: string) => String(id)); - }, [globalState?.ml?.jobIds, jobs]); - - const invalidIds = useMemo(() => { - return getInvalidJobIds(jobs, tmpIds); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [tmpIds]); - - const validIds = useMemo(() => { - const res = difference(tmpIds, invalidIds); - res.sort(); - return res; - }, [tmpIds, invalidIds]); - - const jobSelection: JobSelection = useMemo(() => { - const selectedGroups = globalState?.ml?.groups ?? []; - return { jobIds: validIds, selectedGroups }; - }, [validIds, globalState?.ml?.groups]); + const getJobSelection = useJobSelectionFlyout(); + const selectedIds = useMemo(() => { + return selectedJobs?.map((j) => j.id); + }, [selectedJobs]); useEffect(() => { - if (invalidIds.length > 0) { + if (invalidJobIds.length > 0) { toastNotifications.addWarning( i18n.translate('xpack.ml.jobSelect.requestedJobsDoesNotExistWarningMessage', { defaultMessage: `Requested -{invalidIdsLength, plural, one {job {invalidIds} does not exist} other {jobs {invalidIds} do not exist}}`, + {invalidIdsLength, plural, one {job {invalidIds} does not exist} other {jobs {invalidIds} do not exist}}`, values: { - invalidIdsLength: invalidIds.length, - invalidIds: invalidIds.join(), + invalidIdsLength: invalidJobIds.length, + invalidIds: invalidJobIds.join(), }, }) ); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [invalidIds]); + }, [invalidJobIds]); useEffect(() => { // if there are no valid ids, ask the user to provide job selection with the flyout - if (validIds.length === 0 && jobs.length > 0) { + if (!selectedIds || (selectedIds.length === 0 && jobs.length > 0)) { getJobSelection({ singleSelection: false }) .then(({ jobIds, time }) => { - const mlGlobalState = globalState?.ml || {}; - mlGlobalState.jobIds = jobIds; - - setGlobalState({ - ...{ ml: mlGlobalState }, - ...(time !== undefined ? { time } : {}), - }); + anomalyExplorerCommonStateService.setSelectedJobs(jobIds, time); }) .catch(() => { // flyout closed without selection }); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [jobs, validIds, setGlobalState, globalState?.ml]); + }, [jobs]); - return jobSelection; + return { selectedIds, selectedJobs }; }; diff --git a/x-pack/platform/plugins/shared/ml/public/application/contexts/ml/use_job_selection_flyout.tsx b/x-pack/platform/plugins/shared/ml/public/application/contexts/ml/use_job_selection_flyout.tsx index a95c9ad41fd42..fd19689d68b5e 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/contexts/ml/use_job_selection_flyout.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/contexts/ml/use_job_selection_flyout.tsx @@ -10,9 +10,10 @@ import moment from 'moment'; import type { KibanaReactOverlays } from '@kbn/kibana-react-plugin/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { useMlKibana } from '../kibana'; -import { JobSelectorFlyout } from '../../../embeddables/common/components/job_selector_flyout'; -import { getInitialGroupsMap } from '../../components/job_selector/job_selector'; -import type { JobSelectionResult } from '../../components/job_selector/job_selector_flyout'; +import { + JobSelectorFlyoutContent, + type JobSelectionResult, +} from '../../components/job_selector/job_selector_flyout'; export type GetJobSelection = ReturnType; @@ -49,16 +50,12 @@ export function useJobSelectionFlyout() { const tzConfig = uiSettings.get('dateFormat:tz'); const dateFormatTz = tzConfig !== 'Browser' ? tzConfig : moment.tz.guess(); - const maps = { - groupsMap: getInitialGroupsMap([]), - jobsMap: {}, - }; return new Promise(async (resolve, reject) => { try { flyoutRef.current = overlays.openFlyout( - ); diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/actions/index.ts b/x-pack/platform/plugins/shared/ml/public/application/explorer/actions/index.ts index 2a7b30d4ed6d8..12185d2799174 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/actions/index.ts +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/actions/index.ts @@ -5,5 +5,4 @@ * 2.0. */ -export { jobSelectionActionCreator } from './job_selection'; export { useExplorerData } from './load_explorer_data'; diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/actions/job_selection.ts b/x-pack/platform/plugins/shared/ml/public/application/explorer/actions/job_selection.ts deleted file mode 100644 index bd6bcd6e95657..0000000000000 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/actions/job_selection.ts +++ /dev/null @@ -1,48 +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. - */ - -import type { Observable } from 'rxjs'; -import { from } from 'rxjs'; -import { map } from 'rxjs'; - -import type { MlFieldFormatService } from '../../services/field_format_service'; -import type { MlJobService } from '../../services/job_service'; - -import { EXPLORER_ACTION } from '../explorer_constants'; -import { createJobs, getInfluencers } from '../explorer_utils'; -import type { ExplorerActions } from '../explorer_dashboard_service'; - -export function jobSelectionActionCreator( - mlJobService: MlJobService, - mlFieldFormatService: MlFieldFormatService, - selectedJobIds: string[] -): Observable { - return from(mlFieldFormatService.populateFormats(selectedJobIds)).pipe( - map((resp) => { - if (resp.error) { - return null; - } - - const jobs = createJobs(mlJobService.jobs).map((job) => { - job.selected = selectedJobIds.some((id) => job.id === id); - return job; - }); - - const selectedJobs = jobs.filter((job) => job.selected); - const noInfluencersConfigured = getInfluencers(mlJobService, selectedJobs).length === 0; - - return { - type: EXPLORER_ACTION.JOB_SELECTION_CHANGE, - payload: { - loading: false, - selectedJobs, - noInfluencersConfigured, - }, - }; - }) - ); -} diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/actions/load_explorer_data.ts b/x-pack/platform/plugins/shared/ml/public/application/explorer/actions/load_explorer_data.ts index 19bf333e0d2bd..6035b0327740f 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/actions/load_explorer_data.ts +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/actions/load_explorer_data.ts @@ -31,7 +31,7 @@ import { loadTopInfluencers, loadOverallAnnotations, } from '../explorer_utils'; -import type { ExplorerState } from '../reducers'; + import { useMlApi, useUiSettings } from '../../contexts/kibana'; import type { MlResultsService } from '../../services/results_service'; import { mlResultsServiceProvider } from '../../services/results_service'; @@ -39,6 +39,7 @@ import type { AnomalyExplorerChartsService } from '../../services/anomaly_explor import { useAnomalyExplorerContext } from '../anomaly_explorer_context'; import type { MlApi } from '../../services/ml_api_service'; import { useMlJobService, type MlJobService } from '../../services/job_service'; +import type { ExplorerState } from '../explorer_data'; // Memoize the data fetching methods. // wrapWithLastRefreshArg() wraps any given function and preprends a `lastRefresh` argument diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_charts_state_service.ts b/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_charts_state_service.ts index 05c6bda4057d4..9944dd2d6591f 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_charts_state_service.ts +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_charts_state_service.ts @@ -8,7 +8,7 @@ import type { Observable } from 'rxjs'; import { BehaviorSubject, combineLatest, of, Subscription } from 'rxjs'; import { distinctUntilChanged, map, skipWhile, switchMap } from 'rxjs'; -import type { PageUrlStateService } from '@kbn/ml-url-state'; +import type { UrlStateService } from '@kbn/ml-url-state'; import { StateService } from '../services/state_service'; import type { AnomalyExplorerCommonStateService } from './anomaly_explorer_common_state'; import type { AnomalyTimelineStateService } from './anomaly_timeline_state_service'; @@ -29,7 +29,7 @@ export class AnomalyChartsStateService extends StateService { private _anomalyTimelineStateServices: AnomalyTimelineStateService, private _anomalyExplorerChartsService: AnomalyExplorerChartsService, private _anomalyExplorerUrlStateService: AnomalyExplorerUrlStateService, - private _tableSeverityState: PageUrlStateService + private _tableSeverityState: UrlStateService ) { super(); this._init(); @@ -40,7 +40,7 @@ export class AnomalyChartsStateService extends StateService { subscription.add( this._anomalyExplorerUrlStateService - .getPageUrlState$() + .getUrlState$() .pipe( map((urlState) => urlState?.mlShowCharts ?? true), distinctUntilChanged() @@ -55,12 +55,12 @@ export class AnomalyChartsStateService extends StateService { private initChartDataSubscription() { return combineLatest([ - this._anomalyExplorerCommonStateService.getSelectedJobs$(), - this._anomalyExplorerCommonStateService.getInfluencerFilterQuery$(), + this._anomalyExplorerCommonStateService.selectedJobs$, + this._anomalyExplorerCommonStateService.influencerFilterQuery$, this._anomalyTimelineStateServices.getContainerWidth$().pipe(skipWhile((v) => v === 0)), this._anomalyTimelineStateServices.getSelectedCells$(), this._anomalyTimelineStateServices.getViewBySwimlaneFieldName$(), - this._tableSeverityState.getPageUrlState$(), + this._tableSeverityState.getUrlState$(), ]) .pipe( switchMap( diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_context_menu.tsx b/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_context_menu.tsx index b27f8efe4fcc6..7904a55264d08 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_context_menu.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_context_menu.tsx @@ -59,6 +59,7 @@ interface AnomalyContextMenuProps { bounds?: TimeRangeBounds; interval?: number; chartsCount: number; + mergedGroupsAndJobsIds: string[]; } const SavedObjectSaveModalDashboard = withSuspense(LazySavedObjectSaveModalDashboard); @@ -76,6 +77,7 @@ export const AnomalyContextMenu: FC = ({ bounds, interval, chartsCount, + mergedGroupsAndJobsIds, }) => { const { services: { @@ -104,8 +106,8 @@ export const AnomalyContextMenu: FC = ({ const { anomalyExplorerCommonStateService, chartsStateService } = useAnomalyExplorerContext(); const { queryString } = useObservable( - anomalyExplorerCommonStateService.getFilterSettings$(), - anomalyExplorerCommonStateService.getFilterSettings() + anomalyExplorerCommonStateService.filterSettings$, + anomalyExplorerCommonStateService.filterSettings ); const chartsData = useObservable( @@ -137,8 +139,6 @@ export const AnomalyContextMenu: FC = ({ maxSeriesToPlot >= 1 && maxSeriesToPlot <= MAX_ANOMALY_CHARTS_ALLOWED; - const jobIds = selectedJobs.map(({ id }) => id); - const getEmbeddableInput = useCallback( (timeRange?: TimeRange) => { // Respect the query and the influencers selected @@ -151,7 +151,8 @@ export const AnomalyContextMenu: FC = ({ ); const influencers = selectionInfluencers ?? []; - const config = getDefaultEmbeddablePanelConfig(jobIds, queryString); + const config = getDefaultEmbeddablePanelConfig(mergedGroupsAndJobsIds, queryString); + const queryFromSelectedCells = influencers .map((s) => escapeKueryForEmbeddableFieldValuePair(s.fieldName, s.fieldValue)) .join(' or '); @@ -161,7 +162,7 @@ export const AnomalyContextMenu: FC = ({ return { ...config, ...(timeRange ? { timeRange } : {}), - jobIds, + jobIds: mergedGroupsAndJobsIds, maxSeriesToPlot: maxSeriesToPlot ?? DEFAULT_MAX_SERIES_TO_PLOT, severityThreshold: severity.val, ...((isDefined(queryString) && queryString !== '') || @@ -175,7 +176,7 @@ export const AnomalyContextMenu: FC = ({ : {}), }; }, - [jobIds, maxSeriesToPlot, severity, queryString, selectedCells] + [selectedCells, mergedGroupsAndJobsIds, queryString, maxSeriesToPlot, severity.val] ); const onSaveCallback: SaveModalDashboardProps['onSave'] = useCallback( @@ -350,7 +351,7 @@ export const AnomalyContextMenu: FC = ({ defaultMessage: 'Anomaly charts', })} documentInfo={{ - title: getDefaultExplorerChartsPanelTitle(selectedJobs.map(({ id }) => id)), + title: getDefaultExplorerChartsPanelTitle(mergedGroupsAndJobsIds), }} onClose={setIsAddDashboardActive.bind(null, false)} onSave={onSaveCallback} diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_explorer_common_state.ts b/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_explorer_common_state.ts index 800b3b2bc5eca..ebd3c9f4ded6d 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_explorer_common_state.ts +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_explorer_common_state.ts @@ -5,16 +5,20 @@ * 2.0. */ -import type { Observable, Subscription } from 'rxjs'; +import type { Observable } from 'rxjs'; +import { Subscription } from 'rxjs'; import { BehaviorSubject } from 'rxjs'; import { distinctUntilChanged, map, shareReplay, filter } from 'rxjs'; import { isEqual } from 'lodash'; import type { InfluencersFilterQuery } from '@kbn/ml-anomaly-utils'; -import type { ExplorerJob } from './explorer_utils'; +import type { GlobalState, UrlStateService } from '@kbn/ml-url-state/src/url_state'; +import { createJobs, type ExplorerJob } from './explorer_utils'; import type { AnomalyExplorerUrlStateService } from './hooks/use_explorer_url_state'; import type { AnomalyExplorerFilterUrlState } from '../../../common/types/locator'; import type { KQLFilterSettings } from './components/explorer_query_bar/explorer_query_bar'; import { StateService } from '../services/state_service'; +import type { MlJobService } from '../services/job_service'; +import type { GroupObj } from '../components/job_selector/job_selector'; export interface AnomalyExplorerState { selectedJobs: ExplorerJob[]; @@ -30,8 +34,10 @@ export type FilterSettings = Required< * Manages related values in the URL state and applies required formatting. */ export class AnomalyExplorerCommonStateService extends StateService { - private _selectedJobs$ = new BehaviorSubject(undefined); + private _selectedJobs$ = new BehaviorSubject([]); + private _selectedGroups$ = new BehaviorSubject([]); private _filterSettings$ = new BehaviorSubject(this._getDefaultFilterSettings()); + private _invalidJobIds$ = new BehaviorSubject([]); private _getDefaultFilterSettings(): FilterSettings { return { @@ -42,65 +48,135 @@ export class AnomalyExplorerCommonStateService extends StateService { }; } - constructor(private anomalyExplorerUrlStateService: AnomalyExplorerUrlStateService) { + constructor( + private anomalyExplorerUrlStateService: AnomalyExplorerUrlStateService, + private globalUrlStateService: UrlStateService, + private mlJobsService: MlJobService + ) { super(); this._init(); } - protected _initSubscriptions(): Subscription { - return this.anomalyExplorerUrlStateService - .getPageUrlState$() - .pipe( - map((urlState) => urlState?.mlExplorerFilter), - distinctUntilChanged(isEqual) - ) - .subscribe((v) => { - const result = { - ...this._getDefaultFilterSettings(), - ...v, - }; - this._filterSettings$.next(result); - }); - } - - public setSelectedJobs(explorerJobs: ExplorerJob[] | undefined) { - this._selectedJobs$.next(explorerJobs); - } + public readonly selectedGroups$: Observable = this._selectedGroups$.pipe( + distinctUntilChanged(isEqual), + shareReplay(1) + ); - public getSelectedJobs$(): Observable { - return this._selectedJobs$.pipe( - filter((v) => Array.isArray(v) && v.length > 0), - distinctUntilChanged(isEqual), - shareReplay(1) - ); - } + public readonly invalidJobIds$: Observable = this._invalidJobIds$.pipe( + distinctUntilChanged(isEqual), + shareReplay(1) + ); - private readonly _smvJobs$ = this.getSelectedJobs$().pipe( - map((jobs) => jobs.filter((j) => j.isSingleMetricViewerJob)), + public readonly selectedJobs$: Observable = this._selectedJobs$.pipe( + filter((v) => Array.isArray(v) && v.length > 0), + distinctUntilChanged(isEqual), shareReplay(1) ); - public getSingleMetricJobs$(): Observable { - return this._smvJobs$; + public readonly influencerFilterQuery$: Observable = + this._filterSettings$.pipe( + map((v) => v?.influencersFilterQuery), + distinctUntilChanged(isEqual) + ); + + public readonly filterSettings$ = this._filterSettings$.asObservable(); + + public get selectedGroups(): GroupObj[] { + return this._selectedGroups$.getValue(); } - public getSelectedJobs(): ExplorerJob[] | undefined { + public get invalidJobIds(): string[] { + return this._invalidJobIds$.getValue(); + } + + public get selectedJobs(): ExplorerJob[] { return this._selectedJobs$.getValue(); } - public getInfluencerFilterQuery$(): Observable { - return this._filterSettings$.pipe( - map((v) => v?.influencersFilterQuery), - distinctUntilChanged(isEqual) + public get filterSettings(): FilterSettings { + return this._filterSettings$.getValue(); + } + + protected _initSubscriptions(): Subscription { + const subscriptions = new Subscription(); + + subscriptions.add( + this.anomalyExplorerUrlStateService + .getUrlState$() + .pipe( + map((urlState) => urlState?.mlExplorerFilter), + distinctUntilChanged(isEqual) + ) + .subscribe((v) => { + const result = { + ...this._getDefaultFilterSettings(), + ...v, + }; + this._filterSettings$.next(result); + }) + ); + + subscriptions.add( + this.globalUrlStateService + .getUrlState$() + .pipe( + map((urlState) => urlState?.ml?.jobIds), + distinctUntilChanged(isEqual) + ) + .subscribe((selectedJobIds: string[]) => { + this._processSelectedJobs(selectedJobIds); + }) ); + + return subscriptions; } - public getFilterSettings$(): Observable { - return this._filterSettings$.asObservable(); + private _processSelectedJobs(selectedJobIds: string[]) { + if (!selectedJobIds || selectedJobIds.length === 0) { + this._selectedJobs$.next([]); + this._invalidJobIds$.next([]); + this._selectedGroups$.next([]); + return; + } + // TODO: We are using mlJobService jobs, which has stale data. + + const groupIds = selectedJobIds.filter((id) => + this.mlJobsService.jobs.some((job) => job.groups?.includes(id)) + ); + + const selectedGroups = groupIds.map((groupId) => ({ + groupId, + jobIds: this.mlJobsService.jobs + .filter((job) => job.groups?.includes(groupId)) + .map((job) => job.job_id), + })); + + const selectedJobs = this.mlJobsService.jobs.filter( + (j) => selectedJobIds.includes(j.job_id) || j.groups?.some((g) => groupIds.includes(g)) + ); + + const mappedJobs = createJobs(selectedJobs); + + const invalidJobIds = this._getInvalidJobIds(selectedJobIds); + + this._invalidJobIds$.next(invalidJobIds); + this._selectedJobs$.next(mappedJobs); + this._selectedGroups$.next(selectedGroups); } - public getFilterSettings(): FilterSettings { - return this._filterSettings$.getValue(); + private _getInvalidJobIds(jobIds: string[]): string[] { + return jobIds.filter( + (id) => !this.mlJobsService.jobs.some((j) => j.job_id === id || j.groups?.includes(id)) + ); + } + + public setSelectedJobs(jobIds: string[], time?: { from: string; to: string }) { + this.globalUrlStateService.updateUrlState({ + ml: { + jobIds, + }, + ...(time ? { time } : {}), + }); } public setFilterSettings(update: KQLFilterSettings) { diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_explorer_context.tsx b/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_explorer_context.tsx index 2c1fb6dc8c182..cb29924d2373c 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_explorer_context.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_explorer_context.tsx @@ -8,6 +8,7 @@ import type { PropsWithChildren } from 'react'; import React, { useContext, useEffect, useMemo, useState, type FC } from 'react'; import { useTimefilter } from '@kbn/ml-date-picker'; +import { useGlobalUrlState } from '@kbn/ml-url-state/src/url_state'; import { AnomalyTimelineStateService } from './anomaly_timeline_state_service'; import { AnomalyExplorerCommonStateService } from './anomaly_explorer_common_state'; import { useMlKibana } from '../contexts/kibana'; @@ -18,7 +19,6 @@ import { AnomalyChartsStateService } from './anomaly_charts_state_service'; import { AnomalyExplorerChartsService } from '../services/anomaly_explorer_charts_service'; import { useTableSeverity } from '../components/controls/select_severity'; import { AnomalyDetectionAlertsStateService } from './alerts'; -import { explorerServiceFactory, type ExplorerService } from './explorer_dashboard_service'; import { useMlJobService } from '../services/job_service'; export interface AnomalyExplorerContextValue { @@ -28,7 +28,6 @@ export interface AnomalyExplorerContextValue { anomalyTimelineStateService: AnomalyTimelineStateService; chartsStateService: AnomalyChartsStateService; anomalyDetectionAlertsStateService: AnomalyDetectionAlertsStateService; - explorerService: ExplorerService; } /** @@ -57,11 +56,12 @@ export function useAnomalyExplorerContext() { export const AnomalyExplorerContextProvider: FC> = ({ children }) => { const [, , anomalyExplorerUrlStateService] = useExplorerUrlState(); + const [, , globalUrlStateService] = useGlobalUrlState(); const timefilter = useTimefilter(); const { services: { - mlServices: { mlApi, mlFieldFormatService }, + mlServices: { mlApi }, uiSettings, data, }, @@ -82,8 +82,6 @@ export const AnomalyExplorerContextProvider: FC> = ({ // updates so using `useEffect` is the right thing to do here to not get errors // related to React lifecycle methods. useEffect(() => { - const explorerService = explorerServiceFactory(mlJobService, mlFieldFormatService); - const anomalyTimelineService = new AnomalyTimelineService( timefilter, uiSettings, @@ -91,7 +89,9 @@ export const AnomalyExplorerContextProvider: FC> = ({ ); const anomalyExplorerCommonStateService = new AnomalyExplorerCommonStateService( - anomalyExplorerUrlStateService + anomalyExplorerUrlStateService, + globalUrlStateService, + mlJobService ); const anomalyTimelineStateService = new AnomalyTimelineStateService( @@ -129,7 +129,6 @@ export const AnomalyExplorerContextProvider: FC> = ({ anomalyTimelineStateService, chartsStateService, anomalyDetectionAlertsStateService, - explorerService, }); return () => { diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_timeline.tsx b/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_timeline.tsx index b64afb345f5bc..cad2ef9376890 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_timeline.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_timeline.tsx @@ -49,13 +49,12 @@ import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '../..'; import type { SwimlaneType } from './explorer_constants'; import { OVERALL_LABEL, SWIMLANE_TYPE, VIEW_BY_JOB_LABEL } from './explorer_constants'; import { useMlKibana } from '../contexts/kibana'; -import type { ExplorerState } from './reducers/explorer_reducer'; import { ExplorerNoInfluencersFound } from './components/explorer_no_influencers_found'; import { SwimlaneContainer } from './swimlane_container'; -import type { - AppStateSelectedCells, - OverallSwimlaneData, - ViewBySwimLaneData, +import { + type AppStateSelectedCells, + type OverallSwimlaneData, + type ViewBySwimLaneData, } from './explorer_utils'; import { NoOverallData } from './components/no_overall_data'; import { SeverityControl } from '../components/severity_control'; @@ -67,6 +66,8 @@ import { useAnomalyExplorerContext } from './anomaly_explorer_context'; import { getTimeBoundsFromSelection } from './hooks/use_selected_cells'; import { SwimLaneWrapper } from './alerts'; import { Y_AXIS_LABEL_WIDTH } from './constants'; +import type { ExplorerState } from './explorer_data'; +import { useJobSelection } from './hooks/use_job_selection'; function mapSwimlaneOptionsToEuiOptions(options: string[]) { return options.map((option) => ({ @@ -120,16 +121,13 @@ export const AnomalyTimeline: FC = React.memo( const { overallAnnotations } = explorerState; const { filterActive, queryString } = useObservable( - anomalyExplorerCommonStateService.getFilterSettings$(), - anomalyExplorerCommonStateService.getFilterSettings() + anomalyExplorerCommonStateService.filterSettings$, + anomalyExplorerCommonStateService.filterSettings ); const swimlaneLimit = useObservable(anomalyTimelineStateService.getSwimLaneCardinality$()); - const selectedJobs = useObservable( - anomalyExplorerCommonStateService.getSelectedJobs$(), - anomalyExplorerCommonStateService.getSelectedJobs() - ); + const { selectedJobs, mergedGroupsAndJobsIds } = useJobSelection(); const loading = useObservable(anomalyTimelineStateService.isOverallSwimLaneLoading$(), true); @@ -196,6 +194,7 @@ export const AnomalyTimeline: FC = React.memo( openCasesModalCallback({ swimlaneType: swimLaneType, ...(swimLaneType === SWIMLANE_TYPE.VIEW_BY ? { viewBy: viewBySwimlaneFieldName } : {}), + // For cases attachment, pass just the job IDs to maintain stale data jobIds: selectedJobs?.map((v) => v.id), timeRange: globalTimeRange, ...(isDefined(queryString) && queryString !== '' @@ -359,15 +358,13 @@ export const AnomalyTimeline: FC = React.memo( const stateTransfer = embeddable!.getStateTransfer(); - const jobIds = selectedJobs.map((j) => j.id); - - const config = getDefaultEmbeddablePanelConfig(jobIds, queryString); + const config = getDefaultEmbeddablePanelConfig(mergedGroupsAndJobsIds, queryString); const embeddableInput: Partial = { id: config.id, title: newTitle, description: newDescription, - jobIds, + jobIds: mergedGroupsAndJobsIds, swimlaneType: selectedSwimlane, ...(selectedSwimlane === SWIMLANE_TYPE.VIEW_BY ? { viewBy: viewBySwimlaneFieldName } @@ -389,7 +386,14 @@ export const AnomalyTimeline: FC = React.memo( path, }); }, - [embeddable, queryString, selectedJobs, selectedSwimlane, viewBySwimlaneFieldName] + [ + embeddable, + mergedGroupsAndJobsIds, + queryString, + selectedJobs, + selectedSwimlane, + viewBySwimlaneFieldName, + ] ); return ( @@ -621,7 +625,7 @@ export const AnomalyTimeline: FC = React.memo( defaultMessage: 'Anomaly swim lane', })} documentInfo={{ - title: getDefaultSwimlanePanelTitle(selectedJobs.map(({ id }) => id)), + title: getDefaultSwimlanePanelTitle(mergedGroupsAndJobsIds), }} onClose={() => { setSelectedSwimlane(undefined); diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_timeline_state_service.ts b/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_timeline_state_service.ts index 371284d0ac047..0da777072052f 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_timeline_state_service.ts +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/anomaly_timeline_state_service.ts @@ -124,7 +124,7 @@ export class AnomalyTimelineStateService extends StateService { update: AnomalyExplorerSwimLaneUrlState, replaceState?: boolean ) => { - const explorerUrlState = this.anomalyExplorerUrlStateService.getPageUrlState(); + const explorerUrlState = this.anomalyExplorerUrlStateService.getUrlState(); const mlExplorerSwimLaneState = explorerUrlState?.mlExplorerSwimlane; const resultUpdate = replaceState ? update : { ...mlExplorerSwimLaneState, ...update }; return this.anomalyExplorerUrlStateService.updateUrlState({ @@ -145,7 +145,7 @@ export class AnomalyTimelineStateService extends StateService { subscription.add( this.anomalyExplorerUrlStateService - .getPageUrlState$() + .getUrlState$() .pipe( map((v) => v?.mlExplorerSwimlane), distinctUntilChanged(isEqual) @@ -171,7 +171,7 @@ export class AnomalyTimelineStateService extends StateService { subscription.add( combineLatest([ - this.anomalyExplorerCommonStateService.getSelectedJobs$(), + this.anomalyExplorerCommonStateService.selectedJobs$, this.getContainerWidth$(), this._timeBounds$, ]).subscribe(([selectedJobs, containerWidth]) => { @@ -192,8 +192,8 @@ export class AnomalyTimelineStateService extends StateService { map((v) => v?.viewByFieldName), distinctUntilChanged() ), - this.anomalyExplorerCommonStateService.getSelectedJobs$(), - this.anomalyExplorerCommonStateService.getFilterSettings$(), + this.anomalyExplorerCommonStateService.selectedJobs$, + this.anomalyExplorerCommonStateService.filterSettings$, this._selectedCells$, ]).subscribe(([currentlySelected, selectedJobs, filterSettings, selectedCells]) => { const { viewBySwimlaneFieldName, viewBySwimlaneOptions } = this._getViewBySwimlaneOptions( @@ -220,7 +220,7 @@ export class AnomalyTimelineStateService extends StateService { }), distinctUntilChanged(isEqual) ), - this.anomalyExplorerCommonStateService.getInfluencerFilterQuery$(), + this.anomalyExplorerCommonStateService.influencerFilterQuery$, this._timeBounds$, ]).subscribe(([pagination, influencersFilerQuery]) => { let resultPaginaiton: SwimLanePagination = pagination; @@ -233,7 +233,7 @@ export class AnomalyTimelineStateService extends StateService { private _initOverallSwimLaneData() { return combineLatest([ - this.anomalyExplorerCommonStateService.getSelectedJobs$(), + this.anomalyExplorerCommonStateService.selectedJobs$, this._swimLaneSeverity$, this.getSwimLaneBucketInterval$(), this._timeBounds$, @@ -263,8 +263,8 @@ export class AnomalyTimelineStateService extends StateService { private _initTopFieldValues() { return ( combineLatest([ - this.anomalyExplorerCommonStateService.getSelectedJobs$(), - this.anomalyExplorerCommonStateService.getInfluencerFilterQuery$(), + this.anomalyExplorerCommonStateService.selectedJobs$, + this.anomalyExplorerCommonStateService.influencerFilterQuery$, this.getViewBySwimlaneFieldName$(), this.getSwimLanePagination$(), this.getSwimLaneCardinality$(), @@ -331,8 +331,8 @@ export class AnomalyTimelineStateService extends StateService { private _initViewBySwimLaneData() { return combineLatest([ this._overallSwimLaneData$.pipe(skipWhile((v) => !v)), - this.anomalyExplorerCommonStateService.getSelectedJobs$(), - this.anomalyExplorerCommonStateService.getInfluencerFilterQuery$(), + this.anomalyExplorerCommonStateService.selectedJobs$, + this.anomalyExplorerCommonStateService.influencerFilterQuery$, this._swimLaneSeverity$, this.getSwimLaneBucketInterval$(), this.getViewBySwimlaneFieldName$(), @@ -671,7 +671,7 @@ export class AnomalyTimelineStateService extends StateService { */ public getSwimLaneJobs$(): Observable { return combineLatest([ - this.anomalyExplorerCommonStateService.getSelectedJobs$(), + this.anomalyExplorerCommonStateService.selectedJobs$, this.getViewBySwimlaneFieldName$(), this._viewBySwimLaneData$, this._selectedCells$, diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer.tsx b/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer.tsx index ba95fc6671bcb..0ffb38f3631d1 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer.tsx @@ -69,16 +69,16 @@ import { FILTER_ACTION } from './explorer_constants'; // Anomalies Table // @ts-ignore import { AnomaliesTable } from '../components/anomalies_table/anomalies_table'; -import { ANOMALY_DETECTION_DEFAULT_TIME_RANGE } from '../../../common/constants/settings'; import { AnomalyContextMenu } from './anomaly_context_menu'; import type { JobSelectorProps } from '../components/job_selector/job_selector'; -import type { ExplorerState } from './reducers'; import { useToastNotificationService } from '../services/toast_notification_service'; import { useMlKibana, useMlLocator } from '../contexts/kibana'; import { useAnomalyExplorerContext } from './anomaly_explorer_context'; import { ML_ANOMALY_EXPLORER_PANELS } from '../../../common/types/storage'; import { AlertsPanel } from './alerts'; import { useMlIndexUtils } from '../util/index_service'; +import type { ExplorerState } from './explorer_data'; +import { useJobSelection } from './hooks/use_job_selection'; const AnnotationFlyout = dynamic(async () => ({ default: (await import('../components/annotations/annotation_flyout')).AnnotationFlyout, @@ -94,8 +94,6 @@ const ExplorerChartsContainer = dynamic(async () => ({ interface ExplorerPageProps { jobSelectorProps: JobSelectorProps; - noInfluencersConfigured?: boolean; - influencers?: ExplorerState['influencers']; filterActive?: boolean; filterPlaceHolder?: string; indexPattern?: DataView; @@ -107,8 +105,6 @@ interface ExplorerPageProps { const ExplorerPage: FC> = ({ children, jobSelectorProps, - noInfluencersConfigured, - influencers, filterActive, filterPlaceHolder, indexPattern, @@ -147,7 +143,6 @@ interface ExplorerUIProps { showCharts: boolean; selectedJobsRunning: boolean; overallSwimlaneData: OverallSwimlaneData | null; - invalidTimeRangeError?: boolean; stoppedPartitions?: string[]; // TODO Remove timefilter: TimefilterContract; @@ -155,6 +150,7 @@ interface ExplorerUIProps { timeBuckets: TimeBuckets; selectedCells: AppStateSelectedCells | undefined | null; swimLaneSeverity?: number; + noInfluencersConfigured?: boolean; } export function getDefaultPanelsState() { @@ -171,7 +167,6 @@ export function getDefaultPanelsState() { } export const Explorer: FC = ({ - invalidTimeRangeError, showCharts, severity, stoppedPartitions, @@ -182,6 +177,7 @@ export const Explorer: FC = ({ swimLaneSeverity, explorerState, overallSwimlaneData, + noInfluencersConfigured, }) => { const isMobile = useIsWithinBreakpoints(['xs', 's']); @@ -275,7 +271,7 @@ export const Explorer: FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [anomalyExplorerPanelState]); - const { displayWarningToast, displayDangerToast } = useToastNotificationService(); + const { displayDangerToast } = useToastNotificationService(); const { anomalyTimelineStateService, anomalyExplorerCommonStateService, @@ -291,14 +287,11 @@ export const Explorer: FC = ({ const [dataViews, setDataViews] = useState(); const filterSettings = useObservable( - anomalyExplorerCommonStateService.getFilterSettings$(), - anomalyExplorerCommonStateService.getFilterSettings() + anomalyExplorerCommonStateService.filterSettings$, + anomalyExplorerCommonStateService.filterSettings ); - const selectedJobs = useObservable( - anomalyExplorerCommonStateService.getSelectedJobs$(), - anomalyExplorerCommonStateService.getSelectedJobs() - ); + const { selectedJobs, selectedGroups, mergedGroupsAndJobsIds } = useJobSelection(); const alertsData = useObservable(anomalyDetectionAlertsStateService.anomalyDetectionAlerts$, []); @@ -361,21 +354,6 @@ export const Explorer: FC = ({ [explorerState, language, filterSettings] ); - useEffect(() => { - if (invalidTimeRangeError) { - displayWarningToast( - i18n.translate('xpack.ml.explorer.invalidTimeRangeInUrlCallout', { - defaultMessage: - 'The time filter was changed to the full range due to an invalid default time filter. Check the advanced settings for {field}.', - values: { - field: ANOMALY_DETECTION_DEFAULT_TIME_RANGE, - }, - }) - ); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - const { services: { charts: chartsService, @@ -387,15 +365,8 @@ export const Explorer: FC = ({ const mlIndexUtils = useMlIndexUtils(); const mlLocator = useMlLocator(); - const { - annotations, - filterPlaceHolder, - indexPattern, - influencers, - loading, - noInfluencersConfigured, - tableData, - } = explorerState; + const { annotations, filterPlaceHolder, indexPattern, influencers, loading, tableData } = + explorerState; const chartsData = useObservable( chartsStateService.getChartsData$(), @@ -442,11 +413,24 @@ export const Explorer: FC = ({ ); + const handleJobSelectionChange = useCallback( + ({ jobIds, time }: { jobIds: string[]; time?: { from: string; to: string } }) => { + anomalyExplorerCommonStateService.setSelectedJobs(jobIds, time); + }, + [anomalyExplorerCommonStateService] + ); + + const selectedJobIds = Array.isArray(selectedJobs) ? selectedJobs.map((job) => job.id) : []; + const jobSelectorProps = { dateFormatTz: getDateFormatTz(uiSettings), - } as JobSelectorProps; + onSelectionChange: handleJobSelectionChange, + selectedJobIds, + selectedGroups, + } as unknown as JobSelectorProps; const noJobsSelected = !selectedJobs || selectedJobs.length === 0; + const hasResults: boolean = !!overallSwimlaneData?.points && overallSwimlaneData.points.length > 0; const hasResultsWithAnomalies = @@ -454,7 +438,6 @@ export const Explorer: FC = ({ tableData.anomalies?.length > 0; const hasActiveFilter = isDefined(swimLaneSeverity); - const selectedJobIds = Array.isArray(selectedJobs) ? selectedJobs.map((job) => job.id) : []; useEffect(() => { if (!noJobsSelected) { @@ -592,6 +575,7 @@ export const Explorer: FC = ({ = ({ ; - [EXPLORER_ACTION.JOB_SELECTION_CHANGE]: { - loading: boolean; - selectedJobs: ExplorerJob[]; - noInfluencersConfigured: boolean; - }; -} - -export type ExplorerActions = { - [K in ExplorerAction]: K extends keyof ExplorerActionPayloads - ? { - type: K; - payload: ExplorerActionPayloads[K]; - } - : { - type: K; - }; -}[ExplorerAction]; - -type ExplorerActionMaybeObservable = ExplorerActions | Observable; - -export const explorerAction$ = new Subject(); - -const explorerFilteredAction$ = explorerAction$.pipe( - // consider observables as side-effects - flatMap((action: ExplorerActionMaybeObservable) => - isObservable(action) ? action : (from([action]) as Observable) - ), - distinctUntilChanged(isEqual) -); - -// applies action and returns state -const explorerState$: Observable = explorerFilteredAction$.pipe( - scan(explorerReducer, getExplorerDefaultState()), - // share the last emitted value among new subscribers - shareReplay(1) -); - -// Export observable state and action dispatchers as service -export const explorerServiceFactory = ( - mlJobService: MlJobService, - mlFieldFormatService: MlFieldFormatService -) => ({ - state$: explorerState$, - clearExplorerData: () => { - explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_EXPLORER_DATA }); - }, - clearInfluencerFilterSettings: () => { - explorerAction$.next({ - type: EXPLORER_ACTION.CLEAR_INFLUENCER_FILTER_SETTINGS, - }); - }, - clearJobs: () => { - explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_JOBS }); - }, - updateJobSelection: (selectedJobIds: string[]) => { - explorerAction$.next( - jobSelectionActionCreator(mlJobService, mlFieldFormatService, selectedJobIds) - ); - }, - setExplorerData: (payload: DeepPartial) => { - explorerAction$.next({ type: EXPLORER_ACTION.SET_EXPLORER_DATA, payload }); - }, - setChartsDataLoading: () => { - explorerAction$.next({ type: EXPLORER_ACTION.SET_CHARTS_DATA_LOADING }); - }, -}); - -export type ExplorerService = ReturnType; diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/explorer_reducer/state.ts b/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_data.ts similarity index 76% rename from x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/explorer_reducer/state.ts rename to x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_data.ts index 3eed0c410b0da..7e40dd07268d2 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/explorer_reducer/state.ts +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_data.ts @@ -6,12 +6,14 @@ */ import type { DataView } from '@kbn/data-views-plugin/common'; -import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns'; -import type { ExplorerChartsData } from '../../explorer_charts/explorer_charts_container_service'; -import { getDefaultChartsData } from '../../explorer_charts/explorer_charts_container_service'; -import type { AnomaliesTableData, ExplorerJob } from '../../explorer_utils'; -import type { AnnotationsTable } from '../../../../../common/types/annotations'; -import type { InfluencerValueData } from '../../../components/influencers_list/influencers_list'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; +import type { AnomaliesTableData, ExplorerJob } from './explorer_utils'; +import type { AnnotationsTable } from '../../../common/types/annotations'; +import type { InfluencerValueData } from '../components/influencers_list/influencers_list'; +import { + type ExplorerChartsData, + getDefaultChartsData, +} from './explorer_charts/explorer_charts_container_service'; export interface ExplorerState { overallAnnotations: AnnotationsTable; @@ -56,7 +58,7 @@ export function getExplorerDefaultState(): ExplorerState { indexPattern: getDefaultIndexPattern(), influencers: {}, isAndOperator: false, - loading: true, + loading: false, maskAll: false, noInfluencersConfigured: true, queryString: '', diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_utils.test.ts b/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_utils.test.ts new file mode 100644 index 0000000000000..f784b5c16412c --- /dev/null +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_utils.test.ts @@ -0,0 +1,159 @@ +/* + * 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 { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; +import type { GroupObj } from '../components/job_selector/job_selector'; +import type { ExplorerJob } from './explorer_utils'; +import { getIndexPattern, getMergedGroupsAndJobsIds } from './explorer_utils'; + +describe('getIndexPattern', () => { + it('should create correct index pattern format from a list of Explorer jobs', () => { + const mockExplorerJobs: ExplorerJob[] = [ + { + id: 'job-1', + selected: true, + bucketSpanSeconds: 3600, + modelPlotEnabled: false, + }, + { + id: 'job-2', + selected: false, + bucketSpanSeconds: 7200, + modelPlotEnabled: true, + sourceIndices: ['index-1'], + groups: ['group-1'], + }, + ]; + + const result = getIndexPattern(mockExplorerJobs); + + expect(result).toEqual({ + title: ML_RESULTS_INDEX_PATTERN, + fields: [ + { + name: 'job-1', + type: 'string', + aggregatable: true, + searchable: true, + }, + { + name: 'job-2', + type: 'string', + aggregatable: true, + searchable: true, + }, + ], + }); + }); + + it('should handle empty jobs array', () => { + const result = getIndexPattern([]); + + expect(result).toEqual({ + title: ML_RESULTS_INDEX_PATTERN, + fields: [], + }); + }); +}); + +describe('getMergedGroupsAndJobsIds', () => { + it('should merge group ids and standalone job ids correctly', () => { + const mockGroups: GroupObj[] = [ + { + groupId: 'group-1', + jobIds: ['job-1', 'job-2'], + }, + { + groupId: 'group-2', + jobIds: ['job-3', 'job-4'], + }, + ]; + + const mockSelectedJobs: ExplorerJob[] = [ + { + id: 'job-1', // part of group-1 + selected: true, + bucketSpanSeconds: 3600, + modelPlotEnabled: false, + }, + { + id: 'job-5', // standalone job + selected: true, + bucketSpanSeconds: 3600, + modelPlotEnabled: false, + }, + { + id: 'job-6', // standalone job + selected: true, + bucketSpanSeconds: 3600, + modelPlotEnabled: false, + }, + ]; + + const result = getMergedGroupsAndJobsIds(mockGroups, mockSelectedJobs); + + expect(result).toEqual(['group-1', 'group-2', 'job-5', 'job-6']); + }); + + it('should handle empty groups and jobs', () => { + const result = getMergedGroupsAndJobsIds([], []); + + expect(result).toEqual([]); + }); + + it('should handle overlapping jobs between groups', () => { + const mockGroups: GroupObj[] = [ + { + groupId: 'group-1', + jobIds: ['job-1', 'job-2'], + }, + { + groupId: 'group-2', + jobIds: ['job-2', 'job-3'], // job-2 is in both groups + }, + ]; + + const mockSelectedJobs: ExplorerJob[] = [ + { + id: 'job-4', + selected: true, + bucketSpanSeconds: 3600, + modelPlotEnabled: false, + }, + ]; + + const result = getMergedGroupsAndJobsIds(mockGroups, mockSelectedJobs); + + expect(result).toEqual(['group-1', 'group-2', 'job-4']); + }); + + it('should handle groups with no jobs', () => { + const mockGroups: GroupObj[] = [ + { + groupId: 'group-1', + jobIds: [], + }, + { + groupId: 'group-2', + jobIds: ['job-1'], + }, + ]; + + const mockSelectedJobs: ExplorerJob[] = [ + { + id: 'job-2', + selected: true, + bucketSpanSeconds: 3600, + modelPlotEnabled: false, + }, + ]; + + const result = getMergedGroupsAndJobsIds(mockGroups, mockSelectedJobs); + + expect(result).toEqual(['group-1', 'group-2', 'job-2']); + }); +}); diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_utils.ts b/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_utils.ts index abe921ee4352e..ce68528040b0d 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_utils.ts +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer_utils.ts @@ -54,6 +54,8 @@ import type { MlResultsService } from '../services/results_service'; import type { Annotations, AnnotationsTable } from '../../../common/types/annotations'; import { useMlKibana } from '../contexts/kibana'; import type { MlApi } from '../services/ml_api_service'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; +import type { GroupObj } from '../components/job_selector/job_selector'; export interface ExplorerJob { id: string; @@ -62,6 +64,7 @@ export interface ExplorerJob { isSingleMetricViewerJob?: boolean; sourceIndices?: string[]; modelPlotEnabled: boolean; + groups?: string[]; } export function isExplorerJob(arg: unknown): arg is ExplorerJob { @@ -149,6 +152,7 @@ export function createJobs(jobs: CombinedJob[]): ExplorerJob[] { isSingleMetricViewerJob: isTimeSeriesViewJob(job), sourceIndices: job.datafeed_config.indices, modelPlotEnabled: job.model_plot_config?.enabled === true, + groups: job.groups, }; }); } @@ -488,6 +492,7 @@ export async function loadAnomaliesTableData( influencersFilterQuery?: InfluencersFilterQuery ): Promise { const jobIds = getSelectionJobIds(selectedCells, selectedJobs); + const influencers = getSelectionInfluencers(selectedCells, fieldName); const timeRange = getSelectionTimeRange(selectedCells, bounds); @@ -700,3 +705,28 @@ export async function getDataViewsAndIndicesWithGeoFields( } return { sourceIndicesWithGeoFieldsMap, dataViews: [...dataViewsMap.values()] }; } + +// Creates index pattern in the format expected by the kuery bar/kuery autocomplete provider +// Field objects required fields: name, type, aggregatable, searchable +export function getIndexPattern(influencers: ExplorerJob[]) { + return { + title: ML_RESULTS_INDEX_PATTERN, + fields: influencers.map((influencer) => ({ + name: influencer.id, + type: 'string', + aggregatable: true, + searchable: true, + })), + }; +} + +// Returns a list of unique group ids and job ids +export function getMergedGroupsAndJobsIds(groups: GroupObj[], selectedJobs: ExplorerJob[]) { + const jobIdsFromGroups = groups.flatMap((group) => group.jobIds); + const groupIds = groups.map((group) => group.groupId); + const uniqueJobIds = selectedJobs + .filter((job) => !jobIdsFromGroups.includes(job.id)) + .map((job) => job.id); + + return [...groupIds, ...uniqueJobIds]; +} diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/hooks/use_explorer_url_state.ts b/x-pack/platform/plugins/shared/ml/public/application/explorer/hooks/use_explorer_url_state.ts index cfa3fdc03d343..cbea33986eab7 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/hooks/use_explorer_url_state.ts +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/hooks/use_explorer_url_state.ts @@ -5,12 +5,11 @@ * 2.0. */ -import type { PageUrlStateService } from '@kbn/ml-url-state'; -import { usePageUrlState } from '@kbn/ml-url-state'; +import { usePageUrlState, type UrlStateService } from '@kbn/ml-url-state'; import type { ExplorerAppState } from '../../../../common/types/locator'; import { ML_PAGES } from '../../../../common/constants/locator'; -export type AnomalyExplorerUrlStateService = PageUrlStateService; +export type AnomalyExplorerUrlStateService = UrlStateService; interface LegacyExplorerPageUrlState { pageKey: 'mlExplorerSwimlane'; diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/hooks/use_job_selection.ts b/x-pack/platform/plugins/shared/ml/public/application/explorer/hooks/use_job_selection.ts new file mode 100644 index 0000000000000..36fdc076e4cef --- /dev/null +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/hooks/use_job_selection.ts @@ -0,0 +1,36 @@ +/* + * 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 useObservable from 'react-use/lib/useObservable'; +import { useMemo } from 'react'; +import { useAnomalyExplorerContext } from '../anomaly_explorer_context'; +import { getMergedGroupsAndJobsIds } from '../explorer_utils'; + +export const useJobSelection = () => { + const { anomalyExplorerCommonStateService } = useAnomalyExplorerContext(); + + const selectedJobs = useObservable( + anomalyExplorerCommonStateService.selectedJobs$, + anomalyExplorerCommonStateService.selectedJobs + ); + + const selectedGroups = useObservable( + anomalyExplorerCommonStateService.selectedGroups$, + anomalyExplorerCommonStateService.selectedGroups + ); + + const mergedGroupsAndJobsIds = useMemo( + () => getMergedGroupsAndJobsIds(selectedGroups, selectedJobs), + [selectedGroups, selectedJobs] + ); + + return { + selectedJobs, + selectedGroups, + mergedGroupsAndJobsIds, + }; +}; diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/explorer_reducer/clear_influencer_filter_settings.ts b/x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/explorer_reducer/clear_influencer_filter_settings.ts deleted file mode 100644 index 5eec96170b238..0000000000000 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/explorer_reducer/clear_influencer_filter_settings.ts +++ /dev/null @@ -1,21 +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. - */ - -import { getClearedSelectedAnomaliesState } from '../../explorer_utils'; - -import type { ExplorerState } from './state'; - -export function clearInfluencerFilterSettings(state: ExplorerState): ExplorerState { - return { - ...state, - isAndOperator: false, - maskAll: false, - queryString: '', - tableQueryString: '', - ...getClearedSelectedAnomaliesState(), - }; -} diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/explorer_reducer/get_index_pattern.ts b/x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/explorer_reducer/get_index_pattern.ts deleted file mode 100644 index 878ba9370c95b..0000000000000 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/explorer_reducer/get_index_pattern.ts +++ /dev/null @@ -1,23 +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. - */ - -import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns'; -import type { ExplorerJob } from '../../explorer_utils'; - -// Creates index pattern in the format expected by the kuery bar/kuery autocomplete provider -// Field objects required fields: name, type, aggregatable, searchable -export function getIndexPattern(influencers: ExplorerJob[]) { - return { - title: ML_RESULTS_INDEX_PATTERN, - fields: influencers.map((influencer) => ({ - name: influencer.id, - type: 'string', - aggregatable: true, - searchable: true, - })), - }; -} diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts b/x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts deleted file mode 100644 index 58f7461b11047..0000000000000 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts +++ /dev/null @@ -1,45 +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. - */ - -import type { EXPLORER_ACTION } from '../../explorer_constants'; -import type { ExplorerActionPayloads } from '../../explorer_dashboard_service'; - -import { getIndexPattern } from './get_index_pattern'; -import type { ExplorerState } from './state'; - -export const jobSelectionChange = ( - state: ExplorerState, - payload: ExplorerActionPayloads[typeof EXPLORER_ACTION.JOB_SELECTION_CHANGE] -): ExplorerState => { - const { selectedJobs, noInfluencersConfigured } = payload; - const stateUpdate: ExplorerState = { - ...state, - noInfluencersConfigured, - selectedJobs, - }; - - // clear filter if selected jobs have no influencers - if (stateUpdate.noInfluencersConfigured === true) { - const noFilterState = { - filterActive: false, - filteredFields: [], - influencersFilterQuery: undefined, - maskAll: false, - queryString: '', - tableQueryString: '', - }; - - Object.assign(stateUpdate, noFilterState); - } else { - // indexPattern will not be used if there are no influencers so set up can be skipped - // indexPattern is passed to KqlFilterBar which is only shown if (noInfluencersConfigured === false) - stateUpdate.indexPattern = getIndexPattern(selectedJobs); - } - - stateUpdate.loading = true; - return stateUpdate; -}; diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts b/x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts deleted file mode 100644 index 4be342c9333ad..0000000000000 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts +++ /dev/null @@ -1,77 +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. - */ - -import { getDefaultChartsData } from '../../explorer_charts/explorer_charts_container_service'; -import { EXPLORER_ACTION } from '../../explorer_constants'; -import type { ExplorerActionPayloads, ExplorerActions } from '../../explorer_dashboard_service'; -import { getClearedSelectedAnomaliesState } from '../../explorer_utils'; - -import { clearInfluencerFilterSettings } from './clear_influencer_filter_settings'; -import { jobSelectionChange } from './job_selection_change'; -import type { ExplorerState } from './state'; -import { getExplorerDefaultState } from './state'; -import { setKqlQueryBarPlaceholder } from './set_kql_query_bar_placeholder'; - -export const explorerReducer = ( - state: ExplorerState, - nextAction: ExplorerActions -): ExplorerState => { - const { type } = nextAction; - const payload = 'payload' in nextAction ? nextAction.payload : {}; - - let nextState: ExplorerState; - - switch (type) { - case EXPLORER_ACTION.CLEAR_EXPLORER_DATA: - nextState = getExplorerDefaultState(); - break; - - case EXPLORER_ACTION.CLEAR_INFLUENCER_FILTER_SETTINGS: - nextState = clearInfluencerFilterSettings(state); - break; - - case EXPLORER_ACTION.CLEAR_JOBS: - nextState = { - ...state, - ...getClearedSelectedAnomaliesState(), - loading: false, - selectedJobs: [], - }; - break; - - case EXPLORER_ACTION.JOB_SELECTION_CHANGE: - nextState = jobSelectionChange( - state, - payload as ExplorerActionPayloads[typeof EXPLORER_ACTION.JOB_SELECTION_CHANGE] - ); - break; - - case EXPLORER_ACTION.SET_CHARTS_DATA_LOADING: - nextState = { - ...state, - anomalyChartsDataLoading: true, - chartsData: getDefaultChartsData(), - }; - break; - - case EXPLORER_ACTION.SET_EXPLORER_DATA: - nextState = { ...state, ...(payload as Partial) }; - break; - - default: - nextState = state; - } - - if (nextState.selectedJobs === null) { - return nextState; - } - - return { - ...nextState, - ...setKqlQueryBarPlaceholder(nextState), - }; -}; diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/explorer_reducer/set_kql_query_bar_placeholder.ts b/x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/explorer_reducer/set_kql_query_bar_placeholder.ts deleted file mode 100644 index e68037f0da471..0000000000000 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/explorer_reducer/set_kql_query_bar_placeholder.ts +++ /dev/null @@ -1,32 +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. - */ - -import { i18n } from '@kbn/i18n'; - -import type { ExplorerState } from './state'; - -// Set the KQL query bar placeholder value -export const setKqlQueryBarPlaceholder = (state: ExplorerState) => { - const { influencers, noInfluencersConfigured } = state; - - if (influencers !== undefined && !noInfluencersConfigured) { - for (const influencerName in influencers) { - if (influencers[influencerName][0] && influencers[influencerName][0].influencerFieldValue) { - return { - filterPlaceHolder: i18n.translate('xpack.ml.explorer.kueryBar.filterPlaceholder', { - defaultMessage: 'Filter by influencer fields… ({queryExample})', - values: { - queryExample: `${influencerName} : ${influencers[influencerName][0].influencerFieldValue}`, - }, - }), - }; - } - } - } - - return {}; -}; diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/index.ts b/x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/index.ts deleted file mode 100644 index db44d1864daa1..0000000000000 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/index.ts +++ /dev/null @@ -1,9 +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. - */ - -export type { ExplorerState } from './explorer_reducer'; -export { explorerReducer, getExplorerDefaultState, getIndexPattern } from './explorer_reducer'; diff --git a/x-pack/platform/plugins/shared/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx b/x-pack/platform/plugins/shared/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx index d46610a69483f..9f3c11f4bc04d 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx @@ -58,8 +58,8 @@ export function useGroupActions(): Array> { const path = await locator?.getUrl({ page: ML_PAGES.ANOMALY_EXPLORER, pageState: { - jobIds: item.jobIds, timeRange: timefilter.getTime(), + jobIds: isUngrouped(item) ? item.jobIds : [item.id], }, }); await navigateToPath(path); @@ -67,3 +67,5 @@ export function useGroupActions(): Array> { }, ]; } + +const isUngrouped = (item: Group) => item.id === 'ungrouped'; diff --git a/x-pack/platform/plugins/shared/ml/public/application/routing/routes/explorer/state_manager.tsx b/x-pack/platform/plugins/shared/ml/public/application/routing/routes/explorer/state_manager.tsx index 87983d2c61603..a077afae69b3b 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/routing/routes/explorer/state_manager.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/routing/routes/explorer/state_manager.tsx @@ -6,13 +6,12 @@ */ import type { FC } from 'react'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useUrlState } from '@kbn/ml-url-state'; import { useTimefilter } from '@kbn/ml-date-picker'; import { ML_JOB_ID } from '@kbn/ml-anomaly-utils'; import { useTimeBuckets } from '@kbn/ml-time-buckets'; @@ -30,6 +29,10 @@ import { PageTitle } from '../../../components/page_title'; import { AnomalyResultsViewSelector } from '../../../components/anomaly_results_view_selector'; import { AnomalyDetectionEmptyState } from '../../../jobs/jobs_list/components/anomaly_detection_empty_state'; import { useAnomalyExplorerContext } from '../../../explorer/anomaly_explorer_context'; +import { getInfluencers } from '../../../explorer/explorer_utils'; +import { useMlJobService } from '../../../services/job_service'; +import type { ExplorerState } from '../../../explorer/explorer_data'; +import { getExplorerDefaultState } from '../../../explorer/explorer_data'; export interface ExplorerUrlStateManagerProps { jobsWithTimeRange: MlJobWithTimeRange[]; @@ -43,38 +46,24 @@ export const ExplorerUrlStateManager: FC = ({ } = useMlKibana(); const { mlApi } = mlServices; - const [globalState] = useUrlState('_g'); const [stoppedPartitions, setStoppedPartitions] = useState(); - const [invalidTimeRangeError, setInValidTimeRangeError] = useState(false); const timeBuckets = useTimeBuckets(uiSettings); const timefilter = useTimefilter({ timeRangeSelector: true, autoRefreshSelector: true }); + const mlJobService = useMlJobService(); + const { selectedIds: jobIds, selectedJobs } = useJobSelection(jobsWithTimeRange); + const noInfluencersConfigured = getInfluencers(mlJobService, selectedJobs).length === 0; - const { jobIds } = useJobSelection(jobsWithTimeRange); const selectedJobsRunning = jobsWithTimeRange.some( - (job) => jobIds.includes(job.id) && job.isRunning === true + (job) => jobIds?.includes(job.id) && job.isRunning === true ); const anomalyExplorerContext = useAnomalyExplorerContext(); - const { explorerService } = anomalyExplorerContext; - const explorerState = useObservable(anomalyExplorerContext.explorerService.state$); + const [explorerState, setExplorerState] = useState(getExplorerDefaultState()); const refresh = useRefresh(); const lastRefresh = refresh?.lastRefresh ?? 0; - // We cannot simply infer bounds from the globalState's `time` attribute - // with `moment` since it can contain custom strings such as `now-15m`. - // So when globalState's `time` changes, we update the timefilter and use - // `timefilter.getBounds()` to update `bounds` in this component's state. - useEffect(() => { - if (globalState?.time !== undefined) { - if (globalState.time.mode === 'invalid') { - setInValidTimeRangeError(true); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [globalState?.time?.from, globalState?.time?.to, globalState?.time?.ts]); - const getJobsWithStoppedPartitions = useCallback(async (selectedJobIds: string[]) => { try { const fetchedStoppedPartitions = await mlApi.results.getCategoryStoppedPartitions( @@ -97,35 +86,18 @@ export const ExplorerUrlStateManager: FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useEffect( - function handleJobSelection() { - if (jobIds.length > 0) { - explorerService.updateJobSelection(jobIds); - getJobsWithStoppedPartitions(jobIds); - } else { - explorerService.clearJobs(); - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [JSON.stringify(jobIds)] - ); - useEffect(() => { - return () => { - // upon component unmounting - // clear any data to prevent next page from rendering old charts - explorerService.clearExplorerData(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + if (jobIds && jobIds.length > 0) { + getJobsWithStoppedPartitions(jobIds); + } + }, [getJobsWithStoppedPartitions, jobIds]); const [explorerData, loadExplorerData] = useExplorerData(); useEffect(() => { if (explorerData !== undefined && Object.keys(explorerData).length > 0) { - explorerService.setExplorerData(explorerData); + setExplorerState((prevState) => ({ ...prevState, ...explorerData })); } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [explorerData]); const [tableInterval] = useTableInterval(); @@ -151,35 +123,36 @@ export const ExplorerUrlStateManager: FC = ({ ); const influencersFilterQuery = useObservable( - anomalyExplorerContext.anomalyExplorerCommonStateService.getInfluencerFilterQuery$() + anomalyExplorerContext.anomalyExplorerCommonStateService.influencerFilterQuery$ ); - const loadExplorerDataConfig = - explorerState !== undefined - ? { - lastRefresh, - influencersFilterQuery, - noInfluencersConfigured: explorerState.noInfluencersConfigured, - selectedCells, - selectedJobs: explorerState.selectedJobs, - tableInterval: tableInterval.val, - tableSeverity: tableSeverity.val, - viewBySwimlaneFieldName: viewByFieldName, - } - : undefined; - - useEffect( - function updateAnomalyExplorerCommonState() { - anomalyExplorerContext.anomalyExplorerCommonStateService.setSelectedJobs( - loadExplorerDataConfig?.selectedJobs! - ); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [loadExplorerDataConfig] + const loadExplorerDataConfig = useMemo( + () => ({ + lastRefresh, + influencersFilterQuery, + noInfluencersConfigured, + selectedCells, + selectedJobs, + tableInterval: tableInterval.val, + tableSeverity: tableSeverity.val, + viewBySwimlaneFieldName: viewByFieldName, + }), + [ + lastRefresh, + influencersFilterQuery, + noInfluencersConfigured, + selectedCells, + selectedJobs, + tableInterval, + tableSeverity, + viewByFieldName, + ] ); useEffect(() => { if (!loadExplorerDataConfig || loadExplorerDataConfig?.selectedCells === undefined) return; + // TODO: Find other way to set loading state as it causes unnecessary re-renders - handle it in anomaly_explorer_common_state + setExplorerState((prevState) => ({ ...prevState, loading: true })); loadExplorerData(loadExplorerDataConfig); // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(loadExplorerDataConfig)]); @@ -202,10 +175,7 @@ export const ExplorerUrlStateManager: FC = ({ - + = ({ { + setGlobalState({ + ml: { + jobIds, + }, + ...(time !== undefined ? { time } : {}), + }); + }, + [setGlobalState] + ); + // Use a side effect to clear appState when changing jobs. useEffect(() => { if (selectedJobIds !== undefined && previousSelectedJobIds !== undefined) { @@ -268,7 +287,11 @@ export const TimeSeriesExplorerUrlStateManager: FC + ); @@ -276,7 +299,11 @@ export const TimeSeriesExplorerUrlStateManager: FC + ); @@ -306,6 +333,7 @@ export const TimeSeriesExplorerUrlStateManager: FC ); diff --git a/x-pack/platform/plugins/shared/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts b/x-pack/platform/plugins/shared/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts index 90dfe24946195..bcba172b9523b 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts +++ b/x-pack/platform/plugins/shared/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts @@ -23,6 +23,13 @@ interface TimeSeriesExplorerProps { tableInterval?: string; tableSeverity?: number; zoom?: { from?: string; to?: string }; + handleJobSelectionChange: ({ + jobIds, + time, + }: { + jobIds: string[]; + time?: { from: string; to: string }; + }) => void; } // eslint-disable-next-line react/prefer-stateless-function diff --git a/x-pack/platform/plugins/shared/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/platform/plugins/shared/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 9fce79d3d1fab..92b3c480a47ee 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/platform/plugins/shared/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -115,6 +115,7 @@ export class TimeSeriesExplorer extends React.Component { tableInterval: PropTypes.string, tableSeverity: PropTypes.number, zoom: PropTypes.object, + handleJobSelectionChange: PropTypes.func, }; state = getTimeseriesexplorerDefaultState(); @@ -1009,7 +1010,11 @@ export class TimeSeriesExplorer extends React.Component { if (selectedDetectorIndex === undefined || mlJobService.getJob(selectedJobId) === undefined) { return ( - + ); @@ -1039,7 +1044,12 @@ export class TimeSeriesExplorer extends React.Component { this.previousShowModelBounds = showModelBounds; return ( - + {fieldNamesWithEmptyValues.length > 0 && ( <> void; + selectedJobId?: string[]; } const timeseriesExplorerStyles = getTimeseriesExplorerStyles(); @@ -35,6 +43,8 @@ export const TimeSeriesExplorerPage: FC { const { services: { cases, docLinks }, @@ -66,7 +76,13 @@ export const TimeSeriesExplorerPage: FC {noSingleMetricJobsFound ? null : ( - + )} {children} diff --git a/x-pack/platform/plugins/shared/ml/public/embeddables/common/components/job_selector_flyout.tsx b/x-pack/platform/plugins/shared/ml/public/embeddables/common/components/job_selector_flyout.tsx deleted file mode 100644 index 44888067c839e..0000000000000 --- a/x-pack/platform/plugins/shared/ml/public/embeddables/common/components/job_selector_flyout.tsx +++ /dev/null @@ -1,39 +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. - */ - -import type { FC } from 'react'; -import React, { useState } from 'react'; -import type { JobSelectorFlyoutProps } from '../../../application/components/job_selector/job_selector_flyout'; -import { JobSelectorFlyoutContent } from '../../../application/components/job_selector/job_selector_flyout'; - -export const JobSelectorFlyout: FC = ({ - selectedIds, - withTimeRangeSelector, - dateFormatTz, - singleSelection, - timeseriesOnly, - onFlyoutClose, - onSelectionConfirmed, - maps, -}) => { - const [applyTimeRangeState, setApplyTimeRangeState] = useState(true); - - return ( - - ); -}; diff --git a/x-pack/platform/plugins/shared/ml/public/maps/util.ts b/x-pack/platform/plugins/shared/ml/public/maps/util.ts index f661c08b6c5f6..8563380da5642 100644 --- a/x-pack/platform/plugins/shared/ml/public/maps/util.ts +++ b/x-pack/platform/plugins/shared/ml/public/maps/util.ts @@ -24,9 +24,9 @@ import { formatHumanReadableDateTimeSeconds } from '@kbn/ml-date-utils'; import { SEARCH_QUERY_LANGUAGE } from '@kbn/ml-query-utils'; import type { MlApi } from '../application/services/ml_api_service'; import { tabColor } from '../../common/util/group_color_utils'; -import { getIndexPattern } from '../application/explorer/reducers/explorer_reducer/get_index_pattern'; import { AnomalySource } from './anomaly_source'; -import type { SourceIndexGeoFields } from '../application/explorer/explorer_utils'; +import type { ExplorerJob } from '../application/explorer/explorer_utils'; +import { getIndexPattern, type SourceIndexGeoFields } from '../application/explorer/explorer_utils'; export const ML_ANOMALY_LAYERS = { TYPICAL: 'typical', @@ -170,8 +170,8 @@ export async function getResultsForJobId( const { query, timeFilters } = searchFilters; const hasQuery = query && query.query !== ''; let queryFilter; - // @ts-ignore missing properties from ExplorerJob - those fields aren't required for this - const indexPattern = getIndexPattern([{ id: jobId }]); + + const indexPattern = getIndexPattern([{ id: jobId }] as ExplorerJob[]); if (hasQuery && query.language === SEARCH_QUERY_LANGUAGE.KUERY) { queryFilter = toElasticsearchQuery(fromKueryExpression(query.query), indexPattern); diff --git a/x-pack/platform/plugins/shared/ml/server/lib/ml_client/ml_client.ts b/x-pack/platform/plugins/shared/ml/server/lib/ml_client/ml_client.ts index aa8bb89c47ea5..bbd5b7ef7d90f 100644 --- a/x-pack/platform/plugins/shared/ml/server/lib/ml_client/ml_client.ts +++ b/x-pack/platform/plugins/shared/ml/server/lib/ml_client/ml_client.ts @@ -472,7 +472,7 @@ export function getMlClient( throw error; } if (error.statusCode === 404) { - throw new MLJobNotFound(error.body.error.reason); + throw new MLJobNotFound(formatJobNotFoundError(error.body.error.reason)); } throw error; } @@ -786,3 +786,14 @@ function filterAll(ids: string[]) { // something called _all, which will subsequently fail. return ids.length === 1 && ids[0] === '_all' ? [] : ids; } + +function formatJobNotFoundError(errorReason: string) { + const failingJobMatch = errorReason.match(/No known job with id '([^']+)'/); + const failingJobIds = failingJobMatch?.[1]?.split(','); + const errorMessage = failingJobIds?.length + ? `No known job or group with ${ + failingJobIds.length === 1 ? 'id' : 'ids' + } '${failingJobIds.join("', '")}'` + : errorReason; + return errorMessage; +} diff --git a/x-pack/plugins/alerting/docs/openapi/bundled.json b/x-pack/plugins/alerting/docs/openapi/bundled.json index 7d6d85c229202..92e261620bde1 100644 --- a/x-pack/plugins/alerting/docs/openapi/bundled.json +++ b/x-pack/plugins/alerting/docs/openapi/bundled.json @@ -497,1059 +497,6 @@ } } } - }, - "/api/alerts/alert/{alertId}": { - "delete": { - "summary": "Delete an alert", - "operationId": "legaryDeleteAlert", - "deprecated": true, - "description": "Deprecated in 7.13.0. Use the delete rule API instead. WARNING: After you delete an alert, you cannot recover it.\n", - "tags": [ - "alerting" - ], - "parameters": [ - { - "$ref": "#/components/parameters/kbn_xsrf" - }, - { - "in": "path", - "name": "alertId", - "description": "The identifier for the alert.", - "required": true, - "schema": { - "type": "string", - "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" - } - } - ], - "responses": { - "204": { - "description": "Indicates a successful call." - }, - "401": { - "description": "Authorization information is missing or invalid.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/401_response" - } - } - } - } - } - }, - "get": { - "summary": "Get an alert by identifier", - "operationId": "legacyGetAlert", - "deprecated": true, - "description": "Deprecated in 7.13.0. Use the get rule API instead.", - "tags": [ - "alerting" - ], - "parameters": [ - { - "in": "path", - "name": "alertId", - "description": "The identifier for the alert.", - "required": true, - "schema": { - "type": "string", - "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" - } - } - ], - "responses": { - "200": { - "description": "Indicates a successful call.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/alert_response_properties" - } - } - } - }, - "401": { - "description": "Authorization information is missing or invalid.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/401_response" - } - } - } - } - } - }, - "post": { - "summary": "Create an alert", - "operationId": "legacyCreateAlert", - "deprecated": true, - "description": "Deprecated in 7.13.0. Use the create rule API instead.", - "tags": [ - "alerting" - ], - "parameters": [ - { - "$ref": "#/components/parameters/kbn_xsrf" - }, - { - "in": "path", - "name": "alertId", - "description": "An UUID v1 or v4 identifier for the alert. If this parameter is omitted, the identifier is randomly generated.", - "required": true, - "schema": { - "type": "string", - "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "title": "Legacy create alert request properties", - "type": "object", - "required": [ - "alertTypeId", - "consumer", - "name", - "notifyWhen", - "params", - "schedule" - ], - "properties": { - "actions": { - "type": "array", - "items": { - "type": "object", - "required": [ - "actionTypeId", - "group", - "id", - "params" - ], - "properties": { - "actionTypeId": { - "type": "string", - "description": "The identifier for the action type." - }, - "group": { - "type": "string", - "description": "Grouping actions is recommended for escalations for different types of alert instances. If you don't need this functionality, set it to `default`.\n" - }, - "id": { - "type": "string", - "description": "The ID of the action saved object." - }, - "params": { - "type": "object", - "description": "The map to the `params` that the action type will receive. `params` are handled as Mustache templates and passed a default set of context.\n" - } - } - } - }, - "alertTypeId": { - "type": "string", - "description": "The ID of the alert type that you want to call when the alert is scheduled to run." - }, - "consumer": { - "type": "string", - "description": "The name of the application that owns the alert. This name has to match the Kibana feature name, as that dictates the required role-based access control privileges." - }, - "enabled": { - "type": "boolean", - "description": "Indicates if you want to run the alert on an interval basis after it is created." - }, - "name": { - "type": "string", - "description": "A name to reference and search." - }, - "notifyWhen": { - "type": "string", - "description": "The condition for throttling the notification.", - "enum": [ - "onActionGroupChange", - "onActiveAlert", - "onThrottleInterval" - ] - }, - "params": { - "type": "object", - "description": "The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined." - }, - "schedule": { - "type": "object", - "description": "The schedule specifying when this alert should be run. A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule.\n", - "properties": { - "interval": { - "type": "string", - "description": "The interval format specifies the interval in seconds, minutes, hours or days at which the alert should run.", - "example": "10s" - } - } - }, - "tags": { - "type": "array", - "items": { - "type": "string" - }, - "description": "A list of keywords to reference and search." - }, - "throttle": { - "type": "string", - "description": "How often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, setting a throttle of `10m` or `1h` will prevent it from sending 90 notifications during this period.\n" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Indicates a successful call.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/alert_response_properties" - } - } - } - }, - "401": { - "description": "Authorization information is missing or invalid.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/401_response" - } - } - } - } - } - }, - "put": { - "summary": "Update an alert", - "operationId": "legacyUpdateAlert", - "deprecated": true, - "description": "Deprecated in 7.13.0. Use the update rule API instead.", - "tags": [ - "alerting" - ], - "parameters": [ - { - "$ref": "#/components/parameters/kbn_xsrf" - }, - { - "in": "path", - "name": "alertId", - "description": "The identifier for the alert.", - "required": true, - "schema": { - "type": "string", - "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "title": "Legacy update alert request properties", - "type": "object", - "required": [ - "name", - "notifyWhen", - "params", - "schedule" - ], - "properties": { - "actions": { - "type": "array", - "items": { - "type": "object", - "required": [ - "actionTypeId", - "group", - "id", - "params" - ], - "properties": { - "actionTypeId": { - "type": "string", - "description": "The identifier for the action type." - }, - "group": { - "type": "string", - "description": "Grouping actions is recommended for escalations for different types of alert instances. If you don't need this functionality, set it to `default`.\n" - }, - "id": { - "type": "string", - "description": "The ID of the action saved object." - }, - "params": { - "type": "object", - "description": "The map to the `params` that the action type will receive. `params` are handled as Mustache templates and passed a default set of context.\n" - } - } - } - }, - "name": { - "type": "string", - "description": "A name to reference and search." - }, - "notifyWhen": { - "type": "string", - "description": "The condition for throttling the notification.", - "enum": [ - "onActionGroupChange", - "onActiveAlert", - "onThrottleInterval" - ] - }, - "params": { - "type": "object", - "description": "The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined." - }, - "schedule": { - "type": "object", - "description": "The schedule specifying when this alert should be run. A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule.\n", - "properties": { - "interval": { - "type": "string", - "description": "The interval format specifies the interval in seconds, minutes, hours or days at which the alert should run.", - "example": "1d" - } - } - }, - "tags": { - "type": "array", - "items": { - "type": "string" - }, - "description": "A list of keywords to reference and search." - }, - "throttle": { - "type": "string", - "description": "How often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, setting a throttle of `10m` or `1h` will prevent it from sending 90 notifications during this period.\n" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Indicates a successful call.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/alert_response_properties" - } - } - } - }, - "401": { - "description": "Authorization information is missing or invalid.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/401_response" - } - } - } - } - } - } - }, - "/api/alerts/alert/{alertId}/_disable": { - "post": { - "summary": "Disable an alert", - "operationId": "legacyDisableAlert", - "deprecated": true, - "description": "Deprecated in 7.13.0. Use the disable rule API instead.", - "tags": [ - "alerting" - ], - "parameters": [ - { - "$ref": "#/components/parameters/kbn_xsrf" - }, - { - "in": "path", - "name": "alertId", - "description": "The identifier for the alert.", - "required": true, - "schema": { - "type": "string", - "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" - } - } - ], - "responses": { - "204": { - "description": "Indicates a successful call." - }, - "401": { - "description": "Authorization information is missing or invalid.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/401_response" - } - } - } - } - } - } - }, - "/api/alerts/alert/{alertId}/_enable": { - "post": { - "summary": "Enable an alert", - "operationId": "legacyEnableAlert", - "deprecated": true, - "description": "Deprecated in 7.13.0. Use the enable rule API instead.", - "tags": [ - "alerting" - ], - "parameters": [ - { - "$ref": "#/components/parameters/kbn_xsrf" - }, - { - "in": "path", - "name": "alertId", - "description": "The identifier for the alert.", - "required": true, - "schema": { - "type": "string", - "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" - } - } - ], - "responses": { - "204": { - "description": "Indicates a successful call." - }, - "401": { - "description": "Authorization information is missing or invalid.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/401_response" - } - } - } - } - } - } - }, - "/api/alerts/alert/{alertId}/_mute_all": { - "post": { - "summary": "Mute all alert instances", - "operationId": "legacyMuteAllAlertInstances", - "deprecated": true, - "description": "Deprecated in 7.13.0. Use the mute all alerts API instead.", - "tags": [ - "alerting" - ], - "parameters": [ - { - "$ref": "#/components/parameters/kbn_xsrf" - }, - { - "in": "path", - "name": "alertId", - "description": "The identifier for the alert.", - "required": true, - "schema": { - "type": "string", - "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" - } - } - ], - "responses": { - "204": { - "description": "Indicates a successful call." - }, - "401": { - "description": "Authorization information is missing or invalid.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/401_response" - } - } - } - } - } - } - }, - "/api/alerts/alert/{alertId}/_unmute_all": { - "post": { - "summary": "Unmute all alert instances", - "operationId": "legacyUnmuteAllAlertInstances", - "deprecated": true, - "description": "Deprecated in 7.13.0. Use the unmute all alerts API instead.", - "tags": [ - "alerting" - ], - "parameters": [ - { - "$ref": "#/components/parameters/kbn_xsrf" - }, - { - "in": "path", - "name": "alertId", - "description": "The identifier for the alert.", - "required": true, - "schema": { - "type": "string", - "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" - } - } - ], - "responses": { - "204": { - "description": "Indicates a successful call." - }, - "401": { - "description": "Authorization information is missing or invalid.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/401_response" - } - } - } - } - } - } - }, - "/api/alerts/alerts/_find": { - "get": { - "summary": "Get a paginated set of alerts", - "operationId": "legacyFindAlerts", - "deprecated": true, - "description": "Deprecated in 7.13.0. Use the find rules API instead. NOTE: Alert `params` are stored as a flattened field type and analyzed as keywords. As alerts change in Kibana, the results on each page of the response also change. Use the find API for traditional paginated results, but avoid using it to export large amounts of data.\n", - "tags": [ - "alerting" - ], - "parameters": [ - { - "name": "default_search_operator", - "in": "query", - "description": "The default operator to use for the `simple_query_string`.", - "schema": { - "type": "string", - "default": "OR" - }, - "example": "OR" - }, - { - "name": "fields", - "in": "query", - "description": "The fields to return in the `attributes` key of the response.", - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - { - "name": "filter", - "in": "query", - "description": "A KQL string that you filter with an attribute from your saved object. It should look like `savedObjectType.attributes.title: \"myTitle\"`. However, if you used a direct attribute of a saved object, such as `updatedAt`, you must define your filter, for example, `savedObjectType.updatedAt > 2018-12-22`.\n", - "schema": { - "type": "string" - } - }, - { - "name": "has_reference", - "in": "query", - "description": "Filters the rules that have a relation with the reference objects with a specific type and identifier.", - "schema": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string" - } - } - } - }, - { - "name": "page", - "in": "query", - "description": "The page number to return.", - "schema": { - "type": "integer", - "default": 1 - }, - "example": 1 - }, - { - "name": "per_page", - "in": "query", - "description": "The number of alerts to return per page.", - "schema": { - "type": "integer", - "default": 20 - }, - "example": 20 - }, - { - "name": "search", - "in": "query", - "description": "An Elasticsearch `simple_query_string` query that filters the alerts in the response.", - "schema": { - "type": "string" - } - }, - { - "name": "search_fields", - "in": "query", - "description": "The fields to perform the `simple_query_string` parsed query against.", - "schema": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] - } - }, - { - "name": "sort_field", - "in": "query", - "description": "Determines which field is used to sort the results. The field must exist in the `attributes` key of the response.\n", - "schema": { - "type": "string" - } - }, - { - "name": "sort_order", - "in": "query", - "description": "Determines the sort order.", - "schema": { - "type": "string", - "enum": [ - "asc", - "desc" - ], - "default": "desc" - }, - "example": "asc" - } - ], - "responses": { - "200": { - "description": "Indicates a successful call.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/alert_response_properties" - } - }, - "page": { - "type": "integer" - }, - "perPage": { - "type": "integer" - }, - "total": { - "type": "integer" - } - } - } - } - } - }, - "401": { - "description": "Authorization information is missing or invalid.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/401_response" - } - } - } - } - } - } - }, - "/api/alerts/alerts/_health": { - "get": { - "summary": "Get the alerting framework health", - "operationId": "legacyGetAlertingHealth", - "deprecated": true, - "description": "Deprecated in 7.13.0. Use the get alerting framework health API instead.", - "tags": [ - "alerting" - ], - "responses": { - "200": { - "description": "Indicates a successful call.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "alertingFrameworkHealth": { - "type": "object", - "description": "Three substates identify the health of the alerting framework: `decryptionHealth`, `executionHealth`, and `readHealth`.\n", - "properties": { - "decryptionHealth": { - "type": "object", - "description": "The timestamp and status of the alert decryption.", - "properties": { - "status": { - "type": "string", - "example": "ok", - "enum": [ - "error", - "ok", - "warn" - ] - }, - "timestamp": { - "type": "string", - "format": "date-time", - "example": "2023-01-13T01:28:00.280Z" - } - } - }, - "executionHealth": { - "type": "object", - "description": "The timestamp and status of the alert execution.", - "properties": { - "status": { - "type": "string", - "example": "ok", - "enum": [ - "error", - "ok", - "warn" - ] - }, - "timestamp": { - "type": "string", - "format": "date-time", - "example": "2023-01-13T01:28:00.280Z" - } - } - }, - "readHealth": { - "type": "object", - "description": "The timestamp and status of the alert reading events.", - "properties": { - "status": { - "type": "string", - "example": "ok", - "enum": [ - "error", - "ok", - "warn" - ] - }, - "timestamp": { - "type": "string", - "format": "date-time", - "example": "2023-01-13T01:28:00.280Z" - } - } - } - } - }, - "hasPermanentEncryptionKey": { - "type": "boolean", - "description": "If `false`, the encrypted saved object plugin does not have a permanent encryption key.", - "example": true - }, - "isSufficientlySecure": { - "type": "boolean", - "description": "If `false`, security is enabled but TLS is not.", - "example": true - } - } - } - } - } - }, - "401": { - "description": "Authorization information is missing or invalid.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/401_response" - } - } - } - } - } - } - }, - "/api/alerts/alerts/list_alert_types": { - "get": { - "summary": "Get the alert types", - "operationId": "legacyGetAlertTypes", - "deprecated": true, - "description": "Deprecated in 7.13.0. Use the get rule types API instead.", - "tags": [ - "alerting" - ], - "responses": { - "200": { - "description": "Indicates a successful call.", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "actionGroups": { - "description": "An explicit list of groups for which the alert type can schedule actions, each with the action group's unique ID and human readable name. Alert actions validation uses this configuration to ensure that groups are valid.\n", - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - } - } - } - }, - "actionVariables": { - "description": "A list of action variables that the alert type makes available via context and state in action parameter templates, and a short human readable description. The Alert UI will use this information to prompt users for these variables in action parameter editors.\n", - "type": "object", - "properties": { - "context": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - } - } - } - }, - "params": { - "type": "array", - "items": { - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "name": { - "type": "string" - } - } - } - }, - "state": { - "type": "array", - "items": { - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "name": { - "type": "string" - } - } - } - } - } - }, - "authorizedConsumers": { - "description": "The list of the plugins IDs that have access to the alert type.", - "type": "object" - }, - "defaultActionGroupId": { - "description": "The default identifier for the alert type group.", - "type": "string" - }, - "enabledInLicense": { - "description": "Indicates whether the rule type is enabled based on the subscription.", - "type": "boolean" - }, - "id": { - "description": "The unique identifier for the alert type.", - "type": "string" - }, - "isExportable": { - "description": "Indicates whether the alert type is exportable in Saved Objects Management UI.", - "type": "boolean" - }, - "minimumLicenseRequired": { - "description": "The subscriptions required to use the alert type.", - "type": "string" - }, - "name": { - "description": "The descriptive name of the alert type.", - "type": "string" - }, - "producer": { - "description": "An identifier for the application that produces this alert type.", - "type": "string" - }, - "recoveryActionGroup": { - "description": "An action group to use when an alert instance goes from an active state to an inactive one. If it is not specified, the default recovered action group is used.\n", - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "401": { - "description": "Authorization information is missing or invalid.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/401_response" - } - } - } - } - } - } - }, - "/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_mute": { - "post": { - "summary": "Mute an alert instance", - "operationId": "legacyMuteAlertInstance", - "deprecated": true, - "description": "Deprecated in 7.13.0. Use the mute alert API instead.", - "tags": [ - "alerting" - ], - "parameters": [ - { - "$ref": "#/components/parameters/kbn_xsrf" - }, - { - "in": "path", - "name": "alertId", - "description": "An identifier for the alert.", - "required": true, - "schema": { - "type": "string", - "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" - } - }, - { - "in": "path", - "name": "alertInstanceId", - "description": "An identifier for the alert instance.", - "required": true, - "schema": { - "type": "string", - "example": "dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2" - } - } - ], - "responses": { - "204": { - "description": "Indicates a successful call." - }, - "401": { - "description": "Authorization information is missing or invalid.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/401_response" - } - } - } - } - } - } - }, - "/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute": { - "post": { - "summary": "Unmute an alert instance", - "operationId": "legacyUnmuteAlertInstance", - "deprecated": true, - "description": "Deprecated in 7.13.0. Use the unmute alert API instead.", - "tags": [ - "alerting" - ], - "parameters": [ - { - "$ref": "#/components/parameters/kbn_xsrf" - }, - { - "in": "path", - "name": "alertId", - "description": "An identifier for the alert.", - "required": true, - "schema": { - "type": "string", - "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" - } - }, - { - "in": "path", - "name": "alertInstanceId", - "description": "An identifier for the alert instance.", - "required": true, - "schema": { - "type": "string", - "example": "dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2" - } - } - ], - "responses": { - "204": { - "description": "Indicates a successful call." - }, - "401": { - "description": "Authorization information is missing or invalid.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/401_response" - } - } - } - } - } - } } }, "components": { @@ -1978,129 +925,6 @@ "example": "scaled_float" } } - }, - "alert_response_properties": { - "title": "Legacy alert response properties", - "type": "object", - "properties": { - "actions": { - "type": "array", - "items": { - "type": "object" - } - }, - "alertTypeId": { - "type": "string", - "example": ".index-threshold" - }, - "apiKeyOwner": { - "type": "string", - "nullable": true, - "example": "elastic" - }, - "createdAt": { - "type": "string", - "description": "The date and time that the alert was created.", - "format": "date-time", - "example": "2022-12-05T23:36:58.284Z" - }, - "createdBy": { - "type": "string", - "description": "The identifier for the user that created the alert.", - "example": "elastic" - }, - "enabled": { - "type": "boolean", - "description": "Indicates whether the alert is currently enabled.", - "example": true - }, - "executionStatus": { - "type": "object", - "properties": { - "lastExecutionDate": { - "type": "string", - "format": "date-time", - "example": "2022-12-06T00:13:43.890Z" - }, - "status": { - "type": "string", - "example": "ok" - } - } - }, - "id": { - "type": "string", - "description": "The identifier for the alert.", - "example": "b530fed0-74f5-11ed-9801-35303b735aef" - }, - "muteAll": { - "type": "boolean", - "example": false - }, - "mutedInstanceIds": { - "type": "array", - "nullable": true, - "items": { - "type": "string" - } - }, - "name": { - "type": "string", - "description": "The name of the alert.", - "example": "my alert" - }, - "notifyWhen": { - "type": "string", - "example": "onActionGroupChange" - }, - "params": { - "type": "object", - "additionalProperties": true - }, - "schedule": { - "type": "object", - "properties": { - "interval": { - "type": "string" - } - } - }, - "scheduledTaskId": { - "type": "string", - "example": "b530fed0-74f5-11ed-9801-35303b735aef" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "throttle": { - "type": "string", - "nullable": true - }, - "updatedAt": { - "type": "string", - "example": "2022-12-05T23:36:58.284Z" - }, - "updatedBy": { - "type": "string", - "description": "The identifier for the user that updated this alert most recently.", - "nullable": true, - "example": "elastic" - } - } - } - }, - "parameters": { - "kbn_xsrf": { - "schema": { - "type": "string" - }, - "in": "header", - "name": "kbn-xsrf", - "description": "Cross-site request forgery protection", - "required": true } } } diff --git a/x-pack/plugins/alerting/docs/openapi/bundled.yaml b/x-pack/plugins/alerting/docs/openapi/bundled.yaml index 87601c463e50e..967d08eadf5fa 100644 --- a/x-pack/plugins/alerting/docs/openapi/bundled.yaml +++ b/x-pack/plugins/alerting/docs/openapi/bundled.yaml @@ -353,724 +353,6 @@ paths: application/json: schema: $ref: '#/components/schemas/401_response' - /api/alerts/alert/{alertId}: - delete: - summary: Delete an alert - operationId: legaryDeleteAlert - deprecated: true - description: | - Deprecated in 7.13.0. Use the delete rule API instead. WARNING: After you delete an alert, you cannot recover it. - tags: - - alerting - parameters: - - $ref: '#/components/parameters/kbn_xsrf' - - in: path - name: alertId - description: The identifier for the alert. - required: true - schema: - type: string - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - responses: - '204': - description: Indicates a successful call. - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/401_response' - get: - summary: Get an alert by identifier - operationId: legacyGetAlert - deprecated: true - description: Deprecated in 7.13.0. Use the get rule API instead. - tags: - - alerting - parameters: - - in: path - name: alertId - description: The identifier for the alert. - required: true - schema: - type: string - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - responses: - '200': - description: Indicates a successful call. - content: - application/json: - schema: - $ref: '#/components/schemas/alert_response_properties' - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/401_response' - post: - summary: Create an alert - operationId: legacyCreateAlert - deprecated: true - description: Deprecated in 7.13.0. Use the create rule API instead. - tags: - - alerting - parameters: - - $ref: '#/components/parameters/kbn_xsrf' - - in: path - name: alertId - description: An UUID v1 or v4 identifier for the alert. If this parameter is omitted, the identifier is randomly generated. - required: true - schema: - type: string - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - requestBody: - required: true - content: - application/json: - schema: - title: Legacy create alert request properties - type: object - required: - - alertTypeId - - consumer - - name - - notifyWhen - - params - - schedule - properties: - actions: - type: array - items: - type: object - required: - - actionTypeId - - group - - id - - params - properties: - actionTypeId: - type: string - description: The identifier for the action type. - group: - type: string - description: | - Grouping actions is recommended for escalations for different types of alert instances. If you don't need this functionality, set it to `default`. - id: - type: string - description: The ID of the action saved object. - params: - type: object - description: | - The map to the `params` that the action type will receive. `params` are handled as Mustache templates and passed a default set of context. - alertTypeId: - type: string - description: The ID of the alert type that you want to call when the alert is scheduled to run. - consumer: - type: string - description: The name of the application that owns the alert. This name has to match the Kibana feature name, as that dictates the required role-based access control privileges. - enabled: - type: boolean - description: Indicates if you want to run the alert on an interval basis after it is created. - name: - type: string - description: A name to reference and search. - notifyWhen: - type: string - description: The condition for throttling the notification. - enum: - - onActionGroupChange - - onActiveAlert - - onThrottleInterval - params: - type: object - description: The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined. - schedule: - type: object - description: | - The schedule specifying when this alert should be run. A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule. - properties: - interval: - type: string - description: The interval format specifies the interval in seconds, minutes, hours or days at which the alert should run. - example: 10s - tags: - type: array - items: - type: string - description: A list of keywords to reference and search. - throttle: - type: string - description: | - How often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, setting a throttle of `10m` or `1h` will prevent it from sending 90 notifications during this period. - responses: - '200': - description: Indicates a successful call. - content: - application/json: - schema: - $ref: '#/components/schemas/alert_response_properties' - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/401_response' - put: - summary: Update an alert - operationId: legacyUpdateAlert - deprecated: true - description: Deprecated in 7.13.0. Use the update rule API instead. - tags: - - alerting - parameters: - - $ref: '#/components/parameters/kbn_xsrf' - - in: path - name: alertId - description: The identifier for the alert. - required: true - schema: - type: string - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - requestBody: - required: true - content: - application/json: - schema: - title: Legacy update alert request properties - type: object - required: - - name - - notifyWhen - - params - - schedule - properties: - actions: - type: array - items: - type: object - required: - - actionTypeId - - group - - id - - params - properties: - actionTypeId: - type: string - description: The identifier for the action type. - group: - type: string - description: | - Grouping actions is recommended for escalations for different types of alert instances. If you don't need this functionality, set it to `default`. - id: - type: string - description: The ID of the action saved object. - params: - type: object - description: | - The map to the `params` that the action type will receive. `params` are handled as Mustache templates and passed a default set of context. - name: - type: string - description: A name to reference and search. - notifyWhen: - type: string - description: The condition for throttling the notification. - enum: - - onActionGroupChange - - onActiveAlert - - onThrottleInterval - params: - type: object - description: The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined. - schedule: - type: object - description: | - The schedule specifying when this alert should be run. A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule. - properties: - interval: - type: string - description: The interval format specifies the interval in seconds, minutes, hours or days at which the alert should run. - example: 1d - tags: - type: array - items: - type: string - description: A list of keywords to reference and search. - throttle: - type: string - description: | - How often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, setting a throttle of `10m` or `1h` will prevent it from sending 90 notifications during this period. - responses: - '200': - description: Indicates a successful call. - content: - application/json: - schema: - $ref: '#/components/schemas/alert_response_properties' - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/401_response' - /api/alerts/alert/{alertId}/_disable: - post: - summary: Disable an alert - operationId: legacyDisableAlert - deprecated: true - description: Deprecated in 7.13.0. Use the disable rule API instead. - tags: - - alerting - parameters: - - $ref: '#/components/parameters/kbn_xsrf' - - in: path - name: alertId - description: The identifier for the alert. - required: true - schema: - type: string - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - responses: - '204': - description: Indicates a successful call. - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/401_response' - /api/alerts/alert/{alertId}/_enable: - post: - summary: Enable an alert - operationId: legacyEnableAlert - deprecated: true - description: Deprecated in 7.13.0. Use the enable rule API instead. - tags: - - alerting - parameters: - - $ref: '#/components/parameters/kbn_xsrf' - - in: path - name: alertId - description: The identifier for the alert. - required: true - schema: - type: string - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - responses: - '204': - description: Indicates a successful call. - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/401_response' - /api/alerts/alert/{alertId}/_mute_all: - post: - summary: Mute all alert instances - operationId: legacyMuteAllAlertInstances - deprecated: true - description: Deprecated in 7.13.0. Use the mute all alerts API instead. - tags: - - alerting - parameters: - - $ref: '#/components/parameters/kbn_xsrf' - - in: path - name: alertId - description: The identifier for the alert. - required: true - schema: - type: string - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - responses: - '204': - description: Indicates a successful call. - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/401_response' - /api/alerts/alert/{alertId}/_unmute_all: - post: - summary: Unmute all alert instances - operationId: legacyUnmuteAllAlertInstances - deprecated: true - description: Deprecated in 7.13.0. Use the unmute all alerts API instead. - tags: - - alerting - parameters: - - $ref: '#/components/parameters/kbn_xsrf' - - in: path - name: alertId - description: The identifier for the alert. - required: true - schema: - type: string - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - responses: - '204': - description: Indicates a successful call. - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/401_response' - /api/alerts/alerts/_find: - get: - summary: Get a paginated set of alerts - operationId: legacyFindAlerts - deprecated: true - description: | - Deprecated in 7.13.0. Use the find rules API instead. NOTE: Alert `params` are stored as a flattened field type and analyzed as keywords. As alerts change in Kibana, the results on each page of the response also change. Use the find API for traditional paginated results, but avoid using it to export large amounts of data. - tags: - - alerting - parameters: - - name: default_search_operator - in: query - description: The default operator to use for the `simple_query_string`. - schema: - type: string - default: OR - example: OR - - name: fields - in: query - description: The fields to return in the `attributes` key of the response. - schema: - type: array - items: - type: string - - name: filter - in: query - description: | - A KQL string that you filter with an attribute from your saved object. It should look like `savedObjectType.attributes.title: "myTitle"`. However, if you used a direct attribute of a saved object, such as `updatedAt`, you must define your filter, for example, `savedObjectType.updatedAt > 2018-12-22`. - schema: - type: string - - name: has_reference - in: query - description: Filters the rules that have a relation with the reference objects with a specific type and identifier. - schema: - type: object - properties: - id: - type: string - type: - type: string - - name: page - in: query - description: The page number to return. - schema: - type: integer - default: 1 - example: 1 - - name: per_page - in: query - description: The number of alerts to return per page. - schema: - type: integer - default: 20 - example: 20 - - name: search - in: query - description: An Elasticsearch `simple_query_string` query that filters the alerts in the response. - schema: - type: string - - name: search_fields - in: query - description: The fields to perform the `simple_query_string` parsed query against. - schema: - oneOf: - - type: string - - type: array - items: - type: string - - name: sort_field - in: query - description: | - Determines which field is used to sort the results. The field must exist in the `attributes` key of the response. - schema: - type: string - - name: sort_order - in: query - description: Determines the sort order. - schema: - type: string - enum: - - asc - - desc - default: desc - example: asc - responses: - '200': - description: Indicates a successful call. - content: - application/json: - schema: - type: object - properties: - data: - type: array - items: - $ref: '#/components/schemas/alert_response_properties' - page: - type: integer - perPage: - type: integer - total: - type: integer - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/401_response' - /api/alerts/alerts/_health: - get: - summary: Get the alerting framework health - operationId: legacyGetAlertingHealth - deprecated: true - description: Deprecated in 7.13.0. Use the get alerting framework health API instead. - tags: - - alerting - responses: - '200': - description: Indicates a successful call. - content: - application/json: - schema: - type: object - properties: - alertingFrameworkHealth: - type: object - description: | - Three substates identify the health of the alerting framework: `decryptionHealth`, `executionHealth`, and `readHealth`. - properties: - decryptionHealth: - type: object - description: The timestamp and status of the alert decryption. - properties: - status: - type: string - example: ok - enum: - - error - - ok - - warn - timestamp: - type: string - format: date-time - example: '2023-01-13T01:28:00.280Z' - executionHealth: - type: object - description: The timestamp and status of the alert execution. - properties: - status: - type: string - example: ok - enum: - - error - - ok - - warn - timestamp: - type: string - format: date-time - example: '2023-01-13T01:28:00.280Z' - readHealth: - type: object - description: The timestamp and status of the alert reading events. - properties: - status: - type: string - example: ok - enum: - - error - - ok - - warn - timestamp: - type: string - format: date-time - example: '2023-01-13T01:28:00.280Z' - hasPermanentEncryptionKey: - type: boolean - description: If `false`, the encrypted saved object plugin does not have a permanent encryption key. - example: true - isSufficientlySecure: - type: boolean - description: If `false`, security is enabled but TLS is not. - example: true - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/401_response' - /api/alerts/alerts/list_alert_types: - get: - summary: Get the alert types - operationId: legacyGetAlertTypes - deprecated: true - description: Deprecated in 7.13.0. Use the get rule types API instead. - tags: - - alerting - responses: - '200': - description: Indicates a successful call. - content: - application/json: - schema: - type: array - items: - type: object - properties: - actionGroups: - description: | - An explicit list of groups for which the alert type can schedule actions, each with the action group's unique ID and human readable name. Alert actions validation uses this configuration to ensure that groups are valid. - type: array - items: - type: object - properties: - id: - type: string - name: - type: string - actionVariables: - description: | - A list of action variables that the alert type makes available via context and state in action parameter templates, and a short human readable description. The Alert UI will use this information to prompt users for these variables in action parameter editors. - type: object - properties: - context: - type: array - items: - type: object - properties: - name: - type: string - description: - type: string - params: - type: array - items: - type: object - properties: - description: - type: string - name: - type: string - state: - type: array - items: - type: object - properties: - description: - type: string - name: - type: string - authorizedConsumers: - description: The list of the plugins IDs that have access to the alert type. - type: object - defaultActionGroupId: - description: The default identifier for the alert type group. - type: string - enabledInLicense: - description: Indicates whether the rule type is enabled based on the subscription. - type: boolean - id: - description: The unique identifier for the alert type. - type: string - isExportable: - description: Indicates whether the alert type is exportable in Saved Objects Management UI. - type: boolean - minimumLicenseRequired: - description: The subscriptions required to use the alert type. - type: string - name: - description: The descriptive name of the alert type. - type: string - producer: - description: An identifier for the application that produces this alert type. - type: string - recoveryActionGroup: - description: | - An action group to use when an alert instance goes from an active state to an inactive one. If it is not specified, the default recovered action group is used. - type: object - properties: - id: - type: string - name: - type: string - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/401_response' - /api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_mute: - post: - summary: Mute an alert instance - operationId: legacyMuteAlertInstance - deprecated: true - description: Deprecated in 7.13.0. Use the mute alert API instead. - tags: - - alerting - parameters: - - $ref: '#/components/parameters/kbn_xsrf' - - in: path - name: alertId - description: An identifier for the alert. - required: true - schema: - type: string - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - - in: path - name: alertInstanceId - description: An identifier for the alert instance. - required: true - schema: - type: string - example: dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2 - responses: - '204': - description: Indicates a successful call. - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/401_response' - /api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute: - post: - summary: Unmute an alert instance - operationId: legacyUnmuteAlertInstance - deprecated: true - description: Deprecated in 7.13.0. Use the unmute alert API instead. - tags: - - alerting - parameters: - - $ref: '#/components/parameters/kbn_xsrf' - - in: path - name: alertId - description: An identifier for the alert. - required: true - schema: - type: string - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - - in: path - name: alertInstanceId - description: An identifier for the alert instance. - required: true - schema: - type: string - example: dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2 - responses: - '204': - description: Indicates a successful call. - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/401_response' components: examples: get_health_response: @@ -1370,94 +652,3 @@ components: type: string description: Specifies the data type for the field. example: scaled_float - alert_response_properties: - title: Legacy alert response properties - type: object - properties: - actions: - type: array - items: - type: object - alertTypeId: - type: string - example: .index-threshold - apiKeyOwner: - type: string - nullable: true - example: elastic - createdAt: - type: string - description: The date and time that the alert was created. - format: date-time - example: '2022-12-05T23:36:58.284Z' - createdBy: - type: string - description: The identifier for the user that created the alert. - example: elastic - enabled: - type: boolean - description: Indicates whether the alert is currently enabled. - example: true - executionStatus: - type: object - properties: - lastExecutionDate: - type: string - format: date-time - example: '2022-12-06T00:13:43.890Z' - status: - type: string - example: ok - id: - type: string - description: The identifier for the alert. - example: b530fed0-74f5-11ed-9801-35303b735aef - muteAll: - type: boolean - example: false - mutedInstanceIds: - type: array - nullable: true - items: - type: string - name: - type: string - description: The name of the alert. - example: my alert - notifyWhen: - type: string - example: onActionGroupChange - params: - type: object - additionalProperties: true - schedule: - type: object - properties: - interval: - type: string - scheduledTaskId: - type: string - example: b530fed0-74f5-11ed-9801-35303b735aef - tags: - type: array - items: - type: string - throttle: - type: string - nullable: true - updatedAt: - type: string - example: '2022-12-05T23:36:58.284Z' - updatedBy: - type: string - description: The identifier for the user that updated this alert most recently. - nullable: true - example: elastic - parameters: - kbn_xsrf: - schema: - type: string - in: header - name: kbn-xsrf - description: Cross-site request forgery protection - required: true diff --git a/x-pack/plugins/alerting/docs/openapi/entrypoint.yaml b/x-pack/plugins/alerting/docs/openapi/entrypoint.yaml index 48f7efd53b839..1b169a6c4f760 100644 --- a/x-pack/plugins/alerting/docs/openapi/entrypoint.yaml +++ b/x-pack/plugins/alerting/docs/openapi/entrypoint.yaml @@ -15,24 +15,3 @@ paths: $ref: paths/api@alerting@_health.yaml '/api/alerting/rule_types': $ref: 'paths/api@alerting@rule_types.yaml' -# Deprecated APIs - '/api/alerts/alert/{alertId}': - $ref: 'paths/api@alerts@alert@{alertid}.yaml' - '/api/alerts/alert/{alertId}/_disable': - $ref: 'paths/api@alerts@alert@{alertid}@_disable.yaml' - '/api/alerts/alert/{alertId}/_enable': - $ref: 'paths/api@alerts@alert@{alertid}@_enable.yaml' - '/api/alerts/alert/{alertId}/_mute_all': - $ref: 'paths/api@alerts@alert@{alertid}@_mute_all.yaml' - '/api/alerts/alert/{alertId}/_unmute_all': - $ref: 'paths/api@alerts@alert@{alertid}@_unmute_all.yaml' - '/api/alerts/alerts/_find': - $ref: 'paths/api@alerts@_find.yaml' - '/api/alerts/alerts/_health': - $ref: 'paths/api@alerts@_health.yaml' - '/api/alerts/alerts/list_alert_types': - $ref: 'paths/api@alerts@list_alert_types.yaml' - '/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_mute': - $ref: 'paths/api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_mute.yaml' - '/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute': - $ref: 'paths/api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_unmute.yaml' diff --git a/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@_find.yaml b/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@_find.yaml deleted file mode 100644 index 397653acb1c90..0000000000000 --- a/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@_find.yaml +++ /dev/null @@ -1,115 +0,0 @@ -get: - summary: Get a paginated set of alerts - operationId: legacyFindAlerts - deprecated: true - description: > - Deprecated in 7.13.0. Use the find rules API instead. - NOTE: Alert `params` are stored as a flattened field type and analyzed as keywords. - As alerts change in Kibana, the results on each page of the response also change. - Use the find API for traditional paginated results, but avoid using it to export large amounts of data. - tags: - - alerting - parameters: - - name: default_search_operator - in: query - description: The default operator to use for the `simple_query_string`. - schema: - type: string - default: OR - example: OR - - name: fields - in: query - description: The fields to return in the `attributes` key of the response. - schema: - type: array - items: - type: string - - name: filter - in: query - description: > - A KQL string that you filter with an attribute from your saved object. - It should look like `savedObjectType.attributes.title: "myTitle"`. - However, if you used a direct attribute of a saved object, such as - `updatedAt`, you must define your filter, for example, - `savedObjectType.updatedAt > 2018-12-22`. - schema: - type: string - - name: has_reference - in: query - description: Filters the rules that have a relation with the reference objects with a specific type and identifier. - schema: - type: object - properties: - id: - type: string - type: - type: string - - name: page - in: query - description: The page number to return. - schema: - type: integer - default: 1 - example: 1 - - name: per_page - in: query - description: The number of alerts to return per page. - schema: - type: integer - default: 20 - example: 20 - - name: search - in: query - description: An Elasticsearch `simple_query_string` query that filters the alerts in the response. - schema: - type: string - - name: search_fields - in: query - description: The fields to perform the `simple_query_string` parsed query against. - schema: - oneOf: - - type: string - - type: array - items: - type: string - - name: sort_field - in: query - description: > - Determines which field is used to sort the results. The field must exist - in the `attributes` key of the response. - schema: - type: string - - name: sort_order - in: query - description: Determines the sort order. - schema: - type: string - enum: - - asc - - desc - default: desc - example: asc - responses: - '200': - description: Indicates a successful call. - content: - application/json: - schema: - type: object - properties: - data: - type: array - items: - $ref: '../components/schemas/alert_response_properties.yaml' - page: - type: integer - perPage: - type: integer - total: - type: integer - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '../components/schemas/401_response.yaml' \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@_health.yaml b/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@_health.yaml deleted file mode 100644 index b8436a08abf1f..0000000000000 --- a/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@_health.yaml +++ /dev/null @@ -1,79 +0,0 @@ -get: - summary: Get the alerting framework health - operationId: legacyGetAlertingHealth - deprecated: true - description: Deprecated in 7.13.0. Use the get alerting framework health API instead. - tags: - - alerting - responses: - '200': - description: Indicates a successful call. - content: - application/json: - schema: - type: object - properties: - alertingFrameworkHealth: - type: object - description: > - Three substates identify the health of the alerting framework: `decryptionHealth`, `executionHealth`, and `readHealth`. - properties: - decryptionHealth: - type: object - description: The timestamp and status of the alert decryption. - properties: - status: - type: string - example: ok - enum: - - error - - ok - - warn - timestamp: - type: string - format: date-time - example: "2023-01-13T01:28:00.280Z" - executionHealth: - type: object - description: The timestamp and status of the alert execution. - properties: - status: - type: string - example: ok - enum: - - error - - ok - - warn - timestamp: - type: string - format: date-time - example: "2023-01-13T01:28:00.280Z" - readHealth: - type: object - description: The timestamp and status of the alert reading events. - properties: - status: - type: string - example: ok - enum: - - error - - ok - - warn - timestamp: - type: string - format: date-time - example: "2023-01-13T01:28:00.280Z" - hasPermanentEncryptionKey: - type: boolean - description: If `false`, the encrypted saved object plugin does not have a permanent encryption key. - example: true - isSufficientlySecure: - type: boolean - description: If `false`, security is enabled but TLS is not. - example: true - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '../components/schemas/401_response.yaml' \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@alert@{alertid}.yaml b/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@alert@{alertid}.yaml deleted file mode 100644 index bf6d69d117450..0000000000000 --- a/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@alert@{alertid}.yaml +++ /dev/null @@ -1,275 +0,0 @@ -delete: - summary: Delete an alert - operationId: legaryDeleteAlert - deprecated: true - description: > - Deprecated in 7.13.0. Use the delete rule API instead. - WARNING: After you delete an alert, you cannot recover it. - tags: - - alerting - parameters: - - $ref: ../components/headers/kbn_xsrf.yaml - - in: path - name: alertId - description: The identifier for the alert. - required: true - schema: - type: string - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - responses: - '204': - description: Indicates a successful call. - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '../components/schemas/401_response.yaml' - -get: - summary: Get an alert by identifier - operationId: legacyGetAlert - deprecated: true - description: Deprecated in 7.13.0. Use the get rule API instead. - tags: - - alerting - parameters: - - in: path - name: alertId - description: The identifier for the alert. - required: true - schema: - type: string - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - responses: - '200': - description: Indicates a successful call. - content: - application/json: - schema: - $ref: '../components/schemas/alert_response_properties.yaml' - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '../components/schemas/401_response.yaml' - -post: - summary: Create an alert - operationId: legacyCreateAlert - deprecated: true - description: Deprecated in 7.13.0. Use the create rule API instead. - tags: - - alerting - parameters: - - $ref: ../components/headers/kbn_xsrf.yaml - - in: path - name: alertId - description: An UUID v1 or v4 identifier for the alert. If this parameter is omitted, the identifier is randomly generated. - required: true - schema: - type: string - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - requestBody: - required: true - content: - application/json: - schema: - title: Legacy create alert request properties - type: object - required: - - alertTypeId - - consumer - - name - - notifyWhen - - params - - schedule - properties: - actions: - type: array - items: - type: object - required: - - actionTypeId - - group - - id - - params - properties: - actionTypeId: - type: string - description: The identifier for the action type. - group: - type: string - description: > - Grouping actions is recommended for escalations for different types of alert instances. - If you don't need this functionality, set it to `default`. - id: - type: string - description: The ID of the action saved object. - params: - type: object - description: > - The map to the `params` that the action type will receive. - `params` are handled as Mustache templates and passed a default set of context. - alertTypeId: - type: string - description: The ID of the alert type that you want to call when the alert is scheduled to run. - consumer: - type: string - description: The name of the application that owns the alert. This name has to match the Kibana feature name, as that dictates the required role-based access control privileges. - enabled: - type: boolean - description: Indicates if you want to run the alert on an interval basis after it is created. - name: - type: string - description: A name to reference and search. - notifyWhen: - type: string - description: The condition for throttling the notification. - enum: - - onActionGroupChange - - onActiveAlert - - onThrottleInterval - params: - type: object - description: The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined. - schedule: - type: object - description: > - The schedule specifying when this alert should be run. - A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule. - properties: - interval: - type: string - description: The interval format specifies the interval in seconds, minutes, hours or days at which the alert should run. - example: "10s" - tags: - type: array - items: - type: string - description: A list of keywords to reference and search. - throttle: - type: string - description: > - How often this alert should fire the same actions. - This will prevent the alert from sending out the same notification over and over. - For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, - setting a throttle of `10m` or `1h` will prevent it from sending 90 notifications during this period. - responses: - '200': - description: Indicates a successful call. - content: - application/json: - schema: - $ref: '../components/schemas/alert_response_properties.yaml' - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '../components/schemas/401_response.yaml' - -put: - summary: Update an alert - operationId: legacyUpdateAlert - deprecated: true - description: Deprecated in 7.13.0. Use the update rule API instead. - tags: - - alerting - parameters: - - $ref: ../components/headers/kbn_xsrf.yaml - - in: path - name: alertId - description: The identifier for the alert. - required: true - schema: - type: string - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - requestBody: - required: true - content: - application/json: - schema: - title: Legacy update alert request properties - type: object - required: - - name - - notifyWhen - - params - - schedule - properties: - actions: - type: array - items: - type: object - required: - - actionTypeId - - group - - id - - params - properties: - actionTypeId: - type: string - description: The identifier for the action type. - group: - type: string - description: > - Grouping actions is recommended for escalations for different types of alert instances. - If you don't need this functionality, set it to `default`. - id: - type: string - description: The ID of the action saved object. - params: - type: object - description: > - The map to the `params` that the action type will receive. - `params` are handled as Mustache templates and passed a default set of context. - name: - type: string - description: A name to reference and search. - notifyWhen: - type: string - description: The condition for throttling the notification. - enum: - - onActionGroupChange - - onActiveAlert - - onThrottleInterval - params: - type: object - description: The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined. - schedule: - type: object - description: > - The schedule specifying when this alert should be run. - A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule. - properties: - interval: - type: string - description: The interval format specifies the interval in seconds, minutes, hours or days at which the alert should run. - example: "1d" - tags: - type: array - items: - type: string - description: A list of keywords to reference and search. - throttle: - type: string - description: > - How often this alert should fire the same actions. - This will prevent the alert from sending out the same notification over and over. - For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, - setting a throttle of `10m` or `1h` will prevent it from sending 90 notifications during this period. - responses: - '200': - description: Indicates a successful call. - content: - application/json: - schema: - $ref: '../components/schemas/alert_response_properties.yaml' - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '../components/schemas/401_response.yaml' \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@alert@{alertid}@_disable.yaml b/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@alert@{alertid}@_disable.yaml deleted file mode 100644 index 70874a8bea396..0000000000000 --- a/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@alert@{alertid}@_disable.yaml +++ /dev/null @@ -1,25 +0,0 @@ -post: - summary: Disable an alert - operationId: legacyDisableAlert - deprecated: true - description: Deprecated in 7.13.0. Use the disable rule API instead. - tags: - - alerting - parameters: - - $ref: ../components/headers/kbn_xsrf.yaml - - in: path - name: alertId - description: The identifier for the alert. - required: true - schema: - type: string - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - responses: - '204': - description: Indicates a successful call. - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '../components/schemas/401_response.yaml' \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@alert@{alertid}@_enable.yaml b/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@alert@{alertid}@_enable.yaml deleted file mode 100644 index 3b79778c26c6c..0000000000000 --- a/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@alert@{alertid}@_enable.yaml +++ /dev/null @@ -1,25 +0,0 @@ -post: - summary: Enable an alert - operationId: legacyEnableAlert - deprecated: true - description: Deprecated in 7.13.0. Use the enable rule API instead. - tags: - - alerting - parameters: - - $ref: ../components/headers/kbn_xsrf.yaml - - in: path - name: alertId - description: The identifier for the alert. - required: true - schema: - type: string - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - responses: - '204': - description: Indicates a successful call. - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '../components/schemas/401_response.yaml' \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@alert@{alertid}@_mute_all.yaml b/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@alert@{alertid}@_mute_all.yaml deleted file mode 100644 index 462e6994d4553..0000000000000 --- a/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@alert@{alertid}@_mute_all.yaml +++ /dev/null @@ -1,25 +0,0 @@ -post: - summary: Mute all alert instances - operationId: legacyMuteAllAlertInstances - deprecated: true - description: Deprecated in 7.13.0. Use the mute all alerts API instead. - tags: - - alerting - parameters: - - $ref: ../components/headers/kbn_xsrf.yaml - - in: path - name: alertId - description: The identifier for the alert. - required: true - schema: - type: string - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - responses: - '204': - description: Indicates a successful call. - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '../components/schemas/401_response.yaml' \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@alert@{alertid}@_unmute_all.yaml b/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@alert@{alertid}@_unmute_all.yaml deleted file mode 100644 index bfdec84525aae..0000000000000 --- a/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@alert@{alertid}@_unmute_all.yaml +++ /dev/null @@ -1,25 +0,0 @@ -post: - summary: Unmute all alert instances - operationId: legacyUnmuteAllAlertInstances - deprecated: true - description: Deprecated in 7.13.0. Use the unmute all alerts API instead. - tags: - - alerting - parameters: - - $ref: ../components/headers/kbn_xsrf.yaml - - in: path - name: alertId - description: The identifier for the alert. - required: true - schema: - type: string - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - responses: - '204': - description: Indicates a successful call. - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '../components/schemas/401_response.yaml' \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_mute.yaml b/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_mute.yaml deleted file mode 100644 index 4af89ec42616c..0000000000000 --- a/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_mute.yaml +++ /dev/null @@ -1,32 +0,0 @@ -post: - summary: Mute an alert instance - operationId: legacyMuteAlertInstance - deprecated: true - description: Deprecated in 7.13.0. Use the mute alert API instead. - tags: - - alerting - parameters: - - $ref: ../components/headers/kbn_xsrf.yaml - - in: path - name: alertId - description: An identifier for the alert. - required: true - schema: - type: string - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - - in: path - name: alertInstanceId - description: An identifier for the alert instance. - required: true - schema: - type: string - example: dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2 - responses: - '204': - description: Indicates a successful call. - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '../components/schemas/401_response.yaml' \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_unmute.yaml b/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_unmute.yaml deleted file mode 100644 index a939d74c3d1d0..0000000000000 --- a/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_unmute.yaml +++ /dev/null @@ -1,32 +0,0 @@ -post: - summary: Unmute an alert instance - operationId: legacyUnmuteAlertInstance - deprecated: true - description: Deprecated in 7.13.0. Use the unmute alert API instead. - tags: - - alerting - parameters: - - $ref: ../components/headers/kbn_xsrf.yaml - - in: path - name: alertId - description: An identifier for the alert. - required: true - schema: - type: string - example: 41893910-6bca-11eb-9e0d-85d233e3ee35 - - in: path - name: alertInstanceId - description: An identifier for the alert instance. - required: true - schema: - type: string - example: dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2 - responses: - '204': - description: Indicates a successful call. - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '../components/schemas/401_response.yaml' \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@list_alert_types.yaml b/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@list_alert_types.yaml deleted file mode 100644 index 14b3cd4e07b9b..0000000000000 --- a/x-pack/plugins/alerting/docs/openapi/paths/api@alerts@list_alert_types.yaml +++ /dev/null @@ -1,107 +0,0 @@ -get: - summary: Get the alert types - operationId: legacyGetAlertTypes - deprecated: true - description: Deprecated in 7.13.0. Use the get rule types API instead. - tags: - - alerting - responses: - '200': - description: Indicates a successful call. - content: - application/json: - schema: - type: array - items: - type: object - properties: - actionGroups: - description: > - An explicit list of groups for which the alert type can - schedule actions, each with the action group's unique ID and - human readable name. Alert actions validation uses this - configuration to ensure that groups are valid. - type: array - items: - type: object - properties: - id: - type: string - name: - type: string - actionVariables: - description: > - A list of action variables that the alert type makes available - via context and state in action parameter templates, and a - short human readable description. The Alert UI will use this - information to prompt users for these variables in action - parameter editors. - type: object - properties: - context: - type: array - items: - type: object - properties: - name: - type: string - description: - type: string - params: - type: array - items: - type: object - properties: - description: - type: string - name: - type: string - state: - type: array - items: - type: object - properties: - description: - type: string - name: - type: string - authorizedConsumers: - description: The list of the plugins IDs that have access to the alert type. - type: object - defaultActionGroupId: - description: The default identifier for the alert type group. - type: string - enabledInLicense: - description: Indicates whether the rule type is enabled based on the subscription. - type: boolean - id: - description: The unique identifier for the alert type. - type: string - isExportable: - description: Indicates whether the alert type is exportable in Saved Objects Management UI. - type: boolean - minimumLicenseRequired: - description: The subscriptions required to use the alert type. - type: string - name: - description: The descriptive name of the alert type. - type: string - producer: - description: An identifier for the application that produces this alert type. - type: string - recoveryActionGroup: - description: > - An action group to use when an alert instance goes from an active state to an inactive one. - If it is not specified, the default recovered action group is used. - type: object - properties: - id: - type: string - name: - type: string - '401': - description: Authorization information is missing or invalid. - content: - application/json: - schema: - $ref: '../components/schemas/401_response.yaml' \ No newline at end of file diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts index d9692dec9c0f8..58c6cda9f3b12 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -11,7 +11,6 @@ import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-p import type { ConfigSchema } from '@kbn/unified-search-plugin/server/config'; import { Observable } from 'rxjs'; import { GetAlertIndicesAlias, ILicenseState } from '../lib'; -import { defineLegacyRoutes } from './legacy'; import { AlertingRequestHandlerContext } from '../types'; import { createRuleRoute } from './rule/apis/create'; import { getRuleRoute, getInternalRuleRoute } from './rule/apis/get/get_rule_route'; @@ -94,10 +93,6 @@ export function defineRoutes(opts: RouteOptions) { getAlertIndicesAlias, } = opts; - // Legacy APIs - defineLegacyRoutes(opts); - - // Rule APIs createRuleRoute(opts); getRuleRoute(router, licenseState); getInternalRuleRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/legacy/create.test.ts b/x-pack/plugins/alerting/server/routes/legacy/create.test.ts deleted file mode 100644 index f3df843899f22..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/create.test.ts +++ /dev/null @@ -1,617 +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. - */ - -import { createAlertRoute } from './create'; -import { httpServiceMock } from '@kbn/core/server/mocks'; -import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; -import { licenseStateMock } from '../../lib/license_state.mock'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { mockHandlerArguments } from '../_mock_handler_arguments'; -import { rulesClientMock } from '../../rules_client.mock'; -import { Rule, RuleSystemAction } from '../../../common/rule'; -import { RuleTypeDisabledError } from '../../lib/errors/rule_type_disabled'; -import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { docLinksServiceMock } from '@kbn/core/server/mocks'; - -const rulesClient = rulesClientMock.create(); - -jest.mock('../../lib/license_api_access', () => ({ - verifyApiAccess: jest.fn(), -})); - -jest.mock('../../lib/track_legacy_route_usage', () => ({ - trackLegacyRouteUsage: jest.fn(), -})); - -beforeEach(() => { - jest.resetAllMocks(); -}); - -describe('createAlertRoute', () => { - const docLinks = docLinksServiceMock.createSetupContract(); - const createdAt = new Date(); - const updatedAt = new Date(); - - const mockedAlert = { - alertTypeId: '1', - consumer: 'bar', - name: 'abc', - schedule: { interval: '10s' }, - tags: ['foo'], - params: { - bar: true, - }, - throttle: '30s', - notifyWhen: 'onActionGroupChange', - actions: [ - { - group: 'default', - id: '2', - params: { - foo: true, - }, - }, - ], - }; - - const systemAction: RuleSystemAction = { - actionTypeId: 'test-2', - id: 'system_action-id', - params: { - foo: true, - }, - uuid: '123-456', - }; - - const createResult: Rule<{ bar: boolean }> = { - ...mockedAlert, - enabled: true, - muteAll: false, - createdBy: '', - updatedBy: '', - apiKey: '', - apiKeyOwner: '', - mutedInstanceIds: [], - notifyWhen: 'onActionGroupChange', - createdAt, - updatedAt, - id: '123', - actions: [ - { - ...mockedAlert.actions[0], - actionTypeId: 'test', - }, - ], - executionStatus: { - status: 'unknown', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - }, - revision: 0, - }; - - it('creates an alert with proper parameters', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - createAlertRoute({ - router, - licenseState, - encryptedSavedObjects, - usageCounter: mockUsageCounter, - docLinks, - }); - - const [config, handler] = router.post.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id?}"`); - - expect(config.options?.access).toBe('public'); - - rulesClient.create.mockResolvedValueOnce(createResult); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - body: mockedAlert, - }, - ['ok'] - ); - - expect(await handler(context, req, res)).toEqual({ body: createResult }); - - expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); - expect(rulesClient.create).toHaveBeenCalledTimes(1); - expect(rulesClient.create.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "data": Object { - "actions": Array [ - Object { - "group": "default", - "id": "2", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "1", - "consumer": "bar", - "name": "abc", - "notifyWhen": "onActionGroupChange", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "tags": Array [ - "foo", - ], - "throttle": "30s", - }, - "options": Object { - "id": undefined, - }, - }, - ] - `); - - expect(res.ok).toHaveBeenCalledWith({ - body: createResult, - }); - }); - - it('should have internal access for serverless', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - createAlertRoute({ - router, - licenseState, - encryptedSavedObjects, - usageCounter: mockUsageCounter, - isServerless: true, - docLinks, - }); - - const [config] = router.post.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id?}"`); - - expect(config.options?.access).toBe('internal'); - }); - - it('allows providing a custom id when space is undefined', async () => { - const expectedResult = { - ...createResult, - id: 'custom-id', - }; - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - createAlertRoute({ - router, - licenseState, - encryptedSavedObjects, - usageCounter: mockUsageCounter, - docLinks, - }); - - const [config, handler] = router.post.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id?}"`); - - rulesClient.create.mockResolvedValueOnce(expectedResult); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { id: 'custom-id' }, - body: mockedAlert, - }, - ['ok'] - ); - - expect(await handler(context, req, res)).toEqual({ body: expectedResult }); - - expect(mockUsageCounter.incrementCounter).toHaveBeenCalledTimes(1); - expect(rulesClient.create).toHaveBeenCalledTimes(1); - expect(rulesClient.create.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "data": Object { - "actions": Array [ - Object { - "group": "default", - "id": "2", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "1", - "consumer": "bar", - "name": "abc", - "notifyWhen": "onActionGroupChange", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "tags": Array [ - "foo", - ], - "throttle": "30s", - }, - "options": Object { - "id": "custom-id", - }, - }, - ] - `); - - expect(res.ok).toHaveBeenCalledWith({ - body: expectedResult, - }); - }); - - it('allows providing a custom id in default space', async () => { - const expectedResult = { - ...createResult, - id: 'custom-id', - }; - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - createAlertRoute({ - router, - licenseState, - encryptedSavedObjects, - usageCounter: mockUsageCounter, - docLinks, - }); - - const [config, handler] = router.post.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id?}"`); - - rulesClient.create.mockResolvedValueOnce(expectedResult); - rulesClient.getSpaceId.mockReturnValueOnce('default'); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { id: 'custom-id' }, - body: mockedAlert, - }, - ['ok'] - ); - - expect(await handler(context, req, res)).toEqual({ body: expectedResult }); - - expect(mockUsageCounter.incrementCounter).toHaveBeenCalledTimes(1); - expect(rulesClient.create).toHaveBeenCalledTimes(1); - expect(rulesClient.create.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "data": Object { - "actions": Array [ - Object { - "group": "default", - "id": "2", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "1", - "consumer": "bar", - "name": "abc", - "notifyWhen": "onActionGroupChange", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "tags": Array [ - "foo", - ], - "throttle": "30s", - }, - "options": Object { - "id": "custom-id", - }, - }, - ] - `); - - expect(res.ok).toHaveBeenCalledWith({ - body: expectedResult, - }); - }); - - it('allows providing a custom id in non-default space', async () => { - const expectedResult = { - ...createResult, - id: 'custom-id', - }; - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - createAlertRoute({ - router, - licenseState, - encryptedSavedObjects, - usageCounter: mockUsageCounter, - docLinks, - }); - - const [config, handler] = router.post.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id?}"`); - - rulesClient.create.mockResolvedValueOnce(expectedResult); - rulesClient.getSpaceId.mockReturnValueOnce('another-space'); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { id: 'custom-id' }, - body: mockedAlert, - }, - ['ok'] - ); - - expect(await handler(context, req, res)).toEqual({ body: expectedResult }); - - expect(mockUsageCounter.incrementCounter).toHaveBeenCalledTimes(2); - expect(rulesClient.create).toHaveBeenCalledTimes(1); - expect(rulesClient.create.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "data": Object { - "actions": Array [ - Object { - "group": "default", - "id": "2", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "1", - "consumer": "bar", - "name": "abc", - "notifyWhen": "onActionGroupChange", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "tags": Array [ - "foo", - ], - "throttle": "30s", - }, - "options": Object { - "id": "custom-id", - }, - }, - ] - `); - - expect(res.ok).toHaveBeenCalledWith({ - body: expectedResult, - }); - }); - - it('ensures the license allows creating alerts', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); - - createAlertRoute({ router, licenseState, encryptedSavedObjects, docLinks }); - - const [, handler] = router.post.mock.calls[0]; - - rulesClient.create.mockResolvedValueOnce(createResult); - - const [context, req, res] = mockHandlerArguments({ rulesClient }, {}); - - await handler(context, req, res); - - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); - }); - - it('ensures the license check prevents creating alerts', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); - - (verifyApiAccess as jest.Mock).mockImplementation(() => { - throw new Error('OMG'); - }); - - createAlertRoute({ router, licenseState, encryptedSavedObjects, docLinks }); - - const [, handler] = router.post.mock.calls[0]; - - rulesClient.create.mockResolvedValueOnce(createResult); - - const [context, req, res] = mockHandlerArguments({ rulesClient }, {}); - - await expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); - - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); - }); - - it('ensures the alert type gets validated for the license', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); - - createAlertRoute({ router, licenseState, encryptedSavedObjects, docLinks }); - - const [, handler] = router.post.mock.calls[0]; - - rulesClient.create.mockRejectedValue(new RuleTypeDisabledError('Fail', 'license_invalid')); - - const [context, req, res] = mockHandlerArguments({ rulesClient }, {}, ['ok', 'forbidden']); - - await handler(context, req, res); - - expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); - }); - - it('should track every call', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - createAlertRoute({ - router, - licenseState, - encryptedSavedObjects, - usageCounter: mockUsageCounter, - docLinks, - }); - const [, handler] = router.post.mock.calls[0]; - rulesClient.create.mockResolvedValueOnce(createResult); - const [context, req, res] = mockHandlerArguments({ rulesClient }, {}, ['ok']); - await handler(context, req, res); - expect(trackLegacyRouteUsage).toHaveBeenCalledWith('create', mockUsageCounter); - }); - - it('does not return system actions', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - createAlertRoute({ - router, - licenseState, - encryptedSavedObjects, - usageCounter: mockUsageCounter, - docLinks, - }); - - const [config, handler] = router.post.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id?}"`); - - rulesClient.create.mockResolvedValueOnce({ ...createResult, systemActions: [systemAction] }); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - body: mockedAlert, - }, - ['ok'] - ); - - expect(await handler(context, req, res)).toEqual({ body: createResult }); - - expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); - expect(rulesClient.create).toHaveBeenCalledTimes(1); - expect(rulesClient.create.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "data": Object { - "actions": Array [ - Object { - "group": "default", - "id": "2", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "1", - "consumer": "bar", - "name": "abc", - "notifyWhen": "onActionGroupChange", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "tags": Array [ - "foo", - ], - "throttle": "30s", - }, - "options": Object { - "id": undefined, - }, - }, - ] - `); - - expect(res.ok).toHaveBeenCalledWith({ - body: createResult, - }); - }); - - it('should be deprecated', () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - createAlertRoute({ - router, - licenseState, - encryptedSavedObjects, - usageCounter: mockUsageCounter, - docLinks, - }); - - const [config] = router.post.mock.calls[0]; - - expect(config.options?.deprecated).toMatchInlineSnapshot( - { - documentationUrl: expect.stringMatching(/#breaking-201550$/), - }, - ` - Object { - "documentationUrl": StringMatching /#breaking-201550\\$/, - "reason": Object { - "newApiMethod": "POST", - "newApiPath": "/api/alerting/rule/{id?}", - "type": "migrate", - }, - "severity": "warning", - } - ` - ); - }); -}); diff --git a/x-pack/plugins/alerting/server/routes/legacy/create.ts b/x-pack/plugins/alerting/server/routes/legacy/create.ts deleted file mode 100644 index 7e18897554431..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/create.ts +++ /dev/null @@ -1,122 +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. - */ - -import { schema } from '@kbn/config-schema'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { validateDurationSchema } from '../../lib'; -import { handleDisabledApiKeysError } from '../lib/error_handler'; -import { - SanitizedRule, - RuleNotifyWhenType, - RuleTypeParams, - LEGACY_BASE_ALERT_API_PATH, - validateNotifyWhenType, -} from '../../types'; -import { RuleTypeDisabledError } from '../../lib/errors/rule_type_disabled'; -import { RouteOptions } from '..'; -import { countUsageOfPredefinedIds } from '../lib'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../constants'; - -export const bodySchema = schema.object({ - name: schema.string(), - alertTypeId: schema.string(), - enabled: schema.boolean({ defaultValue: true }), - consumer: schema.string(), - tags: schema.arrayOf(schema.string(), { defaultValue: [] }), - throttle: schema.nullable(schema.string({ validate: validateDurationSchema })), - params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), - schedule: schema.object({ - interval: schema.string({ validate: validateDurationSchema }), - }), - actions: schema.arrayOf( - schema.object({ - group: schema.string(), - id: schema.string(), - actionTypeId: schema.maybe(schema.string()), - params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), - }), - { defaultValue: [] } - ), - notifyWhen: schema.nullable(schema.string({ validate: validateNotifyWhenType })), -}); - -export const createAlertRoute = ({ - router, - licenseState, - usageCounter, - isServerless, - docLinks, -}: RouteOptions) => { - router.post( - { - path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{id?}`, - validate: { - params: schema.maybe( - schema.object({ - id: schema.maybe(schema.string()), - }) - ), - body: bodySchema, - }, - security: DEFAULT_ALERTING_ROUTE_SECURITY, - options: { - access: isServerless ? 'internal' : 'public', - summary: 'Create an alert', - tags: ['oas-tag:alerting'], - deprecated: { - documentationUrl: docLinks.links.alerting.legacyRuleApiDeprecations, - severity: 'warning', - reason: { - type: 'migrate', - newApiMethod: 'POST', - newApiPath: '/api/alerting/rule/{id?}', - }, - }, - }, - }, - handleDisabledApiKeysError( - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - - if (!context.alerting) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); - } - - const alertingContext = await context.alerting; - const rulesClient = await alertingContext.getRulesClient(); - const alert = req.body; - const params = req.params; - const notifyWhen = alert?.notifyWhen ? (alert.notifyWhen as RuleNotifyWhenType) : null; - - trackLegacyRouteUsage('create', usageCounter); - - countUsageOfPredefinedIds({ - predefinedId: params?.id, - spaceId: rulesClient.getSpaceId(), - usageCounter, - }); - - try { - const { systemActions, ...alertRes }: SanitizedRule = - await rulesClient.create({ - data: { ...alert, notifyWhen }, - options: { id: params?.id }, - }); - return res.ok({ - body: alertRes, - }); - } catch (e) { - if (e instanceof RuleTypeDisabledError) { - return e.sendResponse(res); - } - throw e; - } - }) - ) - ); -}; diff --git a/x-pack/plugins/alerting/server/routes/legacy/delete.test.ts b/x-pack/plugins/alerting/server/routes/legacy/delete.test.ts deleted file mode 100644 index d8fd0effc50e4..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/delete.test.ts +++ /dev/null @@ -1,172 +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. - */ - -import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; -import { deleteAlertRoute } from './delete'; -import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../../lib/license_state.mock'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { mockHandlerArguments } from '../_mock_handler_arguments'; -import { rulesClientMock } from '../../rules_client.mock'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { docLinksServiceMock } from '@kbn/core/server/mocks'; - -const rulesClient = rulesClientMock.create(); - -jest.mock('../../lib/license_api_access', () => ({ - verifyApiAccess: jest.fn(), -})); - -jest.mock('../../lib/track_legacy_route_usage', () => ({ - trackLegacyRouteUsage: jest.fn(), -})); - -beforeEach(() => { - jest.resetAllMocks(); -}); - -describe('deleteAlertRoute', () => { - const docLinks = docLinksServiceMock.createSetupContract(); - - it('deletes an alert with proper parameters', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - deleteAlertRoute(router, licenseState, docLinks); - - const [config, handler] = router.delete.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`); - expect(config.options?.access).toBe('public'); - - rulesClient.delete.mockResolvedValueOnce({}); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { - id: '1', - }, - }, - ['noContent'] - ); - - expect(await handler(context, req, res)).toEqual(undefined); - - expect(rulesClient.delete).toHaveBeenCalledTimes(1); - expect(rulesClient.delete.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "id": "1", - }, - ] - `); - - expect(res.noContent).toHaveBeenCalled(); - }); - - it('should have internal access for serverless', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - deleteAlertRoute(router, licenseState, docLinks, undefined, true); - - const [config] = router.delete.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`); - expect(config.options?.access).toBe('internal'); - }); - - it('ensures the license allows deleting alerts', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - deleteAlertRoute(router, licenseState, docLinks); - - const [, handler] = router.delete.mock.calls[0]; - - rulesClient.delete.mockResolvedValueOnce({}); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { id: '1' }, - } - ); - - await handler(context, req, res); - - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); - }); - - it('ensures the license check prevents deleting alerts', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - (verifyApiAccess as jest.Mock).mockImplementation(() => { - throw new Error('OMG'); - }); - - deleteAlertRoute(router, licenseState, docLinks); - - const [, handler] = router.delete.mock.calls[0]; - - rulesClient.delete.mockResolvedValueOnce({}); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - id: '1', - } - ); - - await expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); - - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); - }); - - it('should track every call', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - deleteAlertRoute(router, licenseState, docLinks, mockUsageCounter); - const [, handler] = router.delete.mock.calls[0]; - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: { id: '1' } }, [ - 'ok', - ]); - await handler(context, req, res); - expect(trackLegacyRouteUsage).toHaveBeenCalledWith('delete', mockUsageCounter); - }); - - it('should be deprecated', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - deleteAlertRoute(router, licenseState, docLinks); - - const [config] = router.delete.mock.calls[0]; - - expect(config.options?.deprecated).toMatchInlineSnapshot( - { - documentationUrl: expect.stringMatching(/#breaking-201550$/), - }, - ` - Object { - "documentationUrl": StringMatching /#breaking-201550\\$/, - "reason": Object { - "newApiMethod": "DELETE", - "newApiPath": "/api/alerting/rule/{id}", - "type": "migrate", - }, - "severity": "warning", - } - ` - ); - }); -}); diff --git a/x-pack/plugins/alerting/server/routes/legacy/delete.ts b/x-pack/plugins/alerting/server/routes/legacy/delete.ts deleted file mode 100644 index 6afa7474d81e6..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/delete.ts +++ /dev/null @@ -1,64 +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. - */ - -import { schema } from '@kbn/config-schema'; -import { UsageCounter } from '@kbn/usage-collection-plugin/server'; -import { DocLinksServiceSetup } from '@kbn/core/server'; -import type { AlertingRouter } from '../../types'; -import { ILicenseState } from '../../lib/license_state'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../constants'; - -const paramSchema = schema.object({ - id: schema.string(), -}); - -export const deleteAlertRoute = ( - router: AlertingRouter, - licenseState: ILicenseState, - docLinks: DocLinksServiceSetup, - usageCounter?: UsageCounter, - isServerless?: boolean -) => { - router.delete( - { - path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{id}`, - validate: { - params: paramSchema, - }, - security: DEFAULT_ALERTING_ROUTE_SECURITY, - options: { - access: isServerless ? 'internal' : 'public', - summary: 'Delete an alert', - tags: ['oas-tag:alerting'], - deprecated: { - documentationUrl: docLinks.links.alerting.legacyRuleApiDeprecations, - severity: 'warning', - reason: { - type: 'migrate', - newApiMethod: 'DELETE', - newApiPath: '/api/alerting/rule/{id}', - }, - }, - }, - }, - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - if (!context.alerting) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); - } - trackLegacyRouteUsage('delete', usageCounter); - const alertingContext = await context.alerting; - const rulesClient = await alertingContext.getRulesClient(); - const { id } = req.params; - await rulesClient.delete({ id }); - return res.noContent(); - }) - ); -}; diff --git a/x-pack/plugins/alerting/server/routes/legacy/disable.test.ts b/x-pack/plugins/alerting/server/routes/legacy/disable.test.ts deleted file mode 100644 index 13fec185429ed..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/disable.test.ts +++ /dev/null @@ -1,143 +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. - */ -import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; -import { disableAlertRoute } from './disable'; -import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../../lib/license_state.mock'; -import { mockHandlerArguments } from '../_mock_handler_arguments'; -import { rulesClientMock } from '../../rules_client.mock'; -import { RuleTypeDisabledError } from '../../lib/errors/rule_type_disabled'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { docLinksServiceMock } from '@kbn/core/server/mocks'; - -const rulesClient = rulesClientMock.create(); - -jest.mock('../../lib/license_api_access', () => ({ - verifyApiAccess: jest.fn(), -})); - -jest.mock('../../lib/track_legacy_route_usage', () => ({ - trackLegacyRouteUsage: jest.fn(), -})); - -beforeEach(() => { - jest.resetAllMocks(); -}); - -describe('disableAlertRoute', () => { - const docLinks = docLinksServiceMock.createSetupContract(); - - it('disables an alert', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - disableAlertRoute(router, licenseState, docLinks); - - const [config, handler] = router.post.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_disable"`); - expect(config.options?.access).toBe('public'); - - rulesClient.disableRule.mockResolvedValueOnce(); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { - id: '1', - }, - }, - ['noContent'] - ); - - expect(await handler(context, req, res)).toEqual(undefined); - - expect(rulesClient.disableRule).toHaveBeenCalledTimes(1); - expect(rulesClient.disableRule.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "id": "1", - }, - ] - `); - - expect(res.noContent).toHaveBeenCalled(); - }); - - it('should have internal access for serverless', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - disableAlertRoute(router, licenseState, docLinks, undefined, true); - - const [config] = router.post.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_disable"`); - expect(config.options?.access).toBe('internal'); - }); - - it('ensures the alert type gets validated for the license', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - disableAlertRoute(router, licenseState, docLinks); - - const [, handler] = router.post.mock.calls[0]; - - rulesClient.disableRule.mockRejectedValue(new RuleTypeDisabledError('Fail', 'license_invalid')); - - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, body: {} }, [ - 'ok', - 'forbidden', - ]); - - await handler(context, req, res); - - expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); - }); - - it('should track every call', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - disableAlertRoute(router, licenseState, docLinks, mockUsageCounter); - const [, handler] = router.post.mock.calls[0]; - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: { id: '1' } }, [ - 'ok', - ]); - await handler(context, req, res); - expect(trackLegacyRouteUsage).toHaveBeenCalledWith('disable', mockUsageCounter); - }); - - it('should be deprecated', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - disableAlertRoute(router, licenseState, docLinks); - - const [config] = router.post.mock.calls[0]; - - expect(config.options?.deprecated).toMatchInlineSnapshot( - { - documentationUrl: expect.stringMatching(/#breaking-201550$/), - }, - ` - Object { - "documentationUrl": StringMatching /#breaking-201550\\$/, - "reason": Object { - "newApiMethod": "POST", - "newApiPath": "/api/alerting/rule/{id}/_disable", - "type": "migrate", - }, - "severity": "warning", - } - ` - ); - }); -}); diff --git a/x-pack/plugins/alerting/server/routes/legacy/disable.ts b/x-pack/plugins/alerting/server/routes/legacy/disable.ts deleted file mode 100644 index 77fe548d06dac..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/disable.ts +++ /dev/null @@ -1,72 +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. - */ - -import { schema } from '@kbn/config-schema'; -import { UsageCounter } from '@kbn/usage-collection-plugin/server'; -import { DocLinksServiceSetup } from '@kbn/core/server'; -import type { AlertingRouter } from '../../types'; -import { ILicenseState } from '../../lib/license_state'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; -import { RuleTypeDisabledError } from '../../lib/errors/rule_type_disabled'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../constants'; - -const paramSchema = schema.object({ - id: schema.string(), -}); - -export const disableAlertRoute = ( - router: AlertingRouter, - licenseState: ILicenseState, - docLinks: DocLinksServiceSetup, - usageCounter?: UsageCounter, - isServerless?: boolean -) => { - router.post( - { - path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{id}/_disable`, - validate: { - params: paramSchema, - }, - security: DEFAULT_ALERTING_ROUTE_SECURITY, - options: { - access: isServerless ? 'internal' : 'public', - summary: 'Disable an alert', - tags: ['oas-tag:alerting'], - deprecated: { - documentationUrl: docLinks.links.alerting.legacyRuleApiDeprecations, - severity: 'warning', - reason: { - type: 'migrate', - newApiMethod: 'POST', - newApiPath: '/api/alerting/rule/{id}/_disable', - }, - }, - }, - }, - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - if (!context.alerting) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); - } - trackLegacyRouteUsage('disable', usageCounter); - const alertingContext = await context.alerting; - const rulesClient = await alertingContext.getRulesClient(); - const { id } = req.params; - try { - await rulesClient.disableRule({ id }); - return res.noContent(); - } catch (e) { - if (e instanceof RuleTypeDisabledError) { - return e.sendResponse(res); - } - throw e; - } - }) - ); -}; diff --git a/x-pack/plugins/alerting/server/routes/legacy/enable.test.ts b/x-pack/plugins/alerting/server/routes/legacy/enable.test.ts deleted file mode 100644 index 88df304ea07c0..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/enable.test.ts +++ /dev/null @@ -1,143 +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. - */ -import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; -import { enableAlertRoute } from './enable'; -import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../../lib/license_state.mock'; -import { mockHandlerArguments } from '../_mock_handler_arguments'; -import { rulesClientMock } from '../../rules_client.mock'; -import { RuleTypeDisabledError } from '../../lib/errors/rule_type_disabled'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { docLinksServiceMock } from '@kbn/core/server/mocks'; - -const rulesClient = rulesClientMock.create(); - -jest.mock('../../lib/license_api_access', () => ({ - verifyApiAccess: jest.fn(), -})); - -jest.mock('../../lib/track_legacy_route_usage', () => ({ - trackLegacyRouteUsage: jest.fn(), -})); - -beforeEach(() => { - jest.resetAllMocks(); -}); - -describe('enableAlertRoute', () => { - const docLinks = docLinksServiceMock.createSetupContract(); - - it('enables an alert', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - enableAlertRoute(router, licenseState, docLinks); - - const [config, handler] = router.post.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_enable"`); - expect(config.options?.access).toBe('public'); - - rulesClient.enableRule.mockResolvedValueOnce(); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { - id: '1', - }, - }, - ['noContent'] - ); - - expect(await handler(context, req, res)).toEqual(undefined); - - expect(rulesClient.enableRule).toHaveBeenCalledTimes(1); - expect(rulesClient.enableRule.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "id": "1", - }, - ] - `); - - expect(res.noContent).toHaveBeenCalled(); - }); - - it('should have internal access for serverless', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - enableAlertRoute(router, licenseState, docLinks, undefined, true); - - const [config] = router.post.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_enable"`); - expect(config.options?.access).toBe('internal'); - }); - - it('ensures the alert type gets validated for the license', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - enableAlertRoute(router, licenseState, docLinks); - - const [, handler] = router.post.mock.calls[0]; - - rulesClient.enableRule.mockRejectedValue(new RuleTypeDisabledError('Fail', 'license_invalid')); - - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, body: {} }, [ - 'ok', - 'forbidden', - ]); - - await handler(context, req, res); - - expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); - }); - - it('should track every call', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - enableAlertRoute(router, licenseState, docLinks, mockUsageCounter); - const [, handler] = router.post.mock.calls[0]; - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: { id: '1' } }, [ - 'ok', - ]); - await handler(context, req, res); - expect(trackLegacyRouteUsage).toHaveBeenCalledWith('enable', mockUsageCounter); - }); - - it('should be deprecated', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - enableAlertRoute(router, licenseState, docLinks, undefined, true); - - const [config] = router.post.mock.calls[0]; - - expect(config.options?.deprecated).toMatchInlineSnapshot( - { - documentationUrl: expect.stringMatching(/#breaking-201550$/), - }, - ` - Object { - "documentationUrl": StringMatching /#breaking-201550\\$/, - "reason": Object { - "newApiMethod": "POST", - "newApiPath": "/api/alerting/rule/{id}/_enable", - "type": "migrate", - }, - "severity": "warning", - } - ` - ); - }); -}); diff --git a/x-pack/plugins/alerting/server/routes/legacy/enable.ts b/x-pack/plugins/alerting/server/routes/legacy/enable.ts deleted file mode 100644 index 954c8bc727174..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/enable.ts +++ /dev/null @@ -1,75 +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. - */ - -import { schema } from '@kbn/config-schema'; -import { UsageCounter } from '@kbn/usage-collection-plugin/server'; -import { DocLinksServiceSetup } from '@kbn/core/server'; -import type { AlertingRouter } from '../../types'; -import { ILicenseState } from '../../lib/license_state'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; -import { handleDisabledApiKeysError } from '../lib/error_handler'; -import { RuleTypeDisabledError } from '../../lib/errors/rule_type_disabled'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../constants'; - -const paramSchema = schema.object({ - id: schema.string(), -}); - -export const enableAlertRoute = ( - router: AlertingRouter, - licenseState: ILicenseState, - docLinks: DocLinksServiceSetup, - usageCounter?: UsageCounter, - isServerless?: boolean -) => { - router.post( - { - path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{id}/_enable`, - validate: { - params: paramSchema, - }, - security: DEFAULT_ALERTING_ROUTE_SECURITY, - options: { - access: isServerless ? 'internal' : 'public', - summary: 'Enable an alert', - tags: ['oas-tag:alerting'], - deprecated: { - documentationUrl: docLinks.links.alerting.legacyRuleApiDeprecations, - severity: 'warning', - reason: { - type: 'migrate', - newApiMethod: 'POST', - newApiPath: '/api/alerting/rule/{id}/_enable', - }, - }, - }, - }, - handleDisabledApiKeysError( - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - if (!context.alerting) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); - } - trackLegacyRouteUsage('enable', usageCounter); - const alertingContext = await context.alerting; - const rulesClient = await alertingContext.getRulesClient(); - const { id } = req.params; - try { - await rulesClient.enableRule({ id }); - return res.noContent(); - } catch (e) { - if (e instanceof RuleTypeDisabledError) { - return e.sendResponse(res); - } - throw e; - } - }) - ) - ); -}; diff --git a/x-pack/plugins/alerting/server/routes/legacy/find.test.ts b/x-pack/plugins/alerting/server/routes/legacy/find.test.ts deleted file mode 100644 index 646f18fa072ea..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/find.test.ts +++ /dev/null @@ -1,439 +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. - */ - -import { omit } from 'lodash'; -import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; -import { findAlertRoute } from './find'; -import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../../lib/license_state.mock'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { mockHandlerArguments } from '../_mock_handler_arguments'; -import { rulesClientMock } from '../../rules_client.mock'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { trackLegacyTerminology } from '../lib/track_legacy_terminology'; -import { docLinksServiceMock } from '@kbn/core/server/mocks'; - -const rulesClient = rulesClientMock.create(); - -jest.mock('../../lib/license_api_access', () => ({ - verifyApiAccess: jest.fn(), -})); - -jest.mock('../../lib/track_legacy_route_usage', () => ({ - trackLegacyRouteUsage: jest.fn(), -})); - -jest.mock('../lib/track_legacy_terminology', () => ({ - trackLegacyTerminology: jest.fn(), -})); - -beforeEach(() => { - jest.resetAllMocks(); -}); - -describe('findAlertRoute', () => { - const docLinks = docLinksServiceMock.createSetupContract(); - - it('finds alerts with proper parameters', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - findAlertRoute(router, licenseState, docLinks); - - const [config, handler] = router.get.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/_find"`); - expect(config.options?.access).toBe('public'); - - const findResult = { - page: 1, - perPage: 1, - total: 0, - data: [], - }; - rulesClient.find.mockResolvedValueOnce(findResult); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - query: { - per_page: 1, - page: 1, - default_search_operator: 'OR', - }, - }, - ['ok'] - ); - - expect(await handler(context, req, res)).toMatchInlineSnapshot(` - Object { - "body": Object { - "data": Array [], - "page": 1, - "perPage": 1, - "total": 0, - }, - } - `); - - expect(rulesClient.find).toHaveBeenCalledTimes(1); - expect(rulesClient.find.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "excludeFromPublicApi": true, - "options": Object { - "defaultSearchOperator": "OR", - "page": 1, - "perPage": 1, - }, - }, - ] - `); - - expect(res.ok).toHaveBeenCalledWith({ - body: findResult, - }); - }); - - it('should have internal access for serverless', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - findAlertRoute(router, licenseState, docLinks, undefined, true); - - const [config] = router.get.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/_find"`); - expect(config.options?.access).toBe('internal'); - }); - - it('ensures the license allows finding alerts', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - findAlertRoute(router, licenseState, docLinks); - - const [, handler] = router.get.mock.calls[0]; - - rulesClient.find.mockResolvedValueOnce({ - page: 1, - perPage: 1, - total: 0, - data: [], - }); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - query: { - per_page: 1, - page: 1, - default_search_operator: 'OR', - }, - } - ); - - await handler(context, req, res); - - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); - }); - - it('ensures the license check prevents finding alerts', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - (verifyApiAccess as jest.Mock).mockImplementation(() => { - throw new Error('OMG'); - }); - - findAlertRoute(router, licenseState, docLinks); - - const [, handler] = router.get.mock.calls[0]; - - const [context, req, res] = mockHandlerArguments( - {}, - { - query: { - per_page: 1, - page: 1, - default_search_operator: 'OR', - }, - }, - ['ok'] - ); - await expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); - - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); - }); - - it('should track every call', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - findAlertRoute(router, licenseState, docLinks, mockUsageCounter); - const [, handler] = router.get.mock.calls[0]; - const findResult = { - page: 1, - perPage: 1, - total: 0, - data: [], - }; - rulesClient.find.mockResolvedValueOnce(findResult); - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, query: {} }, [ - 'ok', - ]); - await handler(context, req, res); - expect(trackLegacyRouteUsage).toHaveBeenCalledWith('find', mockUsageCounter); - }); - - it('should track calls with deprecated param values', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - findAlertRoute(router, licenseState, docLinks, mockUsageCounter); - const [, handler] = router.get.mock.calls[0]; - - const findResult = { - page: 1, - perPage: 1, - total: 0, - data: [], - }; - rulesClient.find.mockResolvedValueOnce(findResult); - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: {}, - query: { - search_fields: ['alertTypeId:1', 'message:foo'], - search: 'alertTypeId:2', - sort_field: 'alertTypeId', - }, - }, - ['ok'] - ); - await handler(context, req, res); - expect(trackLegacyTerminology).toHaveBeenCalledTimes(1); - expect((trackLegacyTerminology as jest.Mock).mock.calls[0][0]).toStrictEqual([ - 'alertTypeId:2', - ['alertTypeId:1', 'message:foo'], - 'alertTypeId', - ]); - }); - - it('should track calls to deprecated functionality', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - findAlertRoute(router, licenseState, docLinks, mockUsageCounter); - const [, handler] = router.get.mock.calls[0]; - const findResult = { - page: 1, - perPage: 1, - total: 0, - data: [], - }; - rulesClient.find.mockResolvedValueOnce(findResult); - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: {}, - query: { - fields: ['foo', 'bar'], - }, - }, - ['ok'] - ); - await handler(context, req, res); - expect(mockUsageCounter.incrementCounter).toHaveBeenCalledWith({ - counterName: `legacyAlertingFieldsUsage`, - counterType: 'alertingFieldsUsage', - incrementBy: 1, - }); - }); - - it('does not return system actions', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - findAlertRoute(router, licenseState, docLinks); - - const [config, handler] = router.get.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/_find"`); - - const findResult = { - page: 1, - perPage: 1, - total: 0, - data: [ - { - id: '3d534c70-582b-11ec-8995-2b1578a3bc5d', - notifyWhen: 'onActiveAlert' as const, - alertTypeId: '.index-threshold', - name: 'stressing index-threshold 37/200', - consumer: 'alerts', - tags: [], - enabled: true, - throttle: null, - apiKey: null, - apiKeyOwner: '2889684073', - createdBy: 'elastic', - updatedBy: '2889684073', - muteAll: false, - mutedInstanceIds: [], - schedule: { - interval: '1s', - }, - actions: [ - { - actionTypeId: '.server-log', - params: { - message: 'alert 37: {{context.message}}', - }, - group: 'threshold met', - id: '3619a0d0-582b-11ec-8995-2b1578a3bc5d', - uuid: '123-456', - }, - ], - systemActions: [ - { actionTypeId: '.test', id: 'system_action-id', params: {}, uuid: '789' }, - ], - params: { x: 42 }, - updatedAt: '2024-03-21T13:15:00.498Z', - createdAt: '2024-03-21T13:15:00.498Z', - scheduledTaskId: '52125fb0-5895-11ec-ae69-bb65d1a71b72', - executionStatus: { - status: 'ok' as const, - lastExecutionDate: '2024-03-21T13:15:00.498Z', - lastDuration: 1194, - }, - revision: 0, - }, - ], - }; - - // @ts-expect-error: TS complains about dates being string and not a Date object - rulesClient.find.mockResolvedValueOnce(findResult); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - query: { - per_page: 1, - page: 1, - default_search_operator: 'OR', - }, - }, - ['ok'] - ); - - expect(await handler(context, req, res)).toMatchInlineSnapshot(` - Object { - "body": Object { - "data": Array [ - Object { - "actions": Array [ - Object { - "actionTypeId": ".server-log", - "group": "threshold met", - "id": "3619a0d0-582b-11ec-8995-2b1578a3bc5d", - "params": Object { - "message": "alert 37: {{context.message}}", - }, - "uuid": "123-456", - }, - ], - "alertTypeId": ".index-threshold", - "apiKey": null, - "apiKeyOwner": "2889684073", - "consumer": "alerts", - "createdAt": "2024-03-21T13:15:00.498Z", - "createdBy": "elastic", - "enabled": true, - "executionStatus": Object { - "lastDuration": 1194, - "lastExecutionDate": "2024-03-21T13:15:00.498Z", - "status": "ok", - }, - "id": "3d534c70-582b-11ec-8995-2b1578a3bc5d", - "muteAll": false, - "mutedInstanceIds": Array [], - "name": "stressing index-threshold 37/200", - "notifyWhen": "onActiveAlert", - "params": Object { - "x": 42, - }, - "revision": 0, - "schedule": Object { - "interval": "1s", - }, - "scheduledTaskId": "52125fb0-5895-11ec-ae69-bb65d1a71b72", - "tags": Array [], - "throttle": null, - "updatedAt": "2024-03-21T13:15:00.498Z", - "updatedBy": "2889684073", - }, - ], - "page": 1, - "perPage": 1, - "total": 0, - }, - } - `); - - expect(rulesClient.find).toHaveBeenCalledTimes(1); - expect(rulesClient.find.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "excludeFromPublicApi": true, - "options": Object { - "defaultSearchOperator": "OR", - "page": 1, - "perPage": 1, - }, - }, - ] - `); - - expect(res.ok).toHaveBeenCalledWith({ - body: omit(findResult, 'data[0].systemActions'), - }); - }); - - it('should be deprecated', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - findAlertRoute(router, licenseState, docLinks); - - const [config] = router.get.mock.calls[0]; - - expect(config.options?.deprecated).toMatchInlineSnapshot( - { - documentationUrl: expect.stringMatching(/#breaking-201550$/), - }, - ` - Object { - "documentationUrl": StringMatching /#breaking-201550\\$/, - "reason": Object { - "newApiMethod": "GET", - "newApiPath": "/api/alerting/rules/_find", - "type": "migrate", - }, - "severity": "warning", - } - ` - ); - }); -}); diff --git a/x-pack/plugins/alerting/server/routes/legacy/find.ts b/x-pack/plugins/alerting/server/routes/legacy/find.ts deleted file mode 100644 index ece93d082f785..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/find.ts +++ /dev/null @@ -1,150 +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. - */ - -import { schema } from '@kbn/config-schema'; -import { UsageCounter } from '@kbn/usage-collection-plugin/server'; -import { estypes } from '@elastic/elasticsearch'; -import { KueryNode } from '@kbn/es-query'; -import { DocLinksServiceSetup } from '@kbn/core/server'; -import type { AlertingRouter } from '../../types'; - -import { ILicenseState } from '../../lib/license_state'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; -import { renameKeys } from '../lib/rename_keys'; -import { IndexType } from '../../rules_client'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { trackLegacyTerminology } from '../lib/track_legacy_terminology'; -import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../constants'; - -export interface FindOptions extends IndexType { - perPage?: number; - page?: number; - search?: string; - defaultSearchOperator?: 'AND' | 'OR'; - searchFields?: string[]; - sortField?: string; - sortOrder?: estypes.SortOrder; - hasReference?: { - type: string; - id: string; - }; - fields?: string[]; - filter?: string | KueryNode; - filterConsumers?: string[]; -} - -// config definition -const querySchema = schema.object({ - per_page: schema.number({ defaultValue: 10, min: 0 }), - page: schema.number({ defaultValue: 1, min: 1 }), - search: schema.maybe(schema.string()), - default_search_operator: schema.oneOf([schema.literal('OR'), schema.literal('AND')], { - defaultValue: 'OR', - }), - search_fields: schema.maybe(schema.oneOf([schema.arrayOf(schema.string()), schema.string()])), - sort_field: schema.maybe(schema.string()), - sort_order: schema.maybe(schema.oneOf([schema.literal('asc'), schema.literal('desc')])), - has_reference: schema.maybe( - // use nullable as maybe is currently broken - // in config-schema - schema.nullable( - schema.object({ - type: schema.string(), - id: schema.string(), - }) - ) - ), - fields: schema.maybe(schema.arrayOf(schema.string())), - filter: schema.maybe(schema.string()), -}); - -export const findAlertRoute = ( - router: AlertingRouter, - licenseState: ILicenseState, - docLinks: DocLinksServiceSetup, - usageCounter?: UsageCounter, - isServerless?: boolean -) => { - router.get( - { - path: `${LEGACY_BASE_ALERT_API_PATH}/_find`, - validate: { - query: querySchema, - }, - security: DEFAULT_ALERTING_ROUTE_SECURITY, - options: { - access: isServerless ? 'internal' : 'public', - summary: 'Find alerts', - tags: ['oas-tag:alerting'], - description: - 'Gets a paginated set of alerts. Alert `params` are stored as a flattened field type and analyzed as keywords. As alerts change in Kibana, the results on each page of the response also change. Use the find API for traditional paginated results, but avoid using it to export large amounts of data.', - deprecated: { - documentationUrl: docLinks.links.alerting.legacyRuleApiDeprecations, - severity: 'warning', - reason: { - type: 'migrate', - newApiMethod: 'GET', - newApiPath: '/api/alerting/rules/_find', - }, - }, - }, - }, - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - if (!context.alerting) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); - } - trackLegacyRouteUsage('find', usageCounter); - trackLegacyTerminology( - [req.query.search, req.query.search_fields, req.query.sort_field].filter( - Boolean - ) as string[], - usageCounter - ); - const alertingContext = await context.alerting; - const rulesClient = await alertingContext.getRulesClient(); - - const query = req.query; - const renameMap = { - default_search_operator: 'defaultSearchOperator', - fields: 'fields', - has_reference: 'hasReference', - page: 'page', - per_page: 'perPage', - search: 'search', - sort_field: 'sortField', - sort_order: 'sortOrder', - filter: 'filter', - }; - - const options = renameKeys>(renameMap, query); - - if (query.search_fields) { - options.searchFields = Array.isArray(query.search_fields) - ? query.search_fields - : [query.search_fields]; - } - - if (query.fields) { - usageCounter?.incrementCounter({ - counterName: `legacyAlertingFieldsUsage`, - counterType: 'alertingFieldsUsage', - incrementBy: 1, - }); - } - - const findResult = await rulesClient.find({ options, excludeFromPublicApi: true }); - return res.ok({ - body: { - ...findResult, - data: findResult.data.map(({ systemActions, ...rule }) => rule), - }, - }); - }) - ); -}; diff --git a/x-pack/plugins/alerting/server/routes/legacy/get.test.ts b/x-pack/plugins/alerting/server/routes/legacy/get.test.ts deleted file mode 100644 index ca8154fc7adae..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/get.test.ts +++ /dev/null @@ -1,243 +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. - */ - -import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; -import { getAlertRoute } from './get'; -import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../../lib/license_state.mock'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { mockHandlerArguments } from '../_mock_handler_arguments'; -import { rulesClientMock } from '../../rules_client.mock'; -import { Rule, RuleSystemAction } from '../../../common'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { docLinksServiceMock } from '@kbn/core/server/mocks'; - -const rulesClient = rulesClientMock.create(); -jest.mock('../../lib/license_api_access', () => ({ - verifyApiAccess: jest.fn(), -})); - -jest.mock('../../lib/track_legacy_route_usage', () => ({ - trackLegacyRouteUsage: jest.fn(), -})); - -beforeEach(() => { - jest.resetAllMocks(); -}); - -describe('getAlertRoute', () => { - const docLinks = docLinksServiceMock.createSetupContract(); - const mockedAlert: Rule<{ - bar: true; - }> = { - id: '1', - alertTypeId: '1', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - createdAt: new Date(), - updatedAt: new Date(), - actions: [ - { - group: 'default', - id: '2', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - consumer: 'bar', - name: 'abc', - tags: ['foo'], - enabled: true, - muteAll: false, - notifyWhen: 'onActionGroupChange', - createdBy: '', - updatedBy: '', - apiKey: '', - apiKeyOwner: '', - throttle: '30s', - mutedInstanceIds: [], - executionStatus: { - status: 'unknown', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - }, - revision: 0, - }; - - const systemAction: RuleSystemAction = { - actionTypeId: 'test-2', - id: 'system_action-id', - params: { - foo: true, - }, - uuid: '123-456', - }; - - it('gets an alert with proper parameters', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - getAlertRoute(router, licenseState, docLinks); - const [config, handler] = router.get.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`); - expect(config.options?.access).toBe('public'); - - rulesClient.get.mockResolvedValueOnce(mockedAlert); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { id: '1' }, - }, - ['ok'] - ); - await handler(context, req, res); - - expect(rulesClient.get).toHaveBeenCalledTimes(1); - expect(rulesClient.get.mock.calls[0][0].id).toEqual('1'); - - expect(res.ok).toHaveBeenCalledWith({ - body: mockedAlert, - }); - }); - - it('should have internal access for serverless', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - getAlertRoute(router, licenseState, docLinks, undefined, true); - const [config] = router.get.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`); - expect(config.options?.access).toBe('internal'); - }); - - it('ensures the license allows getting alerts', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - getAlertRoute(router, licenseState, docLinks); - - const [, handler] = router.get.mock.calls[0]; - - rulesClient.get.mockResolvedValueOnce(mockedAlert); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { id: '1' }, - }, - ['ok'] - ); - - await handler(context, req, res); - - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); - }); - - it('ensures the license check prevents getting alerts', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - (verifyApiAccess as jest.Mock).mockImplementation(() => { - throw new Error('OMG'); - }); - - getAlertRoute(router, licenseState, docLinks); - - const [, handler] = router.get.mock.calls[0]; - - rulesClient.get.mockResolvedValueOnce(mockedAlert); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { id: '1' }, - }, - ['ok'] - ); - - await expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); - - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); - }); - - it('should track every call', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - getAlertRoute(router, licenseState, docLinks, mockUsageCounter); - const [, handler] = router.get.mock.calls[0]; - - rulesClient.get.mockResolvedValueOnce(mockedAlert); - - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: { id: '1' } }, [ - 'ok', - ]); - await handler(context, req, res); - expect(trackLegacyRouteUsage).toHaveBeenCalledWith('get', mockUsageCounter); - }); - - it('does not return system actions', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - getAlertRoute(router, licenseState, docLinks); - const [config, handler] = router.get.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`); - - rulesClient.get.mockResolvedValueOnce({ ...mockedAlert, systemActions: [systemAction] }); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { id: '1' }, - }, - ['ok'] - ); - await handler(context, req, res); - - expect(rulesClient.get).toHaveBeenCalledTimes(1); - expect(rulesClient.get.mock.calls[0][0].id).toEqual('1'); - - expect(res.ok).toHaveBeenCalledWith({ - body: mockedAlert, - }); - }); - - it('should be deprecated', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - getAlertRoute(router, licenseState, docLinks); - const [config] = router.get.mock.calls[0]; - - expect(config.options?.deprecated).toMatchInlineSnapshot( - { - documentationUrl: expect.stringMatching(/#breaking-201550$/), - }, - ` - Object { - "documentationUrl": StringMatching /#breaking-201550\\$/, - "reason": Object { - "newApiMethod": "GET", - "newApiPath": "/api/alerting/rule/{id}", - "type": "migrate", - }, - "severity": "warning", - } - ` - ); - }); -}); diff --git a/x-pack/plugins/alerting/server/routes/legacy/get.ts b/x-pack/plugins/alerting/server/routes/legacy/get.ts deleted file mode 100644 index f1f83a5084c44..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/get.ts +++ /dev/null @@ -1,66 +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. - */ - -import { schema } from '@kbn/config-schema'; -import { UsageCounter } from '@kbn/usage-collection-plugin/server'; -import { DocLinksServiceSetup } from '@kbn/core/server'; -import { ILicenseState } from '../../lib/license_state'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; -import type { AlertingRouter } from '../../types'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../constants'; - -const paramSchema = schema.object({ - id: schema.string(), -}); - -export const getAlertRoute = ( - router: AlertingRouter, - licenseState: ILicenseState, - docLinks: DocLinksServiceSetup, - usageCounter?: UsageCounter, - isServerless?: boolean -) => { - router.get( - { - path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{id}`, - validate: { - params: paramSchema, - }, - security: DEFAULT_ALERTING_ROUTE_SECURITY, - options: { - access: isServerless ? 'internal' : 'public', - summary: 'Get an alert', - tags: ['oas-tag:alerting'], - deprecated: { - documentationUrl: docLinks.links.alerting.legacyRuleApiDeprecations, - severity: 'warning', - reason: { - type: 'migrate', - newApiMethod: 'GET', - newApiPath: '/api/alerting/rule/{id}', - }, - }, - }, - }, - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - if (!context.alerting) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); - } - trackLegacyRouteUsage('get', usageCounter); - const alertingContext = await context.alerting; - const rulesClient = await alertingContext.getRulesClient(); - const { id } = req.params; - const { systemActions, ...rule } = await rulesClient.get({ id, excludeFromPublicApi: true }); - return res.ok({ - body: rule, - }); - }) - ); -}; diff --git a/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts b/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts deleted file mode 100644 index cc3deaad9af99..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts +++ /dev/null @@ -1,177 +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. - */ -import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; -import { getAlertInstanceSummaryRoute } from './get_alert_instance_summary'; -import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../../lib/license_state.mock'; -import { mockHandlerArguments } from '../_mock_handler_arguments'; -import { SavedObjectsErrorHelpers } from '@kbn/core/server'; -import { rulesClientMock } from '../../rules_client.mock'; -import { AlertSummary } from '../../types'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects'; -import { docLinksServiceMock } from '@kbn/core/server/mocks'; - -const rulesClient = rulesClientMock.create(); -jest.mock('../../lib/license_api_access', () => ({ - verifyApiAccess: jest.fn(), -})); - -jest.mock('../../lib/track_legacy_route_usage', () => ({ - trackLegacyRouteUsage: jest.fn(), -})); - -beforeEach(() => { - jest.resetAllMocks(); -}); - -describe('getAlertInstanceSummaryRoute', () => { - const docLinks = docLinksServiceMock.createSetupContract(); - const dateString = new Date().toISOString(); - const mockedAlertInstanceSummary: AlertSummary = { - id: '', - name: '', - tags: [], - ruleTypeId: '', - consumer: '', - muteAll: false, - throttle: null, - enabled: false, - statusStartDate: dateString, - statusEndDate: dateString, - status: 'OK', - errorMessages: [], - alerts: {}, - executionDuration: { - average: 0, - valuesWithTimestamp: {}, - }, - revision: 0, - }; - - it('gets alert instance summary', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - getAlertInstanceSummaryRoute(router, licenseState, docLinks); - - const [config, handler] = router.get.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_instance_summary"`); - expect(config.options?.access).toBe('public'); - - rulesClient.getAlertSummary.mockResolvedValueOnce(mockedAlertInstanceSummary); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { - id: '1', - }, - query: {}, - }, - ['ok'] - ); - - await handler(context, req, res); - - expect(rulesClient.getAlertSummary).toHaveBeenCalledTimes(1); - expect(rulesClient.getAlertSummary.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "dateStart": undefined, - "id": "1", - }, - ] - `); - - expect(res.ok).toHaveBeenCalled(); - }); - - it('should have internal access for serverless', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - getAlertInstanceSummaryRoute(router, licenseState, docLinks, undefined, true); - - const [config] = router.get.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_instance_summary"`); - expect(config.options?.access).toBe('internal'); - }); - - it('returns NOT-FOUND when alert is not found', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - getAlertInstanceSummaryRoute(router, licenseState, docLinks); - - const [, handler] = router.get.mock.calls[0]; - - rulesClient.getAlertSummary = jest - .fn() - .mockResolvedValueOnce( - SavedObjectsErrorHelpers.createGenericNotFoundError(RULE_SAVED_OBJECT_TYPE, '1') - ); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { - id: '1', - }, - query: {}, - }, - ['notFound'] - ); - - expect(await handler(context, req, res)).toEqual(undefined); - }); - - it('should track every call', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - getAlertInstanceSummaryRoute(router, licenseState, docLinks, mockUsageCounter); - const [, handler] = router.get.mock.calls[0]; - - rulesClient.getAlertSummary.mockResolvedValueOnce(mockedAlertInstanceSummary); - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { params: { id: '1' }, query: {} }, - ['ok'] - ); - await handler(context, req, res); - expect(trackLegacyRouteUsage).toHaveBeenCalledWith('instanceSummary', mockUsageCounter); - }); - - it('should be deprecated', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - getAlertInstanceSummaryRoute(router, licenseState, docLinks); - - const [config] = router.get.mock.calls[0]; - - expect(config.options?.deprecated).toMatchInlineSnapshot( - { - documentationUrl: expect.stringMatching(/#breaking-201550$/), - }, - ` - Object { - "documentationUrl": StringMatching /#breaking-201550\\$/, - "reason": Object { - "type": "remove", - }, - "severity": "warning", - } - ` - ); - }); -}); diff --git a/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.ts b/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.ts deleted file mode 100644 index 918cdabc25a34..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.ts +++ /dev/null @@ -1,75 +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. - */ - -import { schema } from '@kbn/config-schema'; -import { UsageCounter } from '@kbn/usage-collection-plugin/server'; -import { DocLinksServiceSetup } from '@kbn/core/server'; -import type { AlertingRouter } from '../../types'; -import { ILicenseState } from '../../lib/license_state'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { AlertSummary, LEGACY_BASE_ALERT_API_PATH } from '../../../common'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../constants'; - -const paramSchema = schema.object({ - id: schema.string(), -}); - -const querySchema = schema.object({ - dateStart: schema.maybe(schema.string()), -}); - -const rewriteBodyRes = ({ ruleTypeId, alerts, ...rest }: AlertSummary) => ({ - ...rest, - alertTypeId: ruleTypeId, - instances: alerts, -}); - -export const getAlertInstanceSummaryRoute = ( - router: AlertingRouter, - licenseState: ILicenseState, - docLinks: DocLinksServiceSetup, - usageCounter?: UsageCounter, - isServerless?: boolean -) => { - router.get( - { - path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{id}/_instance_summary`, - validate: { - params: paramSchema, - query: querySchema, - }, - security: DEFAULT_ALERTING_ROUTE_SECURITY, - options: { - access: isServerless ? 'internal' : 'public', - summary: 'Get an alert summary', - tags: ['oas-tag:alerting'], - deprecated: { - documentationUrl: docLinks.links.alerting.legacyRuleApiDeprecations, - severity: 'warning', - reason: { - type: 'remove', - }, - }, - }, - }, - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - if (!context.alerting) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); - } - trackLegacyRouteUsage('instanceSummary', usageCounter); - const alertingContext = await context.alerting; - const rulesClient = await alertingContext.getRulesClient(); - const { id } = req.params; - const { dateStart } = req.query; - const summary = await rulesClient.getAlertSummary({ id, dateStart }); - - return res.ok({ body: rewriteBodyRes(summary) }); - }) - ); -}; diff --git a/x-pack/plugins/alerting/server/routes/legacy/get_alert_state.test.ts b/x-pack/plugins/alerting/server/routes/legacy/get_alert_state.test.ts deleted file mode 100644 index ca79291b23dbc..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/get_alert_state.test.ts +++ /dev/null @@ -1,212 +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. - */ -import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; -import { getAlertStateRoute } from './get_alert_state'; -import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../../lib/license_state.mock'; -import { mockHandlerArguments } from '../_mock_handler_arguments'; -import { SavedObjectsErrorHelpers } from '@kbn/core/server'; -import { rulesClientMock } from '../../rules_client.mock'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects'; -import { docLinksServiceMock } from '@kbn/core/server/mocks'; - -const rulesClient = rulesClientMock.create(); -jest.mock('../../lib/license_api_access', () => ({ - verifyApiAccess: jest.fn(), -})); - -jest.mock('../../lib/track_legacy_route_usage', () => ({ - trackLegacyRouteUsage: jest.fn(), -})); - -beforeEach(() => { - jest.resetAllMocks(); -}); - -describe('getAlertStateRoute', () => { - const docLinks = docLinksServiceMock.createSetupContract(); - const mockedAlertState = { - alertTypeState: { - some: 'value', - }, - alertInstances: { - first_instance: { - state: {}, - meta: { - lastScheduledActions: { - group: 'first_group', - date: new Date().toISOString(), - }, - }, - }, - second_instance: {}, - }, - }; - - it('gets alert state', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - getAlertStateRoute(router, licenseState, docLinks); - - const [config, handler] = router.get.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/state"`); - expect(config.options?.access).toBe('public'); - - rulesClient.getAlertState.mockResolvedValueOnce(mockedAlertState); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { - id: '1', - }, - }, - ['ok'] - ); - - await handler(context, req, res); - - expect(rulesClient.getAlertState).toHaveBeenCalledTimes(1); - expect(rulesClient.getAlertState.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "id": "1", - }, - ] - `); - - expect(res.ok).toHaveBeenCalled(); - }); - - it('should have internal access for serverless', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - getAlertStateRoute(router, licenseState, docLinks, undefined, true); - - const [config] = router.get.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/state"`); - expect(config.options?.access).toBe('internal'); - }); - - it('returns NO-CONTENT when alert exists but has no task state yet', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - getAlertStateRoute(router, licenseState, docLinks); - - const [config, handler] = router.get.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/state"`); - - rulesClient.getAlertState.mockResolvedValueOnce(undefined); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { - id: '1', - }, - }, - ['noContent'] - ); - - expect(await handler(context, req, res)).toEqual(undefined); - - expect(rulesClient.getAlertState).toHaveBeenCalledTimes(1); - expect(rulesClient.getAlertState.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "id": "1", - }, - ] - `); - - expect(res.noContent).toHaveBeenCalled(); - }); - - it('returns NOT-FOUND when alert is not found', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - getAlertStateRoute(router, licenseState, docLinks); - - const [config, handler] = router.get.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/state"`); - - rulesClient.getAlertState = jest - .fn() - .mockResolvedValueOnce( - SavedObjectsErrorHelpers.createGenericNotFoundError(RULE_SAVED_OBJECT_TYPE, '1') - ); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { - id: '1', - }, - }, - ['notFound'] - ); - - expect(await handler(context, req, res)).toEqual(undefined); - - expect(rulesClient.getAlertState).toHaveBeenCalledTimes(1); - expect(rulesClient.getAlertState.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "id": "1", - }, - ] - `); - }); - - it('should track every call', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - getAlertStateRoute(router, licenseState, docLinks, mockUsageCounter); - const [, handler] = router.get.mock.calls[0]; - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: { id: '1' } }, [ - 'ok', - ]); - await handler(context, req, res); - expect(trackLegacyRouteUsage).toHaveBeenCalledWith('state', mockUsageCounter); - }); - - it('should be deprecated', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - getAlertStateRoute(router, licenseState, docLinks); - - const [config] = router.get.mock.calls[0]; - - expect(config.options?.deprecated).toMatchInlineSnapshot( - { - documentationUrl: expect.stringMatching(/#breaking-201550$/), - }, - ` - Object { - "documentationUrl": StringMatching /#breaking-201550\\$/, - "reason": Object { - "type": "remove", - }, - "severity": "warning", - } - ` - ); - }); -}); diff --git a/x-pack/plugins/alerting/server/routes/legacy/get_alert_state.ts b/x-pack/plugins/alerting/server/routes/legacy/get_alert_state.ts deleted file mode 100644 index fbc82e560a45e..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/get_alert_state.ts +++ /dev/null @@ -1,62 +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. - */ - -import { schema } from '@kbn/config-schema'; -import { UsageCounter } from '@kbn/usage-collection-plugin/server'; -import { DocLinksServiceSetup } from '@kbn/core/server'; -import type { AlertingRouter } from '../../types'; -import { ILicenseState } from '../../lib/license_state'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../constants'; - -const paramSchema = schema.object({ - id: schema.string(), -}); - -export const getAlertStateRoute = ( - router: AlertingRouter, - licenseState: ILicenseState, - docLinks: DocLinksServiceSetup, - usageCounter?: UsageCounter, - isServerless?: boolean -) => { - router.get( - { - path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{id}/state`, - validate: { - params: paramSchema, - }, - security: DEFAULT_ALERTING_ROUTE_SECURITY, - options: { - access: isServerless ? 'internal' : 'public', - summary: 'Get the state of an alert', - tags: ['oas-tag:alerting'], - deprecated: { - documentationUrl: docLinks.links.alerting.legacyRuleApiDeprecations, - severity: 'warning', - reason: { - type: 'remove', - }, - }, - }, - }, - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - if (!context.alerting) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); - } - trackLegacyRouteUsage('state', usageCounter); - const alertingContext = await context.alerting; - const rulesClient = await alertingContext.getRulesClient(); - const { id } = req.params; - const state = await rulesClient.getAlertState({ id }); - return state ? res.ok({ body: state }) : res.noContent(); - }) - ); -}; diff --git a/x-pack/plugins/alerting/server/routes/legacy/health.test.ts b/x-pack/plugins/alerting/server/routes/legacy/health.test.ts deleted file mode 100644 index fea24b831e97d..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/health.test.ts +++ /dev/null @@ -1,473 +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. - */ -import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; -import { HealthStatus } from '@kbn/alerting-types'; -import { healthRoute } from './health'; -import { httpServiceMock } from '@kbn/core/server/mocks'; -import { mockHandlerArguments } from '../_mock_handler_arguments'; -import { licenseStateMock } from '../../lib/license_state.mock'; -import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; -import { rulesClientMock } from '../../rules_client.mock'; -import { RecoveredActionGroup } from '../../types'; -import { alertsMock } from '../../mocks'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { RegistryAlertTypeWithAuth } from '../../authorization'; -import { docLinksServiceMock } from '@kbn/core/server/mocks'; - -const rulesClient = rulesClientMock.create(); - -jest.mock('../../lib/license_api_access', () => ({ - verifyApiAccess: jest.fn(), -})); - -jest.mock('../../lib/track_legacy_route_usage', () => ({ - trackLegacyRouteUsage: jest.fn(), -})); - -const alerting = alertsMock.createStart(); - -const currentDate = new Date().toISOString(); - -const ruleTypes = [ - { - id: '1', - name: 'name', - actionGroups: [ - { - id: 'default', - name: 'Default', - }, - ], - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - isExportable: true, - ruleTaskTimeout: '10m', - recoveryActionGroup: RecoveredActionGroup, - authorizedConsumers: {}, - actionVariables: { - context: [], - state: [], - }, - category: 'test', - producer: 'test', - enabledInLicense: true, - defaultScheduleInterval: '10m', - hasAlertsMappings: false, - hasFieldsForAAD: false, - validLegacyConsumers: [], - } as RegistryAlertTypeWithAuth, -]; - -beforeEach(() => { - jest.resetAllMocks(); - alerting.getFrameworkHealth.mockResolvedValue({ - decryptionHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - executionHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - readHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - }); -}); - -describe('healthRoute', () => { - const docLinks = docLinksServiceMock.createSetupContract(); - - it('registers the route', async () => { - rulesClient.listRuleTypes.mockResolvedValueOnce(ruleTypes); - const router = httpServiceMock.createRouter(); - - const licenseState = licenseStateMock.create(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); - healthRoute(router, licenseState, encryptedSavedObjects, docLinks); - - const [config] = router.get.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/_health"`); - expect(config.options?.access).toBe('public'); - }); - - it('should have internal access for serverless', async () => { - rulesClient.listRuleTypes.mockResolvedValueOnce(ruleTypes); - const router = httpServiceMock.createRouter(); - - const licenseState = licenseStateMock.create(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); - healthRoute(router, licenseState, encryptedSavedObjects, docLinks, undefined, true); - - const [config] = router.get.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/_health"`); - expect(config.options?.access).toBe('internal'); - }); - - it('throws error when user does not have any access to any rule types', async () => { - rulesClient.listRuleTypes.mockResolvedValueOnce([]); - const router = httpServiceMock.createRouter(); - - const licenseState = licenseStateMock.create(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: false }); - healthRoute(router, licenseState, encryptedSavedObjects, docLinks); - const [, handler] = router.get.mock.calls[0]; - - const [context, req, res] = mockHandlerArguments( - { - rulesClient, - getFrameworkHealth: alerting.getFrameworkHealth, - areApiKeysEnabled: () => Promise.resolve(true), - }, - {}, - ['ok'] - ); - - await handler(context, req, res); - - expect(res.forbidden).toHaveBeenCalledWith({ - body: { message: `Unauthorized to access alerting framework health` }, - }); - }); - - it('evaluates whether Encrypted Saved Objects is missing encryption key', async () => { - rulesClient.listRuleTypes.mockResolvedValueOnce(ruleTypes); - const router = httpServiceMock.createRouter(); - - const licenseState = licenseStateMock.create(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: false }); - healthRoute(router, licenseState, encryptedSavedObjects, docLinks); - const [, handler] = router.get.mock.calls[0]; - - const [context, req, res] = mockHandlerArguments( - { - rulesClient, - getFrameworkHealth: alerting.getFrameworkHealth, - areApiKeysEnabled: () => Promise.resolve(true), - }, - {}, - ['ok'] - ); - - expect(await handler(context, req, res)).toStrictEqual({ - body: { - alertingFrameworkHeath: { - // Legacy: pre-v8.0 typo - _deprecated: 'This state property has a typo, use "alertingFrameworkHealth" instead.', - decryptionHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - executionHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - readHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - }, - alertingFrameworkHealth: { - decryptionHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - executionHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - readHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - }, - hasPermanentEncryptionKey: false, - isSufficientlySecure: true, - }, - }); - }); - - test('when ES security status cannot be determined from license state, isSufficientlySecure should return false', async () => { - rulesClient.listRuleTypes.mockResolvedValueOnce(ruleTypes); - const router = httpServiceMock.createRouter(); - const licenseState = licenseStateMock.create(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); - licenseState.getIsSecurityEnabled.mockReturnValueOnce(null); - - healthRoute(router, licenseState, encryptedSavedObjects, docLinks); - const [, handler] = router.get.mock.calls[0]; - - const [context, req, res] = mockHandlerArguments( - { - rulesClient, - getFrameworkHealth: alerting.getFrameworkHealth, - areApiKeysEnabled: () => Promise.resolve(true), - }, - {}, - ['ok'] - ); - - expect(await handler(context, req, res)).toStrictEqual({ - body: { - alertingFrameworkHeath: { - // Legacy: pre-v8.0 typo - _deprecated: 'This state property has a typo, use "alertingFrameworkHealth" instead.', - decryptionHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - executionHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - readHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - }, - alertingFrameworkHealth: { - decryptionHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - executionHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - readHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - }, - hasPermanentEncryptionKey: true, - isSufficientlySecure: false, - }, - }); - }); - - test('when ES security is disabled, isSufficientlySecure should return true', async () => { - rulesClient.listRuleTypes.mockResolvedValueOnce(ruleTypes); - const router = httpServiceMock.createRouter(); - const licenseState = licenseStateMock.create(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); - licenseState.getIsSecurityEnabled.mockReturnValueOnce(false); - - healthRoute(router, licenseState, encryptedSavedObjects, docLinks); - const [, handler] = router.get.mock.calls[0]; - - const [context, req, res] = mockHandlerArguments( - { - rulesClient, - getFrameworkHealth: alerting.getFrameworkHealth, - areApiKeysEnabled: () => Promise.resolve(false), - }, - {}, - ['ok'] - ); - - expect(await handler(context, req, res)).toStrictEqual({ - body: { - alertingFrameworkHeath: { - // Legacy: pre-v8.0 typo - _deprecated: 'This state property has a typo, use "alertingFrameworkHealth" instead.', - decryptionHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - executionHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - readHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - }, - alertingFrameworkHealth: { - decryptionHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - executionHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - readHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - }, - hasPermanentEncryptionKey: true, - isSufficientlySecure: true, - }, - }); - }); - - test('when ES security is enabled but user cannot generate api keys, isSufficientlySecure should return false', async () => { - rulesClient.listRuleTypes.mockResolvedValueOnce(ruleTypes); - const router = httpServiceMock.createRouter(); - const licenseState = licenseStateMock.create(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); - licenseState.getIsSecurityEnabled.mockReturnValueOnce(true); - - healthRoute(router, licenseState, encryptedSavedObjects, docLinks); - const [, handler] = router.get.mock.calls[0]; - - const [context, req, res] = mockHandlerArguments( - { - rulesClient, - getFrameworkHealth: alerting.getFrameworkHealth, - areApiKeysEnabled: () => Promise.resolve(false), - }, - {}, - ['ok'] - ); - - expect(await handler(context, req, res)).toStrictEqual({ - body: { - alertingFrameworkHeath: { - // Legacy: pre-v8.0 typo - _deprecated: 'This state property has a typo, use "alertingFrameworkHealth" instead.', - decryptionHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - executionHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - readHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - }, - alertingFrameworkHealth: { - decryptionHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - executionHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - readHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - }, - hasPermanentEncryptionKey: true, - isSufficientlySecure: false, - }, - }); - }); - - test('when ES security is enabled and user can generate api keys, isSufficientlySecure should return true', async () => { - rulesClient.listRuleTypes.mockResolvedValueOnce(ruleTypes); - const router = httpServiceMock.createRouter(); - const licenseState = licenseStateMock.create(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); - licenseState.getIsSecurityEnabled.mockReturnValueOnce(true); - - healthRoute(router, licenseState, encryptedSavedObjects, docLinks); - const [, handler] = router.get.mock.calls[0]; - - const [context, req, res] = mockHandlerArguments( - { - rulesClient, - getFrameworkHealth: alerting.getFrameworkHealth, - areApiKeysEnabled: () => Promise.resolve(true), - }, - {}, - ['ok'] - ); - - expect(await handler(context, req, res)).toStrictEqual({ - body: { - alertingFrameworkHeath: { - // Legacy: pre-v8.0 typo - _deprecated: 'This state property has a typo, use "alertingFrameworkHealth" instead.', - decryptionHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - executionHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - readHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - }, - alertingFrameworkHealth: { - decryptionHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - executionHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - readHealth: { - status: HealthStatus.OK, - timestamp: currentDate, - }, - }, - hasPermanentEncryptionKey: true, - isSufficientlySecure: true, - }, - }); - }); - - it('should track every call', async () => { - rulesClient.listRuleTypes.mockResolvedValueOnce(ruleTypes); - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - healthRoute(router, licenseState, encryptedSavedObjects, docLinks, mockUsageCounter); - const [, handler] = router.get.mock.calls[0]; - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: { id: '1' } }, [ - 'ok', - ]); - await handler(context, req, res); - expect(trackLegacyRouteUsage).toHaveBeenCalledWith('health', mockUsageCounter); - }); - - it('should be deprecated', async () => { - rulesClient.listRuleTypes.mockResolvedValueOnce(ruleTypes); - const router = httpServiceMock.createRouter(); - - const licenseState = licenseStateMock.create(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); - healthRoute(router, licenseState, encryptedSavedObjects, docLinks); - - const [config] = router.get.mock.calls[0]; - - expect(config.options?.deprecated).toMatchInlineSnapshot( - { - documentationUrl: expect.stringMatching(/#breaking-201550$/), - }, - ` - Object { - "documentationUrl": StringMatching /#breaking-201550\\$/, - "reason": Object { - "newApiMethod": "GET", - "newApiPath": "/api/alerting/rule/_health", - "type": "migrate", - }, - "severity": "warning", - } - ` - ); - }); -}); diff --git a/x-pack/plugins/alerting/server/routes/legacy/health.ts b/x-pack/plugins/alerting/server/routes/legacy/health.ts deleted file mode 100644 index 8b8a64df14cd1..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/health.ts +++ /dev/null @@ -1,93 +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. - */ - -import { UsageCounter } from '@kbn/usage-collection-plugin/server'; -import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server'; -import { DocLinksServiceSetup } from '@kbn/core/server'; -import type { AlertingRouter } from '../../types'; -import { ILicenseState } from '../../lib/license_state'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { AlertingFrameworkHealth } from '../../types'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { getSecurityHealth } from '../../lib/get_security_health'; -import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../constants'; - -export function healthRoute( - router: AlertingRouter, - licenseState: ILicenseState, - encryptedSavedObjects: EncryptedSavedObjectsPluginSetup, - docLinks: DocLinksServiceSetup, - usageCounter?: UsageCounter, - isServerless?: boolean -) { - router.get( - { - path: '/api/alerts/_health', - validate: false, - security: DEFAULT_ALERTING_ROUTE_SECURITY, - options: { - access: isServerless ? 'internal' : 'public', - summary: 'Get the alerting framework health', - tags: ['oas-tag:alerting'], - deprecated: { - documentationUrl: docLinks.links.alerting.legacyRuleApiDeprecations, - severity: 'warning', - reason: { - type: 'migrate', - newApiMethod: 'GET', - newApiPath: '/api/alerting/rule/_health', - }, - }, - }, - }, - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - if (!context.alerting) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); - } - trackLegacyRouteUsage('health', usageCounter); - try { - const alertingContext = await context.alerting; - const rulesClient = await alertingContext.getRulesClient(); - // Verify that user has access to at least one rule type - const ruleTypes = Array.from(await rulesClient.listRuleTypes()); - if (ruleTypes.length > 0) { - const alertingFrameworkHealth = await alertingContext.getFrameworkHealth(); - - const securityHealth = await getSecurityHealth( - async () => (licenseState ? licenseState.getIsSecurityEnabled() : null), - async () => encryptedSavedObjects.canEncrypt, - alertingContext.areApiKeysEnabled - ); - - const frameworkHealth: AlertingFrameworkHealth = { - ...securityHealth, - alertingFrameworkHealth, - }; - - return res.ok({ - body: { - ...frameworkHealth, - alertingFrameworkHeath: { - // Legacy: pre-v8.0 typo - ...alertingFrameworkHealth, - _deprecated: - 'This state property has a typo, use "alertingFrameworkHealth" instead.', - }, - }, - }); - } else { - return res.forbidden({ - body: { message: `Unauthorized to access alerting framework health` }, - }); - } - } catch (error) { - return res.badRequest({ body: error }); - } - }) - ); -} diff --git a/x-pack/plugins/alerting/server/routes/legacy/index.ts b/x-pack/plugins/alerting/server/routes/legacy/index.ts deleted file mode 100644 index e9551d938e6c1..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/index.ts +++ /dev/null @@ -1,46 +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. - */ - -import { createAlertRoute } from './create'; -import { deleteAlertRoute } from './delete'; -import { findAlertRoute } from './find'; -import { getAlertRoute } from './get'; -import { getAlertStateRoute } from './get_alert_state'; -import { getAlertInstanceSummaryRoute } from './get_alert_instance_summary'; -import { listAlertTypesRoute } from './list_alert_types'; -import { updateAlertRoute } from './update'; -import { enableAlertRoute } from './enable'; -import { disableAlertRoute } from './disable'; -import { updateApiKeyRoute } from './update_api_key'; -import { muteAlertInstanceRoute } from './mute_instance'; -import { unmuteAlertInstanceRoute } from './unmute_instance'; -import { muteAllAlertRoute } from './mute_all'; -import { unmuteAllAlertRoute } from './unmute_all'; -import { healthRoute } from './health'; -import { RouteOptions } from '..'; - -export function defineLegacyRoutes(opts: RouteOptions) { - const { router, licenseState, encryptedSavedObjects, usageCounter, isServerless, docLinks } = - opts; - - createAlertRoute(opts); - deleteAlertRoute(router, licenseState, docLinks, usageCounter, isServerless); - findAlertRoute(router, licenseState, docLinks, usageCounter, isServerless); - getAlertRoute(router, licenseState, docLinks, usageCounter, isServerless); - getAlertStateRoute(router, licenseState, docLinks, usageCounter, isServerless); - getAlertInstanceSummaryRoute(router, licenseState, docLinks, usageCounter, isServerless); - listAlertTypesRoute(router, licenseState, docLinks, usageCounter, isServerless); - updateAlertRoute(router, licenseState, docLinks, usageCounter, isServerless); - enableAlertRoute(router, licenseState, docLinks, usageCounter, isServerless); - disableAlertRoute(router, licenseState, docLinks, usageCounter, isServerless); - updateApiKeyRoute(router, licenseState, docLinks, usageCounter, isServerless); - muteAllAlertRoute(router, licenseState, docLinks, usageCounter, isServerless); - unmuteAllAlertRoute(router, licenseState, docLinks, usageCounter, isServerless); - muteAlertInstanceRoute(router, licenseState, docLinks, usageCounter, isServerless); - unmuteAlertInstanceRoute(router, licenseState, docLinks, usageCounter, isServerless); - healthRoute(router, licenseState, encryptedSavedObjects, docLinks, usageCounter, isServerless); -} diff --git a/x-pack/plugins/alerting/server/routes/legacy/list_alert_types.test.ts b/x-pack/plugins/alerting/server/routes/legacy/list_alert_types.test.ts deleted file mode 100644 index 27e9d7ce44865..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/list_alert_types.test.ts +++ /dev/null @@ -1,287 +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. - */ - -import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; -import { listAlertTypesRoute } from './list_alert_types'; -import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../../lib/license_state.mock'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { mockHandlerArguments } from '../_mock_handler_arguments'; -import { rulesClientMock } from '../../rules_client.mock'; -import { RecoveredActionGroup } from '../../../common'; -import { RegistryAlertTypeWithAuth } from '../../authorization'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { docLinksServiceMock } from '@kbn/core/server/mocks'; - -const rulesClient = rulesClientMock.create(); - -jest.mock('../../lib/license_api_access', () => ({ - verifyApiAccess: jest.fn(), -})); - -jest.mock('../../lib/track_legacy_route_usage', () => ({ - trackLegacyRouteUsage: jest.fn(), -})); - -beforeEach(() => { - jest.resetAllMocks(); -}); - -describe('listAlertTypesRoute', () => { - const docLinks = docLinksServiceMock.createSetupContract(); - it('lists alert types with proper parameters', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - listAlertTypesRoute(router, licenseState, docLinks); - - const [config, handler] = router.get.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/list_alert_types"`); - expect(config.options?.access).toBe('public'); - - const listTypes: RegistryAlertTypeWithAuth[] = [ - { - id: '1', - name: 'name', - actionGroups: [ - { - id: 'default', - name: 'Default', - }, - ], - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - isExportable: true, - recoveryActionGroup: RecoveredActionGroup, - authorizedConsumers: {}, - actionVariables: { - context: [], - state: [], - }, - category: 'test', - producer: 'test', - enabledInLicense: true, - hasAlertsMappings: false, - hasFieldsForAAD: false, - validLegacyConsumers: [], - }, - ]; - - rulesClient.listRuleTypes.mockResolvedValueOnce(listTypes); - - const [context, req, res] = mockHandlerArguments({ rulesClient }, {}, ['ok']); - - expect(await handler(context, req, res)).toMatchInlineSnapshot(` - Object { - "body": Array [ - Object { - "actionGroups": Array [ - Object { - "id": "default", - "name": "Default", - }, - ], - "actionVariables": Object { - "context": Array [], - "state": Array [], - }, - "authorizedConsumers": Object {}, - "category": "test", - "defaultActionGroupId": "default", - "enabledInLicense": true, - "hasAlertsMappings": false, - "hasFieldsForAAD": false, - "id": "1", - "isExportable": true, - "minimumLicenseRequired": "basic", - "name": "name", - "producer": "test", - "recoveryActionGroup": Object { - "id": "recovered", - "name": "Recovered", - }, - "validLegacyConsumers": Array [], - }, - ], - } - `); - - expect(rulesClient.listRuleTypes).toHaveBeenCalledTimes(1); - - expect(res.ok).toHaveBeenCalledWith({ - body: listTypes, - }); - }); - - it('should have internal access for serverless', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - listAlertTypesRoute(router, licenseState, docLinks, undefined, true); - - const [config] = router.get.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/list_alert_types"`); - expect(config.options?.access).toBe('internal'); - }); - - it('ensures the license allows listing alert types', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - listAlertTypesRoute(router, licenseState, docLinks); - - const [config, handler] = router.get.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/list_alert_types"`); - - const listTypes = [ - { - id: '1', - name: 'name', - actionGroups: [ - { - id: 'default', - name: 'Default', - }, - ], - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - isExportable: true, - recoveryActionGroup: RecoveredActionGroup, - authorizedConsumers: {}, - actionVariables: { - context: [], - state: [], - }, - category: 'test', - producer: 'alerts', - enabledInLicense: true, - hasAlertsMappings: false, - hasFieldsForAAD: false, - validLegacyConsumers: [], - } as RegistryAlertTypeWithAuth, - ]; - - rulesClient.listRuleTypes.mockResolvedValueOnce(listTypes); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { id: '1' }, - }, - ['ok'] - ); - - await handler(context, req, res); - - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); - }); - - it('ensures the license check prevents listing alert types', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - (verifyApiAccess as jest.Mock).mockImplementation(() => { - throw new Error('OMG'); - }); - - listAlertTypesRoute(router, licenseState, docLinks); - - const [config, handler] = router.get.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/list_alert_types"`); - - const listTypes = [ - { - id: '1', - name: 'name', - actionGroups: [ - { - id: 'default', - name: 'Default', - }, - ], - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - isExportable: true, - recoveryActionGroup: RecoveredActionGroup, - authorizedConsumers: {}, - actionVariables: { - context: [], - state: [], - }, - category: 'test', - producer: 'alerts', - enabledInLicense: true, - hasAlertsMappings: false, - hasFieldsForAAD: false, - validLegacyConsumers: [], - } as RegistryAlertTypeWithAuth, - ]; - - rulesClient.listRuleTypes.mockResolvedValueOnce(listTypes); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { id: '1' }, - }, - ['ok'] - ); - - await expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); - - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); - }); - - it('should track every call', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - rulesClient.listRuleTypes.mockResolvedValueOnce([]); - - listAlertTypesRoute(router, licenseState, docLinks, mockUsageCounter); - const [, handler] = router.get.mock.calls[0]; - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { params: { id: '1' }, body: {} }, - ['ok'] - ); - await handler(context, req, res); - expect(trackLegacyRouteUsage).toHaveBeenCalledWith('listAlertTypes', mockUsageCounter); - }); - - it('should be deprecated', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - listAlertTypesRoute(router, licenseState, docLinks); - - const [config] = router.get.mock.calls[0]; - - expect(config.options?.deprecated).toMatchInlineSnapshot( - { - documentationUrl: expect.stringMatching(/#breaking-201550$/), - }, - ` - Object { - "documentationUrl": StringMatching /#breaking-201550\\$/, - "reason": Object { - "newApiMethod": "GET", - "newApiPath": "/api/alerting/rule_types", - "type": "migrate", - }, - "severity": "warning", - } - ` - ); - }); -}); diff --git a/x-pack/plugins/alerting/server/routes/legacy/list_alert_types.ts b/x-pack/plugins/alerting/server/routes/legacy/list_alert_types.ts deleted file mode 100644 index 6e6b4e3499f72..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/list_alert_types.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { UsageCounter } from '@kbn/usage-collection-plugin/server'; -import { DocLinksServiceSetup } from '@kbn/core/server'; -import type { AlertingRouter } from '../../types'; -import { ILicenseState } from '../../lib/license_state'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../constants'; - -export const listAlertTypesRoute = ( - router: AlertingRouter, - licenseState: ILicenseState, - docLinks: DocLinksServiceSetup, - usageCounter?: UsageCounter, - isServerless?: boolean -) => { - router.get( - { - path: `${LEGACY_BASE_ALERT_API_PATH}/list_alert_types`, - validate: {}, - security: DEFAULT_ALERTING_ROUTE_SECURITY, - options: { - access: isServerless ? 'internal' : 'public', - summary: 'Get the alert types', - tags: ['oas-tag:alerting'], - deprecated: { - documentationUrl: docLinks.links.alerting.legacyRuleApiDeprecations, - severity: 'warning', - reason: { - type: 'migrate', - newApiMethod: 'GET', - newApiPath: '/api/alerting/rule_types', - }, - }, - }, - }, - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - if (!context.alerting) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); - } - trackLegacyRouteUsage('listAlertTypes', usageCounter); - const alertingContext = await context.alerting; - const rulesClient = await alertingContext.getRulesClient(); - - return res.ok({ - body: Array.from(await rulesClient.listRuleTypes()), - }); - }) - ); -}; diff --git a/x-pack/plugins/alerting/server/routes/legacy/mute_all.test.ts b/x-pack/plugins/alerting/server/routes/legacy/mute_all.test.ts deleted file mode 100644 index d80cf415283d0..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/mute_all.test.ts +++ /dev/null @@ -1,143 +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. - */ - -import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; -import { muteAllAlertRoute } from './mute_all'; -import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../../lib/license_state.mock'; -import { mockHandlerArguments } from '../_mock_handler_arguments'; -import { rulesClientMock } from '../../rules_client.mock'; -import { RuleTypeDisabledError } from '../../lib/errors/rule_type_disabled'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { docLinksServiceMock } from '@kbn/core/server/mocks'; - -const rulesClient = rulesClientMock.create(); -jest.mock('../../lib/license_api_access', () => ({ - verifyApiAccess: jest.fn(), -})); - -jest.mock('../../lib/track_legacy_route_usage', () => ({ - trackLegacyRouteUsage: jest.fn(), -})); - -beforeEach(() => { - jest.resetAllMocks(); -}); - -describe('muteAllAlertRoute', () => { - const docLinks = docLinksServiceMock.createSetupContract(); - - it('mute an alert', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - muteAllAlertRoute(router, licenseState, docLinks); - - const [config, handler] = router.post.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_mute_all"`); - expect(config.options?.access).toBe('public'); - - rulesClient.muteAll.mockResolvedValueOnce(); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { - id: '1', - }, - }, - ['noContent'] - ); - - expect(await handler(context, req, res)).toEqual(undefined); - - expect(rulesClient.muteAll).toHaveBeenCalledTimes(1); - expect(rulesClient.muteAll.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "id": "1", - }, - ] - `); - - expect(res.noContent).toHaveBeenCalled(); - }); - - it('should have internal access for serverless', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - muteAllAlertRoute(router, licenseState, docLinks, undefined, true); - - const [config] = router.post.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_mute_all"`); - expect(config.options?.access).toBe('internal'); - }); - - it('ensures the alert type gets validated for the license', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - muteAllAlertRoute(router, licenseState, docLinks); - - const [, handler] = router.post.mock.calls[0]; - - rulesClient.muteAll.mockRejectedValue(new RuleTypeDisabledError('Fail', 'license_invalid')); - - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, body: {} }, [ - 'ok', - 'forbidden', - ]); - - await handler(context, req, res); - - expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); - }); - - it('should track every call', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - muteAllAlertRoute(router, licenseState, docLinks, mockUsageCounter); - const [, handler] = router.post.mock.calls[0]; - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, body: {} }, [ - 'ok', - ]); - await handler(context, req, res); - expect(trackLegacyRouteUsage).toHaveBeenCalledWith('muteAll', mockUsageCounter); - }); - - it('should be deprecated', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - muteAllAlertRoute(router, licenseState, docLinks, undefined, true); - - const [config] = router.post.mock.calls[0]; - - expect(config.options?.deprecated).toMatchInlineSnapshot( - { - documentationUrl: expect.stringMatching(/#breaking-201550$/), - }, - ` - Object { - "documentationUrl": StringMatching /#breaking-201550\\$/, - "reason": Object { - "newApiMethod": "POST", - "newApiPath": "/api/alerting/rule/{id}/_mute_all", - "type": "migrate", - }, - "severity": "warning", - } - ` - ); - }); -}); diff --git a/x-pack/plugins/alerting/server/routes/legacy/mute_all.ts b/x-pack/plugins/alerting/server/routes/legacy/mute_all.ts deleted file mode 100644 index b978cc223b88b..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/mute_all.ts +++ /dev/null @@ -1,72 +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. - */ - -import { schema } from '@kbn/config-schema'; -import { UsageCounter } from '@kbn/usage-collection-plugin/server'; -import { DocLinksServiceSetup } from '@kbn/core/server'; -import type { AlertingRouter } from '../../types'; -import { ILicenseState } from '../../lib/license_state'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; -import { RuleTypeDisabledError } from '../../lib/errors/rule_type_disabled'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../constants'; - -const paramSchema = schema.object({ - id: schema.string(), -}); - -export const muteAllAlertRoute = ( - router: AlertingRouter, - licenseState: ILicenseState, - docLinks: DocLinksServiceSetup, - usageCounter?: UsageCounter, - isServerless?: boolean -) => { - router.post( - { - path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{id}/_mute_all`, - validate: { - params: paramSchema, - }, - security: DEFAULT_ALERTING_ROUTE_SECURITY, - options: { - access: isServerless ? 'internal' : 'public', - summary: 'Mute all alert instances', - tags: ['oas-tag:alerting'], - deprecated: { - documentationUrl: docLinks.links.alerting.legacyRuleApiDeprecations, - severity: 'warning', - reason: { - type: 'migrate', - newApiMethod: 'POST', - newApiPath: '/api/alerting/rule/{id}/_mute_all', - }, - }, - }, - }, - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - if (!context.alerting) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); - } - trackLegacyRouteUsage('muteAll', usageCounter); - const alertingContext = await context.alerting; - const rulesClient = await alertingContext.getRulesClient(); - const { id } = req.params; - try { - await rulesClient.muteAll({ id }); - return res.noContent(); - } catch (e) { - if (e instanceof RuleTypeDisabledError) { - return e.sendResponse(res); - } - throw e; - } - }) - ); -}; diff --git a/x-pack/plugins/alerting/server/routes/legacy/mute_instance.test.ts b/x-pack/plugins/alerting/server/routes/legacy/mute_instance.test.ts deleted file mode 100644 index 08e03a6d053ef..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/mute_instance.test.ts +++ /dev/null @@ -1,151 +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. - */ - -import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; -import { muteAlertInstanceRoute } from './mute_instance'; -import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../../lib/license_state.mock'; -import { mockHandlerArguments } from '../_mock_handler_arguments'; -import { rulesClientMock } from '../../rules_client.mock'; -import { RuleTypeDisabledError } from '../../lib/errors/rule_type_disabled'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { docLinksServiceMock } from '@kbn/core/server/mocks'; - -const rulesClient = rulesClientMock.create(); -jest.mock('../../lib/license_api_access', () => ({ - verifyApiAccess: jest.fn(), -})); - -jest.mock('../../lib/track_legacy_route_usage', () => ({ - trackLegacyRouteUsage: jest.fn(), -})); - -beforeEach(() => { - jest.resetAllMocks(); -}); - -describe('muteAlertInstanceRoute', () => { - const docLinks = docLinksServiceMock.createSetupContract(); - - it('mutes an alert instance', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - muteAlertInstanceRoute(router, licenseState, docLinks); - - const [config, handler] = router.post.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot( - `"/api/alerts/alert/{alert_id}/alert_instance/{alert_instance_id}/_mute"` - ); - expect(config.options?.access).toBe('public'); - - rulesClient.muteInstance.mockResolvedValueOnce(); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { - alert_id: '1', - alert_instance_id: '2', - }, - }, - ['noContent'] - ); - - expect(await handler(context, req, res)).toEqual(undefined); - - expect(rulesClient.muteInstance).toHaveBeenCalledTimes(1); - expect(rulesClient.muteInstance.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "alertId": "1", - "alertInstanceId": "2", - }, - ] - `); - - expect(res.noContent).toHaveBeenCalled(); - }); - - it('should have internal access for serverless', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - muteAlertInstanceRoute(router, licenseState, docLinks, undefined, true); - - const [config] = router.post.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot( - `"/api/alerts/alert/{alert_id}/alert_instance/{alert_instance_id}/_mute"` - ); - expect(config.options?.access).toBe('internal'); - }); - - it('ensures the alert type gets validated for the license', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - muteAlertInstanceRoute(router, licenseState, docLinks); - - const [, handler] = router.post.mock.calls[0]; - - rulesClient.muteInstance.mockRejectedValue( - new RuleTypeDisabledError('Fail', 'license_invalid') - ); - - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, body: {} }, [ - 'ok', - 'forbidden', - ]); - - await handler(context, req, res); - - expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); - }); - - it('should track every call', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - muteAlertInstanceRoute(router, licenseState, docLinks, mockUsageCounter); - const [, handler] = router.post.mock.calls[0]; - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, body: {} }, [ - 'ok', - ]); - await handler(context, req, res); - expect(trackLegacyRouteUsage).toHaveBeenCalledWith('muteInstance', mockUsageCounter); - }); - - it('should be deprecated', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - muteAlertInstanceRoute(router, licenseState, docLinks); - - const [config] = router.post.mock.calls[0]; - - expect(config.options?.deprecated).toMatchInlineSnapshot( - { - documentationUrl: expect.stringMatching(/#breaking-201550$/), - }, - ` - Object { - "documentationUrl": StringMatching /#breaking-201550\\$/, - "reason": Object { - "newApiMethod": "POST", - "newApiPath": "/api/alerting/rule/{rule_id}/alert/{alert_id}/_mute", - "type": "migrate", - }, - "severity": "warning", - } - ` - ); - }); -}); diff --git a/x-pack/plugins/alerting/server/routes/legacy/mute_instance.ts b/x-pack/plugins/alerting/server/routes/legacy/mute_instance.ts deleted file mode 100644 index 155086ebc247d..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/mute_instance.ts +++ /dev/null @@ -1,83 +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. - */ - -import { schema } from '@kbn/config-schema'; -import { UsageCounter } from '@kbn/usage-collection-plugin/server'; -import { DocLinksServiceSetup } from '@kbn/core/server'; -import type { AlertingRouter } from '../../types'; -import { ILicenseState } from '../../lib/license_state'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; -import { renameKeys } from '../lib/rename_keys'; -import { MuteOptions } from '../../rules_client'; -import { RuleTypeDisabledError } from '../../lib/errors/rule_type_disabled'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../constants'; - -const paramSchema = schema.object({ - alert_id: schema.string(), - alert_instance_id: schema.string(), -}); - -export const muteAlertInstanceRoute = ( - router: AlertingRouter, - licenseState: ILicenseState, - docLinks: DocLinksServiceSetup, - usageCounter?: UsageCounter, - isServerless?: boolean -) => { - router.post( - { - path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{alert_id}/alert_instance/{alert_instance_id}/_mute`, - validate: { - params: paramSchema, - }, - security: DEFAULT_ALERTING_ROUTE_SECURITY, - options: { - access: isServerless ? 'internal' : 'public', - summary: 'Mute an alert', - tags: ['oas-tag:alerting'], - deprecated: { - documentationUrl: docLinks.links.alerting.legacyRuleApiDeprecations, - severity: 'warning', - reason: { - type: 'migrate', - newApiMethod: 'POST', - newApiPath: '/api/alerting/rule/{rule_id}/alert/{alert_id}/_mute', - }, - }, - }, - }, - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - if (!context.alerting) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); - } - - trackLegacyRouteUsage('muteInstance', usageCounter); - - const alertingContext = await context.alerting; - const rulesClient = await alertingContext.getRulesClient(); - - const renameMap = { - alert_id: 'alertId', - alert_instance_id: 'alertInstanceId', - }; - - const renamedQuery = renameKeys>(renameMap, req.params); - try { - await rulesClient.muteInstance(renamedQuery); - return res.noContent(); - } catch (e) { - if (e instanceof RuleTypeDisabledError) { - return e.sendResponse(res); - } - throw e; - } - }) - ); -}; diff --git a/x-pack/plugins/alerting/server/routes/legacy/unmute_all.test.ts b/x-pack/plugins/alerting/server/routes/legacy/unmute_all.test.ts deleted file mode 100644 index 5ae337e6a3f51..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/unmute_all.test.ts +++ /dev/null @@ -1,143 +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. - */ - -import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; -import { unmuteAllAlertRoute } from './unmute_all'; -import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../../lib/license_state.mock'; -import { mockHandlerArguments } from '../_mock_handler_arguments'; -import { rulesClientMock } from '../../rules_client.mock'; -import { RuleTypeDisabledError } from '../../lib/errors/rule_type_disabled'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { docLinksServiceMock } from '@kbn/core/server/mocks'; - -const rulesClient = rulesClientMock.create(); -jest.mock('../../lib/license_api_access', () => ({ - verifyApiAccess: jest.fn(), -})); - -jest.mock('../../lib/track_legacy_route_usage', () => ({ - trackLegacyRouteUsage: jest.fn(), -})); - -beforeEach(() => { - jest.resetAllMocks(); -}); - -describe('unmuteAllAlertRoute', () => { - const docLinks = docLinksServiceMock.createSetupContract(); - - it('unmutes an alert', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - unmuteAllAlertRoute(router, licenseState, docLinks); - - const [config, handler] = router.post.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_unmute_all"`); - expect(config.options?.access).toBe('public'); - - rulesClient.unmuteAll.mockResolvedValueOnce(); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { - id: '1', - }, - }, - ['noContent'] - ); - - expect(await handler(context, req, res)).toEqual(undefined); - - expect(rulesClient.unmuteAll).toHaveBeenCalledTimes(1); - expect(rulesClient.unmuteAll.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "id": "1", - }, - ] - `); - - expect(res.noContent).toHaveBeenCalled(); - }); - - it('should have internal access for serverless', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - unmuteAllAlertRoute(router, licenseState, docLinks, undefined, true); - - const [config] = router.post.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_unmute_all"`); - expect(config.options?.access).toBe('internal'); - }); - - it('ensures the alert type gets validated for the license', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - unmuteAllAlertRoute(router, licenseState, docLinks); - - const [, handler] = router.post.mock.calls[0]; - - rulesClient.unmuteAll.mockRejectedValue(new RuleTypeDisabledError('Fail', 'license_invalid')); - - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, body: {} }, [ - 'ok', - 'forbidden', - ]); - - await handler(context, req, res); - - expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); - }); - - it('should track every call', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - unmuteAllAlertRoute(router, licenseState, docLinks, mockUsageCounter); - const [, handler] = router.post.mock.calls[0]; - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, body: {} }, [ - 'ok', - ]); - await handler(context, req, res); - expect(trackLegacyRouteUsage).toHaveBeenCalledWith('unmuteAll', mockUsageCounter); - }); - - it('should be deprecated', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - unmuteAllAlertRoute(router, licenseState, docLinks); - - const [config] = router.post.mock.calls[0]; - - expect(config.options?.deprecated).toMatchInlineSnapshot( - { - documentationUrl: expect.stringMatching(/#breaking-201550$/), - }, - ` - Object { - "documentationUrl": StringMatching /#breaking-201550\\$/, - "reason": Object { - "newApiMethod": "POST", - "newApiPath": "/api/alerting/rule/{id}/_unmute_all", - "type": "migrate", - }, - "severity": "warning", - } - ` - ); - }); -}); diff --git a/x-pack/plugins/alerting/server/routes/legacy/unmute_all.ts b/x-pack/plugins/alerting/server/routes/legacy/unmute_all.ts deleted file mode 100644 index 5ae7dedcc8666..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/unmute_all.ts +++ /dev/null @@ -1,72 +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. - */ - -import { schema } from '@kbn/config-schema'; -import { UsageCounter } from '@kbn/usage-collection-plugin/server'; -import { DocLinksServiceSetup } from '@kbn/core/server'; -import type { AlertingRouter } from '../../types'; -import { ILicenseState } from '../../lib/license_state'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; -import { RuleTypeDisabledError } from '../../lib/errors/rule_type_disabled'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../constants'; - -const paramSchema = schema.object({ - id: schema.string(), -}); - -export const unmuteAllAlertRoute = ( - router: AlertingRouter, - licenseState: ILicenseState, - docLinks: DocLinksServiceSetup, - usageCounter?: UsageCounter, - isServerless?: boolean -) => { - router.post( - { - path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{id}/_unmute_all`, - validate: { - params: paramSchema, - }, - security: DEFAULT_ALERTING_ROUTE_SECURITY, - options: { - access: isServerless ? 'internal' : 'public', - summary: 'Unmute all alert instances', - tags: ['oas-tag:alerting'], - deprecated: { - documentationUrl: docLinks.links.alerting.legacyRuleApiDeprecations, - severity: 'warning', - reason: { - type: 'migrate', - newApiMethod: 'POST', - newApiPath: '/api/alerting/rule/{id}/_unmute_all', - }, - }, - }, - }, - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - if (!context.alerting) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); - } - trackLegacyRouteUsage('unmuteAll', usageCounter); - const alertingContext = await context.alerting; - const rulesClient = await alertingContext.getRulesClient(); - const { id } = req.params; - try { - await rulesClient.unmuteAll({ id }); - return res.noContent(); - } catch (e) { - if (e instanceof RuleTypeDisabledError) { - return e.sendResponse(res); - } - throw e; - } - }) - ); -}; diff --git a/x-pack/plugins/alerting/server/routes/legacy/unmute_instance.test.ts b/x-pack/plugins/alerting/server/routes/legacy/unmute_instance.test.ts deleted file mode 100644 index b6fba61aaff8a..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/unmute_instance.test.ts +++ /dev/null @@ -1,151 +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. - */ - -import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; -import { unmuteAlertInstanceRoute } from './unmute_instance'; -import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../../lib/license_state.mock'; -import { mockHandlerArguments } from '../_mock_handler_arguments'; -import { rulesClientMock } from '../../rules_client.mock'; -import { RuleTypeDisabledError } from '../../lib/errors/rule_type_disabled'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { docLinksServiceMock } from '@kbn/core/server/mocks'; - -const rulesClient = rulesClientMock.create(); -jest.mock('../../lib/license_api_access', () => ({ - verifyApiAccess: jest.fn(), -})); - -jest.mock('../../lib/track_legacy_route_usage', () => ({ - trackLegacyRouteUsage: jest.fn(), -})); - -beforeEach(() => { - jest.resetAllMocks(); -}); - -describe('unmuteAlertInstanceRoute', () => { - const docLinks = docLinksServiceMock.createSetupContract(); - - it('unmutes an alert instance', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - unmuteAlertInstanceRoute(router, licenseState, docLinks); - - const [config, handler] = router.post.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot( - `"/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute"` - ); - expect(config.options?.access).toBe('public'); - - rulesClient.unmuteInstance.mockResolvedValueOnce(); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { - alertId: '1', - alertInstanceId: '2', - }, - }, - ['noContent'] - ); - - expect(await handler(context, req, res)).toEqual(undefined); - - expect(rulesClient.unmuteInstance).toHaveBeenCalledTimes(1); - expect(rulesClient.unmuteInstance.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "alertId": "1", - "alertInstanceId": "2", - }, - ] - `); - - expect(res.noContent).toHaveBeenCalled(); - }); - - it('should have internal access for serverless', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - unmuteAlertInstanceRoute(router, licenseState, docLinks, undefined, true); - - const [config] = router.post.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot( - `"/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute"` - ); - expect(config.options?.access).toBe('internal'); - }); - - it('ensures the alert type gets validated for the license', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - unmuteAlertInstanceRoute(router, licenseState, docLinks); - - const [, handler] = router.post.mock.calls[0]; - - rulesClient.unmuteInstance.mockRejectedValue( - new RuleTypeDisabledError('Fail', 'license_invalid') - ); - - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, body: {} }, [ - 'ok', - 'forbidden', - ]); - - await handler(context, req, res); - - expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); - }); - - it('should track every call', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - unmuteAlertInstanceRoute(router, licenseState, docLinks, mockUsageCounter); - const [, handler] = router.post.mock.calls[0]; - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, body: {} }, [ - 'ok', - ]); - await handler(context, req, res); - expect(trackLegacyRouteUsage).toHaveBeenCalledWith('unmuteInstance', mockUsageCounter); - }); - - it('should be deprecated', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - unmuteAlertInstanceRoute(router, licenseState, docLinks); - - const [config] = router.post.mock.calls[0]; - - expect(config.options?.deprecated).toMatchInlineSnapshot( - { - documentationUrl: expect.stringMatching(/#breaking-201550$/), - }, - ` - Object { - "documentationUrl": StringMatching /#breaking-201550\\$/, - "reason": Object { - "newApiMethod": "POST", - "newApiPath": "/api/alerting/rule/{rule_id}/alert/{alert_id}/_unmute", - "type": "migrate", - }, - "severity": "warning", - } - ` - ); - }); -}); diff --git a/x-pack/plugins/alerting/server/routes/legacy/unmute_instance.ts b/x-pack/plugins/alerting/server/routes/legacy/unmute_instance.ts deleted file mode 100644 index 85cf06df83387..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/unmute_instance.ts +++ /dev/null @@ -1,73 +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. - */ - -import { schema } from '@kbn/config-schema'; -import { UsageCounter } from '@kbn/usage-collection-plugin/server'; -import { DocLinksServiceSetup } from '@kbn/core/server'; -import type { AlertingRouter } from '../../types'; -import { ILicenseState } from '../../lib/license_state'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; -import { RuleTypeDisabledError } from '../../lib/errors/rule_type_disabled'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../constants'; - -const paramSchema = schema.object({ - alertId: schema.string(), - alertInstanceId: schema.string(), -}); - -export const unmuteAlertInstanceRoute = ( - router: AlertingRouter, - licenseState: ILicenseState, - docLinks: DocLinksServiceSetup, - usageCounter?: UsageCounter, - isServerless?: boolean -) => { - router.post( - { - path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute`, - validate: { - params: paramSchema, - }, - security: DEFAULT_ALERTING_ROUTE_SECURITY, - options: { - access: isServerless ? 'internal' : 'public', - summary: 'Unmute an alert', - tags: ['oas-tag:alerting'], - deprecated: { - documentationUrl: docLinks.links.alerting.legacyRuleApiDeprecations, - severity: 'warning', - reason: { - type: 'migrate', - newApiMethod: 'POST', - newApiPath: '/api/alerting/rule/{rule_id}/alert/{alert_id}/_unmute', - }, - }, - }, - }, - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - if (!context.alerting) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); - } - trackLegacyRouteUsage('unmuteInstance', usageCounter); - const alertingContext = await context.alerting; - const rulesClient = await alertingContext.getRulesClient(); - const { alertId, alertInstanceId } = req.params; - try { - await rulesClient.unmuteInstance({ alertId, alertInstanceId }); - return res.noContent(); - } catch (e) { - if (e instanceof RuleTypeDisabledError) { - return e.sendResponse(res); - } - throw e; - } - }) - ); -}; diff --git a/x-pack/plugins/alerting/server/routes/legacy/update.test.ts b/x-pack/plugins/alerting/server/routes/legacy/update.test.ts deleted file mode 100644 index 7aaee90b805c5..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/update.test.ts +++ /dev/null @@ -1,390 +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. - */ - -import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; -import { updateAlertRoute } from './update'; -import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../../lib/license_state.mock'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { mockHandlerArguments } from '../_mock_handler_arguments'; -import { rulesClientMock } from '../../rules_client.mock'; -import { RuleTypeDisabledError } from '../../lib/errors/rule_type_disabled'; -import { RuleNotifyWhen, SanitizedRule, RuleSystemAction } from '../../../common'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { docLinksServiceMock } from '@kbn/core/server/mocks'; - -const rulesClient = rulesClientMock.create(); -jest.mock('../../lib/license_api_access', () => ({ - verifyApiAccess: jest.fn(), -})); - -jest.mock('../../lib/track_legacy_route_usage', () => ({ - trackLegacyRouteUsage: jest.fn(), -})); - -beforeEach(() => { - jest.resetAllMocks(); -}); - -describe('updateAlertRoute', () => { - const docLinks = docLinksServiceMock.createSetupContract(); - const mockedResponse = { - id: '1', - alertTypeId: '1', - tags: ['foo'], - schedule: { interval: '12s' }, - params: { - otherField: false, - }, - createdAt: new Date(), - updatedAt: new Date(), - actions: [ - { - group: 'default', - id: '2', - actionTypeId: 'test', - params: { - baz: true, - }, - }, - ], - notifyWhen: RuleNotifyWhen.CHANGE, - }; - - const systemAction: RuleSystemAction = { - actionTypeId: 'test-2', - id: 'system_action-id', - params: { - foo: true, - }, - uuid: '123-456', - }; - - it('updates an alert with proper parameters', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - updateAlertRoute(router, licenseState, docLinks); - - const [config, handler] = router.put.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`); - expect(config.options?.access).toBe('public'); - - rulesClient.update.mockResolvedValueOnce(mockedResponse as unknown as SanitizedRule); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { - id: '1', - }, - body: { - throttle: null, - name: 'abc', - tags: ['bar'], - schedule: { interval: '12s' }, - params: { - otherField: false, - }, - actions: [ - { - group: 'default', - id: '2', - params: { - baz: true, - }, - }, - ], - notifyWhen: 'onActionGroupChange', - }, - }, - ['ok'] - ); - - expect(await handler(context, req, res)).toEqual({ body: mockedResponse }); - - expect(rulesClient.update).toHaveBeenCalledTimes(1); - expect(rulesClient.update.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "data": Object { - "actions": Array [ - Object { - "group": "default", - "id": "2", - "params": Object { - "baz": true, - }, - }, - ], - "name": "abc", - "notifyWhen": "onActionGroupChange", - "params": Object { - "otherField": false, - }, - "schedule": Object { - "interval": "12s", - }, - "tags": Array [ - "bar", - ], - "throttle": null, - }, - "id": "1", - }, - ] - `); - - expect(res.ok).toHaveBeenCalled(); - }); - - it('should have internal access for serverless', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - updateAlertRoute(router, licenseState, docLinks, undefined, true); - - const [config] = router.put.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`); - expect(config.options?.access).toBe('internal'); - }); - - it('ensures the license allows updating alerts', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - updateAlertRoute(router, licenseState, docLinks); - - const [, handler] = router.put.mock.calls[0]; - - rulesClient.update.mockResolvedValueOnce(mockedResponse as unknown as SanitizedRule); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { - id: '1', - }, - body: { - throttle: null, - name: 'abc', - tags: ['bar'], - schedule: { interval: '12s' }, - params: { - otherField: false, - }, - actions: [ - { - group: 'default', - id: '2', - params: { - baz: true, - }, - }, - ], - }, - }, - ['ok'] - ); - - await handler(context, req, res); - - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); - }); - - it('ensures the license check prevents updating alerts', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - (verifyApiAccess as jest.Mock).mockImplementation(() => { - throw new Error('OMG'); - }); - - updateAlertRoute(router, licenseState, docLinks); - - const [, handler] = router.put.mock.calls[0]; - - rulesClient.update.mockResolvedValueOnce(mockedResponse as unknown as SanitizedRule); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { - id: '1', - }, - body: { - throttle: null, - name: 'abc', - tags: ['bar'], - schedule: { interval: '12s' }, - params: { - otherField: false, - }, - actions: [ - { - group: 'default', - id: '2', - params: { - baz: true, - }, - }, - ], - }, - }, - ['ok'] - ); - - await expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); - - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); - }); - - it('ensures the alert type gets validated for the license', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - updateAlertRoute(router, licenseState, docLinks); - - const [, handler] = router.put.mock.calls[0]; - - rulesClient.update.mockRejectedValue(new RuleTypeDisabledError('Fail', 'license_invalid')); - - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, body: {} }, [ - 'ok', - 'forbidden', - ]); - - await handler(context, req, res); - - expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); - }); - - it('should track every call', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - updateAlertRoute(router, licenseState, docLinks, mockUsageCounter); - const [, handler] = router.put.mock.calls[0]; - rulesClient.update.mockResolvedValueOnce(mockedResponse as unknown as SanitizedRule); - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, body: {} }, [ - 'ok', - ]); - await handler(context, req, res); - expect(trackLegacyRouteUsage).toHaveBeenCalledWith('update', mockUsageCounter); - }); - - it('does not return system actions', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - updateAlertRoute(router, licenseState, docLinks); - - const [config, handler] = router.put.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`); - - rulesClient.update.mockResolvedValueOnce({ - ...mockedResponse, - systemActions: [systemAction], - } as unknown as SanitizedRule); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { - id: '1', - }, - body: { - throttle: null, - name: 'abc', - tags: ['bar'], - schedule: { interval: '12s' }, - params: { - otherField: false, - }, - actions: [ - { - group: 'default', - id: '2', - params: { - baz: true, - }, - }, - ], - notifyWhen: 'onActionGroupChange', - }, - }, - ['ok'] - ); - - expect(await handler(context, req, res)).toEqual({ body: mockedResponse }); - - expect(rulesClient.update).toHaveBeenCalledTimes(1); - expect(rulesClient.update.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "data": Object { - "actions": Array [ - Object { - "group": "default", - "id": "2", - "params": Object { - "baz": true, - }, - }, - ], - "name": "abc", - "notifyWhen": "onActionGroupChange", - "params": Object { - "otherField": false, - }, - "schedule": Object { - "interval": "12s", - }, - "tags": Array [ - "bar", - ], - "throttle": null, - }, - "id": "1", - }, - ] - `); - - expect(res.ok).toHaveBeenCalled(); - }); - - it('should be deprecated', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - updateAlertRoute(router, licenseState, docLinks); - - const [config] = router.put.mock.calls[0]; - - expect(config.options?.deprecated).toMatchInlineSnapshot( - { - documentationUrl: expect.stringMatching(/#breaking-201550$/), - }, - ` - Object { - "documentationUrl": StringMatching /#breaking-201550\\$/, - "reason": Object { - "newApiMethod": "PUT", - "newApiPath": "/api/alerting/rule/rule/{id}", - "type": "migrate", - }, - "severity": "warning", - } - ` - ); - }); -}); diff --git a/x-pack/plugins/alerting/server/routes/legacy/update.ts b/x-pack/plugins/alerting/server/routes/legacy/update.ts deleted file mode 100644 index 95bb1bd651761..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/update.ts +++ /dev/null @@ -1,115 +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. - */ - -import { schema } from '@kbn/config-schema'; -import { UsageCounter } from '@kbn/usage-collection-plugin/server'; -import { DocLinksServiceSetup } from '@kbn/core/server'; -import type { AlertingRouter } from '../../types'; -import { ILicenseState } from '../../lib/license_state'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { validateDurationSchema } from '../../lib'; -import { handleDisabledApiKeysError } from '../lib/error_handler'; -import { RuleTypeDisabledError } from '../../lib/errors/rule_type_disabled'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { - RuleNotifyWhenType, - LEGACY_BASE_ALERT_API_PATH, - validateNotifyWhenType, -} from '../../../common'; -import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../constants'; - -const paramSchema = schema.object({ - id: schema.string(), -}); - -const bodySchema = schema.object({ - name: schema.string(), - tags: schema.arrayOf(schema.string(), { defaultValue: [] }), - schedule: schema.object({ - interval: schema.string({ validate: validateDurationSchema }), - }), - throttle: schema.nullable(schema.string({ validate: validateDurationSchema })), - params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), - actions: schema.arrayOf( - schema.object({ - group: schema.string(), - id: schema.string(), - params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), - actionTypeId: schema.maybe(schema.string()), - }), - { defaultValue: [] } - ), - notifyWhen: schema.nullable(schema.string({ validate: validateNotifyWhenType })), -}); - -export const updateAlertRoute = ( - router: AlertingRouter, - licenseState: ILicenseState, - docLinks: DocLinksServiceSetup, - usageCounter?: UsageCounter, - isServerless?: boolean -) => { - router.put( - { - path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{id}`, - validate: { - body: bodySchema, - params: paramSchema, - }, - security: DEFAULT_ALERTING_ROUTE_SECURITY, - options: { - access: isServerless ? 'internal' : 'public', - summary: 'Update an alert', - tags: ['oas-tag:alerting'], - deprecated: { - documentationUrl: docLinks.links.alerting.legacyRuleApiDeprecations, - severity: 'warning', - reason: { - type: 'migrate', - newApiMethod: 'PUT', - newApiPath: '/api/alerting/rule/rule/{id}', - }, - }, - }, - }, - handleDisabledApiKeysError( - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - if (!context.alerting) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); - } - trackLegacyRouteUsage('update', usageCounter); - const alertingContext = await context.alerting; - const rulesClient = await alertingContext.getRulesClient(); - const { id } = req.params; - const { name, actions, params, schedule, tags, throttle, notifyWhen } = req.body; - try { - const { systemActions, ...alertRes } = await rulesClient.update({ - id, - data: { - name, - actions, - params, - schedule, - tags, - throttle, - notifyWhen: notifyWhen as RuleNotifyWhenType, - }, - }); - return res.ok({ - body: alertRes, - }); - } catch (e) { - if (e instanceof RuleTypeDisabledError) { - return e.sendResponse(res); - } - throw e; - } - }) - ) - ); -}; diff --git a/x-pack/plugins/alerting/server/routes/legacy/update_api_key.test.ts b/x-pack/plugins/alerting/server/routes/legacy/update_api_key.test.ts deleted file mode 100644 index cb2817af2ed58..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/update_api_key.test.ts +++ /dev/null @@ -1,144 +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. - */ - -import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; -import { updateApiKeyRoute } from './update_api_key'; -import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../../lib/license_state.mock'; -import { mockHandlerArguments } from '../_mock_handler_arguments'; -import { rulesClientMock } from '../../rules_client.mock'; -import { RuleTypeDisabledError } from '../../lib/errors/rule_type_disabled'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { docLinksServiceMock } from '@kbn/core/server/mocks'; - -const rulesClient = rulesClientMock.create(); -jest.mock('../../lib/license_api_access', () => ({ - verifyApiAccess: jest.fn(), -})); - -jest.mock('../../lib/track_legacy_route_usage', () => ({ - trackLegacyRouteUsage: jest.fn(), -})); - -beforeEach(() => { - jest.resetAllMocks(); -}); - -describe('updateApiKeyRoute', () => { - const docLinks = docLinksServiceMock.createSetupContract(); - - it('updates api key for an alert', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - updateApiKeyRoute(router, licenseState, docLinks); - - const [config, handler] = router.post.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_update_api_key"`); - expect(config.options?.access).toBe('public'); - - rulesClient.updateRuleApiKey.mockResolvedValueOnce(); - - const [context, req, res] = mockHandlerArguments( - { rulesClient }, - { - params: { - id: '1', - }, - }, - ['noContent'] - ); - - expect(await handler(context, req, res)).toEqual(undefined); - - expect(rulesClient.updateRuleApiKey).toHaveBeenCalledTimes(1); - expect(rulesClient.updateRuleApiKey.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "id": "1", - }, - ] - `); - - expect(res.noContent).toHaveBeenCalled(); - }); - - it('should have internal access for serverless', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - updateApiKeyRoute(router, licenseState, docLinks, undefined, true); - - const [config] = router.post.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_update_api_key"`); - expect(config.options?.access).toBe('internal'); - }); - - it('ensures the alert type gets validated for the license', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - updateApiKeyRoute(router, licenseState, docLinks); - - const [, handler] = router.post.mock.calls[0]; - - rulesClient.updateRuleApiKey.mockRejectedValue( - new RuleTypeDisabledError('Fail', 'license_invalid') - ); - - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, body: {} }, [ - 'ok', - 'forbidden', - ]); - - await handler(context, req, res); - - expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); - }); - - it('should track every call', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); - const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); - - updateApiKeyRoute(router, licenseState, docLinks, mockUsageCounter); - const [, handler] = router.post.mock.calls[0]; - const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, body: {} }, [ - 'ok', - ]); - await handler(context, req, res); - expect(trackLegacyRouteUsage).toHaveBeenCalledWith('updateApiKey', mockUsageCounter); - }); - - it('should be deprecated', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - updateApiKeyRoute(router, licenseState, docLinks); - - const [config] = router.post.mock.calls[0]; - expect(config.options?.deprecated).toMatchInlineSnapshot( - { - documentationUrl: expect.stringMatching(/#breaking-201550$/), - }, - ` - Object { - "documentationUrl": StringMatching /#breaking-201550\\$/, - "reason": Object { - "newApiMethod": "POST", - "newApiPath": "/api/alerting/rule/{id}/_update_api_key", - "type": "migrate", - }, - "severity": "warning", - } - ` - ); - }); -}); diff --git a/x-pack/plugins/alerting/server/routes/legacy/update_api_key.ts b/x-pack/plugins/alerting/server/routes/legacy/update_api_key.ts deleted file mode 100644 index 45a3d35e7e5d3..0000000000000 --- a/x-pack/plugins/alerting/server/routes/legacy/update_api_key.ts +++ /dev/null @@ -1,75 +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. - */ - -import { schema } from '@kbn/config-schema'; -import { UsageCounter } from '@kbn/usage-collection-plugin/server'; -import { DocLinksServiceSetup } from '@kbn/core/server'; -import type { AlertingRouter } from '../../types'; -import { ILicenseState } from '../../lib/license_state'; -import { verifyApiAccess } from '../../lib/license_api_access'; -import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; -import { handleDisabledApiKeysError } from '../lib/error_handler'; -import { RuleTypeDisabledError } from '../../lib/errors/rule_type_disabled'; -import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; -import { DEFAULT_ALERTING_ROUTE_SECURITY } from '../constants'; - -const paramSchema = schema.object({ - id: schema.string(), -}); - -export const updateApiKeyRoute = ( - router: AlertingRouter, - licenseState: ILicenseState, - docLinks: DocLinksServiceSetup, - usageCounter?: UsageCounter, - isServerless?: boolean -) => { - router.post( - { - path: `${LEGACY_BASE_ALERT_API_PATH}/alert/{id}/_update_api_key`, - validate: { - params: paramSchema, - }, - security: DEFAULT_ALERTING_ROUTE_SECURITY, - options: { - access: isServerless ? 'internal' : 'public', - summary: 'Update the API key for an alert', - tags: ['oas-tag:alerting'], - deprecated: { - documentationUrl: docLinks.links.alerting.legacyRuleApiDeprecations, - severity: 'warning', - reason: { - type: 'migrate', - newApiMethod: 'POST', - newApiPath: '/api/alerting/rule/{id}/_update_api_key', - }, - }, - }, - }, - handleDisabledApiKeysError( - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - if (!context.alerting) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); - } - trackLegacyRouteUsage('updateApiKey', usageCounter); - const alertingContext = await context.alerting; - const rulesClient = await alertingContext.getRulesClient(); - const { id } = req.params; - try { - await rulesClient.updateRuleApiKey({ id }); - return res.noContent(); - } catch (e) { - if (e instanceof RuleTypeDisabledError) { - return e.sendResponse(res); - } - throw e; - } - }) - ) - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/applications/components/search_application/docs_explorer/document_context.test.tsx b/x-pack/plugins/enterprise_search/public/applications/applications/components/search_application/docs_explorer/document_context.test.tsx index 76a65da6d3d6c..9e6607c278f4f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/applications/components/search_application/docs_explorer/document_context.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/applications/components/search_application/docs_explorer/document_context.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { DocumentProvider, useSelectedDocument } from './document_context'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx index 21b399892269d..19a0fea68969b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx @@ -15,7 +15,7 @@ jest.mock('../../enterprise_search_content/components/search_index/indices/indic import { setMockValues, mockKibanaValues } from '../../__mocks__/kea_logic'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { EuiSideNavItemType } from '@elastic/eui'; diff --git a/x-pack/plugins/graph/server/routes/explore.ts b/x-pack/plugins/graph/server/routes/explore.ts index b97f3fe882c35..918e0addd68b7 100644 --- a/x-pack/plugins/graph/server/routes/explore.ts +++ b/x-pack/plugins/graph/server/routes/explore.ts @@ -27,6 +27,12 @@ export function registerExploreRoute({ router.post( { path: '/internal/graph/graphExplore', + security: { + authz: { + enabled: false, + reason: 'This route delegates authorization to the scoped ES client.', + }, + }, validate: { body: schema.object({ index: schema.string(), diff --git a/x-pack/plugins/graph/server/routes/search.ts b/x-pack/plugins/graph/server/routes/search.ts index 822b22648c7cc..0c0a83257a69e 100644 --- a/x-pack/plugins/graph/server/routes/search.ts +++ b/x-pack/plugins/graph/server/routes/search.ts @@ -20,6 +20,12 @@ export function registerSearchRoute({ router.post( { path: '/internal/graph/searchProxy', + security: { + authz: { + enabled: false, + reason: 'This route delegates authorization to the scoped ES client.', + }, + }, validate: { body: schema.object({ index: schema.string(), diff --git a/x-pack/plugins/observability_solution/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts b/x-pack/plugins/observability_solution/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts index 2da87653c0707..b0e2d99fefd5b 100644 --- a/x-pack/plugins/observability_solution/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts +++ b/x-pack/plugins/observability_solution/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts @@ -46,8 +46,8 @@ export function collectDataTelemetry({ } catch (err) { // catch error and log as debug in production env and warn in dev env const logLevel = isProd ? logger.debug : logger.warn; - logLevel(`Failed executing the APM telemetry task: "${task.name}"`); - logLevel(err); + logLevel.call(logger, `Failed executing the APM telemetry task: "${task.name}"`); + logLevel.call(logger, err); return data; } }); diff --git a/x-pack/plugins/observability_solution/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/observability_solution/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index f245214cfa37a..ad6a6bbcbcc27 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -52,9 +52,8 @@ export class KibanaFramework { config: InfraRouteConfig, handler: RequestHandler ) { - const defaultOptions = { - tags: ['access:infra'], - }; + const defaultSecurity = { authz: { requiredPrivileges: ['infra'] } }; + const routeConfig = { path: config.path, validate: config.validate, @@ -65,7 +64,8 @@ export class KibanaFramework { * using `as ...` below to ensure the route config has * the correct options type. */ - options: { ...config.options, ...defaultOptions }, + options: { ...config.options }, + security: defaultSecurity, }; switch (config.method) { case 'get': @@ -89,15 +89,12 @@ export class KibanaFramework { public registerVersionedRoute( config: InfraVersionedRouteConfig ) { - const defaultOptions = { - tags: ['access:infra'], - }; + const defaultSecurity = { authz: { requiredPrivileges: ['infra'] } }; + const routeConfig = { access: config.access, path: config.path, - // Currently we have no use of custom options beyond tags, this can be extended - // beyond defaultOptions if it's needed. - options: defaultOptions, + security: defaultSecurity, }; switch (config.method) { case 'get': diff --git a/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc b/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc index 69539098f2463..e6ee1a22edc05 100644 --- a/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc +++ b/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc @@ -11,12 +11,13 @@ "requiredPlugins": [ "charts", "data", - "fieldFormats", + "fieldFormats", "dataViews", "discoverShared", "logsDataAccess", "observabilityShared", "share", + "spaces", "usageCollection", "embeddable", ], @@ -27,4 +28,3 @@ "extraPublicDirs": ["common"] } } - \ No newline at end of file diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/explorer_reducer/index.ts b/x-pack/plugins/observability_solution/logs_shared/server/deprecations/constants.ts similarity index 50% rename from x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/explorer_reducer/index.ts rename to x-pack/plugins/observability_solution/logs_shared/server/deprecations/constants.ts index 74b6c88fba8d4..315be28d67510 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/reducers/explorer_reducer/index.ts +++ b/x-pack/plugins/observability_solution/logs_shared/server/deprecations/constants.ts @@ -5,7 +5,8 @@ * 2.0. */ -export { getIndexPattern } from './get_index_pattern'; -export { explorerReducer } from './reducer'; -export type { ExplorerState } from './state'; -export { getExplorerDefaultState } from './state'; +/** + * Number of spaces to address concurrently. + * We don't want to loop through all the spaces concurrently to avoid putting too much pressure on the memory in case that there are too many spaces. + */ +export const CONCURRENT_SPACES_TO_CHECK = 500; diff --git a/x-pack/plugins/observability_solution/logs_shared/server/deprecations/log_sources_setting.ts b/x-pack/plugins/observability_solution/logs_shared/server/deprecations/log_sources_setting.ts index c3e891edf74c9..0bccc83682382 100644 --- a/x-pack/plugins/observability_solution/logs_shared/server/deprecations/log_sources_setting.ts +++ b/x-pack/plugins/observability_solution/logs_shared/server/deprecations/log_sources_setting.ts @@ -6,39 +6,50 @@ */ import { DeprecationsDetails } from '@kbn/core-deprecations-common'; import { GetDeprecationsContext } from '@kbn/core-deprecations-server'; +import pMap from 'p-map'; +import type { Space } from '@kbn/spaces-plugin/common'; import { i18n } from '@kbn/i18n'; -import { defaultLogViewId } from '../../common/log_views'; import { MIGRATE_LOG_VIEW_SETTINGS_URL } from '../../common/http_api/deprecations'; +import { CONCURRENT_SPACES_TO_CHECK } from './constants'; +import { defaultLogViewId } from '../../common/log_views'; import { logSourcesKibanaAdvancedSettingRT } from '../../common'; import { LogsSharedPluginStartServicesAccessor } from '../types'; -export const getLogSourcesSettingDeprecationInfo = async ({ - getStartServices, - context, -}: { +export interface LogSourcesSettingDeprecationParams { context: GetDeprecationsContext; getStartServices: LogsSharedPluginStartServicesAccessor; -}): Promise => { - const [_, pluginStartDeps, pluginStart] = await getStartServices(); - const logSourcesService = - pluginStartDeps.logsDataAccess.services.logSourcesServiceFactory.getLogSourcesService( - context.savedObjectsClient - ); - const logViewsClient = pluginStart.logViews.getClient( - context.savedObjectsClient, - context.esClient.asCurrentUser, - logSourcesService - ); +} + +export const getLogSourcesSettingDeprecationInfo = async ( + params: LogSourcesSettingDeprecationParams +): Promise => { + const [_, pluginStartDeps] = await params.getStartServices(); + + const allAvailableSpaces = await pluginStartDeps.spaces.spacesService + .createSpacesClient(params.context.request) + .getAll({ purpose: 'any' }); + + const deprecationPerSpaceFactory = getLogSourcesSettingDeprecationInfoForSpaceFactory(params); + + const results = await pMap(allAvailableSpaces, deprecationPerSpaceFactory, { + concurrency: CONCURRENT_SPACES_TO_CHECK, // limit the number of spaces handled concurrently to make sure that we cover large deployments + }); - const logView = await logViewsClient.getLogView(defaultLogViewId); + const offendingSpaces = results.filter(Boolean) as string[]; - if (logView && !logSourcesKibanaAdvancedSettingRT.is(logView.attributes.logIndices)) { + if (offendingSpaces.length) { + const shortList = + offendingSpaces.length < 4 + ? offendingSpaces.join(', ') + : `${offendingSpaces.slice(0, 3).join(', ')}, ...`; + const fullList = offendingSpaces.join(', '); return [ { title: i18n.translate( 'xpack.logsShared.deprecations.migrateLogViewSettingsToLogSourcesSetting.title', { - defaultMessage: 'Log sources setting', + defaultMessage: 'Log sources setting in {count} spaces: {shortList}', + values: { count: offendingSpaces.length, shortList }, } ), level: 'warning', @@ -47,19 +58,21 @@ export const getLogSourcesSettingDeprecationInfo = async ({ 'xpack.logsShared.deprecations.migrateLogViewSettingsToLogSourcesSetting.message', { defaultMessage: - 'Indices and Data view options previously provided via the Logs UI settings page are now deprecated. Please migrate to using the Kibana log sources advanced setting.', + 'Indices and Data view options previously provided via the Logs UI settings page are now deprecated. Please migrate to using the Kibana log sources advanced setting in each of the following spaces: {fullList}.', + values: { fullList }, } ), correctiveActions: { - manualSteps: [ + manualSteps: offendingSpaces.map((spaceName) => i18n.translate( 'xpack.logsShared.deprecations.migrateLogViewSettingsToLogSourcesSetting.message.manualStepMessage', { defaultMessage: - 'Update the Log sources Kibana advanced setting (via Management > Advanced Settings) to match the setting previously provided via the Logs UI settings page. Then via the Logs UI settings page use the Kibana log sources advanced setting option.', + 'While in the space "{spaceName}" update the Log sources Kibana advanced setting (via Management > Advanced Settings) to match the setting previously provided via the Logs UI settings page. Then via the Logs UI settings page use the Kibana log sources advanced setting option.', + values: { spaceName }, } - ), - ], + ) + ), api: { method: 'PUT', path: MIGRATE_LOG_VIEW_SETTINGS_URL, @@ -71,3 +84,31 @@ export const getLogSourcesSettingDeprecationInfo = async ({ return []; } }; + +export const getLogSourcesSettingDeprecationInfoForSpaceFactory = ({ + getStartServices, + context, +}: LogSourcesSettingDeprecationParams): ((space: Space) => Promise) => { + return async (space) => { + const [_, pluginStartDeps, pluginStart] = await getStartServices(); + + // Get a new Saved Object Client scoped to the space.id + const spaceScopedSavedObjectsClient = context.savedObjectsClient.asScopedToNamespace(space.id); + + const logSourcesService = + pluginStartDeps.logsDataAccess.services.logSourcesServiceFactory.getLogSourcesService( + spaceScopedSavedObjectsClient + ); + const logViewsClient = pluginStart.logViews.getClient( + spaceScopedSavedObjectsClient, + context.esClient.asCurrentUser, + logSourcesService + ); + + const logView = await logViewsClient.getLogView(defaultLogViewId); + + if (logView && !logSourcesKibanaAdvancedSettingRT.is(logView.attributes.logIndices)) { + return space.name; + } + }; +}; diff --git a/x-pack/plugins/observability_solution/logs_shared/server/routes/deprecations/migrate_log_view_settings.ts b/x-pack/plugins/observability_solution/logs_shared/server/routes/deprecations/migrate_log_view_settings.ts index f3e9db4f1a765..d6284a63e8461 100644 --- a/x-pack/plugins/observability_solution/logs_shared/server/routes/deprecations/migrate_log_view_settings.ts +++ b/x-pack/plugins/observability_solution/logs_shared/server/routes/deprecations/migrate_log_view_settings.ts @@ -5,6 +5,8 @@ * 2.0. */ +import pMap from 'p-map'; +import { CONCURRENT_SPACES_TO_CHECK } from '../../deprecations/constants'; import { defaultLogViewId } from '../../../common/log_views'; import { MIGRATE_LOG_VIEW_SETTINGS_URL } from '../../../common/http_api/deprecations'; import { logSourcesKibanaAdvancedSettingRT } from '../../../common'; @@ -23,17 +25,54 @@ export const initMigrateLogViewSettingsRoute = ({ { path: MIGRATE_LOG_VIEW_SETTINGS_URL, validate: false }, async (context, request, response) => { try { + const { elasticsearch, savedObjects } = await context.core; const [_, pluginStartDeps, pluginStart] = await getStartServices(); - const logSourcesService = - await pluginStartDeps.logsDataAccess.services.logSourcesServiceFactory.getScopedLogSourcesService( - request - ); - const logViewsClient = pluginStart.logViews.getScopedClient(request); + const allAvailableSpaces = await pluginStartDeps.spaces.spacesService + .createSpacesClient(request) + .getAll({ purpose: 'any' }); - const logView = await logViewsClient.getLogView(defaultLogViewId); + const updated = await pMap( + allAvailableSpaces, + async (space) => { + const spaceScopedSavedObjectsClient = savedObjects.client.asScopedToNamespace(space.id); - if (!logView || logSourcesKibanaAdvancedSettingRT.is(logView.attributes.logIndices)) { + const logSourcesServicePromise = + pluginStartDeps.logsDataAccess.services.logSourcesServiceFactory.getLogSourcesService( + spaceScopedSavedObjectsClient + ); + const logViewsClient = pluginStart.logViews.getClient( + spaceScopedSavedObjectsClient, + elasticsearch.client.asCurrentUser, + logSourcesServicePromise + ); + + const logView = await logViewsClient.getLogView(defaultLogViewId); + + if (!logView || logSourcesKibanaAdvancedSettingRT.is(logView.attributes.logIndices)) { + return false; + } + + const indices = ( + await logViewsClient.getResolvedLogView({ + type: 'log-view-reference', + logViewId: defaultLogViewId, + }) + ).indices; + + const logSourcesService = await logSourcesServicePromise; + await logSourcesService.setLogSources([{ indexPattern: indices }]); + await logViewsClient.putLogView(defaultLogViewId, { + logIndices: { type: 'kibana_advanced_setting' }, + }); + + return true; + }, + { concurrency: CONCURRENT_SPACES_TO_CHECK } + ); + + if (!updated.includes(true)) { + // Only throw if none of the spaces was able to migrate return response.customError({ body: new Error( "Unable to migrate log view settings. A log view either doesn't exist or is already using the Kibana advanced setting." @@ -42,17 +81,6 @@ export const initMigrateLogViewSettingsRoute = ({ }); } - const indices = ( - await logViewsClient.getResolvedLogView({ - type: 'log-view-reference', - logViewId: defaultLogViewId, - }) - ).indices; - - await logSourcesService.setLogSources([{ indexPattern: indices }]); - await logViewsClient.putLogView(defaultLogViewId, { - logIndices: { type: 'kibana_advanced_setting' }, - }); return response.ok(); } catch (error) { throw error; diff --git a/x-pack/plugins/observability_solution/logs_shared/server/types.ts b/x-pack/plugins/observability_solution/logs_shared/server/types.ts index 73365ece21a14..770229ec035d8 100644 --- a/x-pack/plugins/observability_solution/logs_shared/server/types.ts +++ b/x-pack/plugins/observability_solution/logs_shared/server/types.ts @@ -12,6 +12,7 @@ import { } from '@kbn/data-plugin/server'; import { PluginStart as DataViewsPluginStart } from '@kbn/data-views-plugin/server'; import { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/server'; +import type { SpacesPluginStart } from '@kbn/spaces-plugin/server'; import { LogsSharedDomainLibs } from './lib/logs_shared_types'; import { LogViewsServiceSetup, LogViewsServiceStart } from './services/log_views/types'; @@ -38,6 +39,7 @@ export interface LogsSharedServerPluginStartDeps { data: DataPluginStart; dataViews: DataViewsPluginStart; logsDataAccess: LogsDataAccessPluginStart; + spaces: SpacesPluginStart; } export interface UsageCollector { diff --git a/x-pack/plugins/observability_solution/logs_shared/tsconfig.json b/x-pack/plugins/observability_solution/logs_shared/tsconfig.json index de14d6ae57492..cf1bb42b058be 100644 --- a/x-pack/plugins/observability_solution/logs_shared/tsconfig.json +++ b/x-pack/plugins/observability_solution/logs_shared/tsconfig.json @@ -52,5 +52,6 @@ "@kbn/field-formats-plugin", "@kbn/embeddable-plugin", "@kbn/saved-search-plugin", + "@kbn/spaces-plugin", ] } diff --git a/x-pack/plugins/reporting/server/deprecations/migrate_existing_indices_ilm_policy.test.ts b/x-pack/plugins/reporting/server/deprecations/migrate_existing_indices_ilm_policy.test.ts index 420809aba646e..0e421063d51a8 100644 --- a/x-pack/plugins/reporting/server/deprecations/migrate_existing_indices_ilm_policy.test.ts +++ b/x-pack/plugins/reporting/server/deprecations/migrate_existing_indices_ilm_policy.test.ts @@ -6,7 +6,11 @@ */ import type { GetDeprecationsContext } from '@kbn/core/server'; -import { elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks'; +import { + elasticsearchServiceMock, + httpServerMock, + savedObjectsClientMock, +} from '@kbn/core/server/mocks'; import { getDeprecationsInfo } from './migrate_existing_indices_ilm_policy'; @@ -20,7 +24,11 @@ describe("Migrate existing indices' ILM policy deprecations", () => { beforeEach(async () => { esClient = elasticsearchServiceMock.createScopedClusterClient(); - deprecationsCtx = { esClient, savedObjectsClient: savedObjectsClientMock.create() }; + deprecationsCtx = { + esClient, + savedObjectsClient: savedObjectsClientMock.create(), + request: httpServerMock.createKibanaRequest(), + }; }); const createIndexSettings = (lifecycleName: string) => ({ diff --git a/x-pack/plugins/search_inference_endpoints/public/hooks/use_delete_endpoint.test.tsx b/x-pack/plugins/search_inference_endpoints/public/hooks/use_delete_endpoint.test.tsx index 7f2c2157a3609..553b648013600 100644 --- a/x-pack/plugins/search_inference_endpoints/public/hooks/use_delete_endpoint.test.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/hooks/use_delete_endpoint.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useKibana } from './use_kibana'; import { useDeleteEndpoint } from './use_delete_endpoint'; @@ -43,7 +43,7 @@ describe('useDeleteEndpoint', () => { }; it('should call delete endpoint and show success toast on success', async () => { - const { result, waitFor } = renderHook(() => useDeleteEndpoint(), { wrapper }); + const { result } = renderHook(() => useDeleteEndpoint(), { wrapper }); result.current.mutate({ type: 'text_embedding', id: 'in-1' }); @@ -60,7 +60,7 @@ describe('useDeleteEndpoint', () => { it('should show error toast on failure', async () => { const error = { body: { message: 'error' } }; mockDelete.mockRejectedValue(error); - const { result, waitFor } = renderHook(() => useDeleteEndpoint(), { wrapper }); + const { result } = renderHook(() => useDeleteEndpoint(), { wrapper }); result.current.mutate({ type: 'model', id: '123' }); diff --git a/x-pack/plugins/search_inference_endpoints/public/hooks/use_scan_usage.test.tsx b/x-pack/plugins/search_inference_endpoints/public/hooks/use_scan_usage.test.tsx index 5f1766b7a6fa4..f243f5f7a20d0 100644 --- a/x-pack/plugins/search_inference_endpoints/public/hooks/use_scan_usage.test.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/hooks/use_scan_usage.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useScanUsage } from './use_scan_usage'; import { useKibana } from './use_kibana'; @@ -43,7 +43,7 @@ describe('useScanUsage', () => { }); test('should call API endpoint with the correct parameters and return response', async () => { - const { result, waitForNextUpdate } = renderHook( + const { result } = renderHook( () => useScanUsage({ type: 'text_embedding', @@ -52,18 +52,18 @@ describe('useScanUsage', () => { { wrapper } ); - await waitForNextUpdate(); + await waitFor(() => { + expect(mockDelete).toHaveBeenCalledWith( + '/internal/inference_endpoint/endpoints/text_embedding/in-1', + { query: { scanUsage: true } } + ); - expect(mockDelete).toHaveBeenCalledWith( - '/internal/inference_endpoint/endpoints/text_embedding/in-1', - { query: { scanUsage: true } } - ); - - expect(result.current.data).toEqual({ - acknowledge: true, - error_message: 'inference id is being used', - indexes: ['index1', 'index2'], - pipelines: ['pipeline1', 'pipeline2'], + expect(result.current.data).toEqual({ + acknowledge: true, + error_message: 'inference id is being used', + indexes: ['index1', 'index2'], + pipelines: ['pipeline1', 'pipeline2'], + }); }); }); }); diff --git a/x-pack/plugins/search_inference_endpoints/public/hooks/use_table_data.test.tsx b/x-pack/plugins/search_inference_endpoints/public/hooks/use_table_data.test.tsx index b16fb5f7675cb..c5d3cf15f1407 100644 --- a/x-pack/plugins/search_inference_endpoints/public/hooks/use_table_data.test.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/hooks/use_table_data.test.tsx @@ -6,7 +6,7 @@ */ import { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { QueryParams } from '../components/all_inference_endpoints/types'; import { useTableData } from './use_table_data'; import { INFERENCE_ENDPOINTS_TABLE_PER_PAGE_VALUES } from '../components/all_inference_endpoints/types'; diff --git a/x-pack/plugins/search_playground/public/hooks/use_llms_models.test.ts b/x-pack/plugins/search_playground/public/hooks/use_llms_models.test.ts index c529a9d4b9aa6..be215e0f9e15a 100644 --- a/x-pack/plugins/search_playground/public/hooks/use_llms_models.test.ts +++ b/x-pack/plugins/search_playground/public/hooks/use_llms_models.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useLoadConnectors } from './use_load_connectors'; import { useLLMsModels } from './use_llms_models'; import { LLMs } from '../types'; diff --git a/x-pack/plugins/search_playground/public/hooks/use_load_connectors.test.ts b/x-pack/plugins/search_playground/public/hooks/use_load_connectors.test.ts index eb2f36eb62e5f..57020cbcd6d3c 100644 --- a/x-pack/plugins/search_playground/public/hooks/use_load_connectors.test.ts +++ b/x-pack/plugins/search_playground/public/hooks/use_load_connectors.test.ts @@ -8,7 +8,7 @@ import { loadAllActions as loadConnectors } from '@kbn/triggers-actions-ui-plugin/public/common/constants'; import { useLoadConnectors } from './use_load_connectors'; import { useKibana } from './use_kibana'; -import { act, renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants'; const mockedLoadConnectors = loadConnectors as jest.Mock; @@ -80,11 +80,9 @@ describe('useLoadConnectors', () => { ]; mockedLoadConnectors.mockResolvedValue(connectors); - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useLoadConnectors()); - await waitForNextUpdate(); - - await expect(result.current).resolves.toStrictEqual([ + const { result } = renderHook(() => useLoadConnectors()); + await waitFor(() => + expect(result.current).resolves.toStrictEqual([ { actionTypeId: '.gen-ai', config: { @@ -122,8 +120,8 @@ describe('useLoadConnectors', () => { title: 'OpenAI Other', type: 'openai_other', }, - ]); - }); + ]) + ); }); it('handles pre-configured connectors', async () => { @@ -149,11 +147,9 @@ describe('useLoadConnectors', () => { ]; mockedLoadConnectors.mockResolvedValue(connectors); - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useLoadConnectors()); - await waitForNextUpdate(); - - await expect(result.current).resolves.toStrictEqual([ + const { result } = renderHook(() => useLoadConnectors()); + await waitFor(() => + expect(result.current).resolves.toStrictEqual([ { actionTypeId: '.gen-ai', id: '1', @@ -171,22 +167,20 @@ describe('useLoadConnectors', () => { title: 'OpenAI Azure', type: 'openai_azure', }, - ]); - }); + ]) + ); }); it('handles errors correctly', async () => { const error = new Error('Test Error'); mockedLoadConnectors.mockRejectedValue(error); - await act(async () => { - const { waitForNextUpdate } = renderHook(() => useLoadConnectors()); - await waitForNextUpdate(); - + renderHook(() => useLoadConnectors()); + await waitFor(() => expect(mockedUseKibana().services.notifications.toasts.addError).toHaveBeenCalledWith( error, expect.any(Object) - ); - }); + ) + ); }); }); diff --git a/x-pack/plugins/search_playground/public/hooks/use_load_fields_by_indices.test.ts b/x-pack/plugins/search_playground/public/hooks/use_load_fields_by_indices.test.ts index 62f6a1acb6f64..c5c70cbdfc9d9 100644 --- a/x-pack/plugins/search_playground/public/hooks/use_load_fields_by_indices.test.ts +++ b/x-pack/plugins/search_playground/public/hooks/use_load_fields_by_indices.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useLoadFieldsByIndices } from './use_load_fields_by_indices'; import { useUsageTracker } from './use_usage_tracker'; import { useIndicesFields } from './use_indices_fields'; diff --git a/x-pack/plugins/search_playground/public/hooks/use_management_link.test.ts b/x-pack/plugins/search_playground/public/hooks/use_management_link.test.ts index ed566bbaee9f5..8093dfcea061a 100644 --- a/x-pack/plugins/search_playground/public/hooks/use_management_link.test.ts +++ b/x-pack/plugins/search_playground/public/hooks/use_management_link.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useManagementLink } from './use_management_link'; import { useKibana } from './use_kibana'; @@ -41,13 +41,13 @@ describe('useManagementLink Hook', () => { 'http://localhost:5601/app/management/insightsAndAlerting/triggersActionsConnectors'; mockGetUrl.mockResolvedValue(expectedUrl); const connectorId = 'test-connector-id'; - const { result, waitForNextUpdate } = renderHook(() => useManagementLink(connectorId)); - await waitForNextUpdate(); - - expect(result.current).toBe(expectedUrl); - expect(mockGetUrl).toHaveBeenCalledWith({ - sectionId: 'insightsAndAlerting', - appId: 'triggersActionsConnectors/connectors/test-connector-id', + const { result } = renderHook(() => useManagementLink(connectorId)); + await waitFor(() => { + expect(result.current).toBe(expectedUrl); + expect(mockGetUrl).toHaveBeenCalledWith({ + sectionId: 'insightsAndAlerting', + appId: 'triggersActionsConnectors/connectors/test-connector-id', + }); }); }); diff --git a/x-pack/plugins/search_playground/public/hooks/use_query_indices.test.ts b/x-pack/plugins/search_playground/public/hooks/use_query_indices.test.ts index f94bccba2bcbd..9e38efcf696b6 100644 --- a/x-pack/plugins/search_playground/public/hooks/use_query_indices.test.ts +++ b/x-pack/plugins/search_playground/public/hooks/use_query_indices.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useQueryIndices } from './use_query_indices'; jest.mock('./use_kibana', () => ({ diff --git a/x-pack/plugins/search_playground/public/hooks/use_source_indices_fields.test.tsx b/x-pack/plugins/search_playground/public/hooks/use_source_indices_fields.test.tsx index 4adf3d18ea92b..1ae008fe39bec 100644 --- a/x-pack/plugins/search_playground/public/hooks/use_source_indices_fields.test.tsx +++ b/x-pack/plugins/search_playground/public/hooks/use_source_indices_fields.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { useUsageTracker } from './use_usage_tracker'; import { useController } from 'react-hook-form'; import { useIndicesFields } from './use_indices_fields'; diff --git a/x-pack/plugins/search_playground/public/hooks/use_usage_tracker.test.ts b/x-pack/plugins/search_playground/public/hooks/use_usage_tracker.test.ts index 9fb74674504f8..7f0202a820894 100644 --- a/x-pack/plugins/search_playground/public/hooks/use_usage_tracker.test.ts +++ b/x-pack/plugins/search_playground/public/hooks/use_usage_tracker.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useKibana } from './use_kibana'; import { useUsageTracker } from './use_usage_tracker'; diff --git a/x-pack/plugins/security/server/deprecations/kibana_user_role.test.ts b/x-pack/plugins/security/server/deprecations/kibana_user_role.test.ts index c0b1c31a91aa9..4f9d9f29253db 100644 --- a/x-pack/plugins/security/server/deprecations/kibana_user_role.test.ts +++ b/x-pack/plugins/security/server/deprecations/kibana_user_role.test.ts @@ -12,6 +12,7 @@ import type { PackageInfo, RegisterDeprecationsConfig } from '@kbn/core/server'; import { deprecationsServiceMock, elasticsearchServiceMock, + httpServerMock, loggingSystemMock, savedObjectsClientMock, } from '@kbn/core/server/mocks'; @@ -39,6 +40,7 @@ function getContextMock() { return { esClient: elasticsearchServiceMock.createScopedClusterClient(), savedObjectsClient: savedObjectsClientMock.create(), + request: httpServerMock.createKibanaRequest(), }; } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_overview_tab.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_overview_tab.tsx index 7fd3cc270286c..6ad182d722517 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_overview_tab.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_overview_tab.tsx @@ -88,7 +88,7 @@ const ExpandableSection = ({ title, isOpen, toggle, children }: ExpandableSectio }; interface RuleOverviewTabProps { - rule: RuleResponse; + rule: Partial; columnWidths?: EuiDescriptionListProps['columnWidths']; expandedOverviewSections: Record; toggleOverviewSection: Record void>; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/index.tsx index 60a44c251e924..584a5fb9320f3 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/rule_details_flyout/index.tsx @@ -126,7 +126,7 @@ export const MigrationRuleDetailsFlyout: React.FC { const elasticRule = ruleMigration?.elastic_rule; if (isMigrationCustomRule(elasticRule)) { - return convertMigrationCustomRuleToSecurityRulePayload(elasticRule, false) as RuleResponse; // TODO: we need to adjust RuleOverviewTab to allow partial RuleResponse as a parameter; + return convertMigrationCustomRuleToSecurityRulePayload(elasticRule, false); } return matchedPrebuiltRule; }, [ruleMigration, matchedPrebuiltRule]); diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/process_query/process_query.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/process_query/process_query.ts index 27a9bca16390d..e4b7e64e85b00 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/process_query/process_query.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/process_query/process_query.ts @@ -32,7 +32,6 @@ export const getProcessQueryNode = ({ const response = await replaceQueryResourcePrompt.invoke({ query: state.original_rule.query, macros: resourceContext.macros, - lookup_tables: resourceContext.lists, }); const splQuery = response.match(/```spl\n([\s\S]*?)\n```/)?.[1] ?? ''; if (splQuery) { diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/process_query/prompts.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/process_query/prompts.ts index be19ca8b0bf10..68eaaeffd11b1 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/process_query/prompts.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/nodes/process_query/prompts.ts @@ -43,7 +43,7 @@ export const REPLACE_QUERY_RESOURCE_PROMPT = ChatPromptTemplate.fromMessages([ [ 'system', `You are an agent expert in Splunk SPL (Search Processing Language). -Your task is to inline a set of macros and lookup tables syntax using their values in a SPL query. +Your task is to inline a set of macros syntax using its values in a SPL query. Here are some context for you to reference for your task, read it carefully as you will get questions about it later: @@ -72,54 +72,14 @@ The correct replacement would be: | table * \`\`\` - - -Always follow the below guidelines when replacing lookup tables: -- OUTPUTNEW and OUTPUT fields should be replaced with the values from the lookup table. -- Use the \`case\` function to evaluate conditions in the same order provided by the lookup table. -- Ensure all lookup matching fields are correctly matched to their respective case conditions. -- If there are more than one field to match, use the \`AND\` operator to combine them inside the \`case\` function. -- The transformed SPL query should function equivalently to the original query with the \`lookup\` command. - - -Having the following lookup table: - uid,username,department - 1066,Claudia Garcia,Engineering - 1690,Rutherford Sullivan,Engineering - 1815,Vanya Patel,IT - 1862,Wei Zhang,Engineering - 1916,Alex Martin,Personnel -And the following SPL query: - \`\`\`spl - ... | lookup users uid OUTPUTNEW username, department - \`\`\` -The correct replacement would be: - \`\`\`spl - ... | eval username=case(uid=1066, "Claudia Garcia", - uid=1690, "Rutherford Sullivan", - uid=1815, "Vanya Patel", - uid=1862, "Wei Zhang", - uid=1916, "Alex Martin", - true, null), - department=case(uid=1066, "Engineering", - uid=1690, "Engineering", - uid=1815, "IT", - uid=1862, "Engineering", - uid=1916, "Personnel", - true, null) - \`\`\` - `, ], [ 'human', - `Go through the SPL query and identify all the macros and lookup tables that are used. + `Go through the SPL query and identify all the macros that are used. {macros} - -{lookup_tables} - \`\`\`spl @@ -127,13 +87,13 @@ The correct replacement would be: \`\`\` -Divide the query up into separate section and go through each section one at a time to identify the macros and lookup tables used that need to be replaced using one of two scenarios: -- The macro or lookup table is provided in the resources: Replace it using its actual content. -- The macro or lookup table is not provided in the resources: Do not replace it, keep it in the query as it is. +Divide the query up into separate section and go through each section one at a time to identify the macros used that need to be replaced using one of two scenarios: +- The macro is provided in the list of available macros: Replace it using its actual content. +- The macro is not in the list of available macros: Do not replace it, keep it in the query as it is. -- You will be provided with a SPL query and also the resources reference with the values of macros and lookup tables. -- You have to replace the macros and lookup tables syntax in the SPL query and use their values inline, if provided. +- You will be provided with a SPL query and also the related macros used in the query. +- You have to replace the macros syntax in the SPL query and use their values inline, if provided. - The original and modified queries must be equivalent. - You must respond only with the modified query inside a \`\`\`spl code block, nothing else similar to the example response below. diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/ecs_mapping/ecs_mapping.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/ecs_mapping/ecs_mapping.ts index 08a6c5904819a..07753432e5dbc 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/ecs_mapping/ecs_mapping.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/ecs_mapping/ecs_mapping.ts @@ -59,7 +59,7 @@ export const getEcsMappingNode = ({ }; const getTranslationResult = (esqlQuery: string): RuleTranslationResult => { - if (esqlQuery.match(/\[(macro|lookup):[\s\S]*\]/)) { + if (esqlQuery.match(/\[(macro):[\s\S]*\]/)) { return RuleTranslationResult.PARTIAL; } return RuleTranslationResult.FULL; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/prompts.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/prompts.ts index 1ea8295c7402f..626251c3c8259 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/prompts.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/prompts.ts @@ -9,33 +9,49 @@ import { ChatPromptTemplate } from '@langchain/core/prompts'; export const ESQL_SYNTAX_TRANSLATION_PROMPT = ChatPromptTemplate.fromTemplate(`You are a helpful cybersecurity (SIEM) expert agent. Your task is to migrate "detection rules" from Splunk SPL to Elasticsearch ES|QL. -Your goal is to translate the SPL query syntax into an equivalent Elastic Search Query Language (ES|QL) query without changing any of the field names and focusing only on translating the syntax and structure. +Your goal is to translate the SPL query syntax into an equivalent Elastic Search Query Language (ES|QL) query without changing any of the field names except lookup lists and macros when relevant and focusing only on translating the syntax and structure. Here are some context for you to reference for your task, read it carefully as you will get questions about it later: {splunk_rule} - -If, in the SPL query, you find a lookup list or macro call, mention it in the summary and add a placeholder in the query with the format [macro:(argumentCount)] or [lookup:] including the [] keys, + +If, in the SPL query, you find a macro call, mention it in the summary and add a placeholder in the query with the format [macro:(argumentCount)] including the [] keys, Examples: - \`get_duration(firstDate,secondDate)\` -> [macro:get_duration(2)] - - lookup dns_domains.csv -> [lookup:dns_domains.csv]. - + + +If in an SPL query you identify a looku list call, it should be translated the following way: +\`\`\`spl +... | lookup users uid OUTPUTNEW username, department +\`\`\` + +In the above example it uses the following syntax: +lookup 'index_name' 'field_to_match' OUTPUTNEW 'field1', 'field2' + +However in the ES|QL query, some of the information is removed and should be used in the following way: +\`\`\`esql +... | LOOKUP JOIN 'index_name' ON 'field_to_match' +\`\`\` +We do not define OUTPUTNEW or which fields is returned, only the index name and the field to match. + Go through each step and part of the splunk rule and query while following the below guide to produce the resulting ES|QL query: - Analyze all the information about the related splunk rule and try to determine the intent of the rule, in order to translate into an equivalent ES|QL rule. - Go through each part of the SPL query and determine the steps required to produce the same end results using ES|QL. Only focus on translating the structure without modifying any of the field names. - Do NOT map any of the fields to the Elastic Common Schema (ECS), this will happen in a later step. -- Always remember to replace any lookup list or macro call with the appropriate placeholder as defined in the context. +- Always remember to translate any lookup list using the lookup_syntax above +- Always remember to replace macro call with the appropriate placeholder as defined in the macro info. - Analyze the SPL query and identify the key components. - Do NOT translate the field names of the SPL query. - Always start the resulting ES|QL query by filtering using FROM and with these index patterns: {indexPatterns}. -- Remember to always replace any lookup list or macro call with the appropriate placeholder as defined in the context. +- Always remember to translate any lookup list using the lookup_syntax above +- Always remember to replace macro call with the appropriate placeholder as defined in the macro info. diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/translate_rule.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/translate_rule.ts index e09c4e6961141..346df02714b67 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/translate_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/agent/sub_graphs/translate_rule/nodes/translate_rule/translate_rule.ts @@ -64,7 +64,7 @@ export const getTranslateRuleNode = ({ }; const getTranslationResult = (esqlQuery: string): RuleTranslationResult => { - if (esqlQuery.match(/\[(macro|lookup):[\s\S]*\]/)) { + if (esqlQuery.match(/\[(macro):[\s\S]*\]/)) { return RuleTranslationResult.PARTIAL; } return RuleTranslationResult.FULL; diff --git a/x-pack/plugins/spaces/server/saved_objects/saved_objects_spaces_extension.test.ts b/x-pack/plugins/spaces/server/saved_objects/saved_objects_spaces_extension.test.ts index a2a4877ac4d9a..6d5c98d2400a0 100644 --- a/x-pack/plugins/spaces/server/saved_objects/saved_objects_spaces_extension.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/saved_objects_spaces_extension.test.ts @@ -141,3 +141,13 @@ describe('#getSearchableNamespaces', () => { ]); }); }); + +describe('#asScopedToNamespace', () => { + test('returns a extension scoped to the provided namespace', () => { + const { spacesExtension } = setup(); + const rescopedExtension = spacesExtension.asScopedToNamespace('space-a'); + expect(rescopedExtension).toBeInstanceOf(SavedObjectsSpacesExtension); + expect(rescopedExtension).not.toStrictEqual(spacesExtension); + expect(rescopedExtension.getCurrentNamespace(undefined)).toStrictEqual('namespace-for-space-a'); + }); +}); diff --git a/x-pack/plugins/spaces/server/saved_objects/saved_objects_spaces_extension.ts b/x-pack/plugins/spaces/server/saved_objects/saved_objects_spaces_extension.ts index 4520ff5ae3353..3701922d9a776 100644 --- a/x-pack/plugins/spaces/server/saved_objects/saved_objects_spaces_extension.ts +++ b/x-pack/plugins/spaces/server/saved_objects/saved_objects_spaces_extension.ts @@ -52,4 +52,11 @@ export class SavedObjectsSpacesExtension implements ISavedObjectsSpacesExtension ); } } + + asScopedToNamespace(namespace: string) { + return new SavedObjectsSpacesExtension({ + activeSpaceId: namespace, + spacesClient: this.spacesClient, + }); + } } diff --git a/x-pack/plugins/stack_alerts/public/rule_types/components/data_view_select_popover.tsx b/x-pack/plugins/stack_alerts/public/rule_types/components/data_view_select_popover.tsx index 988afb8d2182b..97b614a5b5785 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/components/data_view_select_popover.tsx +++ b/x-pack/plugins/stack_alerts/public/rule_types/components/data_view_select_popover.tsx @@ -18,6 +18,7 @@ import { EuiPopover, EuiPopoverFooter, EuiPopoverTitle, + EuiLoadingSpinner, EuiText, useEuiPaddingCSS, useIsWithinBreakpoints, @@ -63,6 +64,7 @@ export const DataViewSelectPopover: React.FunctionComponent { + const [loadingDataViews, setLoadingDataViews] = useState(false); const [dataViewItems, setDataViewsItems] = useState([]); const [dataViewPopoverOpen, setDataViewPopoverOpen] = useState(false); @@ -71,7 +73,7 @@ export const DataViewSelectPopover: React.FunctionComponent void | undefined>(); const allDataViewItems = useMemo( - () => [...dataViewItems, ...metadata.adHocDataViewList.map(toDataViewListItem)], + () => [...(dataViewItems ?? []), ...metadata.adHocDataViewList.map(toDataViewListItem)], [dataViewItems, metadata.adHocDataViewList] ); @@ -87,10 +89,16 @@ export const DataViewSelectPopover: React.FunctionComponent { - const ids = await dataViews.getIds(); - const dataViewsList = await Promise.all(ids.map((id) => dataViews.get(id))); - - setDataViewsItems(dataViewsList.map(toDataViewListItem)); + setLoadingDataViews(true); + try { + // Calling getIds with refresh = true to make sure we don't get stale data + const ids = await dataViews.getIds(true); + const dataViewsList = await Promise.all(ids.map((id) => dataViews.get(id))); + setDataViewsItems(dataViewsList.map(toDataViewListItem)); + } catch (e) { + // Error fetching data views + } + setLoadingDataViews(false); }, [dataViews]); const onAddAdHocDataView = useCallback( @@ -153,8 +161,10 @@ export const DataViewSelectPopover: React.FunctionComponent; } return ( diff --git a/x-pack/plugins/stack_connectors/server/routes/get_inference_services.ts b/x-pack/plugins/stack_connectors/server/routes/get_inference_services.ts index 005b565dff0d5..3c4a1b3aa1a35 100644 --- a/x-pack/plugins/stack_connectors/server/routes/get_inference_services.ts +++ b/x-pack/plugins/stack_connectors/server/routes/get_inference_services.ts @@ -20,6 +20,13 @@ export const getInferenceServicesRoute = (router: IRouter) => { router.get( { path: `${INTERNAL_BASE_STACK_CONNECTORS_API_PATH}/_inference/_services`, + security: { + authz: { + enabled: false, + reason: + 'This route is opted out of authorization as it relies on ES authorization instead.', + }, + }, options: { access: 'internal', }, diff --git a/x-pack/plugins/stack_connectors/server/routes/get_well_known_email_service.ts b/x-pack/plugins/stack_connectors/server/routes/get_well_known_email_service.ts index 5e15ca4c32838..2a0d02cb860a8 100644 --- a/x-pack/plugins/stack_connectors/server/routes/get_well_known_email_service.ts +++ b/x-pack/plugins/stack_connectors/server/routes/get_well_known_email_service.ts @@ -26,6 +26,13 @@ export const getWellKnownEmailServiceRoute = (router: IRouter) => { router.get( { path: `${INTERNAL_BASE_STACK_CONNECTORS_API_PATH}/_email_config/{service}`, + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization as returning SMTP connection details does not require any.', + }, + }, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/stack_connectors/server/routes/valid_slack_api_channels.ts b/x-pack/plugins/stack_connectors/server/routes/valid_slack_api_channels.ts index 420af8d104891..ed58456d1929f 100644 --- a/x-pack/plugins/stack_connectors/server/routes/valid_slack_api_channels.ts +++ b/x-pack/plugins/stack_connectors/server/routes/valid_slack_api_channels.ts @@ -34,6 +34,13 @@ export const validSlackApiChannelsRoute = ( router.post( { path: `${INTERNAL_BASE_STACK_CONNECTORS_API_PATH}/_slack_api/channels/_valid`, + security: { + authz: { + enabled: false, + reason: + "This route is opted out from authorization as it relies on Slack's own authorization.", + }, + }, validate: { body: bodySchema, }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.test.tsx index df52b1729bb8d..1c5008d4e1dde 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.test.tsx @@ -9,7 +9,7 @@ import React, { lazy } from 'react'; import { actionTypeRegistryMock } from '../../../action_type_registry.mock'; import userEvent from '@testing-library/user-event'; -import { waitFor, act } from '@testing-library/react'; +import { waitFor, act, screen } from '@testing-library/react'; import CreateConnectorFlyout from '.'; import { AppMockRenderer, createAppMockRenderer } from '../../test_utils'; import { TECH_PREVIEW_LABEL } from '../../translations'; @@ -426,7 +426,7 @@ describe('CreateConnectorFlyout', () => { describe('Submitting', () => { it('creates a connector correctly', async () => { - const { getByTestId } = appMockRenderer.render( + const { getByTestId, queryByTestId } = appMockRenderer.render( { name: 'My test', secrets: {}, }); + expect(queryByTestId('connector-form-header-error-label')).not.toBeInTheDocument(); + }); + + it('show error message in the form header', async () => { + appMockRenderer.render( + + ); + + await userEvent.click(await screen.findByTestId(`${actionTypeModel.id}-card`)); + expect(await screen.findByTestId('test-connector-text-field')).toBeInTheDocument(); + + await userEvent.type( + await screen.findByTestId('test-connector-text-field'), + 'My text field', + { + delay: 100, + } + ); + + await userEvent.click(await screen.findByTestId('create-connector-flyout-save-btn')); + expect(onClose).not.toHaveBeenCalled(); + expect(onConnectorCreated).not.toHaveBeenCalled(); + expect(await screen.findByTestId('connector-form-header-error-label')).toBeInTheDocument(); + }); + + it('removes error message from the form header', async () => { + appMockRenderer.render( + + ); + + await userEvent.click(await screen.findByTestId(`${actionTypeModel.id}-card`)); + expect(await screen.findByTestId('test-connector-text-field')).toBeInTheDocument(); + + await userEvent.type( + await screen.findByTestId('test-connector-text-field'), + 'My text field', + { + delay: 100, + } + ); + + await userEvent.click(await screen.findByTestId('create-connector-flyout-save-btn')); + expect(onClose).not.toHaveBeenCalled(); + expect(onConnectorCreated).not.toHaveBeenCalled(); + expect(await screen.findByTestId('connector-form-header-error-label')).toBeInTheDocument(); + + await userEvent.type(await screen.findByTestId('nameInput'), 'My test', { + delay: 100, + }); + + await userEvent.click(await screen.findByTestId('create-connector-flyout-save-btn')); + expect(onClose).toHaveBeenCalled(); + expect(onConnectorCreated).toHaveBeenCalled(); + expect(screen.queryByTestId('connector-form-header-error-label')).not.toBeInTheDocument(); }); it('runs pre submit validator correctly', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.tsx index b93d2815bf197..c341f861b5ead 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.tsx @@ -9,6 +9,7 @@ import React, { memo, ReactNode, useCallback, useEffect, useRef, useState } from import { EuiButton, EuiButtonGroup, + EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiFlyout, @@ -18,6 +19,7 @@ import { import { getConnectorCompatibility } from '@kbn/actions-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; import { ActionConnector, ActionType, @@ -60,6 +62,7 @@ const CreateConnectorFlyoutComponent: React.FC = ({ const [actionType, setActionType] = useState(null); const [hasActionsUpgradeableByTrial, setHasActionsUpgradeableByTrial] = useState(false); const canSave = hasSaveActionsCapability(capabilities); + const [showFormErrors, setShowFormErrors] = useState(false); const [preSubmitValidationErrorMessage, setPreSubmitValidationErrorMessage] = useState(null); @@ -106,6 +109,7 @@ const CreateConnectorFlyoutComponent: React.FC = ({ const setResetForm = (reset: ResetForm) => { resetConnectorForm.current = reset; + setShowFormErrors(false); }; const onChangeGroupAction = (id: string) => { @@ -127,6 +131,7 @@ const CreateConnectorFlyoutComponent: React.FC = ({ const validateAndCreateConnector = useCallback(async () => { setPreSubmitValidationErrorMessage(null); + setShowFormErrors(false); const { isValid, data } = await submit(); if (!isMounted.current) { @@ -159,6 +164,8 @@ const CreateConnectorFlyoutComponent: React.FC = ({ const createdConnector = await createConnector(validConnector); return createdConnector; + } else { + setShowFormErrors(true); } }, [submit, preSubmitValidator, createConnector]); @@ -228,6 +235,23 @@ const CreateConnectorFlyoutComponent: React.FC = ({ )} + {showFormErrors && ( + <> + + + + )} { describe('Submitting', () => { it('updates the connector correctly', async () => { - const { getByTestId } = appMockRenderer.render( + const { getByTestId, queryByTestId } = appMockRenderer.render( { name: 'My test', secrets: {}, }); + expect(queryByTestId('connector-form-header-error-label')).not.toBeInTheDocument(); }); it('updates connector form field with latest value', async () => { @@ -555,6 +556,39 @@ describe('EditConnectorFlyout', () => { }); }); + it('show error message in the form header', async () => { + appMockRenderer.render( + + ); + + expect(await screen.findByTestId('test-connector-text-field')).toBeInTheDocument(); + await userEvent.clear(screen.getByTestId('nameInput')); + await userEvent.click(screen.getByTestId('edit-connector-flyout-save-btn')); + expect(await screen.findByTestId('connector-form-header-error-label')).toBeInTheDocument(); + }); + + it('removes error message from the form header', async () => { + appMockRenderer.render( + + ); + + await userEvent.clear(screen.getByTestId('nameInput')); + await userEvent.type(screen.getByTestId('nameInput'), 'My new name'); + await userEvent.type(screen.getByTestId('test-connector-secret-text-field'), 'password'); + await userEvent.click(screen.getByTestId('edit-connector-flyout-save-btn')); + expect(screen.queryByTestId('connector-form-header-error-label')).not.toBeInTheDocument(); + }); + it('runs pre submit validator correctly', async () => { const errorActionTypeModel = actionTypeRegistryMock.createMockActionTypeModel({ actionConnectorFields: lazy(() => import('../connector_error_mock')), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.tsx index e787f3eac42bf..1188f06a87d56 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.tsx @@ -6,7 +6,14 @@ */ import React, { memo, ReactNode, useCallback, useEffect, useRef, useState } from 'react'; -import { EuiFlyout, EuiFlyoutBody, EuiButton, EuiConfirmModal } from '@elastic/eui'; +import { + EuiFlyout, + EuiFlyoutBody, + EuiButton, + EuiConfirmModal, + EuiCallOut, + EuiSpacer, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { ActionTypeExecutorResult, isActionTypeExecutorResult } from '@kbn/actions-plugin/common'; @@ -62,6 +69,7 @@ const EditConnectorFlyoutComponent: React.FC = ({ const canSave = hasSaveActionsCapability(capabilities); const { isLoading: isUpdatingConnector, updateConnector } = useUpdateConnector(); const { isLoading: isExecutingConnector, executeConnector } = useExecuteConnector(); + const [showFormErrors, setShowFormErrors] = useState(false); const [preSubmitValidationErrorMessage, setPreSubmitValidationErrorMessage] = useState(null); @@ -90,6 +98,7 @@ const EditConnectorFlyoutComponent: React.FC = ({ if (nextPage === EditConnectorTabs.Configuration && testExecutionResult !== none) { setTestExecutionResult(none); } + setShowFormErrors(false); setTab(nextPage); }, [testExecutionResult, setTestExecutionResult] @@ -146,6 +155,7 @@ const EditConnectorFlyoutComponent: React.FC = ({ const onClickSave = useCallback(async () => { setPreSubmitValidationErrorMessage(null); + setShowFormErrors(false); const { isValid, data } = await submit(); if (!isMounted.current) { @@ -194,6 +204,8 @@ const EditConnectorFlyoutComponent: React.FC = ({ } return updatedConnector; + } else { + setShowFormErrors(true); } }, [ onConnectorUpdated, @@ -218,6 +230,23 @@ const EditConnectorFlyoutComponent: React.FC = ({ <> {isEdit && ( <> + {showFormErrors && ( + <> + + + + )} = ({ ); }, [ connector, + docLinks.links.alerting.preconfiguredConnectors, actionTypeModel, isEdit, - docLinks.links.alerting.preconfiguredConnectors, - hasErrors, - isFormModified, - isSaved, - isSaving, + showFormErrors, + onFormModifiedChange, preSubmitValidationErrorMessage, showButtons, + isSaved, + isSaving, onClickSave, - onFormModifiedChange, + isFormModified, + hasErrors, ]); const renderTestTab = useCallback(() => { diff --git a/x-pack/plugins/triggers_actions_ui/server/routes/config.ts b/x-pack/plugins/triggers_actions_ui/server/routes/config.ts index 2e10586b76924..644d6bf01f63a 100644 --- a/x-pack/plugins/triggers_actions_ui/server/routes/config.ts +++ b/x-pack/plugins/triggers_actions_ui/server/routes/config.ts @@ -38,6 +38,13 @@ export function createConfigRoute({ router.get( { path, + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization as it uses the alerting client authorization.', + }, + }, validate: false, options: { access: 'internal', diff --git a/x-pack/plugins/triggers_actions_ui/server/routes/health.ts b/x-pack/plugins/triggers_actions_ui/server/routes/health.ts index b57e9b3df4dc6..c9f54b617eff5 100644 --- a/x-pack/plugins/triggers_actions_ui/server/routes/health.ts +++ b/x-pack/plugins/triggers_actions_ui/server/routes/health.ts @@ -25,6 +25,13 @@ export function createHealthRoute( router.get( { path, + security: { + authz: { + enabled: false, + reason: + 'This route is opted out from authorization as the health route does not require any.', + }, + }, validate: false, options: { access: 'internal', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all.ts index 39da3ab33a6ea..b354f0f962d84 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all.ts @@ -6,7 +6,6 @@ */ import expect from '@kbn/expect'; -import { RULE_SAVED_OBJECT_TYPE } from '@kbn/alerting-plugin/server'; import { UserAtSpaceScenarios } from '../../../scenarios'; import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; @@ -199,7 +198,7 @@ export default function getAllConnectorTests({ getService }: FtrProviderContext) }) ) .expect(200); - objectRemover.add(space.id, createdAlert.id, RULE_SAVED_OBJECT_TYPE, 'alerts'); + objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); const response = await supertestWithoutAuth .get(`${getUrlPrefix(space.id)}/api/actions/connectors`) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all_system.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all_system.ts index e19ade26171b1..860ab6daf3eca 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all_system.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all_system.ts @@ -6,7 +6,6 @@ */ import expect from '@kbn/expect'; -import { RULE_SAVED_OBJECT_TYPE } from '@kbn/alerting-plugin/server'; import { SuperuserAtSpace1, UserAtSpaceScenarios } from '../../../scenarios'; import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; @@ -244,7 +243,7 @@ export default function getAllConnectorTests({ getService }: FtrProviderContext) }) ) .expect(200); - objectRemover.add(space.id, createdAlert.id, RULE_SAVED_OBJECT_TYPE, 'alerts'); + objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); const response = await supertestWithoutAuth .get(`${getUrlPrefix(space.id)}/internal/actions/connectors`) diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/connector_types/stack/preconfigured_alert_history_connector.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/connector_types/stack/preconfigured_alert_history_connector.ts index 22f81d7bb7e96..2e98d9129b553 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/connector_types/stack/preconfigured_alert_history_connector.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/connector_types/stack/preconfigured_alert_history_connector.ts @@ -137,10 +137,10 @@ export default function preconfiguredAlertHistoryConnectorTests({ expect().fail(`waiting for alert ${id} statuses ${Array.from(statuses)} timed out`); } - const response = await supertest.get(`/api/alerts/alert/${id}`); + const response = await supertest.get(`/api/alerting/rule/${id}`); expect(response.status).to.eql(200); - const { executionStatus } = response.body || {}; + const { execution_status: executionStatus } = response.body || {}; const { status } = executionStatus || {}; const message = `waitForStatus(${Array.from(statuses)}): got ${JSON.stringify( diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts index 5a6385a3895d2..ecef6541f7181 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts @@ -720,104 +720,5 @@ export default function createAlertTests({ getService }: FtrProviderContext) { }); }); }); - - describe('legacy', function () { - this.tags('skipFIPS'); - it('should handle create alert request appropriately', async () => { - const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'MY action', - connector_type_id: 'test.noop', - config: {}, - secrets: {}, - }) - .expect(200); - - const { - rule_type_id: alertTypeId, - notify_when: notifyWhen, - ...testAlert - } = getTestRuleData({ - actions: [ - { - id: createdAction.id, - group: 'default', - params: {}, - }, - ], - }); - const response = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) - .set('kbn-xsrf', 'foo') - .send({ - ...testAlert, - alertTypeId, - notifyWhen, - }); - - expect(response.status).to.eql(200); - objectRemover.add(Spaces.space1.id, response.body.id, 'rule', 'alerting'); - expect(response.body).to.eql({ - id: response.body.id, - name: 'abc', - tags: ['foo'], - actions: [ - { - id: createdAction.id, - actionTypeId: createdAction.connector_type_id, - group: 'default', - params: {}, - uuid: response.body.actions[0].uuid, - }, - ], - enabled: true, - alertTypeId: 'test.noop', - consumer: 'alertsFixture', - params: {}, - createdBy: null, - schedule: { interval: '1m' }, - scheduledTaskId: response.body.scheduledTaskId, - updatedBy: null, - apiKeyOwner: null, - apiKeyCreatedByUser: null, - throttle: '1m', - notifyWhen: 'onThrottleInterval', - muteAll: false, - mutedInstanceIds: [], - createdAt: response.body.createdAt, - updatedAt: response.body.updatedAt, - executionStatus: response.body.executionStatus, - revision: 0, - running: false, - ...(response.body.next_run ? { next_run: response.body.next_run } : {}), - ...(response.body.last_run ? { last_run: response.body.last_run } : {}), - }); - expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0); - expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0); - expect(Date.parse(response.body.updatedAt)).to.eql(Date.parse(response.body.createdAt)); - if (response.body.next_run) { - expect(Date.parse(response.body.next_run)).to.be.greaterThan(0); - } - expect(typeof response.body.scheduledTaskId).to.be('string'); - const taskRecord = await getScheduledTask(response.body.scheduledTaskId); - expect(taskRecord.type).to.eql('task'); - expect(taskRecord.task.taskType).to.eql('alerting:test.noop'); - expect(JSON.parse(taskRecord.task.params)).to.eql({ - alertId: response.body.id, - spaceId: Spaces.space1.id, - consumer: 'alertsFixture', - }); - expect(taskRecord.task.enabled).to.eql(true); - // Ensure AAD isn't broken - await checkAAD({ - supertest, - spaceId: Spaces.space1.id, - type: RULE_SAVED_OBJECT_TYPE, - id: response.body.id, - }); - }); - }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/delete.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/delete.ts index 811fbbf2b1732..37f482a5d512e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/delete.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/delete.ts @@ -65,27 +65,5 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { message: `Saved object [alert/${createdAlert.id}] not found`, }); }); - - describe('legacy', () => { - it('should handle delete alert request appropriately', async () => { - const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send(getTestRuleData()) - .expect(200); - - await supertest - .delete(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}`) - .set('kbn-xsrf', 'foo') - .expect(204, ''); - - try { - await getScheduledTask(createdAlert.scheduledTaskId); - throw new Error('Should have removed scheduled task'); - } catch (e) { - expect(e.meta.statusCode).to.eql(404); - } - }); - }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/disable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/disable.ts index 01fa746b65f2a..846c4a719522f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/disable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/disable.ts @@ -266,50 +266,5 @@ export default function createDisableRuleTests({ getService }: FtrProviderContex id: createdRule.id, }); }); - - describe('legacy', function () { - it('should handle disable rule request appropriately', async () => { - const { body: createdRule } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send(getTestRuleData({ enabled: true })) - .expect(200); - objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); - - await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdRule.id}/_disable`) - .set('kbn-xsrf', 'foo') - .expect(204); - - // task doc should still exist but be disabled - await retry.try(async () => { - const taskRecord = await getScheduledTask(createdRule.scheduled_task_id); - expect(taskRecord.type).to.eql('task'); - expect(taskRecord.task.taskType).to.eql('alerting:test.noop'); - expect(JSON.parse(taskRecord.task.params)).to.eql({ - alertId: createdRule.id, - spaceId: Spaces.space1.id, - consumer: 'alertsFixture', - }); - expect(taskRecord.task.enabled).to.eql(false); - }); - - const { body: disabledRule } = await supertestWithoutAuth - .get(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${createdRule.id}`) - .set('kbn-xsrf', 'foo') - .expect(200); - - // Ensure revision was not updated - expect(disabledRule.revision).to.eql(0); - - // Ensure AAD isn't broken - await checkAAD({ - supertest: supertestWithoutAuth, - spaceId: Spaces.space1.id, - type: RULE_SAVED_OBJECT_TYPE, - id: createdRule.id, - }); - }); - }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/enable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/enable.ts index 74b8fd0307a86..8eae2dfe7022b 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/enable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/enable.ts @@ -94,48 +94,5 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex }); }); }); - - describe('legacy', function () { - this.tags('skipFIPS'); - it('should handle enable alert request appropriately', async () => { - const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send(getTestRuleData({ enabled: false })) - .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}/_enable`) - .set('kbn-xsrf', 'foo') - .expect(204); - - const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${createdAlert.id}`) - .set('kbn-xsrf', 'foo') - .expect(200); - expect(typeof updatedAlert.scheduled_task_id).to.eql('string'); - const taskRecord = await getScheduledTask(updatedAlert.scheduled_task_id); - expect(taskRecord.type).to.eql('task'); - expect(taskRecord.task.taskType).to.eql('alerting:test.noop'); - expect(JSON.parse(taskRecord.task.params)).to.eql({ - alertId: createdAlert.id, - spaceId: Spaces.space1.id, - consumer: 'alertsFixture', - }); - expect(taskRecord.task.enabled).to.eql(true); - - // Ensure revision was not updated - expect(updatedAlert.revision).to.eql(0); - - // Ensure AAD isn't broken - await checkAAD({ - supertest: supertestWithoutAuth, - spaceId: Spaces.space1.id, - type: RULE_SAVED_OBJECT_TYPE, - id: createdAlert.id, - }); - }); - }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find.ts index f6b48df9b9f17..c902355b68fcc 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find.ts @@ -336,58 +336,5 @@ export default function createFindTests({ getService }: FtrProviderContext) { }); }); }); - - describe('legacy', function () { - this.tags('skipFIPS'); - it('should handle find alert request appropriately', async () => { - const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send(getTestRuleData()) - .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - const response = await supertest.get( - `${getUrlPrefix( - Spaces.space1.id - )}/api/alerts/_find?search=test.noop&search_fields=alertTypeId` - ); - - expect(response.status).to.eql(200); - expect(response.body.page).to.equal(1); - expect(response.body.perPage).to.be.greaterThan(0); - expect(response.body.total).to.be.greaterThan(0); - const match = response.body.data.find((obj: any) => obj.id === createdAlert.id); - expect(match).to.eql({ - id: createdAlert.id, - name: 'abc', - tags: ['foo'], - alertTypeId: 'test.noop', - consumer: 'alertsFixture', - schedule: { interval: '1m' }, - enabled: true, - actions: [], - params: {}, - createdBy: null, - apiKeyOwner: null, - apiKeyCreatedByUser: null, - scheduledTaskId: match.scheduledTaskId, - updatedBy: null, - throttle: '1m', - notifyWhen: 'onThrottleInterval', - muteAll: false, - mutedInstanceIds: [], - createdAt: match.createdAt, - updatedAt: match.updatedAt, - executionStatus: match.executionStatus, - revision: 0, - running: false, - ...(match.nextRun ? { nextRun: match.nextRun } : {}), - ...(match.lastRun ? { lastRun: match.lastRun } : {}), - }); - expect(Date.parse(match.createdAt)).to.be.greaterThan(0); - expect(Date.parse(match.updatedAt)).to.be.greaterThan(0); - }); - }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find_internal.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find_internal.ts index 7c10b9be598bb..e8dc9ac696842 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find_internal.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find_internal.ts @@ -337,54 +337,5 @@ export default function createFindTests({ getService }: FtrProviderContext) { expect(response.body.total).to.equal(1); expect(response.body.data[0].consumer).to.eql('alertsRestrictedFixture'); }); - - describe('legacy', function () { - this.tags('skipFIPS'); - it('should handle find alert request appropriately', async () => { - const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send(getTestRuleData()) - .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - const response = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/_find`); - - expect(response.status).to.eql(200); - expect(response.body.page).to.equal(1); - expect(response.body.perPage).to.be.greaterThan(0); - expect(response.body.total).to.be.greaterThan(0); - const match = response.body.data.find((obj: any) => obj.id === createdAlert.id); - expect(match).to.eql({ - id: createdAlert.id, - name: 'abc', - tags: ['foo'], - alertTypeId: 'test.noop', - consumer: 'alertsFixture', - schedule: { interval: '1m' }, - enabled: true, - actions: [], - params: {}, - createdBy: null, - apiKeyOwner: null, - apiKeyCreatedByUser: null, - scheduledTaskId: match.scheduledTaskId, - updatedBy: null, - throttle: '1m', - notifyWhen: 'onThrottleInterval', - muteAll: false, - mutedInstanceIds: [], - createdAt: match.createdAt, - updatedAt: match.updatedAt, - executionStatus: match.executionStatus, - revision: 0, - running: false, - ...(match.nextRun ? { nextRun: match.nextRun } : {}), - ...(match.lastRun ? { lastRun: match.lastRun } : {}), - }); - expect(Date.parse(match.createdAt)).to.be.greaterThan(0); - expect(Date.parse(match.updatedAt)).to.be.greaterThan(0); - }); - }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get.ts index e559b1d17f196..58fdbf377ecf1 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get.ts @@ -124,55 +124,5 @@ export default function createGetTests({ getService }: FtrProviderContext) { getTestUtils('public', objectRemover, supertest); getTestUtils('internal', objectRemover, supertest); - - describe('legacy', function () { - this.tags('skipFIPS'); - it('should handle get alert request appropriately', async () => { - const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send(getTestRuleData()) - .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}` - ); - - expect(response.status).to.eql(200); - expect(response.body).to.eql({ - id: createdAlert.id, - name: 'abc', - tags: ['foo'], - alertTypeId: 'test.noop', - consumer: 'alertsFixture', - schedule: { interval: '1m' }, - enabled: true, - actions: [], - params: {}, - createdBy: null, - scheduledTaskId: response.body.scheduledTaskId, - updatedBy: null, - apiKeyOwner: null, - apiKeyCreatedByUser: null, - throttle: '1m', - notifyWhen: 'onThrottleInterval', - muteAll: false, - mutedInstanceIds: [], - createdAt: response.body.createdAt, - updatedAt: response.body.updatedAt, - executionStatus: response.body.executionStatus, - revision: 0, - running: false, - ...(response.body.nextRun ? { nextRun: response.body.nextRun } : {}), - ...(response.body.lastRun ? { lastRun: response.body.lastRun } : {}), - }); - expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0); - expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0); - if (response.body.nextRun) { - expect(Date.parse(response.body.nextRun)).to.be.greaterThan(0); - } - }); - }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_state.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_state.ts index 6082e6ff69eb8..87a8449fb37e1 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_state.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_state.ts @@ -91,53 +91,5 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont message: 'Saved object [alert/1] not found', }); }); - - describe('legacy', () => { - it('should fetch updated state', async () => { - const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send({ - enabled: true, - name: 'abc', - tags: ['foo'], - rule_type_id: 'test.cumulative-firing', - consumer: 'alertsFixture', - schedule: { interval: '5s' }, - throttle: '5s', - actions: [], - params: {}, - notify_when: 'onThrottleInterval', - }) - .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - // wait for alert to actually execute - await retry.try(async () => { - const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}/state` - ); - - expect(response.status).to.eql(200); - expect(response.body).to.key('alertInstances', 'alertTypeState', 'previousStartedAt'); - expect(response.body.alertTypeState.runCount).to.greaterThan(1); - }); - - const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdAlert.id}/state` - ); - - expect(response.body.rule_type_state.runCount).to.greaterThan(0); - - const alertInstances = Object.entries>(response.body.alerts); - expect(alertInstances.length).to.eql(response.body.rule_type_state.runCount); - alertInstances.forEach(([key, value], index) => { - expect(key).to.eql(`instance-${index}`); - expect(value.state.instanceStateValue).to.be(true); - expect(value.state.start).not.to.be(undefined); - expect(value.state.duration).not.to.be(undefined); - }); - }); - }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts index 86444543bde73..cee5e44afcc8d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts @@ -384,78 +384,6 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo expect(actualAlerts).to.eql(expectedAlerts); }); }); - - describe('legacy', function () { - this.tags('skipFIPS'); - it('handles multi-alert status', async () => { - // wait so cache expires - await setTimeoutAsync(TEST_CACHE_EXPIRATION_TIME); - - // pattern of when the alert should fire - const pattern = { - alertA: [true, true, true, true], - alertB: [true, true, false, false], - alertC: [true, true, true, true], - }; - - const { body: createdRule } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - rule_type_id: 'test.patternFiring', - params: { pattern }, - schedule: { interval: '1s' }, - }) - ) - .expect(200); - objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); - - await alertUtils.muteInstance(createdRule.id, 'alertC'); - await alertUtils.muteInstance(createdRule.id, 'alertD'); - await waitForEvents(createdRule.id, ['new-instance', 'recovered-instance']); - const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdRule.id}/_instance_summary` - ); - - const actualAlerts = checkAndCleanActualAlerts(response.body.instances, [ - 'alertA', - 'alertB', - 'alertC', - ]); - const expectedAlerts = { - alertA: { - status: 'Active', - muted: false, - actionGroupId: 'default', - activeStartDate: actualAlerts.alertA.activeStartDate, - flapping: false, - tracked: true, - }, - alertB: { - status: 'OK', - muted: false, - flapping: false, - tracked: true, - }, - alertC: { - status: 'Active', - muted: true, - actionGroupId: 'default', - activeStartDate: actualAlerts.alertC.activeStartDate, - flapping: false, - tracked: true, - }, - alertD: { - status: 'OK', - muted: true, - flapping: false, - tracked: true, - }, - }; - expect(actualAlerts).to.eql(expectedAlerts); - }); - }); }); async function waitForEvents(id: string, actions: string[]) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/rule_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/rule_types.ts index e722d058e8b7c..9a0ad8133c02c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/rule_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/rule_types.ts @@ -103,46 +103,5 @@ export default function listRuleTypes({ getService }: FtrProviderContext) { params: [], }); }); - - describe('legacy', () => { - it('should return 200 with list of alert types', async () => { - const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/api/alerts/list_alert_types` - ); - expect(response.status).to.eql(200); - const { authorizedConsumers, ...fixtureAlertType } = response.body.find( - (alertType: any) => alertType.id === 'test.noop' - ); - expect(fixtureAlertType).to.eql({ - actionGroups: [ - { id: 'default', name: 'Default' }, - { id: 'recovered', name: 'Recovered' }, - ], - defaultActionGroupId: 'default', - doesSetRecoveryContext: false, - id: 'test.noop', - name: 'Test: Noop', - actionVariables: { - state: [], - params: [], - context: [], - }, - recoveryActionGroup: { - id: 'recovered', - name: 'Recovered', - }, - category: 'kibana', - producer: 'alertsFixture', - minimumLicenseRequired: 'basic', - isExportable: true, - enabledInLicense: true, - hasFieldsForAAD: false, - hasAlertsMappings: false, - ruleTaskTimeout: '5m', - validLegacyConsumers: ['alerts'], - }); - expect(Object.keys(authorizedConsumers)).to.contain('alertsFixture'); - }); - }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/mute_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/mute_all.ts index e4cbec75460b0..09a12a1371454 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/mute_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/mute_all.ts @@ -51,35 +51,5 @@ export default function createMuteTests({ getService }: FtrProviderContext) { id: createdAlert.id, }); }); - - describe('legacy', () => { - it('should handle mute alert request appropriately', async () => { - const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send(getTestRuleData({ enabled: false })) - .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}/_mute_all`) - .set('kbn-xsrf', 'foo') - .expect(204); - - const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${createdAlert.id}`) - .set('kbn-xsrf', 'foo') - .expect(200); - expect(updatedAlert.mute_all).to.eql(true); - - // Ensure AAD isn't broken - await checkAAD({ - supertest: supertestWithoutAuth, - spaceId: Spaces.space1.id, - type: RULE_SAVED_OBJECT_TYPE, - id: createdAlert.id, - }); - }); - }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/mute_instance.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/mute_instance.ts index ac580ad335c51..65791c86e833b 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/mute_instance.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/mute_instance.ts @@ -52,39 +52,5 @@ export default function createMuteInstanceTests({ getService }: FtrProviderConte id: createdAlert.id, }); }); - - describe('legacy', () => { - it('should handle mute alert instance request appropriately', async () => { - const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send(getTestRuleData({ enabled: false })) - .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - await supertestWithoutAuth - .post( - `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${ - createdAlert.id - }/alert_instance/1/_mute` - ) - .set('kbn-xsrf', 'foo') - .expect(204); - - const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${createdAlert.id}`) - .set('kbn-xsrf', 'foo') - .expect(200); - expect(updatedAlert.muted_alert_ids).to.eql(['1']); - - // Ensure AAD isn't broken - await checkAAD({ - supertest: supertestWithoutAuth, - spaceId: Spaces.space1.id, - type: RULE_SAVED_OBJECT_TYPE, - id: createdAlert.id, - }); - }); - }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/unmute_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/unmute_all.ts index 363086e1549fe..42bca18c07bfc 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/unmute_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/unmute_all.ts @@ -53,39 +53,5 @@ export default function createUnmuteTests({ getService }: FtrProviderContext) { id: createdAlert.id, }); }); - - describe('legacy', () => { - it('should handle unmute alert request appropriately', async () => { - const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send(getTestRuleData({ enabled: false })) - .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}/_mute_all`) - .set('kbn-xsrf', 'foo') - .expect(204); - await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}/_unmute_all`) - .set('kbn-xsrf', 'foo') - .expect(204); - - const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${createdAlert.id}`) - .set('kbn-xsrf', 'foo') - .expect(200); - expect(updatedAlert.mute_all).to.eql(false); - - // Ensure AAD isn't broken - await checkAAD({ - supertest: supertestWithoutAuth, - spaceId: Spaces.space1.id, - type: RULE_SAVED_OBJECT_TYPE, - id: createdAlert.id, - }); - }); - }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/unmute_instance.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/unmute_instance.ts index 5655e52aa4d09..4b54c6c0c134f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/unmute_instance.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/unmute_instance.ts @@ -53,47 +53,5 @@ export default function createUnmuteInstanceTests({ getService }: FtrProviderCon id: createdAlert.id, }); }); - - describe('legacy', () => { - it('should handle unmute alert instance request appropriately', async () => { - const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send(getTestRuleData({ enabled: false })) - .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - await supertestWithoutAuth - .post( - `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${ - createdAlert.id - }/alert_instance/1/_mute` - ) - .set('kbn-xsrf', 'foo') - .expect(204); - await supertestWithoutAuth - .post( - `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${ - createdAlert.id - }/alert_instance/1/_unmute` - ) - .set('kbn-xsrf', 'foo') - .expect(204); - - const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${createdAlert.id}`) - .set('kbn-xsrf', 'foo') - .expect(200); - expect(updatedAlert.muted_alert_ids).to.eql([]); - - // Ensure AAD isn't broken - await checkAAD({ - supertest: supertestWithoutAuth, - spaceId: Spaces.space1.id, - type: RULE_SAVED_OBJECT_TYPE, - id: createdAlert.id, - }); - }); - }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/update.ts index 025fa3b693dce..24fdf571f070d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/update.ts @@ -408,75 +408,5 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .expect(400); }); }); - - describe('legacy', function () { - this.tags('skipFIPS'); - it('should handle update alert request appropriately', async () => { - const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send(getTestRuleData()) - .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - const updatedData = { - name: 'bcd', - tags: ['bar'], - params: { - foo: true, - }, - schedule: { interval: '12s' }, - actions: [], - throttle: '1m', - notifyWhen: 'onThrottleInterval', - }; - - const response = await supertest - .put(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}`) - .set('kbn-xsrf', 'foo') - .send(updatedData) - .expect(200); - - expect(response.body).to.eql({ - ...updatedData, - id: createdAlert.id, - tags: ['bar'], - alertTypeId: 'test.noop', - consumer: 'alertsFixture', - createdBy: null, - enabled: true, - updatedBy: null, - apiKeyOwner: null, - apiKeyCreatedByUser: null, - muteAll: false, - mutedInstanceIds: [], - notifyWhen: 'onThrottleInterval', - scheduledTaskId: createdAlert.scheduled_task_id, - createdAt: response.body.createdAt, - updatedAt: response.body.updatedAt, - executionStatus: response.body.executionStatus, - revision: 1, - running: false, - ...(response.body.nextRun ? { nextRun: response.body.nextRun } : {}), - ...(response.body.lastRun ? { lastRun: response.body.lastRun } : {}), - }); - expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0); - expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0); - expect(Date.parse(response.body.updatedAt)).to.be.greaterThan( - Date.parse(response.body.createdAt) - ); - if (response.body.nextRun) { - expect(Date.parse(response.body.nextRun)).to.be.greaterThan(0); - } - - // Ensure AAD isn't broken - await checkAAD({ - supertest, - spaceId: Spaces.space1.id, - type: RULE_SAVED_OBJECT_TYPE, - id: createdAlert.id, - }); - }); - }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/update_api_key.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/update_api_key.ts index 0aa9ec3354acd..e588d0e606543 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/update_api_key.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/update_api_key.ts @@ -79,41 +79,5 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte }); }); }); - - describe('legacy', function () { - this.tags('skipFIPS'); - it('should handle update alert api key appropriately', async () => { - const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send(getTestRuleData()) - .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - await supertestWithoutAuth - .post( - `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}/_update_api_key` - ) - .set('kbn-xsrf', 'foo') - .expect(204); - - const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${createdAlert.id}`) - .set('kbn-xsrf', 'foo') - .expect(200); - expect(updatedAlert.api_key_owner).to.eql(null); - - // Ensure revision is not incremented when API key is updated - expect(updatedAlert.revision).to.eql(0); - - // Ensure AAD isn't broken - await checkAAD({ - supertest: supertestWithoutAuth, - spaceId: Spaces.space1.id, - type: RULE_SAVED_OBJECT_TYPE, - id: createdAlert.id, - }); - }); - }); }); } diff --git a/x-pack/test/api_integration/apis/entity_manager/count.ts b/x-pack/test/api_integration/apis/entity_manager/count.ts index 46c60e4bd2bdc..79cce666c423a 100644 --- a/x-pack/test/api_integration/apis/entity_manager/count.ts +++ b/x-pack/test/api_integration/apis/entity_manager/count.ts @@ -20,7 +20,8 @@ export default function ({ getService }: FtrProviderContext) { const esClient = getService('es'); const supertest = getService('supertest'); - describe('_count API', () => { + // Failing: See https://github.com/elastic/kibana/issues/204323 + describe.skip('_count API', () => { let cleanup: Function[] = []; before(() => clearEntityDefinitions(esClient)); diff --git a/x-pack/test/api_integration/apis/upgrade_assistant/es_deprecation_logs.ts b/x-pack/test/api_integration/apis/upgrade_assistant/es_deprecation_logs.ts index 875164e8f8945..68ab3e78c2793 100644 --- a/x-pack/test/api_integration/apis/upgrade_assistant/es_deprecation_logs.ts +++ b/x-pack/test/api_integration/apis/upgrade_assistant/es_deprecation_logs.ts @@ -21,7 +21,8 @@ export default function ({ getService }: FtrProviderContext) { const { createDeprecationLog, deleteDeprecationLogs } = initHelpers(getService); - describe('Elasticsearch deprecation logs', function () { + // Skipped to enable ES promotion + describe.skip('Elasticsearch deprecation logs', function () { describe('GET /api/upgrade_assistant/deprecation_logging', () => { describe('/count', () => { it('should filter out the deprecation from Elastic products', async () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/get_monitor.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/get_monitor.ts index d35e1bfc01831..93018d604c269 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/get_monitor.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/get_monitor.ts @@ -25,7 +25,8 @@ import { PrivateLocationTestService } from '../../../services/synthetics_private import { getFixtureJson } from './helpers/get_fixture_json'; export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { - describe('getSyntheticsMonitors', function () { + // Failing: See https://github.com/elastic/kibana/issues/204158 + describe.skip('getSyntheticsMonitors', function () { const supertest = getService('supertestWithoutAuth'); const kibanaServer = getService('kibanaServer'); const retry = getService('retry'); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts index 0578e4f68b986..ccfae3068f5dc 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts @@ -62,7 +62,6 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); describe('find_cases', () => { @@ -560,16 +559,11 @@ export default ({ getService }: FtrProviderContext): void => { }); describe('alerts', () => { - const defaultSignalsIndex = '.siem-signals-default-000001'; + const defaultSignalsIndex = 'siem-signals-default-000001'; const signalID = '4679431ee0ba3209b6fcd60a255a696886fe0a7d18f5375de510ff5b68fa6b78'; const signalID2 = '1023bcfea939643c5e51fd8df53797e0ea693cee547db579ab56d96402365c1e'; - beforeEach(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/cases/signals/default'); - }); - afterEach(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/cases/signals/default'); await deleteAllCaseItems(es); }); @@ -592,11 +586,6 @@ export default ({ getService }: FtrProviderContext): void => { owner: 'securitySolutionFixture', }, }); - - // There is potential for the alert index to not be refreshed by the time the second comment is created - // which could attempt to update the alert status again and will encounter a conflict so this will - // ensure that the index is up to date before we try to update the next alert status - await es.indices.refresh({ index: defaultSignalsIndex }); } const patchedCase = await createComment({ diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts index 53d7712b4fc15..1c3640626436a 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts @@ -545,7 +545,7 @@ export default ({ getService }: FtrProviderContext): void => { caseId: postedCase.id, params: { alertId: '4679431ee0ba3209b6fcd60a255a696886fe0a7d18f5375de510ff5b68fa6b78', - index: '.siem-signals-default-000001', + index: 'siem-signals-default-000001', rule: { id: 'test-rule-id', name: 'test-index-id' }, type: AttachmentType.alert, owner: 'securitySolutionFixture', @@ -593,7 +593,7 @@ export default ({ getService }: FtrProviderContext): void => { caseId: postedCaseId, params: { alertId: '4679431ee0ba3209b6fcd60a255a696886fe0a7d18f5375de510ff5b68fa6b78', - index: '.siem-signals-default-000001', + index: 'siem-signals-default-000001', rule: { id: 'test-rule-id', name: 'test-index-id' }, type: AttachmentType.alert, owner: 'securitySolutionFixture', @@ -1528,7 +1528,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('alerts', () => { describe('Update', () => { - const defaultSignalsIndex = '.siem-signals-default-000001'; + const defaultSignalsIndex = 'siem-signals-default-000001'; beforeEach(async () => { await esArchiver.load('x-pack/test/functional/es_archives/cases/signals/default'); @@ -1662,7 +1662,7 @@ export default ({ getService }: FtrProviderContext): void => { }); describe('No update', () => { - const defaultSignalsIndex = '.siem-signals-default-000001'; + const defaultSignalsIndex = 'siem-signals-default-000001'; beforeEach(async () => { await esArchiver.load('x-pack/test/functional/es_archives/cases/signals/duplicate_ids'); @@ -1681,12 +1681,12 @@ export default ({ getService }: FtrProviderContext): void => { }); }; - // this id exists only in .siem-signals-default-000001 + // this id exists only in siem-signals-default-000001 const signalIDInFirstIndex = 'cae78067e65582a3b277c1ad46ba3cb29044242fe0d24bbf3fcde757fdd31d1c'; - // This id exists in both .siem-signals-default-000001 and .siem-signals-default-000002 + // This id exists in both siem-signals-default-000001 and siem-signals-default-000002 const signalIDInSecondIndex = 'duplicate-signal-id'; - const signalsIndex2 = '.siem-signals-default-000002'; + const signalsIndex2 = 'siem-signals-default-000002'; const individualCase = await createCase(supertest, { ...postCaseReq, diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/client/update_alert_status.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/client/update_alert_status.ts index 522a4d88e12c9..0e2a977aceed2 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/client/update_alert_status.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/client/update_alert_status.ts @@ -24,7 +24,7 @@ export default ({ getService }: FtrProviderContext): void => { const esArchiver = getService('esArchiver'); describe('update_alert_status', () => { - const defaultSignalsIndex = '.siem-signals-default-000001'; + const defaultSignalsIndex = 'siem-signals-default-000001'; beforeEach(async () => { await esArchiver.load('x-pack/test/functional/es_archives/cases/signals/default'); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_case_metrics_alerts.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_case_metrics_alerts.ts index f8ee2e8bc66fb..acfe0e1be6c3c 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_case_metrics_alerts.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_case_metrics_alerts.ts @@ -233,12 +233,12 @@ export default ({ getService }: FtrProviderContext): void => { '48bdf505176b47705da896fb58bc2070768c072778f5412f162abec2ff6ca67b', ], index: [ - '.siem-signals-default-000001', - '.siem-signals-default-000001', - '.siem-signals-default-000001', - '.siem-signals-default-000001', - '.siem-signals-default-000001', - '.siem-signals-default-000001', + 'siem-signals-default-000001', + 'siem-signals-default-000001', + 'siem-signals-default-000001', + 'siem-signals-default-000001', + 'siem-signals-default-000001', + 'siem-signals-default-000001', ], }, }); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/cases/push_case.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/cases/push_case.ts index da1416cbb9430..23a6850326234 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/cases/push_case.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/cases/push_case.ts @@ -615,7 +615,7 @@ export default ({ getService }: FtrProviderContext): void => { }); describe('alerts', () => { - const defaultSignalsIndex = '.siem-signals-default-000001'; + const defaultSignalsIndex = 'siem-signals-default-000001'; const signalID = '4679431ee0ba3209b6fcd60a255a696886fe0a7d18f5375de510ff5b68fa6b78'; const signalID2 = '1023bcfea939643c5e51fd8df53797e0ea693cee547db579ab56d96402365c1e'; diff --git a/x-pack/test/functional/apps/index_management/feature_controls/index_management_security.ts b/x-pack/test/functional/apps/index_management/feature_controls/index_management_security.ts index 9d267f2ed7c33..8458ad6991541 100644 --- a/x-pack/test/functional/apps/index_management/feature_controls/index_management_security.ts +++ b/x-pack/test/functional/apps/index_management/feature_controls/index_management_security.ts @@ -73,7 +73,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { // The index_management_user has been given permissions to advanced settings for Stack Management Tests. // https://github.com/elastic/kibana/pull/113078/ - expect(sections).to.have.length(2); + expect(sections).to.have.length(3); expect(sections[0]).to.eql({ sectionId: 'data', sectionLinks: ['index_management', 'transform'], diff --git a/x-pack/test/functional/config.base.js b/x-pack/test/functional/config.base.js index eb13de1c0c9e8..b3d2ddc1e2f78 100644 --- a/x-pack/test/functional/config.base.js +++ b/x-pack/test/functional/config.base.js @@ -401,7 +401,14 @@ export default async function ({ readConfigFile }) { indices: [ { names: ['*'], - privileges: ['create', 'read', 'view_index_metadata', 'monitor', 'create_index'], + privileges: [ + 'create', + 'read', + 'view_index_metadata', + 'monitor', + 'create_index', + 'manage', + ], }, ], }, diff --git a/x-pack/test/functional/es_archives/cases/signals/default/data.json.gz b/x-pack/test/functional/es_archives/cases/signals/default/data.json.gz index 51a1c96980e77..67b565fa091d0 100644 Binary files a/x-pack/test/functional/es_archives/cases/signals/default/data.json.gz and b/x-pack/test/functional/es_archives/cases/signals/default/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/cases/signals/duplicate_ids/data.json.gz b/x-pack/test/functional/es_archives/cases/signals/duplicate_ids/data.json.gz index 8ee8a3250d73a..f179d2dd52c77 100644 Binary files a/x-pack/test/functional/es_archives/cases/signals/duplicate_ids/data.json.gz and b/x-pack/test/functional/es_archives/cases/signals/duplicate_ids/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/cases/signals/duplicate_ids/mappings.json b/x-pack/test/functional/es_archives/cases/signals/duplicate_ids/mappings.json index 6ec0622bfce71..c852f69741874 100644 --- a/x-pack/test/functional/es_archives/cases/signals/duplicate_ids/mappings.json +++ b/x-pack/test/functional/es_archives/cases/signals/duplicate_ids/mappings.json @@ -5,11 +5,11 @@ ".alerts-security.alerts-default": { "is_write_index": false }, - ".siem-signals-default": { + "siem-signals-default": { "is_write_index": true } }, - "index": ".siem-signals-default-000001", + "index": "siem-signals-default-000001", "mappings": { "_meta": { "aliases_version": 1, @@ -4981,8 +4981,8 @@ "settings": { "index": { "lifecycle": { - "name": ".siem-signals-default", - "rollover_alias": ".siem-signals-default" + "name": "siem-signals-default", + "rollover_alias": "siem-signals-default" }, "mapping": { "total_fields": { @@ -5001,7 +5001,7 @@ "value": { "aliases": { }, - "index": ".siem-signals-default-000002", + "index": "siem-signals-default-000002", "mappings": { "_meta": { "aliases_version": 1, @@ -9973,8 +9973,8 @@ "settings": { "index": { "lifecycle": { - "name": ".siem-signals-default", - "rollover_alias": ".siem-signals-default" + "name": "siem-signals-default", + "rollover_alias": "siem-signals-default" }, "mapping": { "total_fields": { diff --git a/x-pack/test/functional/es_archives/cases/signals/hosts_users/data.json.gz b/x-pack/test/functional/es_archives/cases/signals/hosts_users/data.json.gz index a85b74a389209..cb82021edbcc7 100644 Binary files a/x-pack/test/functional/es_archives/cases/signals/hosts_users/data.json.gz and b/x-pack/test/functional/es_archives/cases/signals/hosts_users/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/cases/signals/hosts_users/mappings.json b/x-pack/test/functional/es_archives/cases/signals/hosts_users/mappings.json index 83d67d913f589..0f57f37c40fd6 100644 --- a/x-pack/test/functional/es_archives/cases/signals/hosts_users/mappings.json +++ b/x-pack/test/functional/es_archives/cases/signals/hosts_users/mappings.json @@ -5,11 +5,11 @@ ".alerts-security.alerts-default": { "is_write_index": false }, - ".siem-signals-default": { + "siem-signals-default": { "is_write_index": true } }, - "index": ".siem-signals-default-000001", + "index": "siem-signals-default-000001", "mappings": { "_meta": { "aliases_version": 1, @@ -4981,8 +4981,8 @@ "settings": { "index": { "lifecycle": { - "name": ".siem-signals-default", - "rollover_alias": ".siem-signals-default" + "name": "siem-signals-default", + "rollover_alias": "siem-signals-default" }, "mapping": { "total_fields": { diff --git a/x-pack/test/functional/services/monitoring/alerts.js b/x-pack/test/functional/services/monitoring/alerts.js index c480cc0c45c03..5e5d880da102c 100644 --- a/x-pack/test/functional/services/monitoring/alerts.js +++ b/x-pack/test/functional/services/monitoring/alerts.js @@ -10,12 +10,12 @@ export function MonitoringAlertsProvider({ getService }) { return new (class MonitoringAlerts { async deleteAlerts() { - const apiResponse = await supertest.get('/api/alerts/_find?per_page=20'); + const apiResponse = await supertest.get('/api/alerting/rules/_find?per_page=20'); const alerts = apiResponse.body.data.filter(({ consumer }) => consumer === 'monitoring'); return await Promise.all( alerts.map(async (alert) => - supertest.delete(`/api/alerts/alert/${alert.id}`).set('kbn-xsrf', 'true').expect(204) + supertest.delete(`/api/alerting/rule/${alert.id}`).set('kbn-xsrf', 'true').expect(204) ) ); } diff --git a/x-pack/test/functional_search/tests/classic_navigation.ts b/x-pack/test/functional_search/tests/classic_navigation.ts index a290f7523f49c..118fb4b7b2a4f 100644 --- a/x-pack/test/functional_search/tests/classic_navigation.ts +++ b/x-pack/test/functional_search/tests/classic_navigation.ts @@ -26,7 +26,10 @@ export default function searchSolutionNavigation({ }); // Create a space with the search solution and navigate to its home page - ({ cleanUp, space: spaceCreated } = await spaces.create({ solution: 'classic' })); + ({ cleanUp, space: spaceCreated } = await spaces.create({ + name: 'search-classic-ftr', + solution: 'classic', + })); await browser.navigateTo(spaces.getRootUrl(spaceCreated.id)); await common.navigateToApp('enterpriseSearch'); }); diff --git a/x-pack/test/functional_search/tests/solution_navigation.ts b/x-pack/test/functional_search/tests/solution_navigation.ts index 03a4614017ba2..5517a9513ea48 100644 --- a/x-pack/test/functional_search/tests/solution_navigation.ts +++ b/x-pack/test/functional_search/tests/solution_navigation.ts @@ -14,10 +14,8 @@ export default function searchSolutionNavigation({ const { common, solutionNavigation } = getPageObjects(['common', 'solutionNavigation']); const spaces = getService('spaces'); const browser = getService('browser'); - const kibanaServer = getService('kibanaServer'); - // FLAKY: https://github.com/elastic/kibana/issues/201037 - describe.skip('Search Solution Navigation', () => { + describe('Search Solution Navigation', () => { let cleanUp: () => Promise; let spaceCreated: { id: string } = { id: '' }; @@ -28,20 +26,14 @@ export default function searchSolutionNavigation({ }); // Create a space with the search solution and navigate to its home page - ({ cleanUp, space: spaceCreated } = await spaces.create({ solution: 'es' })); + ({ cleanUp, space: spaceCreated } = await spaces.create({ + name: 'search-ftr', + solution: 'es', + })); await browser.navigateTo(spaces.getRootUrl(spaceCreated.id)); - - // canvas application is only available when installation contains canvas workpads - await kibanaServer.importExport.load( - 'x-pack/test/functional/fixtures/kbn_archiver/canvas/default' - ); }); after(async () => { - await kibanaServer.importExport.unload( - 'x-pack/test/functional/fixtures/kbn_archiver/canvas/default' - ); - // Clean up space created await cleanUp(); }); @@ -59,8 +51,6 @@ export default function searchSolutionNavigation({ await solutionNavigation.sidenav.expectLinkExists({ text: 'Search applications' }); await solutionNavigation.sidenav.expectLinkExists({ text: 'Behavioral Analytics' }); await solutionNavigation.sidenav.expectLinkExists({ text: 'Inference Endpoints' }); - await solutionNavigation.sidenav.expectLinkExists({ text: 'App Search' }); - await solutionNavigation.sidenav.expectLinkExists({ text: 'Workplace Search' }); await solutionNavigation.sidenav.expectLinkExists({ text: 'Other tools' }); }); @@ -210,38 +200,9 @@ export default function searchSolutionNavigation({ deepLinkId: 'searchInferenceEndpoints:inferenceEndpoints', }); - // check Enterprise Search - // > App Search - await solutionNavigation.sidenav.clickLink({ - deepLinkId: 'appSearch:engines', - }); - await solutionNavigation.sidenav.expectLinkActive({ - deepLinkId: 'appSearch:engines', - }); - // ent-search node not running for FTRs, so we see setup guide without breadcrumbs - // await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ - // text: 'App Search', - // }); - await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ - deepLinkId: 'appSearch:engines', - }); - // > Workplace Search - await solutionNavigation.sidenav.clickLink({ - deepLinkId: 'workplaceSearch', - }); - await solutionNavigation.sidenav.expectLinkActive({ - deepLinkId: 'workplaceSearch', - }); - // ent-search node not running for FTRs, so we see setup guide without breadcrumbs - // await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ - // text: 'Workplace Search', - // }); - await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ - deepLinkId: 'workplaceSearch', - }); - // Other tools await solutionNavigation.sidenav.openSection('search_project_nav.otherTools'); + await solutionNavigation.sidenav.expectSectionOpen('search_project_nav.otherTools'); // > Maps await solutionNavigation.sidenav.clickLink({ deepLinkId: 'maps', @@ -256,20 +217,6 @@ export default function searchSolutionNavigation({ await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ deepLinkId: 'maps', }); - // > Canvas - await solutionNavigation.sidenav.clickLink({ - deepLinkId: 'canvas', - }); - await solutionNavigation.sidenav.expectLinkActive({ - deepLinkId: 'canvas', - }); - await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Other tools' }); - await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ - text: 'Canvas', - }); - await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ - deepLinkId: 'canvas', - }); // > Graph await solutionNavigation.sidenav.clickLink({ deepLinkId: 'graph', @@ -292,6 +239,9 @@ export default function searchSolutionNavigation({ it('renders only expected items', async () => { await solutionNavigation.sidenav.openSection('search_project_nav.otherTools'); await solutionNavigation.sidenav.openSection('project_settings_project_nav'); + await solutionNavigation.sidenav.expectSectionOpen('search_project_nav.otherTools'); + await solutionNavigation.sidenav.expectSectionOpen('project_settings_project_nav'); + await solutionNavigation.sidenav.expectOnlyDefinedLinks([ 'search_project_nav', 'enterpriseSearch', @@ -309,12 +259,8 @@ export default function searchSolutionNavigation({ 'enterpriseSearchAnalytics', 'relevance', 'searchInferenceEndpoints:inferenceEndpoints', - 'entsearch', - 'appSearch:engines', - 'workplaceSearch', 'otherTools', 'maps', - 'canvas', 'graph', 'project_settings_project_nav', 'ml:modelManagement', diff --git a/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/uptime/alert_flyout.ts index 25f77cb8d41ef..8126a3e42d815 100644 --- a/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/uptime/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/uptime/alert_flyout.ts @@ -98,7 +98,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await retry.tryForTime(60 * 1000, async () => { // add a delay before next call to not overload the server await setTimeoutAsync(1500); - const apiResponse = await supertest.get('/api/alerts/_find?search=uptime-test'); + const apiResponse = await supertest.get('/api/alerting/rules/_find?search=uptime-test'); const alertsFromThisTest = apiResponse.body.data.filter( ({ name }: { name: string }) => name === 'uptime-test' ); @@ -111,7 +111,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // for our test helper to input into the flyout. const { actions, - alertTypeId, + rule_type_id: alertTypeId, consumer, id, params: { numTimes, timerangeUnit, timerangeCount, filters }, @@ -134,7 +134,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { `{"tags":[],"url.port":["5678"],"observer.geo.name":["mpls"],"monitor.type":["http"]}` ); } finally { - await supertest.delete(`/api/alerts/alert/${id}`).set('kbn-xsrf', 'true').expect(204); + await supertest.delete(`/api/alerting/rule/${id}`).set('kbn-xsrf', 'true').expect(204); } }); }); @@ -178,7 +178,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('has created a valid alert with expected parameters', async () => { let alert: any; await retry.tryForTime(60 * 1000, async () => { - const apiResponse = await supertest.get(`/api/alerts/_find?search=${alertId}`); + const apiResponse = await supertest.get(`/api/alerting/rules/_find?search=${alertId}`); const alertsFromThisTest = apiResponse.body.data.filter( ({ name }: { name: string }) => name === alertId ); @@ -191,7 +191,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // for our test helper to input into the flyout. const { actions, - alertTypeId, + rule_type_id: alertTypeId, consumer, id, params, @@ -206,7 +206,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(params).to.eql({}); expect(interval).to.eql('11m'); } finally { - await supertest.delete(`/api/alerts/alert/${id}`).set('kbn-xsrf', 'true').expect(204); + await supertest.delete(`/api/alerting/rule/${id}`).set('kbn-xsrf', 'true').expect(204); } }); }); diff --git a/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/uptime/simple_down_alert.ts b/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/uptime/simple_down_alert.ts index e5efd66060e31..d1c4c2c7742fd 100644 --- a/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/uptime/simple_down_alert.ts +++ b/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/uptime/simple_down_alert.ts @@ -83,7 +83,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('has created a valid simple alert with expected parameters', async () => { let alert: any; await retry.tryForTime(15000, async () => { - const apiResponse = await supertest.get(`/api/alerts/_find?search=Simple status alert`); + const apiResponse = await supertest.get( + `/api/alerting/rules/_find?search=Simple status alert` + ); const alertsFromThisTest = apiResponse.body.data.filter(({ params }: { params: any }) => params.search.includes(monitorId) ); @@ -91,10 +93,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { alert = alertsFromThisTest[0]; }); - const { actions, alertTypeId, consumer, tags } = alert ?? {}; + const { actions, rule_type_id: alertTypeId, consumer, tags } = alert ?? {}; expect(actions).to.eql([ { - actionTypeId: '.slack', + connector_type_id: '.slack', group: 'recovered', params: { message: MonitorStatusTranslations.defaultRecoveryMessage, @@ -103,7 +105,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { uuid: actions[0].uuid, }, { - actionTypeId: '.slack', + connector_type_id: '.slack', group: 'xpack.uptime.alerts.actionGroups.monitorStatus', params: { message: MonitorStatusTranslations.defaultActionMessage, diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/general.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/general.ts index 25e6e169ec4a4..1e0123f7e9741 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/general.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/general.ts @@ -321,7 +321,7 @@ export default ({ getPageObjects, getPageObject, getService }: FtrProviderContex }, supertest ); - objectRemover.add(rule.id, 'alert', 'alerts'); + objectRemover.add(rule.id, 'rule', 'alerting'); // refresh to see rule await browser.refresh(); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/slack.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/slack.ts index a7ea6aef95c8a..6e89bcf411121 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/slack.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/slack.ts @@ -115,7 +115,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const getRuleIdByName = async (name: string) => { const response = await supertest - .get(`/api/alerts/_find?search=${name}&search_fields=name`) + .get(`/api/alerting/rules/_find?search=${name}&search_fields=name`) .expect(200); return response.body.data[0].id; }; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index 56150e5693a39..ebe0007b1a4ef 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -47,7 +47,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { .set('kbn-xsrf', 'foo') .send(getTestAlertData(overwrites)) .expect(200); - objectRemover.add(createdRule.id, 'alert', 'alerts'); + objectRemover.add(createdRule.id, 'rule', 'alerting'); return createdRule; } @@ -62,7 +62,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }) ) .expect(200); - objectRemover.add(createdRule.id, 'alert', 'alerts'); + objectRemover.add(createdRule.id, 'rule', 'alerting'); return createdRule; } diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_list/rules_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_list/rules_list.ts index a9de637cd691a..b73659839c9e3 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_list/rules_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_list/rules_list.ts @@ -223,7 +223,7 @@ export default ({ getPageObjects, getPageObject, getService }: FtrProviderContex ) .expect(200); - objectRemover.add(createdRule.id, 'alert', 'alerts'); + objectRemover.add(createdRule.id, 'rule', 'alerting'); await retry.try(async () => { const { alerts: alertInstances } = await getAlertSummary(createdRule.id); @@ -265,7 +265,7 @@ export default ({ getPageObjects, getPageObject, getService }: FtrProviderContex ) .expect(200); - objectRemover.add(createdRule.id, 'alert', 'alerts'); + objectRemover.add(createdRule.id, 'rule', 'alerting'); await retry.try(async () => { const { alerts: alertInstances } = await getAlertSummary(createdRule.id); diff --git a/x-pack/test/functional_with_es_ssl/lib/alert_api_actions.ts b/x-pack/test/functional_with_es_ssl/lib/alert_api_actions.ts index 09b875b43a359..72e1521b4f613 100644 --- a/x-pack/test/functional_with_es_ssl/lib/alert_api_actions.ts +++ b/x-pack/test/functional_with_es_ssl/lib/alert_api_actions.ts @@ -51,7 +51,7 @@ export async function createAlert({ overwrites?: Record; }) { const createdAlert = await createAlertManualCleanup({ supertest, overwrites }); - objectRemover.add(createdAlert.id, 'alert', 'alerts'); + objectRemover.add(createdAlert.id, 'rule', 'alerting'); return createdAlert; } diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/metrics_route.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/metrics_route.ts index 50568fe1c206c..b456e35d5d673 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/metrics_route.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/metrics_route.ts @@ -301,7 +301,7 @@ export default function ({ getService }: FtrProviderContext) { // update apiKey to fix decryption error await request - .post(`/api/alerts/alert/${ruleId}/_update_api_key`) + .post(`/api/alerting/rule/${ruleId}/_update_api_key`) .set('kbn-xsrf', 'xxx') .expect(204); diff --git a/x-pack/test/rule_registry/common/lib/helpers/create_alert.ts b/x-pack/test/rule_registry/common/lib/helpers/create_alert.ts index 40d43ac80d210..b758ac4b1a041 100644 --- a/x-pack/test/rule_registry/common/lib/helpers/create_alert.ts +++ b/x-pack/test/rule_registry/common/lib/helpers/create_alert.ts @@ -17,7 +17,7 @@ export const createAlert = async ( ) => { const supertest = getService('supertestWithoutAuth'); const { body: response, status } = await supertest - .post(`${getSpaceUrlPrefix(spaceId)}/api/alerts/alert`) + .post(`${getSpaceUrlPrefix(spaceId)}/api/alerting/rule`) .auth(user.username, user.password) .send(alertDef) .set('kbn-xsrf', 'foo'); diff --git a/x-pack/test/rule_registry/common/lib/helpers/delete_alert.ts b/x-pack/test/rule_registry/common/lib/helpers/delete_alert.ts index 209b182a958c5..b373d0ffe8667 100644 --- a/x-pack/test/rule_registry/common/lib/helpers/delete_alert.ts +++ b/x-pack/test/rule_registry/common/lib/helpers/delete_alert.ts @@ -22,7 +22,7 @@ export const deleteAlert = async ( const { body: targetIndices } = await getAlertsTargetIndices(getService, user, spaceId); if (id) { const { body, status } = await supertest - .delete(`${getSpaceUrlPrefix(spaceId)}/api/alerts/alert/${id}`) + .delete(`${getSpaceUrlPrefix(spaceId)}/api/alerting/rule/${id}`) .auth(user.username, user.password) .set('kbn-xsrf', 'foo'); diff --git a/x-pack/test/rule_registry/common/lib/helpers/wait_until_next_execution.ts b/x-pack/test/rule_registry/common/lib/helpers/wait_until_next_execution.ts index 73e15c6cdbaeb..08613ba2c143d 100644 --- a/x-pack/test/rule_registry/common/lib/helpers/wait_until_next_execution.ts +++ b/x-pack/test/rule_registry/common/lib/helpers/wait_until_next_execution.ts @@ -27,7 +27,7 @@ export async function waitUntilNextExecution( }); const { body, status } = await supertest - .get(`${getSpaceUrlPrefix(spaceId)}/api/alerts/alert/${alert.id}`) + .get(`${getSpaceUrlPrefix(spaceId)}/api/alerting/rule/${alert.id}`) .auth(user.username, user.password) .set('kbn-xsrf', 'foo'); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/alerts/wait_for_alert_to_complete.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/alerts/wait_for_alert_to_complete.ts index d6799e6be611c..c4a9b7d2a8548 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/alerts/wait_for_alert_to_complete.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/alerts/wait_for_alert_to_complete.ts @@ -17,7 +17,9 @@ export const waitForAlertToComplete = async ( ): Promise => { await waitFor( async () => { - const response = await supertest.get(`/api/alerts/alert/${id}/state`).set('kbn-xsrf', 'true'); + const response = await supertest + .get(`/internal/alerting/rule/${id}/state`) + .set('kbn-xsrf', 'true'); if (response.status !== 200) { log.debug( `Did not get an expected 200 "ok" when waiting for an alert to complete (waitForAlertToComplete). CI issues could happen. Suspect this line if you are seeing CI issues. body: ${JSON.stringify( @@ -25,7 +27,7 @@ export const waitForAlertToComplete = async ( )}, status: ${JSON.stringify(response.status)}` ); } - return response.body.previousStartedAt != null; + return response.body.previous_started_at != null; }, 'waitForAlertToComplete', log diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/endpoint_authz.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/endpoint_authz.ts index 56b3328112e7c..df2f55ccb8d04 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/endpoint_authz.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/endpoint_authz.ts @@ -40,9 +40,7 @@ export default function ({ getService }: FtrProviderContext) { } // @skipInServerlessMKI - this test uses internal index manipulation in before/after hooks // @skipInServerlessMKI - if you are removing this annotation, make sure to add the test suite to the MKI pipeline in .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_defend_workflows.yml - // FLAKY: https://github.com/elastic/kibana/issues/203909 - // FLAKY: https://github.com/elastic/kibana/issues/203910 - describe.skip('@ess @serverless @skipInServerlessMKI When attempting to call an endpoint api', function () { + describe('@ess @serverless @skipInServerlessMKI When attempting to call an endpoint api', function () { let indexedData: IndexedHostsAndAlertsResponse; let actionId = ''; let agentId = ''; diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/policy_response/trial_license_complete_tier/policy_response.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/policy_response/trial_license_complete_tier/policy_response.ts index a737b337faffe..f3f49ffbc7c1f 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/policy_response/trial_license_complete_tier/policy_response.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/policy_response/trial_license_complete_tier/policy_response.ts @@ -13,9 +13,7 @@ export default function ({ getService }: FtrProviderContext) { const utils = getService('securitySolutionUtils'); const endpointTestresources = getService('endpointTestResources'); - // FLAKY: https://github.com/elastic/kibana/issues/203908 - // Failing: See https://github.com/elastic/kibana/issues/203903 - describe.skip('@ess @serverless @skipInServerlessMKI Endpoint policy response api', function () { + describe('@ess @serverless @skipInServerlessMKI Endpoint policy response api', function () { let adminSupertest: TestAgent; before(async () => { diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/execute.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/execute.ts index a82487c31eff9..13afbf6e4551c 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/execute.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/execute.ts @@ -23,9 +23,7 @@ export default function ({ getService }: FtrProviderContext) { const utils = getService('securitySolutionUtils'); // @skipInServerlessMKI - this test uses internal index manipulation in before/after hooks - // FLAKY: https://github.com/elastic/kibana/issues/203906 - // Failing: See https://github.com/elastic/kibana/issues/203897 - describe.skip('@ess @serverless @skipInServerlessMKI Endpoint `execute` response action', function () { + describe('@ess @serverless @skipInServerlessMKI Endpoint `execute` response action', function () { let indexedData: IndexedHostsAndAlertsResponse; let agentId = ''; let t1AnalystSupertest: TestAgent; diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/spaces/trial_license_complete_tier/space_awareness.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/spaces/trial_license_complete_tier/space_awareness.ts index 38710b65465c7..9c83451111f95 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/spaces/trial_license_complete_tier/space_awareness.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/spaces/trial_license_complete_tier/space_awareness.ts @@ -24,9 +24,7 @@ export default function ({ getService }: FtrProviderContext) { const kbnServer = getService('kibanaServer'); const log = getService('log'); - // FLAKY: https://github.com/elastic/kibana/issues/203893 - // Failing: See https://github.com/elastic/kibana/issues/203898 - describe.skip('@ess @serverless @skipInServerlessMKI Endpoint management space awareness support', function () { + describe('@ess @serverless @skipInServerlessMKI Endpoint management space awareness support', function () { let adminSupertest: TestAgent; let dataSpaceA: Awaited>; let dataSpaceB: Awaited>; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/ai_assistant/conversations.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/ai_assistant/conversations.cy.ts index 4d87cce1fdaa8..a97fa8ab9f6e3 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/ai_assistant/conversations.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/ai_assistant/conversations.cy.ts @@ -47,7 +47,8 @@ import { } from '../../screens/ai_assistant'; import { visit, visitGetStartedPage } from '../../tasks/navigation'; -describe('AI Assistant Conversations', { tags: ['@ess', '@serverless'] }, () => { +// Failing: See https://github.com/elastic/kibana/issues/204167 +describe.skip('AI Assistant Conversations', { tags: ['@ess', '@serverless'] }, () => { beforeEach(() => { deleteConnectors(); deleteConversations(); diff --git a/x-pack/test/task_manager_claimer_update_by_query/test_suites/task_manager/metrics_route.ts b/x-pack/test/task_manager_claimer_update_by_query/test_suites/task_manager/metrics_route.ts index 3e37c488e5190..2bab3cf0dc577 100644 --- a/x-pack/test/task_manager_claimer_update_by_query/test_suites/task_manager/metrics_route.ts +++ b/x-pack/test/task_manager_claimer_update_by_query/test_suites/task_manager/metrics_route.ts @@ -296,7 +296,7 @@ export default function ({ getService }: FtrProviderContext) { // update apiKey to fix decryption error await request - .post(`/api/alerts/alert/${ruleId}/_update_api_key`) + .post(`/api/alerting/rule/${ruleId}/_update_api_key`) .set('kbn-xsrf', 'xxx') .expect(204); diff --git a/x-pack/test_serverless/api_integration/test_suites/common/telemetry/telemetry_config.ts b/x-pack/test_serverless/api_integration/test_suites/common/telemetry/telemetry_config.ts index 33726b6af0f64..6819dd0c0e8f7 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/telemetry/telemetry_config.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/telemetry/telemetry_config.ts @@ -59,12 +59,12 @@ export default function telemetryConfigTest({ getService }: FtrProviderContext) .send({ 'telemetry.labels.journeyName': 'my-ftr-test' }) .expect(200, { ok: true }); - await supertestAdminWithApiKey.get('/api/telemetry/v2/config').expect(200, { - ...initialConfig, - labels: { - ...initialConfig.labels, - journeyName: 'my-ftr-test', - }, + await retry.tryForTime(retryTimeout, async function retryTelemetryConfigGetRequest() { + const { body } = await supertestAdminWithApiKey.get('/api/telemetry/v2/config').expect(200); + expect(body).to.eql( + { ...initialConfig, labels: { ...initialConfig.labels, journeyName: 'my-ftr-test' } }, + `Expected the response body to include the dynamically set telemetry.labels.journeyName, but got: [${body}]` + ); }); // Sends "null" to remove the label @@ -78,7 +78,7 @@ export default function telemetryConfigTest({ getService }: FtrProviderContext) const { body } = await supertestAdminWithApiKey.get('/api/telemetry/v2/config').expect(200); expect(body).to.eql( initialConfig, - `Expected the response body to match the intitial config, but got: [${body}]` + `Expected the response body to match the initial config, but got: [${body}]` ); }); });