From a6364540fea7673ab8bed602c53ba6dffe4b2afc Mon Sep 17 00:00:00 2001 From: elena-shostak <165678770+elena-shostak@users.noreply.github.com> Date: Tue, 30 Apr 2024 16:47:34 +0200 Subject: [PATCH 01/10] [Spaces] API endpoint for roles which have access to a given space (#181165) ## Summary Added endpoint `GET kbn:/internal/security/roles/{space-id}` to get all roles for provided space id. **Note**: changes needed for application `*` privileges were cherry-picked [to a separate PR].(https://github.com/elastic/kibana/pull/181400) ## Example Request `GET kbn:/internal/security/roles/space-b` Response ``` [ { "name": "role-a", "metadata": {}, "transient_metadata": { "enabled": true }, "elasticsearch": { "cluster": [ "all" ], "indices": [], "run_as": [] }, "kibana": [ { "base": [], "feature": { "dev_tools": [ "all" ] }, "spaces": [ "default", "space-b" ] } ], "_transform_error": [], "_unrecognized_applications": [] }, { "name": "superuser", "metadata": { "_reserved": true }, "transient_metadata": {}, "elasticsearch": { "cluster": [ "all" ], "indices": [ { "names": [ "*" ], "privileges": [ "all" ], "allow_restricted_indices": false }, { "names": [ "*" ], "privileges": [ "monitor", "read", "view_index_metadata", "read_cross_cluster" ], "allow_restricted_indices": true } ], "remote_indices": [ { "names": [ "*" ], "privileges": [ "all" ], "allow_restricted_indices": false, "clusters": [ "*" ] }, { "names": [ "*" ], "privileges": [ "monitor", "read", "view_index_metadata", "read_cross_cluster" ], "allow_restricted_indices": true, "clusters": [ "*" ] } ], "run_as": [ "*" ] }, "kibana": [ { "base": [ "all" ], "feature": {}, "spaces": [ "*" ] } ], "_transform_error": [], "_unrecognized_applications": [ "*" ] } ] ``` ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) __Fixes: https://github.com/elastic/kibana/issues/180718__ --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../security/server/authorization/index.ts | 2 +- .../privileges/privileges.test.ts | 13 + .../authorization/privileges/privileges.ts | 1 + .../authorization/roles/elasticsearch_role.ts | 12 + .../server/authorization/roles/index.ts | 2 +- .../routes/authorization/roles/get_all.ts | 14 +- .../roles/get_all_by_space.test.ts | 320 ++++++++++++++++++ .../authorization/roles/get_all_by_space.ts | 72 ++++ .../routes/authorization/roles/index.ts | 2 + .../api_integration/apis/security/roles.ts | 82 +++++ 10 files changed, 506 insertions(+), 14 deletions(-) create mode 100644 x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.test.ts create mode 100644 x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts diff --git a/x-pack/plugins/security/server/authorization/index.ts b/x-pack/plugins/security/server/authorization/index.ts index 2a93aeb070011e..0ebd085ba0e421 100644 --- a/x-pack/plugins/security/server/authorization/index.ts +++ b/x-pack/plugins/security/server/authorization/index.ts @@ -9,5 +9,5 @@ export { Actions } from './actions'; export type { AuthorizationServiceSetupInternal } from './authorization_service'; export { AuthorizationService } from './authorization_service'; export type { ElasticsearchRole } from './roles'; -export { transformElasticsearchRoleToRole } from './roles'; +export { transformElasticsearchRoleToRole, compareRolesByName } from './roles'; export type { CasesSupportedOperations } from './privileges'; diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts index 2d8fe4b8f4c242..e27a30ae424457 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts @@ -282,6 +282,7 @@ describe('features', () => { ...(expectDecryptedTelemetry ? [actions.api.get('decryptedTelemetry')] : []), ...(expectGetFeatures ? [actions.api.get('features')] : []), ...(expectGetFeatures ? [actions.api.get('taskManager')] : []), + ...(expectGetFeatures ? [actions.api.get('manageSpaces')] : []), ...(expectManageSpaces ? [ actions.space.manage, @@ -506,6 +507,7 @@ describe('features', () => { ...(expectDecryptedTelemetry ? [actions.api.get('decryptedTelemetry')] : []), ...(expectGetFeatures ? [actions.api.get('features')] : []), ...(expectGetFeatures ? [actions.api.get('taskManager')] : []), + ...(expectGetFeatures ? [actions.api.get('manageSpaces')] : []), ...(expectManageSpaces ? [ actions.space.manage, @@ -574,6 +576,7 @@ describe('features', () => { ...(expectDecryptedTelemetry ? [actions.api.get('decryptedTelemetry')] : []), ...(expectGetFeatures ? [actions.api.get('features')] : []), ...(expectGetFeatures ? [actions.api.get('taskManager')] : []), + ...(expectGetFeatures ? [actions.api.get('manageSpaces')] : []), ...(expectManageSpaces ? [ actions.space.manage, @@ -643,6 +646,7 @@ describe('features', () => { ...(expectDecryptedTelemetry ? [actions.api.get('decryptedTelemetry')] : []), ...(expectGetFeatures ? [actions.api.get('features')] : []), ...(expectGetFeatures ? [actions.api.get('taskManager')] : []), + ...(expectGetFeatures ? [actions.api.get('manageSpaces')] : []), ...(expectManageSpaces ? [ actions.space.manage, @@ -910,6 +914,7 @@ describe('subFeatures', () => { actions.api.get('decryptedTelemetry'), actions.api.get('features'), actions.api.get('taskManager'), + actions.api.get('manageSpaces'), actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), @@ -1068,6 +1073,7 @@ describe('subFeatures', () => { actions.api.get('decryptedTelemetry'), actions.api.get('features'), actions.api.get('taskManager'), + actions.api.get('manageSpaces'), actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), @@ -1303,6 +1309,7 @@ describe('subFeatures', () => { actions.api.get('decryptedTelemetry'), actions.api.get('features'), actions.api.get('taskManager'), + actions.api.get('manageSpaces'), actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), @@ -1441,6 +1448,7 @@ describe('subFeatures', () => { actions.api.get('decryptedTelemetry'), actions.api.get('features'), actions.api.get('taskManager'), + actions.api.get('manageSpaces'), actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), @@ -1619,6 +1627,7 @@ describe('subFeatures', () => { actions.api.get('decryptedTelemetry'), actions.api.get('features'), actions.api.get('taskManager'), + actions.api.get('manageSpaces'), actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), @@ -1755,6 +1764,7 @@ describe('subFeatures', () => { actions.api.get('decryptedTelemetry'), actions.api.get('features'), actions.api.get('taskManager'), + actions.api.get('manageSpaces'), actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), @@ -1969,6 +1979,7 @@ describe('subFeatures', () => { actions.api.get('decryptedTelemetry'), actions.api.get('features'), actions.api.get('taskManager'), + actions.api.get('manageSpaces'), actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), @@ -2202,6 +2213,7 @@ describe('subFeatures', () => { actions.api.get('decryptedTelemetry'), actions.api.get('features'), actions.api.get('taskManager'), + actions.api.get('manageSpaces'), actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), @@ -2471,6 +2483,7 @@ describe('subFeatures', () => { actions.api.get('decryptedTelemetry'), actions.api.get('features'), actions.api.get('taskManager'), + actions.api.get('manageSpaces'), actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.ts index 51c61962c946f1..e9e8ccbfe8d927 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.ts @@ -109,6 +109,7 @@ export function privilegesFactory( actions.api.get('decryptedTelemetry'), actions.api.get('features'), actions.api.get('taskManager'), + actions.api.get('manageSpaces'), actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), diff --git a/x-pack/plugins/security/server/authorization/roles/elasticsearch_role.ts b/x-pack/plugins/security/server/authorization/roles/elasticsearch_role.ts index 6a46072712dfce..2dcf11bb53fe81 100644 --- a/x-pack/plugins/security/server/authorization/roles/elasticsearch_role.ts +++ b/x-pack/plugins/security/server/authorization/roles/elasticsearch_role.ts @@ -321,3 +321,15 @@ const extractUnrecognizedApplicationNames = ( function getUniqueList(list: T[]) { return Array.from(new Set(list)); } + +export const compareRolesByName = (roleA: Role, roleB: Role) => { + if (roleA.name < roleB.name) { + return -1; + } + + if (roleA.name > roleB.name) { + return 1; + } + + return 0; +}; diff --git a/x-pack/plugins/security/server/authorization/roles/index.ts b/x-pack/plugins/security/server/authorization/roles/index.ts index 205c339b45fbc8..2b5aca721c2b70 100644 --- a/x-pack/plugins/security/server/authorization/roles/index.ts +++ b/x-pack/plugins/security/server/authorization/roles/index.ts @@ -6,4 +6,4 @@ */ export type { ElasticsearchRole } from './elasticsearch_role'; -export { transformElasticsearchRoleToRole } from './elasticsearch_role'; +export { transformElasticsearchRoleToRole, compareRolesByName } from './elasticsearch_role'; diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts index 919144af7cadc8..0e7280b6e21304 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts @@ -6,7 +6,7 @@ */ import type { RouteDefinitionParams } from '../..'; -import { transformElasticsearchRoleToRole } from '../../../authorization'; +import { compareRolesByName, transformElasticsearchRoleToRole } from '../../../authorization'; import { wrapIntoCustomErrorResponse } from '../../../errors'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; @@ -45,17 +45,7 @@ export function defineGetAllRolesRoutes({ .filter((role) => { return !hideReservedRoles || !role.metadata?._reserved; }) - .sort((roleA, roleB) => { - if (roleA.name < roleB.name) { - return -1; - } - - if (roleA.name > roleB.name) { - return 1; - } - - return 0; - }), + .sort(compareRolesByName), }); } catch (error) { return response.customError(wrapIntoCustomErrorResponse(error)); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.test.ts new file mode 100644 index 00000000000000..1bb63f7deca617 --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.test.ts @@ -0,0 +1,320 @@ +/* + * 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 Boom from '@hapi/boom'; + +import { kibanaResponseFactory } from '@kbn/core/server'; +import { coreMock, httpServerMock } from '@kbn/core/server/mocks'; +import type { LicenseCheck } from '@kbn/licensing-plugin/server'; + +import { defineGetAllRolesBySpaceRoutes } from './get_all_by_space'; +import { routeDefinitionParamsMock } from '../../index.mock'; + +const application = 'kibana-.kibana'; + +interface TestOptions { + name?: string; + licenseCheckResult?: LicenseCheck; + apiResponse?: () => unknown; + asserts: { statusCode: number; result?: Record }; + spaceId?: string; +} + +describe('GET all roles by space id', () => { + it('correctly defines route.', () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + mockRouteDefinitionParams.authz.applicationName = application; + mockRouteDefinitionParams.getFeatures = jest.fn().mockResolvedValue([]); + + defineGetAllRolesBySpaceRoutes(mockRouteDefinitionParams); + const [[config]] = mockRouteDefinitionParams.router.get.mock.calls; + + const paramsSchema = (config.validate as any).params; + + expect(config.options).toEqual({ tags: ['access:manageSpaces'] }); + expect(() => paramsSchema.validate({})).toThrowErrorMatchingInlineSnapshot( + `"[spaceId]: expected value of type [string] but got [undefined]"` + ); + expect(() => paramsSchema.validate({ spaceId: '' })).toThrowErrorMatchingInlineSnapshot( + `"[spaceId]: value has length [0] but it must have a minimum length of [1]."` + ); + }); + + const getRolesTest = ( + description: string, + { licenseCheckResult = { state: 'valid' }, apiResponse, spaceId = 'test', asserts }: TestOptions + ) => { + test(description, async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + mockRouteDefinitionParams.authz.applicationName = application; + mockRouteDefinitionParams.getFeatures = jest.fn().mockResolvedValue([]); + + const mockCoreContext = coreMock.createRequestHandlerContext(); + const mockLicensingContext = { + license: { check: jest.fn().mockReturnValue(licenseCheckResult) }, + } as any; + const mockContext = coreMock.createCustomRequestHandlerContext({ + core: mockCoreContext, + licensing: mockLicensingContext, + }); + + if (apiResponse) { + mockCoreContext.elasticsearch.client.asCurrentUser.security.getRole.mockResponseImplementation( + (() => ({ body: apiResponse() })) as any + ); + } + + defineGetAllRolesBySpaceRoutes(mockRouteDefinitionParams); + const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: '/api/security/roles/{spaceId}', + headers, + params: { + spaceId, + }, + }); + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + + if (apiResponse) { + expect( + mockCoreContext.elasticsearch.client.asCurrentUser.security.getRole + ).toHaveBeenCalled(); + } + expect(mockLicensingContext.license.check).toHaveBeenCalledWith('security', 'basic'); + }); + }; + + describe('failure', () => { + getRolesTest('returns result of license checker', { + licenseCheckResult: { state: 'invalid', message: 'test forbidden message' }, + asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, + }); + + const error = Boom.notAcceptable('test not acceptable message'); + getRolesTest('returns error from cluster client', { + apiResponse: async () => { + throw error; + }, + asserts: { statusCode: 406, result: error }, + }); + + getRolesTest(`returns error if we have empty resources`, { + apiResponse: () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['read'], + resources: [], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 500, + result: new Error("ES returned an application entry without resources, can't process this"), + }, + }); + }); + + describe('success', () => { + getRolesTest(`returns empty roles list if there is no space match`, { + apiResponse: () => ({ + first_role: { + cluster: ['manage_watcher'], + indices: [ + { + names: ['.kibana*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + applications: [ + { + application, + privileges: ['space_all', 'space_read'], + resources: ['space:marketing', 'space:sales'], + }, + ], + run_as: ['other_user'], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [], + }, + }); + + getRolesTest(`returns roles for matching space`, { + apiResponse: () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['space_all', 'space_read'], + resources: ['space:marketing', 'space:sales'], + }, + { + application, + privileges: ['space_read'], + resources: ['space:engineering'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + second_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['space_all', 'space_read'], + resources: ['space:marketing', 'space:sales'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + spaceId: 'engineering', + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + base: ['all', 'read'], + feature: {}, + spaces: ['marketing', 'sales'], + }, + { + base: ['read'], + feature: {}, + spaces: ['engineering'], + }, + ], + _transform_error: [], + _unrecognized_applications: [], + }, + ], + }, + }); + + getRolesTest(`returns roles with access to all spaces`, { + apiResponse: () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['all', 'read'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + second_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['space_all', 'space_read'], + resources: ['space:marketing', 'space:sales'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + base: ['all', 'read'], + feature: {}, + spaces: ['*'], + }, + ], + _transform_error: [], + _unrecognized_applications: [], + }, + ], + }, + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts new file mode 100644 index 00000000000000..3ac74cade02028 --- /dev/null +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts @@ -0,0 +1,72 @@ +/* + * 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 type { RouteDefinitionParams } from '../..'; +import { ALL_SPACES_ID } from '../../../../common/constants'; +import { compareRolesByName, transformElasticsearchRoleToRole } from '../../../authorization'; +import { wrapIntoCustomErrorResponse } from '../../../errors'; +import { createLicensedRouteHandler } from '../../licensed_route_handler'; + +export function defineGetAllRolesBySpaceRoutes({ + router, + authz, + getFeatures, + logger, + buildFlavor, + config, +}: RouteDefinitionParams) { + router.get( + { + path: '/internal/security/roles/{spaceId}', + options: { + tags: ['access:manageSpaces'], + }, + validate: { + params: schema.object({ spaceId: schema.string({ minLength: 1 }) }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + const hideReservedRoles = buildFlavor === 'serverless'; + const esClient = (await context.core).elasticsearch.client; + + const [features, elasticsearchRoles] = await Promise.all([ + getFeatures(), + await esClient.asCurrentUser.security.getRole(), + ]); + + // Transform elasticsearch roles into Kibana roles and return in a list sorted by the role name. + return response.ok({ + body: Object.entries(elasticsearchRoles) + .map(([roleName, elasticsearchRole]) => + transformElasticsearchRoleToRole( + features, + // @ts-expect-error @elastic/elasticsearch SecurityIndicesPrivileges.names expected to be string[] + elasticsearchRole, + roleName, + authz.applicationName, + logger + ) + ) + .filter( + (role) => + !(hideReservedRoles && role.metadata?._reserved) && + role.kibana.some( + (privilege) => + privilege.spaces.includes(request.params.spaceId) || + privilege.spaces.includes(ALL_SPACES_ID) + ) + ) + .sort(compareRolesByName), + }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/authorization/roles/index.ts b/x-pack/plugins/security/server/routes/authorization/roles/index.ts index 257d5e303a4a37..d4e3633f4677ed 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/index.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/index.ts @@ -8,6 +8,7 @@ import { defineDeleteRolesRoutes } from './delete'; import { defineGetRolesRoutes } from './get'; import { defineGetAllRolesRoutes } from './get_all'; +import { defineGetAllRolesBySpaceRoutes } from './get_all_by_space'; import { definePutRolesRoutes } from './put'; import type { RouteDefinitionParams } from '../..'; @@ -16,4 +17,5 @@ export function defineRolesRoutes(params: RouteDefinitionParams) { defineGetAllRolesRoutes(params); defineDeleteRolesRoutes(params); definePutRolesRoutes(params); + defineGetAllRolesBySpaceRoutes(params); } diff --git a/x-pack/test/api_integration/apis/security/roles.ts b/x-pack/test/api_integration/apis/security/roles.ts index 50c252c521ac3f..f6cf615d0f71b9 100644 --- a/x-pack/test/api_integration/apis/security/roles.ts +++ b/x-pack/test/api_integration/apis/security/roles.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import type { Role } from '@kbn/security-plugin-types-common'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { @@ -391,6 +392,87 @@ export default function ({ getService }: FtrProviderContext) { _unrecognized_applications: ['logstash-default'], }); }); + + it('should get roles by space id', async () => { + await es.security.putRole({ + name: 'space_role_not_to_get', + body: { + cluster: ['manage'], + indices: [ + { + names: ['logstash-*'], + privileges: ['read', 'view_index_metadata'], + allow_restricted_indices: false, + }, + ], + applications: [ + { + application: 'kibana-.kibana', + privileges: ['feature_dashboard.read', 'feature_discover.all', 'feature_ml.all'], + resources: ['space:marketing', 'space:sales'], + }, + ], + run_as: ['watcher_user'], + metadata: { + foo: 'test-metadata', + }, + transient_metadata: { + enabled: true, + }, + }, + }); + + await es.security.putRole({ + name: 'space_role_to_get', + body: { + cluster: ['manage'], + indices: [ + { + names: ['logstash-*'], + privileges: ['read', 'view_index_metadata'], + allow_restricted_indices: false, + }, + ], + applications: [ + { + application: 'kibana-.kibana', + privileges: ['feature_dashboard.read', 'feature_discover.all', 'feature_ml.all'], + resources: ['space:engineering', 'space:sales'], + }, + ], + run_as: ['watcher_user'], + metadata: { + foo: 'test-metadata', + }, + transient_metadata: { + enabled: true, + }, + }, + }); + + await supertest + .get('/internal/security/roles/engineering') + .set('kbn-xsrf', 'xxx') + .expect(200) + .expect((res: { body: Role[] }) => { + const roles = res.body; + expect(roles).to.be.an('array'); + + const success = roles.every((role) => { + return ( + role.name !== 'space_role_not_to_get' && + role.kibana.some((privilege) => { + return privilege.spaces.includes('*') || privilege.spaces.includes('engineering'); + }) + ); + }); + + const expectedRole = roles.find((role) => role.name === 'space_role_to_get'); + + expect(success).to.be(true); + expect(expectedRole).to.be.an('object'); + }); + }); }); describe('Delete Role', () => { From 5b0a2a517761cd87369aca7202532fa4317e2bbc Mon Sep 17 00:00:00 2001 From: Marta Bondyra <4283304+mbondyra@users.noreply.github.com> Date: Tue, 30 Apr 2024 17:04:02 +0200 Subject: [PATCH 02/10] [Lens] Change `legendStats` from `values` to `currentAndLastValue`/`value` (#181993) ## Summary Changing this to align with the object from elastic-charts: https://github.com/elastic/elastic-charts/blob/5ed4ea75c6fd3691fca2d5f10081850e3d78a5e4/packages/charts/src/common/legend.ts#L26-L68 --- .../visualization_types/xy_chart.ts | 4 ++-- .../waffle_vis_function.test.ts.snap | 2 +- .../waffle_vis_function.test.ts | 7 ++++-- .../common/types/expression_renderers.ts | 9 +++++--- .../public/__stories__/shared/arg_types.ts | 4 ++-- .../components/partition_vis_component.tsx | 4 ++-- .../expression_partition_vis/public/mocks.ts | 4 ++-- .../common/types/expression_functions.ts | 4 ++-- .../public/components/xy_chart.test.tsx | 9 +++++--- .../public/components/xy_chart.tsx | 6 +++-- .../configurations/index.test.ts | 4 ++-- .../convert_to_lens/configurations/index.ts | 6 ++--- .../convert_to_lens/configurations/index.ts | 4 ++-- src/plugins/vis_types/xy/public/to_ast.ts | 4 ++-- .../visualizations/common/constants.ts | 8 +++++-- .../convert_to_lens/types/configurations.ts | 6 ++--- src/plugins/visualizations/common/index.ts | 8 ++++++- x-pack/plugins/lens/common/types.ts | 4 ++-- .../legend/legend_settings_popover.tsx | 22 ++++++++----------- .../partition/partition_charts_meta.ts | 6 ++--- .../visualizations/partition/persistence.tsx | 6 ++--- .../partition/render_helpers.test.ts | 18 ++++++++++----- .../partition/render_helpers.ts | 4 ++-- .../visualizations/partition/toolbar.tsx | 7 +++--- .../partition/visualization.test.ts | 14 ++++++------ .../public/visualizations/xy/persistence.ts | 8 ++++--- .../visualizations/xy/visualization.test.tsx | 10 ++++----- .../xy/xy_config_panel/index.tsx | 5 +++-- .../kbn_archiver/lens/legend_statistics.json | 4 ++-- 29 files changed, 114 insertions(+), 87 deletions(-) diff --git a/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/xy_chart.ts b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/xy_chart.ts index 4bd38234abd81f..a992a97a23e4b9 100644 --- a/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/xy_chart.ts +++ b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/xy_chart.ts @@ -17,7 +17,7 @@ import type { import type { DataView } from '@kbn/data-views-plugin/public'; import type { SavedObjectReference } from '@kbn/core/server'; import { AxesSettingsConfig } from '@kbn/visualizations-plugin/common'; -import type { LegendStats } from '@kbn/visualizations-plugin/common/constants'; +import type { XYLegendValue } from '@kbn/visualizations-plugin/common/constants'; import type { Chart, ChartConfig, ChartLayer } from '../types'; import { DEFAULT_LAYER_ID } from '../utils'; import { XY_ID } from './constants'; @@ -131,7 +131,7 @@ export const getXYVisualizationState = ( isVisible: false, position: 'right', showSingleSeries: false, - legendStats: ['values' as LegendStats.values], + legendStats: ['currentAndLastValue' as XYLegendValue.CurrentAndLastValue], }, valueLabels: 'show', yLeftScale: 'linear', diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap index edf595c888859c..e50965e47dd000 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap @@ -93,7 +93,7 @@ Object { "legendPosition": "right", "legendSize": "medium", "legendStats": Array [ - "values", + "value", ], "maxLegendLines": 2, "metrics": Array [ diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.test.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.test.ts index ba9b49ce51c05d..e5a14a37e0ffb9 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.test.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.test.ts @@ -13,7 +13,10 @@ import { ValueFormats, LegendDisplay, } from '../types/expression_renderers'; -import { ExpressionValueVisDimension, LegendStats } from '@kbn/visualizations-plugin/common'; +import { + ExpressionValueVisDimension, + PartitionLegendValue, +} from '@kbn/visualizations-plugin/common'; import { Datatable } from '@kbn/expressions-plugin/common/expression_types/specs'; import { waffleVisFunction } from './waffle_vis_function'; import { PARTITION_LABELS_VALUE, PARTITION_VIS_RENDERER_NAME } from '../constants'; @@ -34,7 +37,7 @@ describe('interpreter/functions#waffleVis', () => { const visConfig: WaffleVisConfig = { addTooltip: true, - legendStats: [LegendStats.values], + legendStats: [PartitionLegendValue.Value], metricsToLabels: JSON.stringify({}), legendDisplay: LegendDisplay.SHOW, legendPosition: 'right', diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts index 896fce1cd61f7d..46a69902e63674 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts @@ -11,7 +11,10 @@ import type { AllowedChartOverrides, AllowedSettingsOverrides } from '@kbn/chart import type { PaletteOutput } from '@kbn/coloring'; import type { Datatable, DatatableColumn } from '@kbn/expressions-plugin/common'; import type { SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; -import type { ExpressionValueVisDimension, LegendStats } from '@kbn/visualizations-plugin/common'; +import type { + ExpressionValueVisDimension, + PartitionLegendValue, +} from '@kbn/visualizations-plugin/common'; import type { LegendSize } from '@kbn/visualizations-plugin/public'; import { type AllowedPartitionOverrides, @@ -80,7 +83,7 @@ export interface PartitionVisParams extends VisCommonParams { labels: LabelsParams; palette: PaletteOutput; isDonut?: boolean; - legendStats?: LegendStats[]; + legendStats?: PartitionLegendValue[]; respectSourceOrder?: boolean; emptySizeRatio?: EmptySizeRatios; startFromSecondLargestSlice?: boolean; @@ -110,7 +113,7 @@ export interface MosaicVisConfig export interface WaffleVisConfig extends Omit { bucket?: ExpressionValueVisDimension | string; - legendStats?: LegendStats[]; + legendStats?: PartitionLegendValue[]; } export interface PartitionChartProps { diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/arg_types.ts b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/arg_types.ts index 317a57dfe6c2a5..314eb3369b61de 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/arg_types.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/arg_types.ts @@ -8,7 +8,7 @@ import { Position } from '@elastic/charts'; import { ArgTypes } from '@storybook/addons'; -import { LegendStats } from '@kbn/visualizations-plugin/common/constants'; +import { PartitionLegendValue } from '@kbn/visualizations-plugin/common/constants'; import { EmptySizeRatios, LegendDisplay } from '../../../common'; import { ChartTypes } from '../../../common/types'; @@ -212,7 +212,7 @@ export const waffleArgTypes: ArgTypes = { description: 'Legend stats', type: { name: 'string', required: false }, table: { type: { summary: 'string' }, defaultValue: { summary: undefined } }, - options: [LegendStats.values], + options: [PartitionLegendValue.Value], control: { type: 'select' }, }, }; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx index d187a88491a0c4..4e3017373e2c66 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx +++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx @@ -39,7 +39,7 @@ import { } from '@kbn/expressions-plugin/public'; import type { FieldFormat } from '@kbn/field-formats-plugin/common'; import { getOverridesFor } from '@kbn/chart-expressions-common'; -import { LegendStats } from '@kbn/visualizations-plugin/common/constants'; +import { PartitionLegendValue } from '@kbn/visualizations-plugin/common/constants'; import { consolidateMetricColumns } from '../../common/utils'; import { DEFAULT_PERCENT_DECIMALS } from '../../common/constants'; import { @@ -564,7 +564,7 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { legendColorPicker={props.uiState ? LegendColorPickerWrapper : undefined} flatLegend={flatLegend} legendSort={customLegendSort} - showLegendExtra={visParams.legendStats?.[0] === LegendStats.values} + showLegendExtra={visParams.legendStats?.[0] === PartitionLegendValue.Value} onElementClick={([elementEvent]) => { // this cast is safe because we are rendering a partition chart const [layerValues] = elementEvent as PartitionElementEvent; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/mocks.ts b/src/plugins/chart_expressions/expression_partition_vis/public/mocks.ts index 8bee780a93f135..4f48c03f7f9155 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/mocks.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/mocks.ts @@ -7,7 +7,7 @@ */ import { Datatable } from '@kbn/expressions-plugin/public'; -import { LegendStats } from '@kbn/visualizations-plugin/common/constants'; +import { PartitionLegendValue } from '@kbn/visualizations-plugin/common/constants'; import { BucketColumns, PartitionVisParams, @@ -382,6 +382,6 @@ export const createMockWaffleParams = (): PartitionVisParams => { }, ], }, - legendStats: [LegendStats.values], + legendStats: [PartitionLegendValue.Value], }; }; diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts index 7f5be444c46325..118c585a0dd142 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts @@ -14,7 +14,7 @@ import type { DatatableColumnMeta, ExpressionFunctionDefinition, } from '@kbn/expressions-plugin/common'; -import { LegendSize, LegendStats } from '@kbn/visualizations-plugin/common'; +import { LegendSize, XYLegendValue } from '@kbn/visualizations-plugin/common'; import { EventAnnotationOutput } from '@kbn/event-annotation-plugin/common'; import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common'; @@ -217,7 +217,7 @@ export interface LegendConfig { /** * metrics to display in the legend */ - legendStats?: LegendStats[]; + legendStats?: XYLegendValue[]; } // Arguments to XY chart expression, with computed properties diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx index 8e4d4acd6c599c..cfae463faf2c54 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx @@ -58,7 +58,7 @@ import { XYChart, XYChartRenderProps } from './xy_chart'; import { ExtendedDataLayerConfig, XYProps, AnnotationLayerConfigResult } from '../../common/types'; import { DataLayers } from './data_layers'; import { SplitChart } from './split_chart'; -import { LegendSize, LegendStats } from '@kbn/visualizations-plugin/common'; +import { LegendSize, XYLegendValue } from '@kbn/visualizations-plugin/common'; import type { LayerCellValueActions } from '../types'; const onClickValue = jest.fn(); @@ -742,7 +742,10 @@ describe('XYChart component', () => { const component = shallow( ); expect(component.find(Settings).at(0).prop('showLegendExtra')).toEqual(false); @@ -757,7 +760,7 @@ describe('XYChart component', () => { ...args, legend: { ...args.legend, - legendStats: [LegendStats.values], + legendStats: [XYLegendValue.CurrentAndLastValue], }, layers: [dateHistogramLayer], }} diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx index 88a0d92449728b..f15121159911bc 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -50,7 +50,7 @@ import { import { DEFAULT_LEGEND_SIZE, LegendSizeToPixels, - LegendStats, + XYLegendValue, } from '@kbn/visualizations-plugin/common/constants'; import { PersistedState } from '@kbn/visualizations-plugin/public'; import { getOverridesFor, ChartSizeSpec } from '@kbn/chart-expressions-common'; @@ -869,7 +869,9 @@ export function XYChart({ ) : undefined } - showLegendExtra={isHistogramViz && legend.legendStats?.[0] === LegendStats.values} + showLegendExtra={ + isHistogramViz && legend.legendStats?.[0] === XYLegendValue.CurrentAndLastValue + } ariaLabel={args.ariaLabel} ariaUseDefaultSummary={!args.ariaLabel} orderOrdinalBinsBy={ diff --git a/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.test.ts b/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.test.ts index dbafd35a9e1f32..d1da337a87862b 100644 --- a/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.test.ts +++ b/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { LegendStats } from '@kbn/visualizations-plugin/common/constants'; +import { PartitionLegendValue } from '@kbn/visualizations-plugin/common/constants'; import { getConfiguration } from '.'; import { samplePieVis } from '../../sample_vis.test.mocks'; @@ -39,7 +39,7 @@ describe('getConfiguration', () => { percentDecimals: 2, primaryGroups: ['bucket-1'], secondaryGroups: [], - legendStats: [LegendStats.values], + legendStats: [PartitionLegendValue.Value], truncateLegend: true, }, ], diff --git a/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.ts b/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.ts index 3378712c133d97..acad50d7f3d302 100644 --- a/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.ts +++ b/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.ts @@ -7,7 +7,7 @@ */ import { LegendDisplay, PartitionVisParams } from '@kbn/expression-partition-vis-plugin/common'; -import { LegendStats } from '@kbn/visualizations-plugin/common/constants'; +import { PartitionLegendValue } from '@kbn/visualizations-plugin/common/constants'; import { CategoryDisplayTypes, NumberDisplayTypes, @@ -28,7 +28,7 @@ const getLayers = ( const showValuesInLegend = vis.params.labels.values ?? (vis.params.legendStats - ? vis.params.legendStats?.[0] === LegendStats.values + ? vis.params.legendStats?.[0] === PartitionLegendValue.Value : vis.type.visConfig.defaults.showValuesInLegend); return [ @@ -50,7 +50,7 @@ const getLayers = ( vis.params.legendDisplay ?? vis.type.visConfig.defaults.legendDisplay, legendPosition: vis.params.legendPosition ?? vis.type.visConfig.defaults.legendPosition, - legendStats: showValuesInLegend ? [LegendStats.values] : undefined, + legendStats: showValuesInLegend ? [PartitionLegendValue.Value] : undefined, nestedLegend: vis.params.nestedLegend ?? vis.type.visConfig.defaults.nestedLegend, percentDecimals: vis.params.labels.percentDecimals ?? vis.type.visConfig.defaults.labels.percentDecimals, diff --git a/src/plugins/vis_types/xy/public/convert_to_lens/configurations/index.ts b/src/plugins/vis_types/xy/public/convert_to_lens/configurations/index.ts index f31acc904829a6..bc1bd871406548 100644 --- a/src/plugins/vis_types/xy/public/convert_to_lens/configurations/index.ts +++ b/src/plugins/vis_types/xy/public/convert_to_lens/configurations/index.ts @@ -15,7 +15,7 @@ import { XYReferenceLineLayerConfig, } from '@kbn/visualizations-plugin/common/convert_to_lens'; import { Vis } from '@kbn/visualizations-plugin/public'; -import { LegendStats } from '@kbn/visualizations-plugin/common/constants'; +import { XYLegendValue } from '@kbn/visualizations-plugin/common/constants'; import { Layer } from '..'; import { ChartType } from '../../../common'; import { @@ -237,7 +237,7 @@ export const getConfiguration = ( maxLines: vis.params.maxLegendLines ?? vis.type.visConfig.defaults.maxLegendLines, showSingleSeries: true, legendStats: Boolean(vis.params.labels.show ?? vis.type.visConfig.defaults.labels?.show) - ? [LegendStats.values] + ? [XYLegendValue.CurrentAndLastValue] : undefined, }, fittingFunction: fittingFunction diff --git a/src/plugins/vis_types/xy/public/to_ast.ts b/src/plugins/vis_types/xy/public/to_ast.ts index 7fcf110ed6a6e1..c855aae1865dd6 100644 --- a/src/plugins/vis_types/xy/public/to_ast.ts +++ b/src/plugins/vis_types/xy/public/to_ast.ts @@ -20,7 +20,7 @@ import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugi import { BUCKET_TYPES } from '@kbn/data-plugin/public'; import type { TimeRangeBounds } from '@kbn/data-plugin/common'; import type { PaletteOutput } from '@kbn/charts-plugin/common/expressions/palette/types'; -import { LegendStats } from '@kbn/visualizations-plugin/common/constants'; +import { XYLegendValue } from '@kbn/visualizations-plugin/common/constants'; import { Dimensions, Dimension, @@ -48,7 +48,7 @@ const prepareLengend = (params: VisParams, legendSize?: LegendSize) => { shouldTruncate: params.truncateLegend, showSingleSeries: true, legendSize, - legendStats: params.labels.show ? [LegendStats.values] : undefined, + legendStats: params.labels.show ? [XYLegendValue.CurrentAndLastValue] : undefined, }); return buildExpression([legend]); diff --git a/src/plugins/visualizations/common/constants.ts b/src/plugins/visualizations/common/constants.ts index 8c35acf9513df7..f8f6409b711226 100644 --- a/src/plugins/visualizations/common/constants.ts +++ b/src/plugins/visualizations/common/constants.ts @@ -52,6 +52,10 @@ export const SUPPORTED_AGGREGATIONS = [ ...Object.values(BUCKET_TYPES), ] as const; -export enum LegendStats { - values = 'values', +export enum XYLegendValue { + CurrentAndLastValue = 'currentAndLastValue', +} + +export enum PartitionLegendValue { + Value = 'value', } diff --git a/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts b/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts index 2a387f5eb35d33..675355dc693151 100644 --- a/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts +++ b/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts @@ -10,7 +10,7 @@ import { HorizontalAlignment, LayoutDirection, Position, VerticalAlignment } fro import { $Values } from '@kbn/utility-types'; import type { CustomPaletteParams, PaletteOutput } from '@kbn/coloring'; import { KibanaQueryOutput } from '@kbn/data-plugin/common'; -import { LegendSize, LegendStats } from '../../constants'; +import { LegendSize, XYLegendValue, PartitionLegendValue } from '../../constants'; import { CategoryDisplayTypes, PartitionChartTypes, @@ -142,7 +142,7 @@ export interface LegendConfig { maxLines?: number; shouldTruncate?: boolean; legendSize?: LegendSize; - legendStats?: LegendStats[]; + legendStats?: XYLegendValue[]; } export interface XYConfiguration { @@ -238,7 +238,7 @@ export interface PartitionLayerState { categoryDisplay: CategoryDisplayType; legendDisplay: LegendDisplayType; legendPosition?: Position; - legendStats?: LegendStats[]; + legendStats?: PartitionLegendValue[]; nestedLegend?: boolean; percentDecimals?: number; emptySizeRatio?: number; diff --git a/src/plugins/visualizations/common/index.ts b/src/plugins/visualizations/common/index.ts index 50d3030828e7cf..f5047212603c19 100644 --- a/src/plugins/visualizations/common/index.ts +++ b/src/plugins/visualizations/common/index.ts @@ -16,4 +16,10 @@ export * from './expression_functions'; export * from './convert_to_lens'; export { convertToSchemaConfig } from './vis_schemas'; -export { LegendSize, LegendSizeToPixels, DEFAULT_LEGEND_SIZE, LegendStats } from './constants'; +export { + LegendSize, + LegendSizeToPixels, + DEFAULT_LEGEND_SIZE, + XYLegendValue, + PartitionLegendValue, +} from './constants'; diff --git a/x-pack/plugins/lens/common/types.ts b/x-pack/plugins/lens/common/types.ts index 9c9c72c7108ee4..56054cc60ec514 100644 --- a/x-pack/plugins/lens/common/types.ts +++ b/x-pack/plugins/lens/common/types.ts @@ -10,7 +10,7 @@ import type { Position } from '@elastic/charts'; import type { $Values } from '@kbn/utility-types'; import { CustomPaletteParams, PaletteOutput, ColorMapping } from '@kbn/coloring'; import type { ColorMode } from '@kbn/charts-plugin/common'; -import type { LegendSize, LegendStats } from '@kbn/visualizations-plugin/common/constants'; +import type { LegendSize, PartitionLegendValue } from '@kbn/visualizations-plugin/common/constants'; import { CategoryDisplay, LegendDisplay, NumberDisplay, PieChartTypes } from './constants'; import { layerTypes } from './layer_types'; import { CollapseFunction } from './expressions'; @@ -64,7 +64,7 @@ export interface SharedPieLayerState { categoryDisplay: CategoryDisplayType; legendDisplay: LegendDisplayType; legendPosition?: Position; - legendStats?: LegendStats[]; + legendStats?: PartitionLegendValue[]; nestedLegend?: boolean; percentDecimals?: number; emptySizeRatio?: number; diff --git a/x-pack/plugins/lens/public/shared_components/legend/legend_settings_popover.tsx b/x-pack/plugins/lens/public/shared_components/legend/legend_settings_popover.tsx index 094f7058ce665f..ccfdaa29089b17 100644 --- a/x-pack/plugins/lens/public/shared_components/legend/legend_settings_popover.tsx +++ b/x-pack/plugins/lens/public/shared_components/legend/legend_settings_popover.tsx @@ -19,13 +19,13 @@ import { import { Position, VerticalAlignment, HorizontalAlignment } from '@elastic/charts'; import { LegendSize } from '@kbn/visualizations-plugin/public'; import { useDebouncedValue } from '@kbn/visualization-ui-components'; -import { LegendStats } from '@kbn/visualizations-plugin/common/constants'; +import { XYLegendValue } from '@kbn/visualizations-plugin/common/constants'; import { ToolbarPopover, type ToolbarPopoverProps } from '../toolbar_popover'; import { LegendLocationSettings } from './location/legend_location_settings'; import { ColumnsNumberSetting } from './layout/columns_number_setting'; import { LegendSizeSettings } from './size/legend_size_settings'; -export interface LegendSettingsPopoverProps { +export interface LegendSettingsPopoverProps { /** * Determines the legend display options */ @@ -109,11 +109,11 @@ export interface LegendSettingsPopoverProps { /** * value in legend status */ - legendStats?: LegendStats[]; + legendStats?: S[]; /** * Callback on value in legend status change */ - onLegendStatsChange?: (legendStats?: LegendStats[]) => void; + onLegendStatsChange?: (checked?: boolean) => void; /** * If true, value in legend switch is rendered */ @@ -182,7 +182,7 @@ const PANEL_STYLE = { width: '500px', }; -export const LegendSettingsPopover: React.FunctionComponent = ({ +export function LegendSettingsPopover({ legendOptions, mode, onDisplayChange, @@ -209,7 +209,7 @@ export const LegendSettingsPopover: React.FunctionComponent { +}: LegendSettingsPopoverProps) { return ( { - if (ev.target.checked) { - onLegendStatsChange([LegendStats.values]); - } else { - onLegendStatsChange([]); - } + onLegendStatsChange(ev.target.checked); }} /> @@ -348,4 +344,4 @@ export const LegendSettingsPopover: React.FunctionComponent ); -}; +} diff --git a/x-pack/plugins/lens/public/visualizations/partition/partition_charts_meta.ts b/x-pack/plugins/lens/public/visualizations/partition/partition_charts_meta.ts index 921586e82dfeaa..37c0d49099e2ec 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/partition_charts_meta.ts +++ b/x-pack/plugins/lens/public/visualizations/partition/partition_charts_meta.ts @@ -16,7 +16,7 @@ import { IconChartMosaic, IconChartWaffle, } from '@kbn/chart-icons'; -import { LegendStats } from '@kbn/visualizations-plugin/common/constants'; +import { PartitionLegendValue } from '@kbn/visualizations-plugin/common/constants'; import { SharedPieLayerState, EmptySizeRatios } from '../../../common/types'; import { CategoryDisplay, NumberDisplay } from '../../../common/constants'; import type { PieChartType } from '../../../common/types'; @@ -45,7 +45,7 @@ interface PartitionChartMeta { }; legend: { flat?: boolean; - defaultLegendStats?: LegendStats[]; + defaultLegendStats?: PartitionLegendValue[]; hideNestedLegendSwitch?: boolean; getShowLegendDefault?: (bucketColumns: DatatableColumn[]) => boolean; }; @@ -212,7 +212,7 @@ export const PartitionChartsMeta: Record = { }, legend: { flat: true, - defaultLegendStats: [LegendStats.values], + defaultLegendStats: [PartitionLegendValue.Value], hideNestedLegendSwitch: true, getShowLegendDefault: () => true, }, diff --git a/x-pack/plugins/lens/public/visualizations/partition/persistence.tsx b/x-pack/plugins/lens/public/visualizations/partition/persistence.tsx index 5307a17076ca5a..39373cb6daeab3 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/persistence.tsx +++ b/x-pack/plugins/lens/public/visualizations/partition/persistence.tsx @@ -6,7 +6,7 @@ */ import { cloneDeep } from 'lodash'; -import { LegendStats } from '@kbn/visualizations-plugin/common/constants'; +import { PartitionLegendValue } from '@kbn/visualizations-plugin/common/constants'; import { PieLayerState, PieVisualizationState } from '../../../common/types'; type PersistedPieLayerState = Omit & { @@ -28,7 +28,7 @@ function convertToLegendStats(state: PieVisualizationState) { if ('showValuesInLegend' in l) { l.legendStats = [ ...new Set([ - ...(l.showValuesInLegend ? [LegendStats.values] : []), + ...(l.showValuesInLegend ? [PartitionLegendValue.Value] : []), ...(l.legendStats || []), ]), ]; @@ -44,7 +44,7 @@ export function convertToPersistable(state: PieVisualizationState) { newState.layers.forEach((l) => { if ('legendStats' in l && Array.isArray(l.legendStats)) { - l.showValuesInLegend = l.legendStats.includes(LegendStats.values); + l.showValuesInLegend = l.legendStats.includes(PartitionLegendValue.Value); delete l.legendStats; } }); diff --git a/x-pack/plugins/lens/public/visualizations/partition/render_helpers.test.ts b/x-pack/plugins/lens/public/visualizations/partition/render_helpers.test.ts index e5efd1bed8b840..e273fde23f4db5 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/render_helpers.test.ts +++ b/x-pack/plugins/lens/public/visualizations/partition/render_helpers.test.ts @@ -10,7 +10,7 @@ import type { Datatable } from '@kbn/expressions-plugin/public'; import { checkTableForContainsSmallValues, getLegendStats } from './render_helpers'; import { PieLayerState } from '../../../common/types'; import { PieChartTypes } from '../../../common/constants'; -import { LegendStats } from '@kbn/visualizations-plugin/common/constants'; +import { PartitionLegendValue } from '@kbn/visualizations-plugin/common/constants'; describe('render helpers', () => { describe('#checkTableForContainsSmallValues', () => { @@ -72,22 +72,28 @@ describe('render helpers', () => { describe('#getLegendStats', () => { it('should firstly read the state value', () => { expect( - getLegendStats({ legendStats: [LegendStats.values] } as PieLayerState, PieChartTypes.WAFFLE) - ).toEqual([LegendStats.values]); + getLegendStats( + { legendStats: [PartitionLegendValue.Value] } as PieLayerState, + PieChartTypes.WAFFLE + ) + ).toEqual([PartitionLegendValue.Value]); expect( - getLegendStats({ legendStats: [] as LegendStats[] } as PieLayerState, PieChartTypes.WAFFLE) + getLegendStats( + { legendStats: [] as PartitionLegendValue[] } as PieLayerState, + PieChartTypes.WAFFLE + ) ).toEqual([]); }); it('should read value from meta in case of value in state is undefined', () => { expect(getLegendStats({} as PieLayerState, PieChartTypes.WAFFLE)).toEqual([ - LegendStats.values, + PartitionLegendValue.Value, ]); expect( getLegendStats({ legendStats: undefined } as PieLayerState, PieChartTypes.WAFFLE) - ).toEqual([LegendStats.values]); + ).toEqual([PartitionLegendValue.Value]); expect(getLegendStats({} as PieLayerState, PieChartTypes.PIE)).toEqual(undefined); }); diff --git a/x-pack/plugins/lens/public/visualizations/partition/render_helpers.ts b/x-pack/plugins/lens/public/visualizations/partition/render_helpers.ts index 6d759e5d56c65d..869597f92dcb12 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/render_helpers.ts +++ b/x-pack/plugins/lens/public/visualizations/partition/render_helpers.ts @@ -6,7 +6,7 @@ */ import type { Datatable } from '@kbn/expressions-plugin/public'; -import { LegendStats } from '@kbn/visualizations-plugin/common/constants'; +import { PartitionLegendValue } from '@kbn/visualizations-plugin/common/constants'; import type { PieChartType, PieLayerState } from '../../../common/types'; import { PartitionChartsMeta } from './partition_charts_meta'; @@ -14,7 +14,7 @@ export const getLegendStats = (layer: PieLayerState, shape: PieChartType) => { if ('defaultLegendStats' in PartitionChartsMeta[shape]?.legend) { return ( layer.legendStats ?? - PartitionChartsMeta[shape].legend.defaultLegendStats ?? [LegendStats.values] + PartitionChartsMeta[shape].legend.defaultLegendStats ?? [PartitionLegendValue.Value] ); } }; diff --git a/x-pack/plugins/lens/public/visualizations/partition/toolbar.tsx b/x-pack/plugins/lens/public/visualizations/partition/toolbar.tsx index 627072dfda800e..792e45ba99b2bc 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/toolbar.tsx +++ b/x-pack/plugins/lens/public/visualizations/partition/toolbar.tsx @@ -19,6 +19,7 @@ import { import type { Position } from '@elastic/charts'; import { LegendSize } from '@kbn/visualizations-plugin/public'; import { useDebouncedValue } from '@kbn/visualization-ui-components'; +import { PartitionLegendValue } from '@kbn/visualizations-plugin/common/constants'; import { DEFAULT_PERCENT_DECIMALS } from './constants'; import { PartitionChartsMeta } from './partition_charts_meta'; import { PieVisualizationState, SharedPieLayerState } from '../../../common/types'; @@ -134,9 +135,9 @@ export function PieToolbar(props: VisualizationToolbarProps { + (checked) => { onStateChange({ - legendStats, + legendStats: checked ? [PartitionLegendValue.Value] : [], }); }, [onStateChange] @@ -244,7 +245,7 @@ export function PieToolbar(props: VisualizationToolbarProps ) : null} - legendOptions={legendOptions} mode={layer.legendDisplay} onDisplayChange={onLegendDisplayChange} diff --git a/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts index 7fec1f0f6619f7..b72deb7b9dc4de 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts @@ -22,7 +22,7 @@ import { cloneDeep } from 'lodash'; import { PartitionChartsMeta } from './partition_charts_meta'; import { CollapseFunction } from '../../../common/expressions'; import { PaletteOutput } from '@kbn/coloring'; -import { LegendStats } from '@kbn/visualizations-plugin/common/constants'; +import { PartitionLegendValue } from '@kbn/visualizations-plugin/common/constants'; import { PersistedPieVisualizationState } from './persistence'; jest.mock('../../id_generator'); @@ -169,11 +169,11 @@ describe('pie_visualization', () => { describe('converting to legendStats', () => { it('loads a chart with `legendStats` property', () => { const persistedState = getExampleState(); - persistedState.layers[0].legendStats = ['values' as LegendStats.values]; + persistedState.layers[0].legendStats = [PartitionLegendValue.Value]; const runtimeState = pieVisualization.initialize(() => 'first', persistedState); - expect(runtimeState.layers[0].legendStats).toEqual(['values']); + expect(runtimeState.layers[0].legendStats).toEqual(['value']); expect('showValuesInLegend' in runtimeState.layers[0]).toEqual(false); }); it('loads a xy chart with `showValuesInLegend` property equal to false and converts to legendStats: []', () => { @@ -192,7 +192,7 @@ describe('pie_visualization', () => { const runtimeState = pieVisualization.initialize(() => 'first', persistedState); - expect(runtimeState.layers[0].legendStats).toEqual(['values']); + expect(runtimeState.layers[0].legendStats).toEqual(['value']); expect('showValuesInLegend' in runtimeState.layers[0]).toEqual(false); }); @@ -214,15 +214,15 @@ describe('pie_visualization', () => { expect('legendStats' in state.layers[0]).toBeFalsy(); expect(state.layers[0].showValuesInLegend).toEqual(undefined); - // legend stats === ['values'] - runtimeState.layers[0].legendStats = ['values' as LegendStats.values]; + // legend stats === ['value'] + runtimeState.layers[0].legendStats = [PartitionLegendValue.Value]; const { state: stateWithShowValuesInLegendTrue } = pieVisualization.getPersistableState!(runtimeState); expect('legendStats' in stateWithShowValuesInLegendTrue.layers[0]).toBeFalsy(); expect(stateWithShowValuesInLegendTrue.layers[0].showValuesInLegend).toEqual(true); - // legend stats === ['values'] + // legend stats === ['value'] runtimeState.layers[0].legendStats = []; const { state: stateWithShowValuesInLegendFalse } = diff --git a/x-pack/plugins/lens/public/visualizations/xy/persistence.ts b/x-pack/plugins/lens/public/visualizations/xy/persistence.ts index aaa537e5c57d26..df639d538e89ee 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/persistence.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/persistence.ts @@ -10,7 +10,7 @@ import { v4 as uuidv4 } from 'uuid'; import type { SavedObjectReference } from '@kbn/core/public'; import { EVENT_ANNOTATION_GROUP_TYPE } from '@kbn/event-annotation-common'; import { cloneDeep } from 'lodash'; -import { LegendStats } from '@kbn/visualizations-plugin/common/constants'; +import { XYLegendValue } from '@kbn/visualizations-plugin/common/constants'; import { layerTypes } from '../../../common/layer_types'; import { AnnotationGroups } from '../../types'; @@ -280,7 +280,7 @@ function convertToLegendStats(state: XYState & { valuesInLegend?: unknown }) { ...state.legend, legendStats: [ ...new Set([ - ...(valuesInLegend ? [LegendStats.values] : []), + ...(valuesInLegend ? [XYLegendValue.CurrentAndLastValue] : []), ...(state.legend.legendStats || []), ]), ], @@ -296,7 +296,9 @@ function convertToValuesInLegend(state: XYState) { const newState: XYPersistedState = cloneDeep(state); if ('legendStats' in newState.legend && Array.isArray(newState.legend.legendStats)) { - newState.valuesInLegend = newState.legend.legendStats.includes(LegendStats.values); + newState.valuesInLegend = newState.legend.legendStats.includes( + XYLegendValue.CurrentAndLastValue + ); delete newState.legend.legendStats; } return newState; diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.tsx index d702b6f6995f75..7cdcf72ef703b1 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.tsx @@ -54,13 +54,13 @@ import { } from './visualization_helpers'; import { cloneDeep } from 'lodash'; import { DataViewsServicePublic } from '@kbn/data-views-plugin/public'; +import { XYLegendValue } from '@kbn/visualizations-plugin/common/constants'; import { XYPersistedByReferenceAnnotationLayerConfig, XYPersistedByValueAnnotationLayerConfig, XYPersistedLinkedByValueAnnotationLayerConfig, XYPersistedState, } from './persistence'; -import { LegendStats } from '@kbn/visualizations-plugin/common/constants'; const DATE_HISTORGRAM_COLUMN_ID = 'date_histogram_column'; const exampleAnnotation: EventAnnotationConfig = { @@ -612,13 +612,13 @@ describe('xy_visualization', () => { ...exampleState(), legend: { ...exampleState().legend, - legendStats: ['values' as LegendStats.values], + legendStats: [XYLegendValue.CurrentAndLastValue], }, }; const transformedState = xyVisualization.initialize(() => 'first', persistedState); - expect(transformedState.legend.legendStats).toEqual(['values']); + expect(transformedState.legend.legendStats).toEqual(['currentAndLastValue']); expect('valuesInLegend' in transformedState).toEqual(false); }); it('loads a xy chart with `valuesInLegend` property equal to false and transforms to legendStats: []', () => { @@ -641,7 +641,7 @@ describe('xy_visualization', () => { const transformedState = xyVisualization.initialize(() => 'first', persistedState); - expect(transformedState.legend.legendStats).toEqual(['values']); + expect(transformedState.legend.legendStats).toEqual(['currentAndLastValue']); expect('valuesInLegend' in transformedState).toEqual(false); }); @@ -3844,7 +3844,7 @@ describe('xy_visualization', () => { expect(noLegendStatsState.legend.legendStats).not.toBeDefined(); expect(noLegendStatsState.valuesInLegend).not.toBeDefined(); - state.legend.legendStats = ['values' as LegendStats.values]; + state.legend.legendStats = [XYLegendValue.CurrentAndLastValue]; const { state: legendStatsState } = xyVisualization.getPersistableState!(state); expect(legendStatsState.legend.legendStats).not.toBeDefined(); expect(legendStatsState.valuesInLegend).toEqual(true); diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/index.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/index.tsx index 3c906560a45a4c..082a1757e79d8d 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/index.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/index.tsx @@ -12,6 +12,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { AxisExtentConfig } from '@kbn/expression-xy-plugin/common'; import { LegendSize } from '@kbn/visualizations-plugin/public'; import { TooltipWrapper } from '@kbn/visualization-utils'; +import { XYLegendValue } from '@kbn/visualizations-plugin/common/constants'; import type { LegendSettingsPopoverProps } from '../../../shared_components/legend/legend_settings_popover'; import type { VisualizationToolbarProps, FramePublicAPI } from '../../../types'; import { State, XYState, AxesSettingsConfig } from '../types'; @@ -430,12 +431,12 @@ export const XyToolbar = memo(function XyToolbar( }} allowLegendStats={nonOrdinalXAxis} legendStats={state?.legend.legendStats} - onLegendStatsChange={(newLegendStats) => { + onLegendStatsChange={(checked) => { setState({ ...state, legend: { ...state.legend, - legendStats: newLegendStats, + legendStats: checked ? [XYLegendValue.CurrentAndLastValue] : [], }, }); }} diff --git a/x-pack/test/functional/fixtures/kbn_archiver/lens/legend_statistics.json b/x-pack/test/functional/fixtures/kbn_archiver/lens/legend_statistics.json index f7938e92378fb4..37e418901be7e0 100644 --- a/x-pack/test/functional/fixtures/kbn_archiver/lens/legend_statistics.json +++ b/x-pack/test/functional/fixtures/kbn_archiver/lens/legend_statistics.json @@ -400,7 +400,7 @@ "position": "right", "showSingleSeries": true, "legendStats": [ - "values" + "currentAndLastValue" ] }, "preferredSeriesType": "bar_stacked", @@ -729,7 +729,7 @@ "numberDisplay": "percent", "primaryGroups": [], "legendStats": [ - "values" + "value" ] } ], From ad81f857e9ac5e5845aa1c6b2ae47c4913eb6d20 Mon Sep 17 00:00:00 2001 From: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com> Date: Wed, 1 May 2024 00:10:24 +0900 Subject: [PATCH 03/10] [RAM][HTTP Versioning] Version Clone Rule Route (#180549) ## Summary Issue: https://github.com/elastic/kibana/issues/180079 Parent Issue: https://github.com/elastic/kibana/issues/157883 Versions the clone rule API and adds input/output validation ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/routes/rule/apis/clone/index.ts | 15 +++ .../routes/rule/apis/clone/schemas/latest.ts | 8 ++ .../routes/rule/apis/clone/schemas/v1.ts | 13 ++ .../routes/rule/apis/clone/types/latest.ts | 8 ++ .../common/routes/rule/apis/clone/types/v1.ts | 16 +++ .../rule/methods/clone/clone_rule.test.ts} | 28 +++-- .../rule/methods/clone/clone_rule.ts} | 117 +++++++++++++----- .../application/rule/methods/clone/index.ts | 10 ++ .../clone/schemas/clone_rule_params_schema.ts | 13 ++ .../rule/methods/clone/schemas/index.ts | 8 ++ .../methods/clone/types/clone_rule_params.ts | 11 ++ .../rule/methods/clone/types/index.ts | 8 ++ ...ransform_rule_attributes_to_rule_domain.ts | 6 +- .../alerting/server/routes/clone_rule.ts | 109 ---------------- .../plugins/alerting/server/routes/index.ts | 2 +- .../apis/clone/clone_rule_route.test.ts} | 24 ++-- .../rule/apis/clone/clone_rule_route.ts | 60 +++++++++ .../server/rules_client/rules_client.ts | 6 +- .../group3/tests/alerting/clone.ts | 20 +++ .../tests/alerting/user_managed_api_key.ts | 40 ++++++ 20 files changed, 350 insertions(+), 172 deletions(-) create mode 100644 x-pack/plugins/alerting/common/routes/rule/apis/clone/index.ts create mode 100644 x-pack/plugins/alerting/common/routes/rule/apis/clone/schemas/latest.ts create mode 100644 x-pack/plugins/alerting/common/routes/rule/apis/clone/schemas/v1.ts create mode 100644 x-pack/plugins/alerting/common/routes/rule/apis/clone/types/latest.ts create mode 100644 x-pack/plugins/alerting/common/routes/rule/apis/clone/types/v1.ts rename x-pack/plugins/alerting/server/{rules_client/tests/clone.test.ts => application/rule/methods/clone/clone_rule.test.ts} (85%) rename x-pack/plugins/alerting/server/{rules_client/methods/clone.ts => application/rule/methods/clone/clone_rule.ts} (51%) create mode 100644 x-pack/plugins/alerting/server/application/rule/methods/clone/index.ts create mode 100644 x-pack/plugins/alerting/server/application/rule/methods/clone/schemas/clone_rule_params_schema.ts create mode 100644 x-pack/plugins/alerting/server/application/rule/methods/clone/schemas/index.ts create mode 100644 x-pack/plugins/alerting/server/application/rule/methods/clone/types/clone_rule_params.ts create mode 100644 x-pack/plugins/alerting/server/application/rule/methods/clone/types/index.ts delete mode 100644 x-pack/plugins/alerting/server/routes/clone_rule.ts rename x-pack/plugins/alerting/server/routes/{clone_rule.test.ts => rule/apis/clone/clone_rule_route.test.ts} (91%) create mode 100644 x-pack/plugins/alerting/server/routes/rule/apis/clone/clone_rule_route.ts diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/clone/index.ts b/x-pack/plugins/alerting/common/routes/rule/apis/clone/index.ts new file mode 100644 index 00000000000000..0a678546b7e785 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/clone/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { cloneRuleRequestParamsSchema } from './schemas/latest'; +export type { CloneRuleRequestParams, CloneRuleResponse } from './types/latest'; + +export { cloneRuleRequestParamsSchema as cloneRuleRequestParamsSchemaV1 } from './schemas/v1'; +export type { + CloneRuleRequestParams as CloneRuleRequestParamsV1, + CloneRuleResponse as CloneRuleResponseV1, +} from './types/v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/clone/schemas/latest.ts b/x-pack/plugins/alerting/common/routes/rule/apis/clone/schemas/latest.ts new file mode 100644 index 00000000000000..25300c97a6d2e1 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/clone/schemas/latest.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/clone/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/rule/apis/clone/schemas/v1.ts new file mode 100644 index 00000000000000..bca16401b7f20d --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/clone/schemas/v1.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +export const cloneRuleRequestParamsSchema = schema.object({ + id: schema.string(), + newId: schema.maybe(schema.string()), +}); diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/clone/types/latest.ts b/x-pack/plugins/alerting/common/routes/rule/apis/clone/types/latest.ts new file mode 100644 index 00000000000000..25300c97a6d2e1 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/clone/types/latest.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/clone/types/v1.ts b/x-pack/plugins/alerting/common/routes/rule/apis/clone/types/v1.ts new file mode 100644 index 00000000000000..7c2baf997f16c0 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/clone/types/v1.ts @@ -0,0 +1,16 @@ +/* + * 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 { TypeOf } from '@kbn/config-schema'; +import { RuleParamsV1, RuleResponseV1 } from '../../../response'; +import { cloneRuleRequestParamsSchemaV1 } from '..'; + +export type CloneRuleRequestParams = TypeOf; + +export interface CloneRuleResponse { + body: RuleResponseV1; +} diff --git a/x-pack/plugins/alerting/server/rules_client/tests/clone.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/clone/clone_rule.test.ts similarity index 85% rename from x-pack/plugins/alerting/server/rules_client/tests/clone.test.ts rename to x-pack/plugins/alerting/server/application/rule/methods/clone/clone_rule.test.ts index 9676d19ebf2f0f..336224b4f40c10 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/clone.test.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/clone/clone_rule.test.ts @@ -12,19 +12,19 @@ import { uiSettingsServiceMock, } from '@kbn/core/server/mocks'; import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; -import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; -import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; +import { ruleTypeRegistryMock } from '../../../../rule_type_registry.mock'; +import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks'; -import { AlertingAuthorization } from '../../authorization/alerting_authorization'; +import { AlertingAuthorization } from '../../../../authorization/alerting_authorization'; import { ActionsAuthorization } from '@kbn/actions-plugin/server'; import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks'; -import { ConnectorAdapterRegistry } from '../../connector_adapters/connector_adapter_registry'; -import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects'; -import { getBeforeSetup } from './lib'; -import { RuleDomain } from '../../application/rule/types'; -import { ConstructorOptions, RulesClient } from '../rules_client'; -import { backfillClientMock } from '../../backfill_client/backfill_client.mock'; +import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry'; +import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects'; +import { getBeforeSetup } from '../../../../rules_client/tests/lib'; +import { RuleDomain } from '../../types'; +import { ConstructorOptions, RulesClient } from '../../../../rules_client/rules_client'; +import { backfillClientMock } from '../../../../backfill_client/backfill_client.mock'; describe('clone', () => { const taskManager = taskManagerMock.createStart(); @@ -129,7 +129,10 @@ describe('clone', () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(rule); unsecuredSavedObjectsClient.create.mockResolvedValue(rule); - const res = await rulesClient.clone('test-rule', { newId: 'test-rule-2' }); + const res = await rulesClient.clone({ + id: 'test-rule', + newId: 'test-rule-2', + }); expect(res.actions).toEqual([ { @@ -151,7 +154,10 @@ describe('clone', () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(rule); unsecuredSavedObjectsClient.create.mockResolvedValue(rule); - await rulesClient.clone('test-rule', { newId: 'test-rule-2' }); + await rulesClient.clone({ + id: 'test-rule', + newId: 'test-rule-2', + }); const results = unsecuredSavedObjectsClient.create.mock.calls[0][1] as RuleDomain; expect(results.actions).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/alerting/server/rules_client/methods/clone.ts b/x-pack/plugins/alerting/server/application/rule/methods/clone/clone_rule.ts similarity index 51% rename from x-pack/plugins/alerting/server/rules_client/methods/clone.ts rename to x-pack/plugins/alerting/server/application/rule/methods/clone/clone_rule.ts index acc8b66d6fde85..bb095103f937d4 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/clone.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/clone/clone_rule.ts @@ -10,37 +10,50 @@ import Boom from '@hapi/boom'; import { AlertConsumers } from '@kbn/rule-data-utils'; import { SavedObject, SavedObjectsUtils } from '@kbn/core/server'; import { withSpan } from '@kbn/apm-utils'; -import { RawRule, SanitizedRule, RuleTypeParams } from '../../types'; -import { getDefaultMonitoring } from '../../lib'; -import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization'; -import { parseDuration } from '../../../common/parse_duration'; -import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; -import { getRuleExecutionStatusPendingAttributes } from '../../lib/rule_execution_status'; -import { isDetectionEngineAADRuleType } from '../../saved_objects/migrations/utils'; -import { createNewAPIKeySet, createRuleSavedObject } from '../lib'; -import { RulesClientContext } from '../types'; -import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects'; +import { SanitizedRule, RawRule } from '../../../../types'; +import { getDefaultMonitoring } from '../../../../lib'; +import { WriteOperations, AlertingAuthorizationEntity } from '../../../../authorization'; +import { parseDuration } from '../../../../../common/parse_duration'; +import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events'; +import { getRuleExecutionStatusPendingAttributes } from '../../../../lib/rule_execution_status'; +import { isDetectionEngineAADRuleType } from '../../../../saved_objects/migrations/utils'; +import { createNewAPIKeySet, createRuleSavedObject } from '../../../../rules_client/lib'; +import { RulesClientContext } from '../../../../rules_client/types'; +import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects'; +import { CloneRuleParams } from './types'; +import { RuleAttributes } from '../../../../data/rule/types'; +import { RuleDomain, RuleParams } from '../../types'; +import { getDecryptedRuleSo, getRuleSo } from '../../../../data/rule'; +import { transformRuleAttributesToRuleDomain, transformRuleDomainToRule } from '../../transforms'; +import { ruleDomainSchema } from '../../schemas'; +import { cloneRuleParamsSchema } from './schemas'; -export type CloneArguments = [string, { newId?: string }]; - -export async function clone( +export async function cloneRule( context: RulesClientContext, - id: string, - { newId }: { newId?: string } + params: CloneRuleParams ): Promise> { - let ruleSavedObject: SavedObject; + const { id, newId } = params; + + try { + cloneRuleParamsSchema.validate(params); + } catch (error) { + throw Boom.badRequest(`Error validating clone data - ${error.message}`); + } + + let ruleSavedObject: SavedObject; try { ruleSavedObject = await withSpan( { name: 'encryptedSavedObjectsClient.getDecryptedAsInternalUser', type: 'rules' }, - () => - context.encryptedSavedObjectsClient.getDecryptedAsInternalUser( - RULE_SAVED_OBJECT_TYPE, + () => { + return getDecryptedRuleSo({ id, - { + encryptedSavedObjectsClient: context.encryptedSavedObjectsClient, + savedObjectsGetOptions: { namespace: context.namespace, - } - ) + }, + }); + } ); } catch (e) { // We'll skip invalidating the API key since we failed to load the decrypted saved object @@ -50,7 +63,12 @@ export async function clone( // Still attempt to load the object using SOC ruleSavedObject = await withSpan( { name: 'unsecuredSavedObjectsClient.get', type: 'rules' }, - () => context.unsecuredSavedObjectsClient.get(RULE_SAVED_OBJECT_TYPE, id) + () => { + return getRuleSo({ + id, + savedObjectsClient: context.unsecuredSavedObjectsClient, + }); + } ); } @@ -60,7 +78,8 @@ export async function clone( * functionality until we resolve our difference */ if ( - isDetectionEngineAADRuleType(ruleSavedObject) || + // TODO (http-versioning): Remove this cast to RawRule + isDetectionEngineAADRuleType(ruleSavedObject as SavedObject) || ruleSavedObject.attributes.consumer === AlertConsumers.SIEM ) { throw Boom.badRequest( @@ -107,7 +126,7 @@ export async function clone( errorMessage: 'Error creating rule: could not create API key', }); - const rawRule: RawRule = { + const ruleAttributes: RuleAttributes = { ...ruleSavedObject.attributes, name: ruleName, ...apiKeyAttributes, @@ -120,7 +139,10 @@ export async function clone( muteAll: false, mutedInstanceIds: [], executionStatus: getRuleExecutionStatusPendingAttributes(lastRunTimestamp.toISOString()), - monitoring: getDefaultMonitoring(lastRunTimestamp.toISOString()), + // TODO (http-versioning): Remove this cast to RuleAttributes + monitoring: getDefaultMonitoring( + lastRunTimestamp.toISOString() + ) as RuleAttributes['monitoring'], revision: 0, scheduledTaskId: null, running: false, @@ -134,12 +156,41 @@ export async function clone( }) ); - return await withSpan({ name: 'createRuleSavedObject', type: 'rules' }, () => - createRuleSavedObject(context, { - intervalInMs: parseDuration(rawRule.schedule.interval), - rawRule, - references: ruleSavedObject.references, - ruleId, - }) + const clonedRuleAttributes = await withSpan( + { name: 'createRuleSavedObject', type: 'rules' }, + () => + createRuleSavedObject(context, { + intervalInMs: parseDuration(ruleAttributes.schedule.interval), + rawRule: ruleAttributes, + references: ruleSavedObject.references, + ruleId, + returnRuleAttributes: true, + }) ); + + // Convert ES RuleAttributes back to domain rule object + const ruleDomain: RuleDomain = transformRuleAttributesToRuleDomain( + clonedRuleAttributes.attributes, + { + id: clonedRuleAttributes.id, + logger: context.logger, + ruleType: context.ruleTypeRegistry.get(clonedRuleAttributes.attributes.alertTypeId), + references: clonedRuleAttributes.references, + }, + (connectorId: string) => context.isSystemAction(connectorId) + ); + + // Try to validate created rule, but don't throw. + try { + ruleDomainSchema.validate(ruleDomain); + } catch (e) { + context.logger.warn(`Error validating clone rule domain object for id: ${id}, ${e}`); + } + + // Convert domain rule to rule (Remove certain properties) + const rule = transformRuleDomainToRule(ruleDomain, { isPublic: false }); + + // TODO (http-versioning): Remove this cast, this enables us to move forward + // without fixing all of other solution types + return rule as SanitizedRule; } diff --git a/x-pack/plugins/alerting/server/application/rule/methods/clone/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/clone/index.ts new file mode 100644 index 00000000000000..1164b73b820f73 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/clone/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { CloneRuleParams } from './types'; + +export { cloneRule } from './clone_rule'; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/clone/schemas/clone_rule_params_schema.ts b/x-pack/plugins/alerting/server/application/rule/methods/clone/schemas/clone_rule_params_schema.ts new file mode 100644 index 00000000000000..a15bb9a6d0ca87 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/clone/schemas/clone_rule_params_schema.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +export const cloneRuleParamsSchema = schema.object({ + id: schema.string(), + newId: schema.maybe(schema.string()), +}); diff --git a/x-pack/plugins/alerting/server/application/rule/methods/clone/schemas/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/clone/schemas/index.ts new file mode 100644 index 00000000000000..73f17fe640751d --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/clone/schemas/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './clone_rule_params_schema'; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/clone/types/clone_rule_params.ts b/x-pack/plugins/alerting/server/application/rule/methods/clone/types/clone_rule_params.ts new file mode 100644 index 00000000000000..349b3b3b538f01 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/clone/types/clone_rule_params.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TypeOf } from '@kbn/config-schema'; +import { cloneRuleParamsSchema } from '../schemas'; + +export type CloneRuleParams = TypeOf; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/clone/types/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/clone/types/index.ts new file mode 100644 index 00000000000000..ee06b36713cf78 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/clone/types/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './clone_rule_params'; diff --git a/x-pack/plugins/alerting/server/application/rule/transforms/transform_rule_attributes_to_rule_domain.ts b/x-pack/plugins/alerting/server/application/rule/transforms/transform_rule_attributes_to_rule_domain.ts index ffcc664b0e76e4..0f0c3805381075 100644 --- a/x-pack/plugins/alerting/server/application/rule/transforms/transform_rule_attributes_to_rule_domain.ts +++ b/x-pack/plugins/alerting/server/application/rule/transforms/transform_rule_attributes_to_rule_domain.ts @@ -10,8 +10,8 @@ import { SavedObjectReference } from '@kbn/core/server'; import { ruleExecutionStatusValues } from '../constants'; import { getRuleSnoozeEndTime } from '../../../lib'; import { RuleDomain, Monitoring, RuleParams } from '../types'; +import { PartialRule, SanitizedRule } from '../../../types'; import { RuleAttributes, RuleExecutionStatusAttributes } from '../../../data/rule/types'; -import { PartialRule } from '../../../types'; import { UntypedNormalizedRuleType } from '../../../rule_type_registry'; import { injectReferencesIntoParams } from '../../../rules_client/common'; import { getActiveScheduledSnoozes } from '../../../lib/is_rule_snoozed'; @@ -148,7 +148,7 @@ export const transformRuleAttributesToRuleDomain = ['snoozeSchedule'], })?.toISOString() : null; @@ -176,7 +176,7 @@ export const transformRuleAttributesToRuleDomain = ['snoozeSchedule'], muteAll: esRule.muteAll ?? false, })?.map((s) => s.id); diff --git a/x-pack/plugins/alerting/server/routes/clone_rule.ts b/x-pack/plugins/alerting/server/routes/clone_rule.ts deleted file mode 100644 index b6107a5fc6907d..00000000000000 --- a/x-pack/plugins/alerting/server/routes/clone_rule.ts +++ /dev/null @@ -1,109 +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 { IRouter } from '@kbn/core/server'; -import { ILicenseState, RuleTypeDisabledError } from '../lib'; -import { verifyAccessAndContext, handleDisabledApiKeysError, rewriteRuleLastRun } from './lib'; -import { - RuleTypeParams, - AlertingRequestHandlerContext, - INTERNAL_BASE_ALERTING_API_PATH, - PartialRule, -} from '../types'; -import { transformRuleActions } from './rule/transforms'; - -const paramSchema = schema.object({ - id: schema.string(), - newId: schema.maybe(schema.string()), -}); - -const rewriteBodyRes = ({ - actions, - systemActions, - alertTypeId, - scheduledTaskId, - createdBy, - updatedBy, - createdAt, - updatedAt, - apiKeyOwner, - apiKeyCreatedByUser, - notifyWhen, - muteAll, - mutedInstanceIds, - executionStatus, - snoozeSchedule, - isSnoozedUntil, - lastRun, - nextRun, - ...rest -}: PartialRule) => ({ - ...rest, - api_key_owner: apiKeyOwner, - created_by: createdBy, - updated_by: updatedBy, - snooze_schedule: snoozeSchedule, - ...(isSnoozedUntil ? { is_snoozed_until: isSnoozedUntil } : {}), - ...(alertTypeId ? { rule_type_id: alertTypeId } : {}), - ...(scheduledTaskId ? { scheduled_task_id: scheduledTaskId } : {}), - ...(createdAt ? { created_at: createdAt } : {}), - ...(updatedAt ? { updated_at: updatedAt } : {}), - ...(notifyWhen ? { notify_when: notifyWhen } : {}), - ...(muteAll !== undefined ? { mute_all: muteAll } : {}), - ...(mutedInstanceIds ? { muted_alert_ids: mutedInstanceIds } : {}), - ...(executionStatus - ? { - execution_status: { - status: executionStatus.status, - last_execution_date: executionStatus.lastExecutionDate, - last_duration: executionStatus.lastDuration, - }, - } - : {}), - ...(actions - ? { - actions: transformRuleActions(actions, systemActions), - } - : {}), - ...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}), - ...(nextRun ? { next_run: nextRun } : {}), - ...(apiKeyCreatedByUser !== undefined ? { api_key_created_by_user: apiKeyCreatedByUser } : {}), -}); - -export const cloneRuleRoute = ( - router: IRouter, - licenseState: ILicenseState -) => { - router.post( - { - path: `${INTERNAL_BASE_ALERTING_API_PATH}/rule/{id}/_clone/{newId?}`, - validate: { - params: paramSchema, - }, - }, - handleDisabledApiKeysError( - router.handleLegacyErrors( - verifyAccessAndContext(licenseState, async function (context, req, res) { - const rulesClient = (await context.alerting).getRulesClient(); - const { id, newId } = req.params; - try { - const cloneRule = await rulesClient.clone(id, { newId }); - return res.ok({ - body: rewriteBodyRes(cloneRule), - }); - } catch (e) { - if (e instanceof RuleTypeDisabledError) { - return e.sendResponse(res); - } - throw e; - } - }) - ) - ) - ); -}; diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts index fc07b8dbeb6e8c..1f5bea82b2c318 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -43,7 +43,7 @@ import { runSoonRoute } from './run_soon'; import { bulkDeleteRulesRoute } from './rule/apis/bulk_delete/bulk_delete_rules_route'; import { bulkEnableRulesRoute } from './rule/apis/bulk_enable/bulk_enable_rules_route'; import { bulkDisableRulesRoute } from './rule/apis/bulk_disable/bulk_disable_rules_route'; -import { cloneRuleRoute } from './clone_rule'; +import { cloneRuleRoute } from './rule/apis/clone/clone_rule_route'; import { getFlappingSettingsRoute } from './get_flapping_settings'; import { updateFlappingSettingsRoute } from './update_flapping_settings'; import { getRuleTagsRoute } from './rule/apis/tags/get_rule_tags'; diff --git a/x-pack/plugins/alerting/server/routes/clone_rule.test.ts b/x-pack/plugins/alerting/server/routes/rule/apis/clone/clone_rule_route.test.ts similarity index 91% rename from x-pack/plugins/alerting/server/routes/clone_rule.test.ts rename to x-pack/plugins/alerting/server/routes/rule/apis/clone/clone_rule_route.test.ts index a721ce45ad6a7b..4ae904261eb249 100644 --- a/x-pack/plugins/alerting/server/routes/clone_rule.test.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/clone/clone_rule_route.test.ts @@ -7,16 +7,16 @@ import { pick } from 'lodash'; 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 { cloneRuleRoute } from './clone_rule'; -import { RuleAction, RuleSystemAction, SanitizedRule } from '../types'; +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 { cloneRuleRoute } from './clone_rule_route'; +import { RuleAction, RuleSystemAction, SanitizedRule } from '../../../../types'; const rulesClient = rulesClientMock.create(); -jest.mock('../lib/license_api_access', () => ({ +jest.mock('../../../../lib/license_api_access', () => ({ verifyApiAccess: jest.fn(), })); @@ -95,13 +95,13 @@ describe('cloneRuleRoute', () => { updated_by: mockedRule.updatedBy, api_key_owner: mockedRule.apiKeyOwner, muted_alert_ids: mockedRule.mutedInstanceIds, - created_at: mockedRule.createdAt, - updated_at: mockedRule.updatedAt, + created_at: mockedRule.createdAt.toISOString(), + updated_at: mockedRule.updatedAt.toISOString(), id: mockedRule.id, revision: 0, execution_status: { status: mockedRule.executionStatus.status, - last_execution_date: mockedRule.executionStatus.lastExecutionDate, + last_execution_date: mockedRule.executionStatus.lastExecutionDate.toISOString(), }, actions: [ { @@ -139,8 +139,8 @@ describe('cloneRuleRoute', () => { expect(rulesClient.clone).toHaveBeenCalledTimes(1); expect(rulesClient.clone.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "1", Object { + "id": "1", "newId": undefined, }, ] diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/clone/clone_rule_route.ts b/x-pack/plugins/alerting/server/routes/rule/apis/clone/clone_rule_route.ts new file mode 100644 index 00000000000000..91214233b61ea8 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/clone/clone_rule_route.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from '@kbn/core/server'; +import { ILicenseState, RuleTypeDisabledError } from '../../../../lib'; +import { verifyAccessAndContext, handleDisabledApiKeysError } from '../../../lib'; +import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../../../types'; +import { + cloneRuleRequestParamsSchemaV1, + CloneRuleRequestParamsV1, + CloneRuleResponseV1, +} from '../../../../../common/routes/rule/apis/clone'; +import type { RuleParamsV1 } from '../../../../../common/routes/rule/response'; +import { Rule } from '../../../../application/rule/types'; +import { transformRuleToRuleResponseV1 } from '../../transforms'; + +export const cloneRuleRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.post( + { + path: `${INTERNAL_BASE_ALERTING_API_PATH}/rule/{id}/_clone/{newId?}`, + validate: { + params: cloneRuleRequestParamsSchemaV1, + }, + }, + handleDisabledApiKeysError( + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const rulesClient = (await context.alerting).getRulesClient(); + const params: CloneRuleRequestParamsV1 = req.params; + try { + // TODO (http-versioning): Remove this cast, this enables us to move forward + // without fixing all of other solution types + const cloneRule: Rule = (await rulesClient.clone({ + id: params.id, + newId: params.newId, + })) as Rule; + + const response: CloneRuleResponseV1 = { + body: transformRuleToRuleResponseV1(cloneRule), + }; + + return res.ok(response); + } catch (e) { + if (e instanceof RuleTypeDisabledError) { + return e.sendResponse(res); + } + throw e; + } + }) + ) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index e7ebb9fe2e64e7..86848fd1598d91 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -10,7 +10,7 @@ import { MuteAlertParams } from '../application/rule/methods/mute_alert/types'; import { SanitizedRule, RuleTypeParams } from '../types'; import { parseDuration } from '../../common/parse_duration'; import { RulesClientContext } from './types'; -import { clone, CloneArguments } from './methods/clone'; +import { cloneRule, CloneRuleParams } from '../application/rule/methods/clone'; import { createRule, CreateRuleParams } from '../application/rule/methods/create'; import { updateRule, UpdateRuleParams } from '../application/rule/methods/update'; import { snoozeRule, SnoozeRuleOptions } from '../application/rule/methods/snooze'; @@ -127,8 +127,8 @@ export class RulesClient { public aggregate = >(params: AggregateParams): Promise => aggregateRules(this.context, params); - public clone = (...args: CloneArguments) => - clone(this.context, ...args); + public clone = (params: CloneRuleParams) => + cloneRule(this.context, params); public create = (params: CreateRuleParams) => createRule(this.context, params); public delete = (params: { id: string }) => deleteRule(this.context, params); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/clone.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/clone.ts index d76c4dbd1a239f..577ffe429bc96d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/clone.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/clone.ts @@ -147,6 +147,26 @@ export default function createAlertTests({ getService }: FtrProviderContext) { uuid: response.body.actions[0].uuid, }, ], + monitoring: { + run: { + history: [], + calculated_metrics: { + success_ratio: 0, + }, + last_run: { + timestamp: response.body.monitoring.run.last_run.timestamp, + metrics: { + duration: 0, + total_search_duration_ms: null, + total_indexing_duration_ms: null, + total_alerts_detected: null, + total_alerts_created: null, + gap_duration_s: null, + }, + }, + }, + }, + snooze_schedule: [], enabled: true, rule_type_id: 'test.noop', running: false, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/user_managed_api_key.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/user_managed_api_key.ts index 2ceb14992688c3..0ebd0726e5f8e8 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/user_managed_api_key.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/user_managed_api_key.ts @@ -243,6 +243,26 @@ export default function userManagedApiKeyTest({ getService }: FtrProviderContext warning: null, }, next_run: response.body.next_run, + monitoring: { + run: { + history: [], + calculated_metrics: { + success_ratio: 0, + }, + last_run: { + timestamp: response.body.monitoring.run.last_run.timestamp, + metrics: { + duration: 0, + total_search_duration_ms: null, + total_indexing_duration_ms: null, + total_alerts_detected: null, + total_alerts_created: null, + gap_duration_s: null, + }, + }, + }, + }, + snooze_schedule: [], }); // Ensure AAD isn't broken @@ -306,6 +326,26 @@ export default function userManagedApiKeyTest({ getService }: FtrProviderContext warning: null, }, next_run: response.body.next_run, + monitoring: { + run: { + history: [], + calculated_metrics: { + success_ratio: 0, + }, + last_run: { + timestamp: response.body.monitoring.run.last_run.timestamp, + metrics: { + duration: 0, + total_search_duration_ms: null, + total_indexing_duration_ms: null, + total_alerts_detected: null, + total_alerts_created: null, + gap_duration_s: null, + }, + }, + }, + }, + snooze_schedule: [], }); // Ensure AAD isn't broken From 97e0f419bf20d62d3685447ddac42560442283ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 30 Apr 2024 16:15:23 +0100 Subject: [PATCH 04/10] [Assets] Creating Assets data access plugin (#182108) Create an empty assets data access plugin so we can start registering the services(APIs) that will be used to fetch assets type documents. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 1 + docs/developer/plugin-list.asciidoc | 4 ++ package.json | 1 + tsconfig.base.json | 2 + .../assets_data_access/jest.config.js | 14 +++++++ .../assets_data_access/kibana.jsonc | 13 +++++++ .../assets_data_access/server/index.ts | 16 ++++++++ .../assets_data_access/server/plugin.ts | 37 +++++++++++++++++++ .../server/services/register_services.ts | 17 +++++++++ .../assets_data_access/server/types.ts | 9 +++++ .../assets_data_access/tsconfig.json | 11 ++++++ yarn.lock | 4 ++ 12 files changed, 129 insertions(+) create mode 100644 x-pack/plugins/observability_solution/assets_data_access/jest.config.js create mode 100644 x-pack/plugins/observability_solution/assets_data_access/kibana.jsonc create mode 100644 x-pack/plugins/observability_solution/assets_data_access/server/index.ts create mode 100644 x-pack/plugins/observability_solution/assets_data_access/server/plugin.ts create mode 100644 x-pack/plugins/observability_solution/assets_data_access/server/services/register_services.ts create mode 100644 x-pack/plugins/observability_solution/assets_data_access/server/types.ts create mode 100644 x-pack/plugins/observability_solution/assets_data_access/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9d536165aa69af..73712f259aa02a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -52,6 +52,7 @@ packages/kbn-apm-utils @elastic/obs-ux-infra_services-team test/plugin_functional/plugins/app_link_test @elastic/kibana-core x-pack/test/usage_collection/plugins/application_usage_test @elastic/kibana-core x-pack/plugins/observability_solution/asset_manager @elastic/obs-knowledge-team +x-pack/plugins/observability_solution/assets_data_access @elastic/obs-knowledge-team x-pack/test/security_api_integration/plugins/audit_log @elastic/kibana-security packages/kbn-axe-config @elastic/kibana-qa packages/kbn-babel-preset @elastic/kibana-operations diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 75d681625b1bad..465c57ed09b387 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -466,6 +466,10 @@ The plugin exposes the static DefaultEditorController class to consume. |This plugin provides access to observed asset data, such as information about hosts, pods, containers, services, and more. +|{kib-repo}blob/{branch}/x-pack/plugins/observability_solution/assets_data_access[assetsDataAccess] +|WARNING: Missing README. + + |{kib-repo}blob/{branch}/x-pack/plugins/banners/README.md[banners] |Allow to add a header banner that will be displayed on every page of the Kibana application diff --git a/package.json b/package.json index de8acbf466e87d..ed0f5a514f2249 100644 --- a/package.json +++ b/package.json @@ -174,6 +174,7 @@ "@kbn/app-link-test-plugin": "link:test/plugin_functional/plugins/app_link_test", "@kbn/application-usage-test-plugin": "link:x-pack/test/usage_collection/plugins/application_usage_test", "@kbn/assetManager-plugin": "link:x-pack/plugins/observability_solution/asset_manager", + "@kbn/assets-data-access-plugin": "link:x-pack/plugins/observability_solution/assets_data_access", "@kbn/audit-log-plugin": "link:x-pack/test/security_api_integration/plugins/audit_log", "@kbn/banners-plugin": "link:x-pack/plugins/banners", "@kbn/bfetch-error": "link:packages/kbn-bfetch-error", diff --git a/tsconfig.base.json b/tsconfig.base.json index a887de77dafbcb..5735854dd386a3 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -98,6 +98,8 @@ "@kbn/application-usage-test-plugin/*": ["x-pack/test/usage_collection/plugins/application_usage_test/*"], "@kbn/assetManager-plugin": ["x-pack/plugins/observability_solution/asset_manager"], "@kbn/assetManager-plugin/*": ["x-pack/plugins/observability_solution/asset_manager/*"], + "@kbn/assets-data-access-plugin": ["x-pack/plugins/observability_solution/assets_data_access"], + "@kbn/assets-data-access-plugin/*": ["x-pack/plugins/observability_solution/assets_data_access/*"], "@kbn/audit-log-plugin": ["x-pack/test/security_api_integration/plugins/audit_log"], "@kbn/audit-log-plugin/*": ["x-pack/test/security_api_integration/plugins/audit_log/*"], "@kbn/axe-config": ["packages/kbn-axe-config"], diff --git a/x-pack/plugins/observability_solution/assets_data_access/jest.config.js b/x-pack/plugins/observability_solution/assets_data_access/jest.config.js new file mode 100644 index 00000000000000..3c145b378762f3 --- /dev/null +++ b/x-pack/plugins/observability_solution/assets_data_access/jest.config.js @@ -0,0 +1,14 @@ +/* + * 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. + */ + +const path = require('path'); + +module.exports = { + preset: '@kbn/test', + rootDir: path.resolve(__dirname, '../../../..'), + roots: ['/x-pack/plugins/observability_solution/assets_data_access'], +}; diff --git a/x-pack/plugins/observability_solution/assets_data_access/kibana.jsonc b/x-pack/plugins/observability_solution/assets_data_access/kibana.jsonc new file mode 100644 index 00000000000000..b5c33ee7e9e905 --- /dev/null +++ b/x-pack/plugins/observability_solution/assets_data_access/kibana.jsonc @@ -0,0 +1,13 @@ +{ + "type": "plugin", + "id": "@kbn/assets-data-access-plugin", + "owner": "@elastic/obs-knowledge-team", + "plugin": { + "id": "assetsDataAccess", + "server": true, + "browser": false, + "requiredPlugins": [], + "optionalPlugins": [], + "requiredBundles": [] + } +} diff --git a/x-pack/plugins/observability_solution/assets_data_access/server/index.ts b/x-pack/plugins/observability_solution/assets_data_access/server/index.ts new file mode 100644 index 00000000000000..cca2f9d4576384 --- /dev/null +++ b/x-pack/plugins/observability_solution/assets_data_access/server/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { PluginInitializerContext } from '@kbn/core/server'; +import type { AssetsDataAccessPluginSetup, AssetsDataAccessPluginStart } from './plugin'; + +export type { AssetsDataAccessPluginSetup, AssetsDataAccessPluginStart }; + +export async function plugin(initializerContext: PluginInitializerContext) { + const { AssetsDataAccessPlugin } = await import('./plugin'); + return new AssetsDataAccessPlugin(initializerContext); +} diff --git a/x-pack/plugins/observability_solution/assets_data_access/server/plugin.ts b/x-pack/plugins/observability_solution/assets_data_access/server/plugin.ts new file mode 100644 index 00000000000000..7de93968ec3ab6 --- /dev/null +++ b/x-pack/plugins/observability_solution/assets_data_access/server/plugin.ts @@ -0,0 +1,37 @@ +/* + * 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 { + CoreSetup, + CoreStart, + Logger, + Plugin, + PluginInitializerContext, +} from '@kbn/core/server'; +import { registerServices } from './services/register_services'; +import { AssetsPluginStartDeps } from './types'; + +export type AssetsDataAccessPluginSetup = ReturnType; +export type AssetsDataAccessPluginStart = ReturnType; + +export class AssetsDataAccessPlugin implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + public setup(core: CoreSetup) {} + + public start(core: CoreStart, plugins: AssetsPluginStartDeps) { + const services = registerServices({ + logger: this.logger, + deps: {}, + }); + + return { services }; + } +} diff --git a/x-pack/plugins/observability_solution/assets_data_access/server/services/register_services.ts b/x-pack/plugins/observability_solution/assets_data_access/server/services/register_services.ts new file mode 100644 index 00000000000000..dfac73c43df136 --- /dev/null +++ b/x-pack/plugins/observability_solution/assets_data_access/server/services/register_services.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger } from '@kbn/core/server'; + +export interface RegisterServicesParams { + logger: Logger; + deps: {}; +} + +export function registerServices(params: RegisterServicesParams) { + return {}; +} diff --git a/x-pack/plugins/observability_solution/assets_data_access/server/types.ts b/x-pack/plugins/observability_solution/assets_data_access/server/types.ts new file mode 100644 index 00000000000000..bfecff00b27291 --- /dev/null +++ b/x-pack/plugins/observability_solution/assets_data_access/server/types.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface AssetsPluginStartDeps {} diff --git a/x-pack/plugins/observability_solution/assets_data_access/tsconfig.json b/x-pack/plugins/observability_solution/assets_data_access/tsconfig.json new file mode 100644 index 00000000000000..5475c04618993f --- /dev/null +++ b/x-pack/plugins/observability_solution/assets_data_access/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": ["common/**/*", "server/**/*", "jest.config.js"], + "exclude": ["target/**/*"], + "kbn_references": [ + "@kbn/core", + ] +} diff --git a/yarn.lock b/yarn.lock index b3f1041b2b5cd3..f748ddf3048d7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3246,6 +3246,10 @@ version "0.0.0" uid "" +"@kbn/assets-data-access-plugin@link:x-pack/plugins/observability_solution/assets_data_access": + version "0.0.0" + uid "" + "@kbn/audit-log-plugin@link:x-pack/test/security_api_integration/plugins/audit_log": version "0.0.0" uid "" From cecd0dd9a3a8f943fb852eee77d75373faa2c8c7 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Tue, 30 Apr 2024 17:16:17 +0200 Subject: [PATCH 05/10] Add requiresPageReload to advanced settings (#181923) ## Summary Adds `requiresPageReload: true` to the advanced settings that require a page reload. --- src/plugins/bfetch/server/ui_settings.ts | 2 ++ src/plugins/data/server/ui_settings.ts | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/plugins/bfetch/server/ui_settings.ts b/src/plugins/bfetch/server/ui_settings.ts index 832ba4d049eb39..697864bec1c59c 100644 --- a/src/plugins/bfetch/server/ui_settings.ts +++ b/src/plugins/bfetch/server/ui_settings.ts @@ -24,6 +24,7 @@ export function getUiSettings(): Record> { }), schema: schema.boolean(), category: [], + requiresPageReload: true, }, [DISABLE_BFETCH_COMPRESSION]: { name: i18n.translate('bfetch.disableBfetchCompression', { @@ -36,6 +37,7 @@ export function getUiSettings(): Record> { }), schema: schema.boolean(), category: [], + requiresPageReload: true, }, }; } diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index b7b665689c1063..9f174a993e2874 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -498,6 +498,7 @@ export function getUiSettings( defaultMessage: 'Whether the filters should have a global state (be pinned) by default', }), schema: schema.boolean(), + requiresPageReload: true, }, [UI_SETTINGS.FILTERS_EDITOR_SUGGEST_VALUES]: { name: i18n.translate('data.advancedSettings.suggestFilterValuesTitle', { @@ -510,6 +511,7 @@ export function getUiSettings( 'Set this property to false to prevent the filter editor from suggesting values for fields.', }), schema: schema.boolean(), + requiresPageReload: true, }, [UI_SETTINGS.AUTOCOMPLETE_VALUE_SUGGESTION_METHOD]: { name: i18n.translate('data.advancedSettings.autocompleteValueSuggestionMethod', { @@ -535,6 +537,7 @@ export function getUiSettings( options: ['terms_enum', 'terms_agg'], category: ['autocomplete'], schema: schema.string(), + requiresPageReload: true, }, [UI_SETTINGS.AUTOCOMPLETE_USE_TIMERANGE]: { name: i18n.translate('data.advancedSettings.autocompleteIgnoreTimerange', { @@ -556,6 +559,7 @@ export function getUiSettings( }), category: ['autocomplete'], schema: schema.boolean(), + requiresPageReload: true, }, [UI_SETTINGS.SEARCH_TIMEOUT]: { name: i18n.translate('data.advancedSettings.searchTimeout', { @@ -569,6 +573,7 @@ export function getUiSettings( type: 'number', category: ['search'], schema: schema.number(), + requiresPageReload: true, }, }; } From 6023164f29f04ce2a9a4ee4c201f44fbead721a8 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 30 Apr 2024 16:17:35 +0100 Subject: [PATCH 06/10] [ML] Using PropsWithChildren type (#182008) Following up on #181257, adding `PropsWithChildren` to the types which were missed. --- .../expanded_row_field_header.tsx | 3 ++- .../expanded_row_content.tsx | 5 ++--- .../expanded_row_panel.tsx | 10 +++++++--- .../collapsible_panel/collapsible_panel.tsx | 4 ++-- .../field_stats_flyout_provider.tsx | 20 ++++++++++--------- .../header_menu_portal/header_menu_portal.tsx | 8 ++------ .../components/help_popover/help_popover.tsx | 9 ++++++--- .../contexts/ml/data_source_context.tsx | 3 ++- .../contexts/ml/ml_notifications_context.tsx | 6 ++---- .../contexts/ml/serverless_context.tsx | 5 ++--- .../components/details_step/description.tsx | 2 +- .../pages/job_map/components/cytoscape.tsx | 5 ++--- .../explorer/alerts/swim_lane_wrapper.tsx | 4 ++-- .../explorer/anomaly_explorer_context.tsx | 5 ++--- .../public/application/explorer/explorer.tsx | 5 ++--- .../loading_wrapper/loading_wrapper.tsx | 10 +++++++--- .../common/model_memory_limit/description.tsx | 5 ++--- .../components/data_view/description.tsx | 3 ++- .../components/frequency/description.tsx | 5 ++--- .../components/query/description.tsx | 3 ++- .../components/query_delay/description.tsx | 5 ++--- .../components/scroll_size/description.tsx | 5 ++--- .../components/time_field/description.tsx | 3 ++- .../components/calendars/description.tsx | 3 ++- .../components/custom_urls/description.tsx | 3 ++- .../components/annotations/description.tsx | 3 ++- .../dedicated_index/description.tsx | 3 ++- .../ignore_unavailable/description.tsx | 3 ++- .../components/model_plot/description.tsx | 3 ++- .../components/groups/description.tsx | 5 ++--- .../job_description/description.tsx | 3 ++- .../components/job_id/description.tsx | 5 ++--- .../advanced_detector_modal/descriptions.tsx | 16 +++++++-------- .../description.tsx | 5 +++-- .../components/geo_field/description.tsx | 4 ++-- .../components/influencers/description.tsx | 4 ++-- .../population_field/description.tsx | 4 ++-- .../components/rare_field/description.tsx | 4 ++-- .../components/sparse_data/description.tsx | 4 ++-- .../components/split_field/description.tsx | 4 ++-- .../test_models/models/ner/ner_output.tsx | 7 +++---- .../question_answering_output.tsx | 4 ++-- .../job_creation/common/job_details.tsx | 5 ++--- 43 files changed, 116 insertions(+), 109 deletions(-) diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/expanded_row_field_header/expanded_row_field_header.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/expanded_row_field_header/expanded_row_field_header.tsx index f0bd29c056985f..b5a02eace216cf 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/expanded_row_field_header/expanded_row_field_header.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/expanded_row_field_header/expanded_row_field_header.tsx @@ -6,10 +6,11 @@ */ import { EuiText, useEuiTheme } from '@elastic/eui'; +import type { FC, PropsWithChildren } from 'react'; import React from 'react'; import { css } from '@emotion/react'; -export const ExpandedRowFieldHeader = ({ children }: { children: React.ReactNode }) => { +export const ExpandedRowFieldHeader: FC> = ({ children }) => { const { euiTheme } = useEuiTheme(); const dvExpandedRowFieldHeader = css({ diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/expanded_row_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/expanded_row_content.tsx index ccac4cd47f9462..5a9a2dbf0f922e 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/expanded_row_content.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/expanded_row_content.tsx @@ -5,15 +5,14 @@ * 2.0. */ -import type { FC, ReactNode } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React from 'react'; import { EuiFlexGroup } from '@elastic/eui'; interface Props { - children: ReactNode; dataTestSubj: string; } -export const ExpandedRowContent: FC = ({ children, dataTestSubj }) => { +export const ExpandedRowContent: FC> = ({ children, dataTestSubj }) => { return ( {children} diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/expanded_row_panel.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/expanded_row_panel.tsx index 5702fd3ab0ccf7..be12875b0556e9 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/expanded_row_panel.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/expanded_row_panel.tsx @@ -5,18 +5,22 @@ * 2.0. */ -import type { FC, ReactNode } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React from 'react'; import { EuiPanel } from '@elastic/eui'; import type { EuiFlexItemProps } from '@elastic/eui/src/components/flex/flex_item'; interface Props { - children: ReactNode; dataTestSubj?: string; grow?: EuiFlexItemProps['grow']; className?: string; } -export const ExpandedRowPanel: FC = ({ children, dataTestSubj, grow, className }) => { +export const ExpandedRowPanel: FC> = ({ + children, + dataTestSubj, + grow, + className, +}) => { return ( void; - children: React.ReactNode; header: React.ReactElement; headerItems?: React.ReactElement[]; } -export const CollapsiblePanel: FC = ({ +export const CollapsiblePanel: FC> = ({ isOpen, onToggle, children, diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx b/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx index a9f19289842179..9b23ac89afcdbb 100644 --- a/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx +++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React, { useCallback, useState, type FC } from 'react'; +import type { PropsWithChildren, FC } from 'react'; +import React, { useCallback, useState } from 'react'; import type { DataView } from '@kbn/data-plugin/common'; import type { FieldStatsServices } from '@kbn/unified-field-list/src/components/field_stats'; import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker'; @@ -21,14 +22,15 @@ import { FieldStatsFlyout } from './field_stats_flyout'; import { MLFieldStatsFlyoutContext } from './use_field_stats_flytout_context'; import { PopulatedFieldsCacheManager } from './populated_fields/populated_fields_cache_manager'; -export const FieldStatsFlyoutProvider: FC<{ - children?: React.ReactNode; - dataView: DataView; - fieldStatsServices: FieldStatsServices; - timeRangeMs?: TimeRangeMs; - dslQuery?: FieldStatsProps['dslQuery']; - disablePopulatedFields?: boolean; -}> = ({ +export const FieldStatsFlyoutProvider: FC< + PropsWithChildren<{ + dataView: DataView; + fieldStatsServices: FieldStatsServices; + timeRangeMs?: TimeRangeMs; + dslQuery?: FieldStatsProps['dslQuery']; + disablePopulatedFields?: boolean; + }> +> = ({ dataView, fieldStatsServices, timeRangeMs, diff --git a/x-pack/plugins/ml/public/application/components/header_menu_portal/header_menu_portal.tsx b/x-pack/plugins/ml/public/application/components/header_menu_portal/header_menu_portal.tsx index f80c8888ee1814..a572a7b195af25 100644 --- a/x-pack/plugins/ml/public/application/components/header_menu_portal/header_menu_portal.tsx +++ b/x-pack/plugins/ml/public/application/components/header_menu_portal/header_menu_portal.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import type { FC, ReactNode } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React, { useContext, useEffect, useMemo } from 'react'; import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; @@ -13,11 +13,7 @@ import { toMountPoint } from '@kbn/react-kibana-mount'; import { useMlKibana } from '../../contexts/kibana'; import { MlPageControlsContext } from '../ml_page'; -export interface HeaderMenuPortalProps { - children: ReactNode; -} - -export const HeaderMenuPortal: FC = ({ children }) => { +export const HeaderMenuPortal: FC> = ({ children }) => { const { services } = useMlKibana(); const { setHeaderActionMenu } = useContext(MlPageControlsContext); diff --git a/x-pack/plugins/ml/public/application/components/help_popover/help_popover.tsx b/x-pack/plugins/ml/public/application/components/help_popover/help_popover.tsx index 7a3178b05e40b1..aa7bf496b10513 100644 --- a/x-pack/plugins/ml/public/application/components/help_popover/help_popover.tsx +++ b/x-pack/plugins/ml/public/application/components/help_popover/help_popover.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import type { FC } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import type { EuiLinkButtonProps, EuiPopoverProps } from '@elastic/eui'; @@ -27,12 +27,15 @@ export const HelpPopoverButton: FC<{ onClick: EuiLinkButtonProps['onClick'] }> = }; interface HelpPopoverProps { - children: React.ReactNode; anchorPosition?: EuiPopoverProps['anchorPosition']; title?: string; } -export const HelpPopover: FC = ({ anchorPosition, children, title }) => { +export const HelpPopover: FC> = ({ + anchorPosition, + children, + title, +}) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); return ( diff --git a/x-pack/plugins/ml/public/application/contexts/ml/data_source_context.tsx b/x-pack/plugins/ml/public/application/contexts/ml/data_source_context.tsx index b87b26ace39555..1c94200794a815 100644 --- a/x-pack/plugins/ml/public/application/contexts/ml/data_source_context.tsx +++ b/x-pack/plugins/ml/public/application/contexts/ml/data_source_context.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import type { PropsWithChildren } from 'react'; import React, { type FC, useCallback, useContext, useEffect, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { useLocation } from 'react-router-dom'; @@ -34,7 +35,7 @@ export const DataSourceContext = React.createContext( * @param children * @constructor */ -export const DataSourceContextProvider: FC<{ children?: React.ReactNode }> = ({ children }) => { +export const DataSourceContextProvider: FC> = ({ children }) => { const [value, setValue] = useState(); const [error, setError] = useState(); diff --git a/x-pack/plugins/ml/public/application/contexts/ml/ml_notifications_context.tsx b/x-pack/plugins/ml/public/application/contexts/ml/ml_notifications_context.tsx index 79c1e8d535c999..dc37535517659e 100644 --- a/x-pack/plugins/ml/public/application/contexts/ml/ml_notifications_context.tsx +++ b/x-pack/plugins/ml/public/application/contexts/ml/ml_notifications_context.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import type { FC } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React, { useContext, useState, useEffect } from 'react'; import { combineLatest, timer } from 'rxjs'; import { switchMap, map, tap, retry } from 'rxjs'; @@ -39,9 +39,7 @@ export const MlNotificationsContext = React.createContext<{ setLastCheckedAt: () => {}, }); -export const MlNotificationsContextProvider: FC<{ children?: React.ReactNode }> = ({ - children, -}) => { +export const MlNotificationsContextProvider: FC> = ({ children }) => { const { services: { mlServices: { mlApiServices }, diff --git a/x-pack/plugins/ml/public/application/contexts/ml/serverless_context.tsx b/x-pack/plugins/ml/public/application/contexts/ml/serverless_context.tsx index 8bba7b67cc4daa..bf39fe5df969b3 100644 --- a/x-pack/plugins/ml/public/application/contexts/ml/serverless_context.tsx +++ b/x-pack/plugins/ml/public/application/contexts/ml/serverless_context.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import type { FC } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React, { createContext, useContext, useMemo } from 'react'; import type { ExperimentalFeatures, MlFeatures } from '../../../../common/constants/app'; @@ -28,14 +28,13 @@ export const EnabledFeaturesContext = createContext({ }); interface Props { - children: React.ReactNode; isServerless: boolean; mlFeatures: MlFeatures; showMLNavMenu?: boolean; experimentalFeatures?: ExperimentalFeatures; } -export const EnabledFeaturesContextProvider: FC = ({ +export const EnabledFeaturesContextProvider: FC> = ({ children, isServerless, showMLNavMenu = true, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/description.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/description.tsx index 3b6ac4c7eb656d..6377634c79a534 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/description.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/description.tsx @@ -13,7 +13,7 @@ import { EuiDescribedFormGroup, EuiFormRow, EuiLink } from '@elastic/eui'; import { css } from '@emotion/react'; import { useMlKibana } from '../../../../../contexts/kibana'; -export const Description: FC> = memo(({ children }) => { +export const Description: FC> = memo(({ children }) => { const { services: { docLinks }, } = useMlKibana(); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx index a30351f4bdd873..16811a8429d185 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/cytoscape.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import type { CSSProperties, ReactNode } from 'react'; +import type { CSSProperties, PropsWithChildren } from 'react'; import React, { useState, useRef, useEffect, createContext, useCallback, useMemo } from 'react'; import { css } from '@emotion/react'; import cytoscape, { type Stylesheet } from 'cytoscape'; @@ -19,7 +19,6 @@ cytoscape.use(dagre); export const CytoscapeContext = createContext(undefined); interface CytoscapeProps { - children?: ReactNode; elements: cytoscape.ElementDefinition[]; theme: EuiThemeType; height: number; @@ -77,7 +76,7 @@ export function Cytoscape({ resetCy, style, width, -}: CytoscapeProps) { +}: PropsWithChildren) { const cytoscapeOptions = useMemo(() => { return { ...getCytoscapeOptions(theme), diff --git a/x-pack/plugins/ml/public/application/explorer/alerts/swim_lane_wrapper.tsx b/x-pack/plugins/ml/public/application/explorer/alerts/swim_lane_wrapper.tsx index ab18827ad6bf6b..fee9ea7c7b4733 100644 --- a/x-pack/plugins/ml/public/application/explorer/alerts/swim_lane_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/explorer/alerts/swim_lane_wrapper.tsx @@ -26,6 +26,7 @@ import { type AlertStatus, } from '@kbn/rule-data-utils'; import { pick } from 'lodash'; +import type { PropsWithChildren } from 'react'; import React, { type FC, useCallback, useMemo, useRef } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { i18n } from '@kbn/i18n'; @@ -49,7 +50,6 @@ import { statusNameMap } from './const'; import { Y_AXIS_LABEL_WIDTH } from '../constants'; export interface SwimLaneWrapperProps { - children: React.ReactNode; selection?: AppStateSelectedCells | null; swimlaneContainerWidth?: number; swimLaneData: SwimlaneData; @@ -59,7 +59,7 @@ export interface SwimLaneWrapperProps { * Wrapper component for the swim lane * that handles the popover for the selected cells. */ -export const SwimLaneWrapper: FC = ({ +export const SwimLaneWrapper: FC> = ({ children, selection, swimlaneContainerWidth, diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_explorer_context.tsx b/x-pack/plugins/ml/public/application/explorer/anomaly_explorer_context.tsx index b3b1fcc97f7a93..dec8d5df57ceb3 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomaly_explorer_context.tsx +++ b/x-pack/plugins/ml/public/application/explorer/anomaly_explorer_context.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import type { PropsWithChildren } from 'react'; import React, { useContext, useEffect, useMemo, useState, type FC } from 'react'; import { useTimefilter } from '@kbn/ml-date-picker'; import { AnomalyTimelineStateService } from './anomaly_timeline_state_service'; @@ -52,9 +53,7 @@ export function useAnomalyExplorerContext() { /** * Anomaly Explorer Context Provider. */ -export const AnomalyExplorerContextProvider: FC<{ children?: React.ReactNode }> = ({ - children, -}) => { +export const AnomalyExplorerContextProvider: FC> = ({ children }) => { const [, , anomalyExplorerUrlStateService] = useExplorerUrlState(); const timefilter = useTimefilter(); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.tsx b/x-pack/plugins/ml/public/application/explorer/explorer.tsx index 47cd51812e452b..23a4cbed76bbc2 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import type { FC } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -102,10 +102,9 @@ interface ExplorerPageProps { queryString?: string; updateLanguage?: (language: string) => void; dataViews?: DataView[]; - children: React.ReactNode; } -const ExplorerPage: FC = ({ +const ExplorerPage: FC> = ({ children, jobSelectorProps, noInfluencersConfigured, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/loading_wrapper/loading_wrapper.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/loading_wrapper/loading_wrapper.tsx index 5fa952da73569a..b2a7f908972059 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/loading_wrapper/loading_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/loading_wrapper/loading_wrapper.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import type { FC } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React, { Fragment } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; @@ -13,10 +13,14 @@ interface Props { hasData: boolean; height?: string; loading?: boolean; - children?: React.ReactNode; } -export const LoadingWrapper: FC = ({ hasData, loading = false, height, children }) => { +export const LoadingWrapper: FC> = ({ + hasData, + loading = false, + height, + children, +}) => { const opacity = loading === true ? (hasData === true ? 0.3 : 0) : 1; return ( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/model_memory_limit/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/model_memory_limit/description.tsx index 592f0bbc2efd90..e2a1d8e90fec52 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/model_memory_limit/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/model_memory_limit/description.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import type { FC } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -13,11 +13,10 @@ import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; import type { Validation } from '../../../../common/job_validator'; interface Props { - children: React.ReactNode; validation: Validation; } -export const Description: FC = memo(({ children, validation }) => { +export const Description: FC> = memo(({ children, validation }) => { const title = i18n.translate( 'xpack.ml.newJob.wizard.jobDetailsStep.advancedSection.modelMemoryLimit.title', { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/description.tsx index 248adaa34ae978..c6c6aee15f276f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/description.tsx @@ -5,12 +5,13 @@ * 2.0. */ +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; -export const Description = memo(({ children }: { children?: React.ReactNode }) => { +export const Description: FC> = memo(({ children }) => { const title = i18n.translate('xpack.ml.newJob.wizard.datafeedStep.dataView.title', { defaultMessage: 'Data view', }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/frequency/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/frequency/description.tsx index 3a43d1a10bbc03..4da755c839219f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/frequency/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/frequency/description.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import type { FC } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -13,11 +13,10 @@ import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; import type { Validation } from '../../../../../common/job_validator'; interface Props { - children: React.ReactNode; validation: Validation; } -export const Description: FC = memo(({ children, validation }) => { +export const Description: FC> = memo(({ children, validation }) => { const title = i18n.translate('xpack.ml.newJob.wizard.datafeedStep.frequency.title', { defaultMessage: 'Frequency', }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query/description.tsx index 78d04584d71c55..9c0fcb0e4785d1 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query/description.tsx @@ -5,11 +5,12 @@ * 2.0. */ +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow } from '@elastic/eui'; -export const Description = memo(({ children }: { children?: React.ReactNode }) => { +export const Description: FC> = memo(({ children }) => { const title = i18n.translate('xpack.ml.newJob.wizard.datafeedStep.query.title', { defaultMessage: 'Elasticsearch query', }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/description.tsx index 05929fae8e7606..f233c8e12a3272 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/description.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import type { FC } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -13,11 +13,10 @@ import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; import type { Validation } from '../../../../../common/job_validator'; interface Props { - children: React.ReactNode; validation: Validation; } -export const Description: FC = memo(({ children, validation }) => { +export const Description: FC> = memo(({ children, validation }) => { const title = i18n.translate('xpack.ml.newJob.wizard.datafeedStep.queryDelay.title', { defaultMessage: 'Query delay', }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/scroll_size/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/scroll_size/description.tsx index a6bb58607f4e77..4656c3e537d2cd 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/scroll_size/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/scroll_size/description.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import type { FC } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -13,11 +13,10 @@ import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; import type { Validation } from '../../../../../common/job_validator'; interface Props { - children: React.ReactNode; validation: Validation; } -export const Description: FC = memo(({ children, validation }) => { +export const Description: FC> = memo(({ children, validation }) => { const title = i18n.translate('xpack.ml.newJob.wizard.datafeedStep.scrollSize.title', { defaultMessage: 'Scroll size', }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/description.tsx index 1f6c8570ae2144..ca69570c51437a 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/description.tsx @@ -5,12 +5,13 @@ * 2.0. */ +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; -export const Description = memo(({ children }: { children?: React.ReactNode }) => { +export const Description: FC> = memo(({ children }) => { const title = i18n.translate('xpack.ml.newJob.wizard.datafeedStep.timeField.title', { defaultMessage: 'Time field', }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/description.tsx index 08282ea8f52da0..517b9063fd7f2f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/description.tsx @@ -5,13 +5,14 @@ * 2.0. */ +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiDescribedFormGroup, EuiFormRow, EuiLink } from '@elastic/eui'; import { useMlKibana } from '../../../../../../../../../contexts/kibana'; -export const Description = memo(({ children }: { children?: React.ReactNode }) => { +export const Description: FC> = memo(({ children }) => { const { services: { docLinks }, } = useMlKibana(); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/custom_urls/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/custom_urls/description.tsx index 42eebff43e055d..e3718862afe297 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/custom_urls/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/custom_urls/description.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -12,7 +13,7 @@ import { EuiDescribedFormGroup, EuiFormRow, EuiLink } from '@elastic/eui'; import { css } from '@emotion/react'; import { useMlKibana } from '../../../../../../../../../contexts/kibana'; -export const Description = memo(({ children }: { children?: React.ReactNode }) => { +export const Description: FC> = memo(({ children }) => { const { services: { docLinks }, } = useMlKibana(); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/annotations/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/annotations/description.tsx index 9f278d698be36c..11a6a3ae632804 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/annotations/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/annotations/description.tsx @@ -5,12 +5,13 @@ * 2.0. */ +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; -export const Description = memo(({ children }: { children?: React.ReactNode }) => { +export const Description: FC> = memo(({ children }) => { const title = i18n.translate( 'xpack.ml.newJob.wizard.jobDetailsStep.advancedSection.enableModelPlotAnnotations.title', { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/description.tsx index a6ab79de38681f..7e7b322f05d9eb 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/description.tsx @@ -5,12 +5,13 @@ * 2.0. */ +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; -export const Description = memo(({ children }: { children?: React.ReactNode }) => { +export const Description: FC> = memo(({ children }) => { const title = i18n.translate( 'xpack.ml.newJob.wizard.jobDetailsStep.advancedSection.useDedicatedIndex.title', { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/ignore_unavailable/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/ignore_unavailable/description.tsx index 5bfcdcdbbc3686..76bba14e2f6674 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/ignore_unavailable/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/ignore_unavailable/description.tsx @@ -5,12 +5,13 @@ * 2.0. */ +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiCode, EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; -export const Description = memo(({ children }: { children?: React.ReactNode }) => { +export const Description: FC> = memo(({ children }) => { const title = i18n.translate( 'xpack.ml.newJob.wizard.jobDetailsStep.advancedSection.ignoreUnavailable.title', { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/description.tsx index 6bfec5f628dfed..25797955ff2891 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/description.tsx @@ -5,12 +5,13 @@ * 2.0. */ +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; -export const Description = memo(({ children }: { children?: React.ReactNode }) => { +export const Description: FC> = memo(({ children }) => { const title = i18n.translate( 'xpack.ml.newJob.wizard.jobDetailsStep.advancedSection.enableModelPlot.title', { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/description.tsx index b070a68d7ebd4a..b151adc8c53a23 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/description.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import type { FC } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -13,11 +13,10 @@ import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; import type { Validation } from '../../../../../common/job_validator'; interface Props { - children: React.ReactNode; validation: Validation; } -export const Description: FC = memo(({ children, validation }) => { +export const Description: FC> = memo(({ children, validation }) => { const title = i18n.translate('xpack.ml.newJob.wizard.jobDetailsStep.jobGroupSelect.title', { defaultMessage: 'Groups', }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_description/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_description/description.tsx index 876d8119800621..0cbb18afd6760a 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_description/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_description/description.tsx @@ -5,12 +5,13 @@ * 2.0. */ +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; -export const Description = memo(({ children }: { children?: React.ReactNode }) => { +export const Description: FC> = memo(({ children }) => { const title = i18n.translate('xpack.ml.newJob.wizard.jobDetailsStep.jobDescription.title', { defaultMessage: 'Job description', }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_id/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_id/description.tsx index 4a5ef4166ed1eb..94654a0d3f228b 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_id/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_id/description.tsx @@ -5,18 +5,17 @@ * 2.0. */ -import type { FC } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; import type { Validation } from '../../../../../common/job_validator'; interface Props { - children: React.ReactNode; validation: Validation; } -export const Description: FC = memo(({ children, validation }) => { +export const Description: FC> = memo(({ children, validation }) => { const title = i18n.translate('xpack.ml.newJob.wizard.jobDetailsStep.jobId.title', { defaultMessage: 'Job ID', }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx index db6f5bb955e99f..40313a9ee43ea7 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import type { PropsWithChildren } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -13,7 +13,7 @@ import { EuiDescribedFormGroup, EuiFormRow, EuiFlexGroup, EuiFlexItem } from '@e import { FunctionHelpPopover } from './function_help'; -export const AggDescription = memo>(({ children }) => { +export const AggDescription: FC> = memo(({ children }) => { const title = i18n.translate( 'xpack.ml.newJob.wizard.pickFieldsStep.advancedDetectorModal.aggSelect.title', { @@ -46,7 +46,7 @@ export const AggDescription = memo>(({ children }) => ); }); -export const FieldDescription = memo>(({ children }) => { +export const FieldDescription: FC> = memo(({ children }) => { const title = i18n.translate( 'xpack.ml.newJob.wizard.pickFieldsStep.advancedDetectorModal.fieldSelect.title', { @@ -70,7 +70,7 @@ export const FieldDescription = memo>(({ children }) ); }); -export const ByFieldDescription = memo>(({ children }) => { +export const ByFieldDescription: FC> = memo(({ children }) => { const title = i18n.translate( 'xpack.ml.newJob.wizard.pickFieldsStep.advancedDetectorModal.byFieldSelect.title', { @@ -94,7 +94,7 @@ export const ByFieldDescription = memo>(({ children } ); }); -export const OverFieldDescription = memo>(({ children }) => { +export const OverFieldDescription: FC> = memo(({ children }) => { const title = i18n.translate( 'xpack.ml.newJob.wizard.pickFieldsStep.advancedDetectorModal.overFieldSelect.title', { @@ -118,7 +118,7 @@ export const OverFieldDescription = memo>(({ children ); }); -export const PartitionFieldDescription = memo>(({ children }) => { +export const PartitionFieldDescription: FC> = memo(({ children }) => { const title = i18n.translate( 'xpack.ml.newJob.wizard.pickFieldsStep.advancedDetectorModal.partitionFieldSelect.title', { @@ -142,7 +142,7 @@ export const PartitionFieldDescription = memo>(({ chi ); }); -export const ExcludeFrequentDescription = memo>(({ children }) => { +export const ExcludeFrequentDescription: FC> = memo(({ children }) => { const title = i18n.translate( 'xpack.ml.newJob.wizard.pickFieldsStep.advancedDetectorModal.excludeFrequent.title', { @@ -166,7 +166,7 @@ export const ExcludeFrequentDescription = memo>(({ ch ); }); -export const DescriptionDescription = memo>(({ children }) => { +export const DescriptionDescription: FC> = memo(({ children }) => { const title = i18n.translate( 'xpack.ml.newJob.wizard.pickFieldsStep.advancedDetectorModal.description.title', { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_partition_field/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_partition_field/description.tsx index 377a71ab800959..5a9cd75e41c83f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_partition_field/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_partition_field/description.tsx @@ -5,12 +5,13 @@ * 2.0. */ -import type { PropsWithChildren } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; -export const Description = memo>(({ children }) => { + +export const Description: FC> = memo(({ children }) => { const title = i18n.translate('xpack.ml.newJob.wizard.perPartitionCategorization.enable.title', { defaultMessage: 'Per-partition categorization', }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/description.tsx index 489fa99a4bbffc..e16f2762be82a4 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/description.tsx @@ -5,13 +5,13 @@ * 2.0. */ -import type { PropsWithChildren } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; -export const Description = memo>(({ children }) => { +export const Description: FC> = memo(({ children }) => { const title = i18n.translate('xpack.ml.newJob.wizard.pickFieldsStep.geoField.title', { defaultMessage: 'Geo field', }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/description.tsx index 8652984566b17f..9c641590d0c700 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/description.tsx @@ -5,13 +5,13 @@ * 2.0. */ -import type { PropsWithChildren } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; -export const Description = memo>(({ children }) => { +export const Description: FC> = memo(({ children }) => { const title = i18n.translate('xpack.ml.newJob.wizard.pickFieldsStep.influencers.title', { defaultMessage: 'Influencers', }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_field/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_field/description.tsx index 04fb60e5fe6813..472eab9405c265 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_field/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_field/description.tsx @@ -5,13 +5,13 @@ * 2.0. */ -import type { PropsWithChildren } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; -export const Description = memo>(({ children }) => { +export const Description: FC> = memo(({ children }) => { const title = i18n.translate('xpack.ml.newJob.wizard.pickFieldsStep.populationField.title', { defaultMessage: 'Population field', }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/description.tsx index 0ea99ed45fa1bd..b15c277bd706e9 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/description.tsx @@ -5,13 +5,13 @@ * 2.0. */ -import type { PropsWithChildren } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; -export const Description = memo>(({ children }) => { +export const Description: FC> = memo(({ children }) => { const title = i18n.translate('xpack.ml.newJob.wizard.pickFieldsStep.splitRareField.title', { defaultMessage: 'Rare field', }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/description.tsx index 2dc6371010bdcc..dc8d5c436a54f7 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/description.tsx @@ -5,13 +5,13 @@ * 2.0. */ -import type { PropsWithChildren } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; -export const Description = memo>(({ children }) => { +export const Description: FC> = memo(({ children }) => { const title = i18n.translate('xpack.ml.newJob.wizard.pickFieldsStep.sparseData.title', { defaultMessage: 'Sparse data', }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx index 1d87ed4f9253d6..11708c5665ba16 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx @@ -5,13 +5,13 @@ * 2.0. */ -import type { PropsWithChildren } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; -export const Description = memo>(({ children }) => { +export const Description: FC> = memo(({ children }) => { const title = i18n.translate('xpack.ml.newJob.wizard.pickFieldsStep.splitField.title', { defaultMessage: 'Split field', }); diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/models/ner/ner_output.tsx b/x-pack/plugins/ml/public/application/model_management/test_models/models/ner/ner_output.tsx index 275d607813ae9d..5582a383fe713a 100644 --- a/x-pack/plugins/ml/public/application/model_management/test_models/models/ner/ner_output.tsx +++ b/x-pack/plugins/ml/public/application/model_management/test_models/models/ner/ner_output.tsx @@ -6,7 +6,7 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { FC, ReactNode } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import React from 'react'; import useObservable from 'react-use/lib/useObservable'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -140,10 +140,9 @@ const Lines: FC<{ result: NerResponse }> = ({ result }) => { const EntityBadge = ({ entity, children, -}: { +}: PropsWithChildren<{ entity: estypes.MlTrainedModelEntities; - children: ReactNode; -}) => { +}>) => { const { euiTheme } = useCurrentThemeVars(); return ( { +const ResultBadge: FC> = ({ children }) => { const { euiTheme } = useCurrentThemeVars(); return ( void; createADJob: (args: CreateADJobParams) => Promise; layer?: LayerResult; @@ -67,7 +66,7 @@ enum STATE { SAVE_FAILED, } -export const JobDetails: FC = ({ +export const JobDetails: FC> = ({ children, createADJobInWizard, createADJob, From e6dcdc7b0380f5b3fe27145f516974fcf713470e Mon Sep 17 00:00:00 2001 From: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com> Date: Wed, 1 May 2024 00:25:22 +0900 Subject: [PATCH 07/10] [RAM][HTTP Versioning] Version Internal and Public Find Rules Route (#180654) ## Summary Issue: https://github.com/elastic/kibana/issues/180551 Parent Issue: https://github.com/elastic/kibana/issues/157883 Versions the internal and public find API routes. Adds Input validation as well. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/routes/rule/apis/find/index.ts | 15 +++ .../routes/rule/apis/find/schemas/latest.ts | 8 ++ .../routes/rule/apis/find/schemas/v1.ts | 33 +++++ .../routes/rule/apis/find/types/latest.ts | 8 ++ .../common/routes/rule/apis/find/types/v1.ts | 21 +++ .../rule/methods/find/find_rules.test.ts} | 89 ++++++++++--- .../rule/methods/find/find_rules.ts} | 122 ++++++++---------- .../application/rule/methods/find/index.ts | 11 ++ .../find/schemas/find_rules_schemas.ts | 41 ++++++ .../rule/methods/find/schemas/index.ts | 8 ++ .../methods/find/types/find_rules_types.ts | 13 ++ .../rule/methods/find/types/index.ts | 8 ++ .../plugins/alerting/server/routes/index.ts | 2 +- .../alerting/server/routes/legacy/find.ts | 21 ++- .../apis/find/find_rules_route.test.ts} | 32 ++--- .../apis/find/find_rules_route.ts} | 111 ++++++---------- .../routes/rule/apis/find/transforms/index.ts | 12 ++ .../transform_find_rules_body/latest.ts | 8 ++ .../transform_find_rules_body/v1.ts | 40 ++++++ .../transform_find_rules_response/latest.ts | 8 ++ .../transform_find_rules_response/v1.ts | 111 ++++++++++++++++ .../server/routes/rule/transforms/index.ts | 16 ++- .../transform_rule_to_rule_response/latest.ts | 2 +- .../transform_rule_to_rule_response/v1.ts | 6 +- .../retrieve_migrated_legacy_actions.test.ts | 8 +- .../retrieve_migrated_legacy_actions.ts | 4 +- .../server/rules_client/rules_client.ts | 6 +- .../alerting/server/rules_client/types.ts | 2 +- .../benchmark_rules/bulk_action/utils.ts | 2 +- .../group1/tests/alerting/find.ts | 6 + .../group1/tests/alerting/find_with_post.ts | 6 + .../spaces_only/tests/alerting/group1/find.ts | 1 + 32 files changed, 587 insertions(+), 194 deletions(-) create mode 100644 x-pack/plugins/alerting/common/routes/rule/apis/find/index.ts create mode 100644 x-pack/plugins/alerting/common/routes/rule/apis/find/schemas/latest.ts create mode 100644 x-pack/plugins/alerting/common/routes/rule/apis/find/schemas/v1.ts create mode 100644 x-pack/plugins/alerting/common/routes/rule/apis/find/types/latest.ts create mode 100644 x-pack/plugins/alerting/common/routes/rule/apis/find/types/v1.ts rename x-pack/plugins/alerting/server/{rules_client/tests/find.test.ts => application/rule/methods/find/find_rules.test.ts} (90%) rename x-pack/plugins/alerting/server/{rules_client/methods/find.ts => application/rule/methods/find/find_rules.ts} (59%) create mode 100644 x-pack/plugins/alerting/server/application/rule/methods/find/index.ts create mode 100644 x-pack/plugins/alerting/server/application/rule/methods/find/schemas/find_rules_schemas.ts create mode 100644 x-pack/plugins/alerting/server/application/rule/methods/find/schemas/index.ts create mode 100644 x-pack/plugins/alerting/server/application/rule/methods/find/types/find_rules_types.ts create mode 100644 x-pack/plugins/alerting/server/application/rule/methods/find/types/index.ts rename x-pack/plugins/alerting/server/routes/{find_rules.test.ts => rule/apis/find/find_rules_route.test.ts} (92%) rename x-pack/plugins/alerting/server/routes/{find_rules.ts => rule/apis/find/find_rules_route.ts} (56%) create mode 100644 x-pack/plugins/alerting/server/routes/rule/apis/find/transforms/index.ts create mode 100644 x-pack/plugins/alerting/server/routes/rule/apis/find/transforms/transform_find_rules_body/latest.ts create mode 100644 x-pack/plugins/alerting/server/routes/rule/apis/find/transforms/transform_find_rules_body/v1.ts create mode 100644 x-pack/plugins/alerting/server/routes/rule/apis/find/transforms/transform_find_rules_response/latest.ts create mode 100644 x-pack/plugins/alerting/server/routes/rule/apis/find/transforms/transform_find_rules_response/v1.ts diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/find/index.ts b/x-pack/plugins/alerting/common/routes/rule/apis/find/index.ts new file mode 100644 index 00000000000000..7722521d2b7074 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/find/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { findRulesRequestQuerySchema } from './schemas/latest'; +export type { FindRulesRequestQuery, FindRulesResponse } from './types/latest'; + +export { findRulesRequestQuerySchema as findRulesRequestQuerySchemaV1 } from './schemas/v1'; +export type { + FindRulesRequestQuery as FindRulesRequestQueryV1, + FindRulesResponse as FindRulesResponseV1, +} from './types/latest'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/find/schemas/latest.ts b/x-pack/plugins/alerting/common/routes/rule/apis/find/schemas/latest.ts new file mode 100644 index 00000000000000..25300c97a6d2e1 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/find/schemas/latest.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/find/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/rule/apis/find/schemas/v1.ts new file mode 100644 index 00000000000000..faa483e1aef1b1 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/find/schemas/v1.ts @@ -0,0 +1,33 @@ +/* + * 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'; + +export const findRulesRequestQuerySchema = 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()), + filter_consumers: schema.maybe(schema.arrayOf(schema.string())), +}); diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/find/types/latest.ts b/x-pack/plugins/alerting/common/routes/rule/apis/find/types/latest.ts new file mode 100644 index 00000000000000..25300c97a6d2e1 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/find/types/latest.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/find/types/v1.ts b/x-pack/plugins/alerting/common/routes/rule/apis/find/types/v1.ts new file mode 100644 index 00000000000000..271e791282f43a --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/find/types/v1.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { TypeOf } from '@kbn/config-schema'; +import { RuleParamsV1, RuleResponseV1 } from '../../../response'; +import { findRulesRequestQuerySchemaV1 } from '..'; + +export type FindRulesRequestQuery = TypeOf; + +export interface FindRulesResponse { + body: { + page: number; + per_page: number; + total: number; + data: Array>>; + }; +} diff --git a/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/find/find_rules.test.ts similarity index 90% rename from x-pack/plugins/alerting/server/rules_client/tests/find.test.ts rename to x-pack/plugins/alerting/server/application/rule/methods/find/find_rules.test.ts index 660bf243324144..ee8038f2c8b6c7 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/find/find_rules.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { RulesClient, ConstructorOptions } from '../rules_client'; +import { RulesClient, ConstructorOptions } from '../../../../rules_client/rules_client'; import { savedObjectsClientMock, loggingSystemMock, @@ -13,25 +13,30 @@ import { uiSettingsServiceMock, } from '@kbn/core/server/mocks'; import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; -import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; -import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; +import { ruleTypeRegistryMock } from '../../../../rule_type_registry.mock'; +import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock'; import { nodeTypes, fromKueryExpression } from '@kbn/es-query'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks'; -import { AlertingAuthorization } from '../../authorization/alerting_authorization'; +import { AlertingAuthorization } from '../../../../authorization/alerting_authorization'; import { ActionsAuthorization } from '@kbn/actions-plugin/server'; import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks'; -import { getBeforeSetup, setGlobalDate } from './lib'; -import { RecoveredActionGroup } from '../../../common'; -import { RegistryRuleType } from '../../rule_type_registry'; +import { getBeforeSetup, setGlobalDate } from '../../../../rules_client/tests/lib'; +import { RecoveredActionGroup } from '../../../../../common'; +import { RegistryRuleType } from '../../../../rule_type_registry'; import { schema } from '@kbn/config-schema'; -import { enabledRule1, enabledRule2, siemRule1, siemRule2 } from './test_helpers'; -import { formatLegacyActions } from '../lib'; -import { ConnectorAdapterRegistry } from '../../connector_adapters/connector_adapter_registry'; -import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects'; -import { backfillClientMock } from '../../backfill_client/backfill_client.mock'; +import { + enabledRule1, + enabledRule2, + siemRule1, + siemRule2, +} from '../../../../rules_client/tests/test_helpers'; +import { formatLegacyActions } from '../../../../rules_client/lib'; +import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry'; +import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects'; +import { backfillClientMock } from '../../../../backfill_client/backfill_client.mock'; -jest.mock('../lib/siem_legacy_actions/format_legacy_actions', () => { +jest.mock('../../../../rules_client/lib/siem_legacy_actions/format_legacy_actions', () => { return { formatLegacyActions: jest.fn(), }; @@ -82,7 +87,7 @@ beforeEach(() => { setGlobalDate(); -jest.mock('../common/map_sort_field', () => ({ +jest.mock('../../../../rules_client/common/map_sort_field', () => ({ mapSortField: jest.fn(), })); @@ -127,6 +132,10 @@ describe('find()', () => { }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), + executionStatus: { + status: 'pending', + lastExecutionDate: new Date('2019-02-12T21:01:22.479Z'), + }, notifyWhen: 'onActiveAlert', actions: [ { @@ -197,6 +206,10 @@ describe('find()', () => { ], "alertTypeId": "myType", "createdAt": 2019-02-12T21:01:22.479Z, + "executionStatus": Object { + "lastExecutionDate": 2019-02-12T21:01:22.000Z, + "status": "pending", + }, "id": "1", "notifyWhen": "onActiveAlert", "params": Object { @@ -244,6 +257,10 @@ describe('find()', () => { params: { bar: true, }, + executionStatus: { + status: 'pending', + lastExecutionDate: new Date('2019-02-12T21:01:22.479Z'), + }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), notifyWhen: 'onActiveAlert', @@ -303,6 +320,10 @@ describe('find()', () => { ], "alertTypeId": "myType", "createdAt": 2019-02-12T21:01:22.479Z, + "executionStatus": Object { + "lastExecutionDate": 2019-02-12T21:01:22.000Z, + "status": "pending", + }, "id": "1", "notifyWhen": "onActiveAlert", "params": Object { @@ -350,6 +371,10 @@ describe('find()', () => { params: { bar: true, }, + executionStatus: { + status: 'pending', + lastExecutionDate: new Date('2019-02-12T21:01:22.479Z'), + }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), notifyWhen: 'onActiveAlert', @@ -400,6 +425,10 @@ describe('find()', () => { ], "alertTypeId": "myType", "createdAt": 2019-02-12T21:01:22.479Z, + "executionStatus": Object { + "lastExecutionDate": 2019-02-12T21:01:22.000Z, + "status": "pending", + }, "id": "1", "notifyWhen": "onActiveAlert", "params": Object { @@ -441,7 +470,9 @@ describe('find()', () => { test('calls mapSortField', async () => { const rulesClient = new RulesClient(rulesClientParams); await rulesClient.find({ options: { sortField: 'name' } }); - expect(jest.requireMock('../common/map_sort_field').mapSortField).toHaveBeenCalledWith('name'); + expect( + jest.requireMock('../../../../rules_client/common/map_sort_field').mapSortField + ).toHaveBeenCalledWith('name'); }); test('should translate filter/sort/search on params to mapped_params', async () => { @@ -559,6 +590,10 @@ describe('find()', () => { params: { bar: true, }, + executionStatus: { + status: 'pending', + lastExecutionDate: new Date('2019-02-12T21:01:22.479Z'), + }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), notifyWhen: 'onActiveAlert', @@ -591,6 +626,10 @@ describe('find()', () => { bar: true, parameterThatIsSavedObjectRef: 'soRef_0', }, + executionStatus: { + status: 'pending', + lastExecutionDate: new Date('2019-02-12T21:01:22.479Z'), + }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), notifyWhen: 'onActiveAlert', @@ -649,6 +688,10 @@ describe('find()', () => { ], "alertTypeId": "myType", "createdAt": 2019-02-12T21:01:22.479Z, + "executionStatus": Object { + "lastExecutionDate": 2019-02-12T21:01:22.000Z, + "status": "pending", + }, "id": "1", "notifyWhen": "onActiveAlert", "params": Object { @@ -675,6 +718,10 @@ describe('find()', () => { ], "alertTypeId": "123", "createdAt": 2019-02-12T21:01:22.479Z, + "executionStatus": Object { + "lastExecutionDate": 2019-02-12T21:01:22.000Z, + "status": "pending", + }, "id": "2", "notifyWhen": "onActiveAlert", "params": Object { @@ -779,6 +826,10 @@ describe('find()', () => { params: { bar: true, }, + executionStatus: { + status: 'pending', + lastExecutionDate: new Date('2019-02-12T21:01:22.479Z'), + }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), notifyWhen: 'onActiveAlert', @@ -811,6 +862,10 @@ describe('find()', () => { bar: true, parameterThatIsSavedObjectRef: 'soRef_0', }, + executionStatus: { + status: 'pending', + lastExecutionDate: new Date('2019-02-12T21:01:22.479Z'), + }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), notifyWhen: 'onActiveAlert', @@ -891,6 +946,10 @@ describe('find()', () => { type: RULE_SAVED_OBJECT_TYPE, attributes: { actions: [], + executionStatus: { + status: 'pending', + lastExecutionDate: new Date('2019-02-12T21:01:22.479Z'), + }, alertTypeId: 'myType', consumer: 'myApp', tags: ['myTag'], diff --git a/x-pack/plugins/alerting/server/rules_client/methods/find.ts b/x-pack/plugins/alerting/server/application/rule/methods/find/find_rules.ts similarity index 59% rename from x-pack/plugins/alerting/server/rules_client/methods/find.ts rename to x-pack/plugins/alerting/server/application/rule/methods/find/find_rules.ts index 7ca51bcb16f19e..10aef42955e741 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/find.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/find/find_rules.ts @@ -6,70 +6,56 @@ */ import Boom from '@hapi/boom'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { isEmpty, pick } from 'lodash'; import { KueryNode, nodeBuilder } from '@kbn/es-query'; import { AlertConsumers } from '@kbn/rule-data-utils'; -import { RawRule, RuleTypeParams, SanitizedRule, Rule } from '../../types'; -import { AlertingAuthorizationEntity } from '../../authorization'; -import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; +import { SanitizedRule, Rule as DeprecatedRule, RawRule } from '../../../../types'; +import { AlertingAuthorizationEntity } from '../../../../authorization'; +import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events'; import { mapSortField, validateOperationOnAttributes, buildKueryNodeFilter, includeFieldsRequiredForAuthentication, -} from '../common'; +} from '../../../../rules_client/common'; import { getModifiedField, getModifiedSearchFields, getModifiedSearch, modifyFilterKueryNode, -} from '../common/mapped_params_utils'; -import { alertingAuthorizationFilterOpts } from '../common/constants'; -import { getAlertFromRaw } from '../lib/get_alert_from_raw'; -import type { IndexType, RulesClientContext } from '../types'; -import { formatLegacyActions } from '../lib'; -import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects'; - -export interface FindParams { - options?: FindOptions; - excludeFromPublicApi?: boolean; - includeSnoozeData?: boolean; - featuresIds?: string[]; -} - -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[]; -} - -export interface FindResult { +} from '../../../../rules_client/common/mapped_params_utils'; +import { alertingAuthorizationFilterOpts } from '../../../../rules_client/common/constants'; +import type { RulesClientContext } from '../../../../rules_client/types'; +import { formatLegacyActions, getAlertFromRaw } from '../../../../rules_client/lib'; +import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects'; +import type { FindRulesParams } from './types'; +import { findRulesParamsSchema } from './schemas'; +import { Rule, RuleParams } from '../../types'; +import { findRulesSo } from '../../../../data/rule'; + +export interface FindResult { page: number; perPage: number; total: number; data: Array>; } -export async function find( +export async function findRules( context: RulesClientContext, - { - options: { fields, filterConsumers, ...options } = {}, - excludeFromPublicApi = false, - includeSnoozeData = false, - }: FindParams = {} + params?: FindRulesParams ): Promise> { + const { options, excludeFromPublicApi = false, includeSnoozeData = false } = params || {}; + + const { fields, filterConsumers, ...restOptions } = options || {}; + + try { + if (params) { + findRulesParamsSchema.validate(params); + } + } catch (error) { + throw Boom.badRequest(`Error validating find data - ${error.message}`); + } + let authorizationTuple; try { authorizationTuple = await context.authorization.getFindAuthorizationFilter( @@ -88,14 +74,14 @@ export async function find( } const { filter: authorizationFilter, ensureRuleTypeIsAuthorized } = authorizationTuple; - const filterKueryNode = buildKueryNodeFilter(options.filter); - let sortField = mapSortField(options.sortField); + const filterKueryNode = buildKueryNodeFilter(restOptions.filter as string | KueryNode); + let sortField = mapSortField(restOptions.sortField); if (excludeFromPublicApi) { try { validateOperationOnAttributes( filterKueryNode, sortField, - options.searchFields, + restOptions.searchFields, context.fieldsToExcludeFromPublicApi ); } catch (error) { @@ -103,16 +89,20 @@ export async function find( } } - sortField = mapSortField(getModifiedField(options.sortField)); + sortField = mapSortField(getModifiedField(restOptions.sortField)); // Generate new modified search and search fields, translating certain params properties // to mapped_params. Thus, allowing for sort/search/filtering on params. // We do the modifcation after the validate check to make sure the public API does not // use the mapped_params in their queries. - options = { - ...options, - ...(options.searchFields && { searchFields: getModifiedSearchFields(options.searchFields) }), - ...(options.search && { search: getModifiedSearch(options.searchFields, options.search) }), + const modifiedOptions = { + ...restOptions, + ...(restOptions.searchFields && { + searchFields: getModifiedSearchFields(restOptions.searchFields), + }), + ...(restOptions.search && { + search: getModifiedSearch(restOptions.searchFields, restOptions.search), + }), }; // Modifies kuery node AST to translate params filter and the filter value to mapped_params. @@ -126,15 +116,17 @@ export async function find( per_page: perPage, total, saved_objects: data, - } = await context.unsecuredSavedObjectsClient.find({ - ...options, - sortField, - filter: - (authorizationFilter && filterKueryNode - ? nodeBuilder.and([filterKueryNode, authorizationFilter as KueryNode]) - : authorizationFilter) ?? filterKueryNode, - fields: fields ? includeFieldsRequiredForAuthentication(fields) : fields, - type: RULE_SAVED_OBJECT_TYPE, + } = await findRulesSo({ + savedObjectsClient: context.unsecuredSavedObjectsClient, + savedObjectsFindOptions: { + ...modifiedOptions, + sortField, + filter: + (authorizationFilter && filterKueryNode + ? nodeBuilder.and([filterKueryNode, authorizationFilter as KueryNode]) + : authorizationFilter) ?? filterKueryNode, + fields: fields ? includeFieldsRequiredForAuthentication(fields) : fields, + }, }); const siemRules: Rule[] = []; @@ -161,7 +153,7 @@ export async function find( context, id, attributes.alertTypeId, - fields ? (pick(attributes, fields) as RawRule) : attributes, + (fields ? pick(attributes, fields) : attributes) as RawRule, references, false, excludeFromPublicApi, @@ -170,7 +162,7 @@ export async function find( // collect SIEM rule for further formatting legacy actions if (attributes.consumer === AlertConsumers.SIEM) { - siemRules.push(rule); + siemRules.push(rule as Rule); } return rule; @@ -187,12 +179,12 @@ export async function find( // format legacy actions for SIEM rules, if there any if (siemRules.length) { - const formattedRules = await formatLegacyActions(siemRules, { + const formattedRules = await formatLegacyActions(siemRules as DeprecatedRule[], { savedObjectsClient: context.unsecuredSavedObjectsClient, logger: context.logger, }); - const formattedRulesMap = formattedRules.reduce>((acc, rule) => { + const formattedRulesMap = formattedRules.reduce>((acc, rule) => { acc[rule.id] = rule; return acc; }, {}); @@ -210,6 +202,6 @@ export async function find( page, perPage, total, - data: authorizedData, + data: authorizedData as Array>, }; } diff --git a/x-pack/plugins/alerting/server/application/rule/methods/find/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/find/index.ts new file mode 100644 index 00000000000000..17287e7136c614 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/find/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { FindResult } from './find_rules'; +export type { FindRulesOptions, FindRulesParams } from './types'; + +export { findRules } from './find_rules'; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/find/schemas/find_rules_schemas.ts b/x-pack/plugins/alerting/server/application/rule/methods/find/schemas/find_rules_schemas.ts new file mode 100644 index 00000000000000..b766ffc283bb24 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/find/schemas/find_rules_schemas.ts @@ -0,0 +1,41 @@ +/* + * 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'; + +export const findRulesOptionsSchema = schema.object( + { + perPage: schema.maybe(schema.number()), + page: schema.maybe(schema.number()), + search: schema.maybe(schema.string()), + defaultSearchOperator: schema.maybe( + schema.oneOf([schema.literal('AND'), schema.literal('OR')]) + ), + searchFields: schema.maybe(schema.arrayOf(schema.string())), + sortField: schema.maybe(schema.string()), + sortOrder: schema.maybe(schema.oneOf([schema.literal('asc'), schema.literal('desc')])), + hasReference: schema.maybe( + schema.object({ + type: schema.string(), + id: schema.string(), + }) + ), + fields: schema.maybe(schema.arrayOf(schema.string())), + filter: schema.maybe( + schema.oneOf([schema.string(), schema.recordOf(schema.string(), schema.any())]) + ), + filterConsumers: schema.maybe(schema.arrayOf(schema.string())), + }, + { unknowns: 'allow' } +); + +export const findRulesParamsSchema = schema.object({ + options: schema.maybe(findRulesOptionsSchema), + excludeFromPublicApi: schema.maybe(schema.boolean()), + includeSnoozeData: schema.maybe(schema.boolean()), + featureIds: schema.maybe(schema.arrayOf(schema.string())), +}); diff --git a/x-pack/plugins/alerting/server/application/rule/methods/find/schemas/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/find/schemas/index.ts new file mode 100644 index 00000000000000..228a64a2dfa6b3 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/find/schemas/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './find_rules_schemas'; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/find/types/find_rules_types.ts b/x-pack/plugins/alerting/server/application/rule/methods/find/types/find_rules_types.ts new file mode 100644 index 00000000000000..615a5b5db8672f --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/find/types/find_rules_types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TypeOf } from '@kbn/config-schema'; +import { findRulesOptionsSchema, findRulesParamsSchema } from '../schemas'; + +export type FindRulesOptions = TypeOf; + +export type FindRulesParams = TypeOf; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/find/types/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/find/types/index.ts new file mode 100644 index 00000000000000..cf7f87036c9045 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/find/types/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './find_rules_types'; diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts index 1f5bea82b2c318..527b4ba48c0823 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -20,7 +20,7 @@ import { deleteRuleRoute } from './delete_rule'; import { aggregateRulesRoute } from './rule/apis/aggregate/aggregate_rules_route'; import { disableRuleRoute } from './disable_rule'; import { enableRuleRoute } from './enable_rule'; -import { findRulesRoute, findInternalRulesRoute } from './find_rules'; +import { findRulesRoute, findInternalRulesRoute } from './rule/apis/find/find_rules_route'; import { getRuleAlertSummaryRoute } from './get_rule_alert_summary'; import { getRuleExecutionLogRoute } from './get_rule_execution_log'; import { getGlobalExecutionLogRoute } from './get_global_execution_logs'; diff --git a/x-pack/plugins/alerting/server/routes/legacy/find.ts b/x-pack/plugins/alerting/server/routes/legacy/find.ts index e0e4ffa34cf33e..dcda2e01271d48 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/find.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/find.ts @@ -7,16 +7,35 @@ 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 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 { FindOptions } from '../../rules_client'; +import { IndexType } from '../../rules_client'; import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; import { trackLegacyTerminology } from '../lib/track_legacy_terminology'; +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 }), diff --git a/x-pack/plugins/alerting/server/routes/find_rules.test.ts b/x-pack/plugins/alerting/server/routes/rule/apis/find/find_rules_route.test.ts similarity index 92% rename from x-pack/plugins/alerting/server/routes/find_rules.test.ts rename to x-pack/plugins/alerting/server/routes/rule/apis/find/find_rules_route.test.ts index 69df6e978cd83f..53c339a66b864e 100644 --- a/x-pack/plugins/alerting/server/routes/find_rules.test.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/find/find_rules_route.test.ts @@ -5,23 +5,23 @@ * 2.0. */ import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; -import { findRulesRoute } from './find_rules'; +import { findRulesRoute } from './find_rules_route'; 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 { trackLegacyTerminology } from './lib/track_legacy_terminology'; +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 { trackLegacyTerminology } from '../../../lib/track_legacy_terminology'; const rulesClient = rulesClientMock.create(); const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); -jest.mock('../lib/license_api_access', () => ({ +jest.mock('../../../../lib/license_api_access', () => ({ verifyApiAccess: jest.fn(), })); -jest.mock('./lib/track_legacy_terminology', () => ({ +jest.mock('../../../lib/track_legacy_terminology', () => ({ trackLegacyTerminology: jest.fn(), })); @@ -79,7 +79,6 @@ describe('findRulesRoute', () => { "includeSnoozeData": true, "options": Object { "defaultSearchOperator": "OR", - "filterConsumers": undefined, "page": 1, "perPage": 1, }, @@ -130,6 +129,7 @@ describe('findRulesRoute', () => { schedule: { interval: '1s', }, + snoozeSchedule: [], actions: [ { actionTypeId: '.server-log', @@ -145,12 +145,12 @@ describe('findRulesRoute', () => { { 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', + updatedAt: new Date('2024-03-21T13:15:00.498Z'), + createdAt: new Date('2024-03-21T13:15:00.498Z'), scheduledTaskId: '52125fb0-5895-11ec-ae69-bb65d1a71b72', executionStatus: { status: 'ok' as const, - lastExecutionDate: '2024-03-21T13:15:00.498Z', + lastExecutionDate: new Date('2024-03-21T13:15:00.498Z'), lastDuration: 1194, }, revision: 0, @@ -158,7 +158,6 @@ describe('findRulesRoute', () => { ], }; - // @ts-expect-error: TS complains about group being undefined in the system action rulesClient.find.mockResolvedValueOnce(findResult); const [context, req, res] = mockHandlerArguments( @@ -195,7 +194,6 @@ describe('findRulesRoute', () => { "uuid": "789", }, ], - "apiKey": null, "api_key_owner": "2889684073", "consumer": "alerts", "created_at": "2024-03-21T13:15:00.498Z", @@ -220,7 +218,7 @@ describe('findRulesRoute', () => { "interval": "1s", }, "scheduled_task_id": "52125fb0-5895-11ec-ae69-bb65d1a71b72", - "snooze_schedule": undefined, + "snooze_schedule": Array [], "tags": Array [], "throttle": null, "updated_at": "2024-03-21T13:15:00.498Z", @@ -242,7 +240,6 @@ describe('findRulesRoute', () => { "includeSnoozeData": true, "options": Object { "defaultSearchOperator": "OR", - "filterConsumers": undefined, "page": 1, "perPage": 1, }, @@ -274,7 +271,6 @@ describe('findRulesRoute', () => { uuid: '789', }, ], - apiKey: null, api_key_owner: '2889684073', consumer: 'alerts', created_at: '2024-03-21T13:15:00.498Z', @@ -299,7 +295,7 @@ describe('findRulesRoute', () => { interval: '1s', }, scheduled_task_id: '52125fb0-5895-11ec-ae69-bb65d1a71b72', - snooze_schedule: undefined, + snooze_schedule: [], tags: [], throttle: null, updated_at: '2024-03-21T13:15:00.498Z', diff --git a/x-pack/plugins/alerting/server/routes/find_rules.ts b/x-pack/plugins/alerting/server/routes/rule/apis/find/find_rules_route.ts similarity index 56% rename from x-pack/plugins/alerting/server/routes/find_rules.ts rename to x-pack/plugins/alerting/server/routes/rule/apis/find/find_rules_route.ts index 711baa3108f357..6baf208e016a12 100644 --- a/x-pack/plugins/alerting/server/routes/find_rules.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/find/find_rules_route.ts @@ -7,70 +7,21 @@ import { IRouter } from '@kbn/core/server'; import { UsageCounter } from '@kbn/usage-collection-plugin/server'; -import { schema } from '@kbn/config-schema'; -import { ILicenseState } from '../lib'; -import { FindOptions, FindResult } from '../rules_client'; -import { RewriteRequestCase, verifyAccessAndContext, rewriteRule } from './lib'; +import { ILicenseState } from '../../../../lib'; +import { verifyAccessAndContext } from '../../../lib'; +import { findRulesRequestQuerySchemaV1 } from '../../../../../common/routes/rule/apis/find'; +import type { + FindRulesRequestQueryV1, + FindRulesResponseV1, +} from '../../../../../common/routes/rule/apis/find'; +import type { RuleParamsV1 } from '../../../../../common/routes/rule/response'; import { - RuleTypeParams, AlertingRequestHandlerContext, BASE_ALERTING_API_PATH, INTERNAL_ALERTING_API_FIND_RULES_PATH, -} from '../types'; -import { trackLegacyTerminology } from './lib/track_legacy_terminology'; - -// query 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()), - filter_consumers: schema.maybe(schema.arrayOf(schema.string())), -}); - -const rewriteQueryReq: RewriteRequestCase = ({ - default_search_operator: defaultSearchOperator, - has_reference: hasReference, - search_fields: searchFields, - per_page: perPage, - sort_field: sortField, - sort_order: sortOrder, - filter_consumers: filterConsumers, - ...rest -}) => ({ - ...rest, - defaultSearchOperator, - perPage, - filterConsumers, - ...(sortField ? { sortField } : {}), - ...(sortOrder ? { sortOrder } : {}), - ...(hasReference ? { hasReference } : {}), - ...(searchFields ? { searchFields } : {}), -}); -const rewriteBodyRes = ({ perPage, data, ...restOfResult }: FindResult) => { - return { - ...restOfResult, - per_page: perPage, - data: data.map(rewriteRule), - }; -}; +} from '../../../../types'; +import { trackLegacyTerminology } from '../../../lib/track_legacy_terminology'; +import { transformFindRulesBodyV1, transformFindRulesResponseV1 } from './transforms'; interface BuildFindRulesRouteParams { licenseState: ILicenseState; @@ -91,24 +42,24 @@ const buildFindRulesRoute = ({ { path, validate: { - query: querySchema, + query: findRulesRequestQuerySchemaV1, }, }, router.handleLegacyErrors( verifyAccessAndContext(licenseState, async function (context, req, res) { const rulesClient = (await context.alerting).getRulesClient(); + const query: FindRulesRequestQueryV1 = req.query; + trackLegacyTerminology( - [req.query.search, req.query.search_fields, req.query.sort_field].filter( - Boolean - ) as string[], + [query.search, query.search_fields, query.sort_field].filter(Boolean) as string[], usageCounter ); - const options = rewriteQueryReq({ - ...req.query, - has_reference: req.query.has_reference || undefined, - search_fields: searchFieldsAsArray(req.query.search_fields), + const options = transformFindRulesBodyV1({ + ...query, + has_reference: query.has_reference || undefined, + search_fields: searchFieldsAsArray(query.search_fields), }); if (req.query.fields) { @@ -124,8 +75,12 @@ const buildFindRulesRoute = ({ excludeFromPublicApi, includeSnoozeData: true, }); + + const responseBody: FindRulesResponseV1['body'] = + transformFindRulesResponseV1(findResult, options.fields); + return res.ok({ - body: rewriteBodyRes(findResult), + body: responseBody, }); }) ) @@ -135,13 +90,15 @@ const buildFindRulesRoute = ({ { path, validate: { - body: querySchema, + body: findRulesRequestQuerySchemaV1, }, }, router.handleLegacyErrors( verifyAccessAndContext(licenseState, async function (context, req, res) { const rulesClient = (await context.alerting).getRulesClient(); + const body: FindRulesRequestQueryV1 = req.body; + trackLegacyTerminology( [req.body.search, req.body.search_fields, req.body.sort_field].filter( Boolean @@ -149,10 +106,10 @@ const buildFindRulesRoute = ({ usageCounter ); - const options = rewriteQueryReq({ - ...req.body, - has_reference: req.body.has_reference || undefined, - search_fields: searchFieldsAsArray(req.body.search_fields), + const options = transformFindRulesBodyV1({ + ...body, + has_reference: body.has_reference || undefined, + search_fields: searchFieldsAsArray(body.search_fields), }); if (req.body.fields) { @@ -168,8 +125,12 @@ const buildFindRulesRoute = ({ excludeFromPublicApi, includeSnoozeData: true, }); + + const responseBody: FindRulesResponseV1['body'] = + transformFindRulesResponseV1(findResult, options.fields); + return res.ok({ - body: rewriteBodyRes(findResult), + body: responseBody, }); }) ) diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/find/transforms/index.ts b/x-pack/plugins/alerting/server/routes/rule/apis/find/transforms/index.ts new file mode 100644 index 00000000000000..044a845f3f8f3b --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/find/transforms/index.ts @@ -0,0 +1,12 @@ +/* + * 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 { transformFindRulesBody } from './transform_find_rules_body/latest'; +export { transformFindRulesResponse } from './transform_find_rules_response/latest'; + +export { transformFindRulesBody as transformFindRulesBodyV1 } from './transform_find_rules_body/v1'; +export { transformFindRulesResponse as transformFindRulesResponseV1 } from './transform_find_rules_response/v1'; diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/find/transforms/transform_find_rules_body/latest.ts b/x-pack/plugins/alerting/server/routes/rule/apis/find/transforms/transform_find_rules_body/latest.ts new file mode 100644 index 00000000000000..25300c97a6d2e1 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/find/transforms/transform_find_rules_body/latest.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './v1'; diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/find/transforms/transform_find_rules_body/v1.ts b/x-pack/plugins/alerting/server/routes/rule/apis/find/transforms/transform_find_rules_body/v1.ts new file mode 100644 index 00000000000000..a2f9d3c99b00d9 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/find/transforms/transform_find_rules_body/v1.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FindRulesRequestQueryV1 } from '../../../../../../../common/routes/rule/apis/find'; +import { FindRulesOptions } from '../../../../../../application/rule/methods/find'; + +export const transformFindRulesBody = (params: FindRulesRequestQueryV1): FindRulesOptions => { + const { + per_page: perPage, + page, + search, + default_search_operator: defaultSearchOperator, + search_fields: searchFields, + sort_field: sortField, + sort_order: sortOrder, + has_reference: hasReference, + fields, + filter, + filter_consumers: filterConsumers, + } = params; + return { + ...(page ? { page } : {}), + ...(search ? { search } : {}), + ...(fields ? { fields } : {}), + ...(filter ? { filter } : {}), + ...(defaultSearchOperator ? { defaultSearchOperator } : {}), + ...(perPage ? { perPage } : {}), + ...(filterConsumers ? { filterConsumers } : {}), + ...(sortField ? { sortField } : {}), + ...(sortOrder ? { sortOrder } : {}), + ...(hasReference ? { hasReference } : {}), + ...(searchFields + ? { searchFields: Array.isArray(searchFields) ? searchFields : [searchFields] } + : {}), + }; +}; diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/find/transforms/transform_find_rules_response/latest.ts b/x-pack/plugins/alerting/server/routes/rule/apis/find/transforms/transform_find_rules_response/latest.ts new file mode 100644 index 00000000000000..25300c97a6d2e1 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/find/transforms/transform_find_rules_response/latest.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './v1'; diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/find/transforms/transform_find_rules_response/v1.ts b/x-pack/plugins/alerting/server/routes/rule/apis/find/transforms/transform_find_rules_response/v1.ts new file mode 100644 index 00000000000000..6754fa71e10d4c --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/find/transforms/transform_find_rules_response/v1.ts @@ -0,0 +1,111 @@ +/* + * 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 { FindRulesResponseV1 } from '../../../../../../../common/routes/rule/apis/find'; +import type { + RuleResponseV1, + RuleParamsV1, +} from '../../../../../../../common/routes/rule/response'; +import type { FindResult } from '../../../../../../application/rule/methods/find'; +import { Rule, RuleParams } from '../../../../../../application/rule/types'; +import { + transformRuleActionsV1, + transformMonitoringV1, + transformRuleLastRunV1, +} from '../../../../transforms'; + +export const transformPartialRule = ( + rule: Partial>, + fields?: string[] +): Partial> => { + const ruleResponse = { + ...(rule.id !== undefined ? { id: rule.id } : {}), + ...(rule.enabled !== undefined ? { enabled: rule.enabled } : {}), + ...(rule.name !== undefined ? { name: rule.name } : {}), + ...(rule.tags ? { tags: rule.tags } : {}), + ...(rule.alertTypeId !== undefined ? { rule_type_id: rule.alertTypeId } : {}), + ...(rule.consumer !== undefined ? { consumer: rule.consumer } : {}), + ...(rule.schedule ? { schedule: rule.schedule } : {}), + ...(rule.actions || rule.systemActions + ? { actions: transformRuleActionsV1(rule.actions || [], rule.systemActions || []) } + : {}), + ...(rule.params ? { params: rule.params } : {}), + ...(rule.mapped_params ? { mapped_params: rule.mapped_params } : {}), + ...(rule.scheduledTaskId !== undefined ? { scheduled_task_id: rule.scheduledTaskId } : {}), + ...(rule.createdBy !== undefined ? { created_by: rule.createdBy } : {}), + ...(rule.updatedBy !== undefined ? { updated_by: rule.updatedBy } : {}), + ...(rule.createdAt ? { created_at: rule.createdAt.toISOString() } : {}), + ...(rule.updatedAt ? { updated_at: rule.updatedAt.toISOString() } : {}), + ...(rule.apiKeyOwner !== undefined ? { api_key_owner: rule.apiKeyOwner } : {}), + ...(rule.apiKeyCreatedByUser !== undefined + ? { api_key_created_by_user: rule.apiKeyCreatedByUser } + : {}), + ...(rule.throttle !== undefined ? { throttle: rule.throttle } : {}), + ...(rule.muteAll !== undefined ? { mute_all: rule.muteAll } : {}), + ...(rule.notifyWhen !== undefined ? { notify_when: rule.notifyWhen } : {}), + ...(rule.mutedInstanceIds ? { muted_alert_ids: rule.mutedInstanceIds } : {}), + ...(rule.scheduledTaskId !== undefined ? { scheduled_task_id: rule.scheduledTaskId } : {}), + ...(rule.executionStatus + ? { + execution_status: { + status: rule.executionStatus.status, + ...(rule.executionStatus.error ? { error: rule.executionStatus.error } : {}), + ...(rule.executionStatus.warning ? { warning: rule.executionStatus.warning } : {}), + last_execution_date: rule.executionStatus.lastExecutionDate?.toISOString(), + ...(rule.executionStatus.lastDuration !== undefined + ? { last_duration: rule.executionStatus.lastDuration } + : {}), + }, + } + : {}), + ...(rule.monitoring ? { monitoring: transformMonitoringV1(rule.monitoring) } : {}), + ...(rule.snoozeSchedule ? { snooze_schedule: rule.snoozeSchedule } : {}), + ...(rule.activeSnoozes ? { active_snoozes: rule.activeSnoozes } : {}), + ...(rule.isSnoozedUntil !== undefined + ? { is_snoozed_until: rule.isSnoozedUntil?.toISOString() || null } + : {}), + ...(rule.lastRun !== undefined + ? { last_run: rule.lastRun ? transformRuleLastRunV1(rule.lastRun) : null } + : {}), + ...(rule.nextRun !== undefined ? { next_run: rule.nextRun?.toISOString() || null } : {}), + ...(rule.revision !== undefined ? { revision: rule.revision } : {}), + ...(rule.running !== undefined ? { running: rule.running } : {}), + ...(rule.viewInAppRelativeUrl !== undefined + ? { view_in_app_relative_url: rule.viewInAppRelativeUrl } + : {}), + ...(rule.alertDelay !== undefined ? { alert_delay: rule.alertDelay } : {}), + }; + + type RuleKeys = keyof RuleResponseV1; + for (const key in ruleResponse) { + if (ruleResponse[key as RuleKeys] !== undefined) { + continue; + } + if (!fields) { + continue; + } + if (fields.includes(key)) { + continue; + } + delete ruleResponse[key as RuleKeys]; + } + return ruleResponse; +}; + +export const transformFindRulesResponse = ( + result: FindResult, + fields?: string[] +): FindRulesResponseV1['body'] => { + return { + page: result.page, + per_page: result.perPage, + total: result.total, + data: result.data.map((rule) => + transformPartialRule(rule as Partial>, fields) + ), + }; +}; diff --git a/x-pack/plugins/alerting/server/routes/rule/transforms/index.ts b/x-pack/plugins/alerting/server/routes/rule/transforms/index.ts index 6e7ba66e752bec..49661dd04f9d19 100644 --- a/x-pack/plugins/alerting/server/routes/rule/transforms/index.ts +++ b/x-pack/plugins/alerting/server/routes/rule/transforms/index.ts @@ -5,7 +5,15 @@ * 2.0. */ -export { transformRuleToRuleResponse } from './transform_rule_to_rule_response/latest'; -export { transformRuleToRuleResponse as transformRuleToRuleResponseV1 } from './transform_rule_to_rule_response/v1'; -export { transformRuleActions } from './transform_rule_to_rule_response/latest'; -export { transformRuleActions as transformRuleActionsV1 } from './transform_rule_to_rule_response/v1'; +export { + transformRuleToRuleResponse, + transformRuleActions, + transformRuleLastRun, + transformMonitoring, +} from './transform_rule_to_rule_response/latest'; +export { + transformRuleToRuleResponse as transformRuleToRuleResponseV1, + transformRuleActions as transformRuleActionsV1, + transformRuleLastRun as transformRuleLastRunV1, + transformMonitoring as transformMonitoringV1, +} from './transform_rule_to_rule_response/v1'; diff --git a/x-pack/plugins/alerting/server/routes/rule/transforms/transform_rule_to_rule_response/latest.ts b/x-pack/plugins/alerting/server/routes/rule/transforms/transform_rule_to_rule_response/latest.ts index f0561497b5d17c..25300c97a6d2e1 100644 --- a/x-pack/plugins/alerting/server/routes/rule/transforms/transform_rule_to_rule_response/latest.ts +++ b/x-pack/plugins/alerting/server/routes/rule/transforms/transform_rule_to_rule_response/latest.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { transformRuleToRuleResponse, transformRuleActions } from './v1'; +export * from './v1'; diff --git a/x-pack/plugins/alerting/server/routes/rule/transforms/transform_rule_to_rule_response/v1.ts b/x-pack/plugins/alerting/server/routes/rule/transforms/transform_rule_to_rule_response/v1.ts index ee4a3a6d14c0f1..2181876f230bc7 100644 --- a/x-pack/plugins/alerting/server/routes/rule/transforms/transform_rule_to_rule_response/v1.ts +++ b/x-pack/plugins/alerting/server/routes/rule/transforms/transform_rule_to_rule_response/v1.ts @@ -13,7 +13,7 @@ import { } from '../../../../../common/routes/rule/response'; import { Rule, RuleLastRun, RuleParams, Monitoring } from '../../../../application/rule/types'; -const transformRuleLastRun = (lastRun: RuleLastRun): RuleLastRunV1 => { +export const transformRuleLastRun = (lastRun: RuleLastRun): RuleLastRunV1 => { return { outcome: lastRun.outcome, ...(lastRun.outcomeOrder !== undefined ? { outcome_order: lastRun.outcomeOrder } : {}), @@ -23,7 +23,7 @@ const transformRuleLastRun = (lastRun: RuleLastRun): RuleLastRunV1 => { }; }; -const transformMonitoring = (monitoring: Monitoring): MonitoringV1 => { +export const transformMonitoring = (monitoring: Monitoring): MonitoringV1 => { return { run: { history: monitoring.run.history.map((history) => ({ @@ -39,7 +39,7 @@ const transformMonitoring = (monitoring: Monitoring): MonitoringV1 => { }; export const transformRuleActions = ( - actions: Rule['actions'], + actions: Rule['actions'] = [], systemActions: Rule['systemActions'] = [] ): RuleResponseV1['actions'] => { return [ diff --git a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.test.ts b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.test.ts index 043754dff226e7..7b0d4352cbb532 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.test.ts @@ -17,12 +17,12 @@ import { import { retrieveMigratedLegacyActions } from './retrieve_migrated_legacy_actions'; -import { find } from '../../methods/find'; +import { findRules } from '../../../application/rule/methods/find/find_rules'; import { deleteRule } from '../../methods/delete'; -jest.mock('../../methods/find', () => { +jest.mock('../../../application/rule/methods/find/find_rules', () => { return { - find: jest.fn(), + findRules: jest.fn(), }; }); @@ -32,7 +32,7 @@ jest.mock('../../methods/delete', () => { }; }); -const findMock = find as jest.Mock; +const findMock = findRules as jest.Mock; const deleteRuleMock = deleteRule as jest.Mock; const getEmptyFindResult = () => ({ diff --git a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.ts b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.ts index fe4bb567138616..a9ab748cfa3d90 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.ts @@ -9,7 +9,7 @@ import type { SavedObjectReference } from '@kbn/core/server'; import { RULE_SAVED_OBJECT_TYPE } from '../../../saved_objects'; import type { RulesClientContext } from '../..'; import { RawRuleAction } from '../../../types'; -import { find } from '../../methods/find'; +import { findRules } from '../../../application/rule/methods/find/find_rules'; import { deleteRule } from '../../methods/delete'; import { LegacyIRuleActionsAttributes, legacyRuleActionsSavedObjectType } from './types'; import { transformFromLegacyActions } from './transform_legacy_actions'; @@ -49,7 +49,7 @@ export const retrieveMigratedLegacyActions: RetrieveMigratedLegacyActions = asyn */ // find it using the references array, not params.ruleAlertId const [siemNotification, legacyRuleActionsSO] = await Promise.all([ - find(context, { + findRules(context, { options: { filter: 'alert.attributes.alertTypeId:(siem.notifications)', hasReference: { diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 86848fd1598d91..86be428ed40788 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -36,7 +36,7 @@ import { getRuleExecutionKPI, GetRuleExecutionKPIParams, } from './methods/get_execution_kpi'; -import { find, FindParams } from './methods/find'; +import { findRules, FindRulesParams } from '../application/rule/methods/find'; import { AggregateParams } from '../application/rule/methods/aggregate/types'; import { aggregateRules } from '../application/rule/methods/aggregate'; import { deleteRule } from './methods/delete'; @@ -132,8 +132,8 @@ export class RulesClient { public create = (params: CreateRuleParams) => createRule(this.context, params); public delete = (params: { id: string }) => deleteRule(this.context, params); - public find = (params?: FindParams) => - find(this.context, params); + public find = (params?: FindRulesParams) => + findRules(this.context, params); public get = (params: GetRuleParams) => getRule(this.context, params); public resolve = (params: ResolveParams) => diff --git a/x-pack/plugins/alerting/server/rules_client/types.ts b/x-pack/plugins/alerting/server/rules_client/types.ts index f83d55ca2bb8be..9a701e1c95c81c 100644 --- a/x-pack/plugins/alerting/server/rules_client/types.ts +++ b/x-pack/plugins/alerting/server/rules_client/types.ts @@ -44,7 +44,7 @@ export type { BulkEditOperation, BulkEditFields, } from '../application/rule/methods/bulk_edit/types'; -export type { FindOptions, FindResult } from './methods/find'; +export type { FindResult } from '../application/rule/methods/find/find_rules'; export type { GetAlertSummaryParams } from './methods/get_alert_summary'; export type { GetExecutionLogByIdParams, diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/bulk_action/utils.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/bulk_action/utils.ts index 6fbd4cf07d5c88..df5db4cb2e1ab1 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/bulk_action/utils.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/bulk_action/utils.ts @@ -59,7 +59,7 @@ export const getDetectionRules = async ( filter: convertRuleTagsToMatchAllKQL(ruleTags), searchFields: ['tags'], page: 1, - per_page: 1, + perPage: 1, }, }); }) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts index 22c2f9ce6489ea..cb933c7cdf6baa 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts @@ -135,6 +135,7 @@ const findTestUtils = ( monitoring: match.monitoring, snooze_schedule: match.snooze_schedule, ...(hasActiveSnoozes && { active_snoozes: activeSnoozes }), + is_snoozed_until: null, } : {}), }); @@ -348,6 +349,7 @@ const findTestUtils = ( monitoring: match.monitoring, snooze_schedule: match.snooze_schedule, ...(hasActiveSnoozes && { active_snoozes: activeSnoozes }), + is_snoozed_until: null, } : {}), }); @@ -426,6 +428,7 @@ const findTestUtils = ( tags: [myTag], ...(describeType === 'internal' && { snooze_schedule: [], + is_snoozed_until: null, }), }); expect(omit(matchSecond, 'updatedAt')).to.eql({ @@ -434,6 +437,7 @@ const findTestUtils = ( tags: [myTag], ...(describeType === 'internal' && { snooze_schedule: [], + is_snoozed_until: null, }), }); break; @@ -510,6 +514,7 @@ const findTestUtils = ( execution_status: matchFirst.execution_status, ...(describeType === 'internal' && { snooze_schedule: [], + is_snoozed_until: null, }), }); expect(omit(matchSecond, 'updatedAt')).to.eql({ @@ -519,6 +524,7 @@ const findTestUtils = ( execution_status: matchSecond.execution_status, ...(describeType === 'internal' && { snooze_schedule: [], + is_snoozed_until: null, }), }); break; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find_with_post.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find_with_post.ts index 9e750fd23d78a2..64044222beb2db 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find_with_post.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find_with_post.ts @@ -99,6 +99,7 @@ const findTestUtils = ( monitoring: match.monitoring, snooze_schedule: match.snooze_schedule, ...(hasActiveSnoozes && { active_snoozes: activeSnoozes }), + is_snoozed_until: null, } : {}), }); @@ -321,6 +322,7 @@ const findTestUtils = ( monitoring: match.monitoring, snooze_schedule: match.snooze_schedule, ...(hasActiveSnoozes && { active_snoozes: activeSnoozes }), + is_snoozed_until: null, } : {}), }); @@ -405,6 +407,7 @@ const findTestUtils = ( tags: [myTag], ...(describeType === 'internal' && { snooze_schedule: [], + is_snoozed_until: null, }), }); expect(omit(matchSecond, 'updatedAt')).to.eql({ @@ -413,6 +416,7 @@ const findTestUtils = ( tags: [myTag], ...(describeType === 'internal' && { snooze_schedule: [], + is_snoozed_until: null, }), }); break; @@ -495,6 +499,7 @@ const findTestUtils = ( execution_status: matchFirst.execution_status, ...(describeType === 'internal' && { snooze_schedule: [], + is_snoozed_until: null, }), }); expect(omit(matchSecond, 'updatedAt')).to.eql({ @@ -504,6 +509,7 @@ const findTestUtils = ( execution_status: matchSecond.execution_status, ...(describeType === 'internal' && { snooze_schedule: [], + is_snoozed_until: null, }), }); break; 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 295c8acebbf1a2..a83ec1e70a0a1a 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 @@ -126,6 +126,7 @@ const findTestUtils = ( monitoring: match.monitoring, snooze_schedule: match.snooze_schedule, ...(hasActiveSnoozes && { active_snoozes: activeSnoozes }), + is_snoozed_until: null, } : {}), }); From 4023f923e575ecfc16afbffba4ba37e5046faf76 Mon Sep 17 00:00:00 2001 From: elena-shostak <165678770+elena-shostak@users.noreply.github.com> Date: Tue, 30 Apr 2024 17:38:41 +0200 Subject: [PATCH 08/10] [Roles] Added transformation of application * privilege to all (#181400) ## Summary Added transformation of application wildcard `*` privilege to `all` to correctly filter and display roles as `superuser`.
[Screenshot] Kibana superuser role before change Screenshot 2024-04-23 at 11 20 31
[Screenshot] Kibana superuser role after change Screenshot 2024-04-23 at 11 17 54
### Checklist - [X] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) __Fixes: https://github.com/elastic/kibana/issues/106561__ ## Release note Added transformation of application wildcard `*` privilege to `all` to correctly filter and display roles as `superuser`. --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/security/common/constants.ts | 5 +++ x-pack/plugins/security/common/index.ts | 2 +- x-pack/plugins/security/common/model/index.ts | 1 + x-pack/plugins/security/common/model/role.ts | 19 ++++++++-- .../roles/edit_role/edit_role_page.test.tsx | 24 +++++++++++++ .../privilege_form_calculator.test.ts | 26 ++++++++++++++ .../privilege_form_calculator.ts | 12 +++++++ .../privilege_space_table.test.tsx | 9 +++++ .../privilege_space_table.tsx | 18 ++++++---- .../space_aware_privilege_section.test.tsx | 21 +++++++++++ .../space_aware_privilege_section.tsx | 8 +++-- .../privilege_serializer.test.ts | 2 +- .../authorization/privilege_serializer.ts | 4 ++- .../roles/elasticsearch_role.test.ts | 35 +++++++++++++++++++ .../authorization/roles/elasticsearch_role.ts | 29 ++++++++++----- 15 files changed, 193 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/security/common/constants.ts b/x-pack/plugins/security/common/constants.ts index aab0c9d6c438b1..8958e392c3a615 100644 --- a/x-pack/plugins/security/common/constants.ts +++ b/x-pack/plugins/security/common/constants.ts @@ -17,6 +17,11 @@ export const UNKNOWN_SPACE = '?'; export const APPLICATION_PREFIX = 'kibana-'; +/** + * The wildcard identifier for all application privileges. + */ +export const PRIVILEGES_ALL_WILDCARD = '*'; + /** * Reserved application privileges are always assigned to this "wildcard" application. * This allows them to be applied to any Kibana "tenant" (`kibana.index`). Since reserved privileges are always assigned to reserved (built-in) roles, diff --git a/x-pack/plugins/security/common/index.ts b/x-pack/plugins/security/common/index.ts index e30fff0a8e76fe..de3e6e8cac0ca7 100644 --- a/x-pack/plugins/security/common/index.ts +++ b/x-pack/plugins/security/common/index.ts @@ -19,7 +19,7 @@ export type { InvalidRoleTemplate, InlineRoleTemplate, } from './model'; -export { getUserDisplayName, isRoleReserved } from './model'; +export { getUserDisplayName, isRoleReserved, isRoleWithWildcardBasePrivilege } from './model'; // Re-export types from the plugin directly to enhance the developer experience for consumers of the Security plugin. export type { diff --git a/x-pack/plugins/security/common/model/index.ts b/x-pack/plugins/security/common/model/index.ts index 006a0104e30d1d..b3fa7644db4122 100644 --- a/x-pack/plugins/security/common/model/index.ts +++ b/x-pack/plugins/security/common/model/index.ts @@ -40,6 +40,7 @@ export { isRoleEnabled, prepareRoleClone, getExtendedRoleDeprecationNotice, + isRoleWithWildcardBasePrivilege, } from './role'; export type { InlineRoleTemplate, diff --git a/x-pack/plugins/security/common/model/role.ts b/x-pack/plugins/security/common/model/role.ts index 1872f3bff2f5a4..0dba6576196759 100644 --- a/x-pack/plugins/security/common/model/role.ts +++ b/x-pack/plugins/security/common/model/role.ts @@ -54,7 +54,9 @@ export function isRoleSystem(role: Partial) { */ export function isRoleAdmin(role: Partial) { return ( - (isRoleReserved(role) && (role.name?.endsWith('_admin') || role.name === 'superuser')) ?? false + ((isRoleReserved(role) && (role.name?.endsWith('_admin') || role.name === 'superuser')) || + isRoleWithWildcardBasePrivilege(role)) ?? + false ); } @@ -73,13 +75,26 @@ export function getExtendedRoleDeprecationNotice(role: Partial) { }); } +/** + * Returns whether given role is editable through the UI or not. + * + * @param role the Role as returned by roles API + */ +export function isRoleWithWildcardBasePrivilege(role: Partial): boolean { + return role.kibana?.some((entry) => entry.base.includes('*')) ?? false; +} + /** * Returns whether given role is editable through the UI or not. * * @param role the Role as returned by roles API */ export function isRoleReadOnly(role: Partial): boolean { - return isRoleReserved(role) || (role._transform_error?.length ?? 0) > 0; + return ( + isRoleReserved(role) || + isRoleWithWildcardBasePrivilege(role) || + (role._transform_error?.length ?? 0) > 0 + ); } /** diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx index 622d10a4faab34..90246ccd6afe97 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx @@ -744,6 +744,30 @@ describe('', () => { expectSaveFormButtons(wrapper); }); + it('render role with wildcard base privilege without edit/delete actions', async () => { + const wrapper = mountWithIntl( + + + + ); + + await waitForRender(wrapper); + + expect(wrapper.find('[data-test-subj="privilegeEditAction-0"]')).toHaveLength(0); + expect(wrapper.find('[data-test-subj="privilegeDeleteAction-0"]')).toHaveLength(0); + expectReadOnlyFormButtons(wrapper); + }); + describe('in create mode', () => { it('renders an error for existing role name', async () => { const props = getProps({ action: 'edit' }); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_form_calculator/privilege_form_calculator.test.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_form_calculator/privilege_form_calculator.test.ts index a2d98fdd350d4b..20c54fd2ea5299 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_form_calculator/privilege_form_calculator.test.ts +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_form_calculator/privilege_form_calculator.test.ts @@ -915,4 +915,30 @@ describe('PrivilegeFormCalculator', () => { expect(calculator.hasSupersededInheritedPrivileges(0)).toEqual(false); }); }); + + describe('#isWildcardBasePrivilege', () => { + it('returns true for the base privilege with wildcard', () => { + const kibanaPrivileges = createKibanaPrivileges(kibanaFeatures); + const role = createRole([ + { + base: ['*'], + feature: { + with_sub_features: ['all'], + }, + spaces: ['foo'], + }, + { + base: ['all'], + feature: { + with_sub_features: ['read'], + }, + spaces: ['*'], + }, + ]); + + const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role); + + expect(calculator.isWildcardBasePrivilege(0)).toEqual(true); + }); + }); }); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_form_calculator/privilege_form_calculator.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_form_calculator/privilege_form_calculator.ts index 0cf554bdc19ddc..227c2be381546c 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_form_calculator/privilege_form_calculator.ts +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_form_calculator/privilege_form_calculator.ts @@ -27,9 +27,21 @@ export class PrivilegeFormCalculator { public getBasePrivilege(privilegeIndex: number) { const entry = this.role.kibana[privilegeIndex]; const basePrivileges = this.kibanaPrivileges.getBasePrivileges(entry); + return basePrivileges.find((bp) => entry.base.includes(bp.id)); } + /** + * Returns true if it is base wildcard (*) privilege. + * + * @param privilegeIndex the index of the kibana privileges role component + */ + public isWildcardBasePrivilege(privilegeIndex: number) { + const entry = this.role.kibana[privilegeIndex]; + + return entry.base.includes('*'); + } + /** * Returns the ID of the *displayed* Primary Feature Privilege for the indicated feature and privilege index. * If the effective primary feature privilege is a "minimal" version, then this returns the corresponding non-minimal version. diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx index 6de312d0369815..56fe843ccededd 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx @@ -204,6 +204,15 @@ describe('only global', () => { ]); }); + it('base *', () => { + const props = buildProps([{ spaces: ['*'], base: ['*'], feature: {} }]); + const component = mountWithIntl(); + const actualTable = getTableFromComponent(component); + expect(actualTable).toEqual([ + { spaces: ['*'], privileges: { summary: '*', overridden: false } }, + ]); + }); + it('base read', () => { const props = buildProps([{ spaces: ['*'], base: ['read'], feature: {} }]); const component = mountWithIntl(); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx index 530f6ffb2ee3c2..4c0ead6e431679 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx @@ -180,6 +180,14 @@ export class PrivilegeSpaceTable extends Component { ); } + const basePrivilege = + privilegeCalculator.getBasePrivilege(record.privilegeIndex)?.id ?? + CUSTOM_PRIVILEGE_VALUE; + + const privilege = privilegeCalculator.isWildcardBasePrivilege(record.privilegeIndex) + ? '*' + : basePrivilege; + let icon = ; if (privilegeCalculator.hasSupersededInheritedPrivileges(record.privilegeIndex)) { icon = ( @@ -202,13 +210,7 @@ export class PrivilegeSpaceTable extends Component { {icon} - + ); @@ -234,6 +236,7 @@ export class PrivilegeSpaceTable extends Component { color={'primary'} iconType={'pencil'} onClick={() => this.props.onEdit(record.privilegeIndex)} + data-test-subj={`privilegeEditAction-${record.privilegeIndex}`} /> ); }, @@ -252,6 +255,7 @@ export class PrivilegeSpaceTable extends Component { color={'danger'} iconType={'trash'} onClick={() => this.onDeleteSpacePrivilege(record)} + data-test-subj={`privilegeDeleteAction-${record.privilegeIndex}`} /> ); }, diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx index 3b44f20336d81e..3c2df19eb20dbf 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx @@ -94,6 +94,27 @@ describe('', () => { expect(wrapper.find('button[data-test-subj="addSpacePrivilegeButton"]')).toHaveLength(0); }); + it('hides privilege buttons if role has a base wildcard privilege', () => { + const props = buildProps({ + role: { + elasticsearch: { + cluster: ['manage'], + }, + kibana: [ + { + spaces: ['*'], + base: ['*'], + feature: {}, + }, + ], + }, + }); + + const wrapper = mountWithIntl(); + expect(wrapper.find('button[data-test-subj="addSpacePrivilegeButton"]')).toHaveLength(0); + expect(wrapper.find('button[data-test-subj="privilegeSummaryButton"]')).toHaveLength(0); + }); + it('Renders flyout after clicking "Add space privilege" button', () => { const props = buildProps({ role: { diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx index 1de6a8a952a502..f499da5c6973c5 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx @@ -25,7 +25,7 @@ import type { Space, SpacesApiUi } from '@kbn/spaces-plugin/public'; import { PrivilegeSpaceForm } from './privilege_space_form'; import { PrivilegeSpaceTable } from './privilege_space_table'; import type { Role } from '../../../../../../../common'; -import { isRoleReserved } from '../../../../../../../common'; +import { isRoleReserved, isRoleWithWildcardBasePrivilege } from '../../../../../../../common'; import type { KibanaPrivileges } from '../../../../model'; import type { RoleValidator } from '../../../validate_role'; import { PrivilegeFormCalculator } from '../privilege_form_calculator'; @@ -188,7 +188,10 @@ export class SpaceAwarePrivilegeSection extends Component { const hasAvailableSpaces = this.getAvailableSpaces().length > 0; // This shouldn't happen organically... - if (!hasAvailableSpaces && !hasPrivilegesAssigned) { + if ( + (!hasAvailableSpaces && !hasPrivilegesAssigned) || + isRoleWithWildcardBasePrivilege(this.props.role) + ) { return null; } @@ -219,6 +222,7 @@ export class SpaceAwarePrivilegeSection extends Component { kibanaPrivileges={this.props.kibanaPrivileges} canCustomizeSubFeaturePrivileges={this.props.canCustomizeSubFeaturePrivileges} spacesApiUi={this.props.spacesApiUi} + data-test-subj={'privilegeSummaryButton'} /> ); diff --git a/x-pack/plugins/security/server/authorization/privilege_serializer.test.ts b/x-pack/plugins/security/server/authorization/privilege_serializer.test.ts index 4c45804f2cf6b4..9e5b46121f768e 100644 --- a/x-pack/plugins/security/server/authorization/privilege_serializer.test.ts +++ b/x-pack/plugins/security/server/authorization/privilege_serializer.test.ts @@ -8,7 +8,7 @@ import { PrivilegeSerializer } from './privilege_serializer'; describe(`#isSerializedGlobalBasePrivilege`, () => { - ['all', 'read'].forEach((validValue) => { + ['all', 'read', '*'].forEach((validValue) => { test(`returns true for '${validValue}'`, () => { expect(PrivilegeSerializer.isSerializedGlobalBasePrivilege(validValue)).toBe(true); }); diff --git a/x-pack/plugins/security/server/authorization/privilege_serializer.ts b/x-pack/plugins/security/server/authorization/privilege_serializer.ts index 304d6fa8eea24e..1b71d55d6846b6 100644 --- a/x-pack/plugins/security/server/authorization/privilege_serializer.ts +++ b/x-pack/plugins/security/server/authorization/privilege_serializer.ts @@ -5,10 +5,12 @@ * 2.0. */ +import { PRIVILEGES_ALL_WILDCARD } from '../../common/constants'; + const featurePrefix = 'feature_'; const spacePrefix = 'space_'; const reservedPrefix = 'reserved_'; -const basePrivilegeNames = ['all', 'read']; +const basePrivilegeNames = ['all', 'read', PRIVILEGES_ALL_WILDCARD]; const globalBasePrivileges = [...basePrivilegeNames]; const spaceBasePrivileges = basePrivilegeNames.map( (privilegeName) => `${spacePrefix}${privilegeName}` diff --git a/x-pack/plugins/security/server/authorization/roles/elasticsearch_role.test.ts b/x-pack/plugins/security/server/authorization/roles/elasticsearch_role.test.ts index e8f4ff719fde1c..47947d2fb2adc2 100644 --- a/x-pack/plugins/security/server/authorization/roles/elasticsearch_role.test.ts +++ b/x-pack/plugins/security/server/authorization/roles/elasticsearch_role.test.ts @@ -289,4 +289,39 @@ describe('#transformElasticsearchRoleToRole', () => { { name: 'default-malformed', _transform_error: ['kibana'] }, ] ); + + it('#When application privilege is set to * return it correctly', () => { + const role = { + name: 'global-all', + cluster: [], + indices: [], + applications: [ + { + application: '*', + privileges: ['*'], + resources: ['*'], + }, + ], + run_as: [], + metadata: {}, + transient_metadata: { + enabled: true, + }, + }; + + const transformedRole = transformElasticsearchRoleToRole( + featuresWithRequireAllSpaces, + omit(role, 'name'), + role.name, + 'kibana-.kibana', + loggerMock.create() + ); + + const [privilege] = transformedRole.kibana; + const [basePrivilege] = privilege.base; + const [spacePrivilege] = privilege.spaces; + + expect(basePrivilege).toBe('*'); + expect(spacePrivilege).toBe('*'); + }); }); diff --git a/x-pack/plugins/security/server/authorization/roles/elasticsearch_role.ts b/x-pack/plugins/security/server/authorization/roles/elasticsearch_role.ts index 2dcf11bb53fe81..e83924f366b918 100644 --- a/x-pack/plugins/security/server/authorization/roles/elasticsearch_role.ts +++ b/x-pack/plugins/security/server/authorization/roles/elasticsearch_role.ts @@ -10,7 +10,10 @@ import type { KibanaFeature } from '@kbn/features-plugin/common'; import { GLOBAL_RESOURCE } from '@kbn/security-plugin-types-server'; import type { Role, RoleKibanaPrivilege } from '../../../common'; -import { RESERVED_PRIVILEGES_APPLICATION_WILDCARD } from '../../../common/constants'; +import { + PRIVILEGES_ALL_WILDCARD, + RESERVED_PRIVILEGES_APPLICATION_WILDCARD, +} from '../../../common/constants'; import { getDetailedErrorMessage } from '../../errors'; import { PrivilegeSerializer } from '../privilege_serializer'; import { ResourceSerializer } from '../resource_serializer'; @@ -27,6 +30,9 @@ export type ElasticsearchRole = Pick app === RESERVED_PRIVILEGES_APPLICATION_WILDCARD; +const isWildcardPrivilage = (app: string) => app === PRIVILEGES_ALL_WILDCARD; + export function transformElasticsearchRoleToRole( features: KibanaFeature[], elasticsearchRole: Omit, @@ -68,7 +74,8 @@ function transformRoleApplicationsToKibanaPrivileges( const roleKibanaApplications = roleApplications.filter( (roleApplication) => roleApplication.application === application || - roleApplication.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD + isReservedPrivilege(roleApplication.application) || + isWildcardPrivilage(roleApplication.application) ); // if any application entry contains an empty resource, we throw an error @@ -81,9 +88,11 @@ function transformRoleApplicationsToKibanaPrivileges( if ( roleKibanaApplications.some( (entry) => - entry.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD && - !entry.privileges.every((privilege) => - PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + (isReservedPrivilege(entry.application) || isWildcardPrivilage(entry.application)) && + !entry.privileges.every( + (privilege) => + PrivilegeSerializer.isSerializedReservedPrivilege(privilege) || + isWildcardPrivilage(privilege) ) ) ) { @@ -96,7 +105,8 @@ function transformRoleApplicationsToKibanaPrivileges( if ( roleKibanaApplications.some( (entry) => - entry.application !== RESERVED_PRIVILEGES_APPLICATION_WILDCARD && + !isReservedPrivilege(entry.application) && + !isWildcardPrivilage(entry.application) && entry.privileges.some((privilege) => PrivilegeSerializer.isSerializedReservedPrivilege(privilege) ) @@ -171,7 +181,9 @@ function transformRoleApplicationsToKibanaPrivileges( } const allResources = roleKibanaApplications - .filter((entry) => entry.application !== RESERVED_PRIVILEGES_APPLICATION_WILDCARD) + .filter( + (entry) => !isReservedPrivilege(entry.application) && !isWildcardPrivilage(entry.application) + ) .flatMap((entry) => entry.resources); // if we have improperly formatted resource entries, we can't transform these @@ -312,7 +324,8 @@ const extractUnrecognizedApplicationNames = ( .filter( (roleApplication) => roleApplication.application !== application && - roleApplication.application !== RESERVED_PRIVILEGES_APPLICATION_WILDCARD + !isReservedPrivilege(roleApplication.application) && + !isWildcardPrivilage(roleApplication.application) ) .map((roleApplication) => roleApplication.application) ); From 52ea57a57e3a0ae83798ec2eef605b06bfcc14d6 Mon Sep 17 00:00:00 2001 From: Elastic Machine Date: Tue, 30 Apr 2024 17:17:51 +0100 Subject: [PATCH 09/10] [main] Sync bundled packages with Package Storage (#182161) Automated by https://buildkite.com/elastic/package-storage-infra-kibana-discover-release-branches/builds/634 --- fleet_packages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fleet_packages.json b/fleet_packages.json index 4de7895192d4ec..317c4f1d60043e 100644 --- a/fleet_packages.json +++ b/fleet_packages.json @@ -56,6 +56,6 @@ }, { "name": "security_detection_engine", - "version": "8.13.4" + "version": "8.13.5" } ] \ No newline at end of file From d4c6e0710d4dc6f389fb4fa4dcd400eb36c6e7c5 Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Tue, 30 Apr 2024 18:29:20 +0200 Subject: [PATCH 10/10] [Security Solution] Unified Timeline - Add Expandable Flyout + fixes old flyout (#181793) ## Summary This feature must be enabled with below feature flag: ```yaml xpack.securitySolution.enableExperimental: - unifiedComponentsInTimelineEnabled ``` This PR enables expandable Flyout in unified timeline and fixes the `z-index` issue with previous flyout. This is essentially a workaround until either https://github.com/elastic/kibana/pull/180646 or https://github.com/elastic/kibana/pull/180645 is resolved. After that https://github.com/elastic/kibana/issues/179520 will make sure to remove this workaround in favour of a permanent solution. This is how it looks after the fix: https://github.com/elastic/kibana/assets/7485038/21952311-92bf-49a4-a8fd-1d12c126bd5c --- .../timelines/components/side_panel/index.tsx | 3 + .../unified_components/data_table/index.tsx | 64 ++++++--- ...nified_timeline_expandable_flyout.test.tsx | 121 ++++++++++++++++++ ...use_unified_timeline_expandable_flyout.tsx | 82 ++++++++++++ 4 files changed, 255 insertions(+), 15 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/hooks/use_unified_timeline_expandable_flyout.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/hooks/use_unified_timeline_expandable_flyout.tsx diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx index f5d971f9eeeb6b..bc865e3c12cee7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx @@ -35,6 +35,8 @@ interface DetailsPanelProps { isReadOnly?: boolean; } +const detailsPanelStyleProp = { zIndex: 1001 }; + /** * This panel is used in both the main timeline as well as the flyouts on the host, detection, cases, and network pages. * To prevent duplication the `isFlyoutView` prop is passed to determine the layout that should be used @@ -169,6 +171,7 @@ export const DetailsPanel = React.memo( = memo( const [expandedDoc, setExpandedDoc] = useState(); const [fetchedPage, setFechedPage] = useState(0); + const onCloseExpandableFlyout = useCallback(() => { + setExpandedDoc((prev) => (!prev ? prev : undefined)); + }, []); + + const { openFlyout, closeFlyout, isTimelineExpandableFlyoutEnabled } = + useUnifiedTableExpandableFlyout({ + onClose: onCloseExpandableFlyout, + }); + const { browserFields, runtimeMappings } = useSourcererDataView(SourcererScopeName.timeline); const showTimeCol = useMemo(() => !!dataView && !!dataView.timeFieldName, [dataView]); @@ -142,26 +153,39 @@ export const TimelineDataTableComponent: React.FC = memo( const updatedExpandedDetail: ExpandedDetailType = { panelView: 'eventDetail', params: { - eventId: eventData.id, - indexName: eventData._index ?? '', // TODO: fix type error + eventId: eventData._id, + indexName: eventData.ecs._index ?? '', // TODO: fix type error refetch, }, }; - dispatch( - timelineActions.toggleDetailPanel({ - ...updatedExpandedDetail, - tabType: activeTab, - id: timelineId, - }) - ); + if (isTimelineExpandableFlyoutEnabled) { + openFlyout({ + right: { + id: DocumentDetailsRightPanelKey, + params: { + id: eventData._id, + indexName: eventData.ecs._index ?? '', + scopeId: timelineId, + }, + }, + }); + } else { + dispatch( + timelineActions.toggleDetailPanel({ + ...updatedExpandedDetail, + tabType: activeTab, + id: timelineId, + }) + ); + } activeTimeline.toggleExpandedDetail({ ...updatedExpandedDetail }); }, - [activeTab, dispatch, refetch, timelineId] + [activeTab, dispatch, refetch, timelineId, isTimelineExpandableFlyoutEnabled, openFlyout] ); - const handleOnPanelClosed = useCallback(() => { + const onTimelineLegacyFlyoutClose = useCallback(() => { if ( expandedDetail[activeTab]?.panelView && timelineId === TimelineId.active && @@ -182,10 +206,20 @@ export const TimelineDataTableComponent: React.FC = memo( handleOnEventDetailPanelOpened(timelineDoc); } } else { - handleOnPanelClosed(); + if (isTimelineExpandableFlyoutEnabled) { + closeFlyout(); + return; + } + onTimelineLegacyFlyoutClose(); } }, - [tableRows, handleOnEventDetailPanelOpened, handleOnPanelClosed] + [ + tableRows, + handleOnEventDetailPanelOpened, + onTimelineLegacyFlyoutClose, + closeFlyout, + isTimelineExpandableFlyoutEnabled, + ] ); const onColumnResize = useCallback( @@ -393,10 +427,10 @@ export const TimelineDataTableComponent: React.FC = memo( trailingControlColumns={trailingControlColumns} renderCustomGridBody={renderCustomBodyCallback} /> - {showExpandedDetails && ( + {showExpandedDetails && !isTimelineExpandableFlyoutEnabled && ( { + return { + useLocation: jest.fn(), + }; +}); +jest.mock('@kbn/expandable-flyout'); + +const onFlyoutCloseMock = jest.fn(); + +describe('useUnifiedTimelineExpandableFlyout', () => { + beforeEach(() => { + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); + (useUiSetting$ as jest.Mock).mockReturnValue([true, jest.fn()]); + (useLocation as jest.Mock).mockReturnValue({ + search: `?${URL_PARAM_KEY.timelineFlyout}=(test:value)`, + }); + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ + openFlyout: jest.fn(), + closeFlyout: jest.fn(), + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should have expandable flyout disabled when flyout is disabled in Advanced Settings', () => { + (useUiSetting$ as jest.Mock).mockReturnValue([false, jest.fn()]); + const { result } = renderHook(() => + useUnifiedTableExpandableFlyout({ + onClose: onFlyoutCloseMock, + }) + ); + + expect(result.current.isTimelineExpandableFlyoutEnabled).toBe(false); + }); + it('should have expandable flyout disabled when flyout is disabled in Experimental Features', () => { + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); + const { result } = renderHook(() => + useUnifiedTableExpandableFlyout({ + onClose: onFlyoutCloseMock, + }) + ); + + expect(result.current.isTimelineExpandableFlyoutEnabled).toBe(false); + }); + describe('when flyout is enabled', () => { + it('should mark flyout as closed when location is empty', () => { + (useLocation as jest.Mock).mockReturnValue({ + search: '', + }); + + const { result } = renderHook(() => + useUnifiedTableExpandableFlyout({ + onClose: onFlyoutCloseMock, + }) + ); + + expect(result.current.isTimelineExpandableFlyoutOpen).toBe(false); + }); + + it('should mark flyout as open when location has `timelineFlyout`', () => { + (useLocation as jest.Mock).mockReturnValue({ + search: `${URL_PARAM_KEY.timelineFlyout}=(test:value)`, + }); + const { result } = renderHook(() => + useUnifiedTableExpandableFlyout({ + onClose: onFlyoutCloseMock, + }) + ); + + expect(result.current.isTimelineExpandableFlyoutOpen).toBe(true); + }); + + it('should mark flyout as close when location has empty `timelineFlyout`', () => { + const { result, rerender } = renderHook(() => + useUnifiedTableExpandableFlyout({ + onClose: onFlyoutCloseMock, + }) + ); + expect(result.current.isTimelineExpandableFlyoutOpen).toBe(true); + + (useLocation as jest.Mock).mockReturnValue({ + search: `${URL_PARAM_KEY.timelineFlyout}=()`, + }); + + rerender(); + + expect(result.current.isTimelineExpandableFlyoutOpen).toBe(false); + expect(onFlyoutCloseMock).toHaveBeenCalledTimes(1); + }); + + it('should call user provided close handler when flyout is closed', () => { + const { result } = renderHook(() => + useUnifiedTableExpandableFlyout({ + onClose: onFlyoutCloseMock, + }) + ); + + result.current.closeFlyout(); + expect(onFlyoutCloseMock).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/hooks/use_unified_timeline_expandable_flyout.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/hooks/use_unified_timeline_expandable_flyout.tsx new file mode 100644 index 00000000000000..31433d54a5cd01 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/hooks/use_unified_timeline_expandable_flyout.tsx @@ -0,0 +1,82 @@ +/* + * 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 { useCallback, useEffect, useMemo, useState } from 'react'; +import { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { useLocation } from 'react-router-dom'; +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; +import { ENABLE_EXPANDABLE_FLYOUT_SETTING } from '../../../../../../common/constants'; +import { URL_PARAM_KEY } from '../../../../../common/hooks/use_url_state'; + +const EMPTY_TIMELINE_FLYOUT_SEARCH_PARAMS = '()'; + +interface UseUnifiedTableExpandableFlyoutArgs { + onClose?: () => void; +} + +export const useUnifiedTableExpandableFlyout = ({ + onClose, +}: UseUnifiedTableExpandableFlyoutArgs) => { + const expandableTimelineFlyoutEnabled = useIsExperimentalFeatureEnabled( + 'expandableTimelineFlyoutEnabled' + ); + + const [isSecurityFlyoutEnabled] = useUiSetting$(ENABLE_EXPANDABLE_FLYOUT_SETTING); + + const location = useLocation(); + + const { openFlyout, closeFlyout } = useExpandableFlyoutApi(); + + const closeFlyoutWithEffect = useCallback(() => { + closeFlyout(); + onClose?.(); + }, [onClose, closeFlyout]); + + const isFlyoutOpen = useMemo(() => { + /** + * Currently, if new expanable flyout is closed, there is not way for + * consumer to trigger an effect `onClose` of flyout. So, we are using + * this hack to know if flyout is open or not. + * + * Raised: https://github.com/elastic/kibana/issues/179520 + * + * */ + const searchParams = new URLSearchParams(location.search); + return ( + searchParams.has(URL_PARAM_KEY.timelineFlyout) && + searchParams.get(URL_PARAM_KEY.timelineFlyout) !== EMPTY_TIMELINE_FLYOUT_SEARCH_PARAMS + ); + }, [location.search]); + + const [isTimelineExpandableFlyoutOpen, setIsTimelineExpandableFlyoutOpen] = + useState(isFlyoutOpen); + + useEffect(() => { + setIsTimelineExpandableFlyoutOpen((prev) => { + if (prev === isFlyoutOpen) { + return prev; + } + if (!isFlyoutOpen && onClose) { + // run onClose only when isFlyoutOpen changed from true to false + // should not be needed when + // https://github.com/elastic/kibana/issues/179520 + // is resolved + + onClose(); + } + return isFlyoutOpen; + }); + }, [isFlyoutOpen, onClose]); + + return { + isTimelineExpandableFlyoutOpen, + openFlyout, + closeFlyout: closeFlyoutWithEffect, + isTimelineExpandableFlyoutEnabled: expandableTimelineFlyoutEnabled && isSecurityFlyoutEnabled, + }; +};