diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_create.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_create.ts index fd32068d79ebf..9c85b1aa0620c 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_create.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_create.ts @@ -8,16 +8,18 @@ import { schema } from '@kbn/config-schema'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +import type { Logger } from '@kbn/logging'; import type { InternalSavedObjectRouter } from '../internal_types'; import { catchAndReturnBoomErrors, throwIfAnyTypeNotVisibleByAPI } from './utils'; interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; + logger: Logger; } export const registerBulkCreateRoute = ( router: InternalSavedObjectRouter, - { coreUsageData }: RouteDependencies + { coreUsageData, logger }: RouteDependencies ) => { router.post( { @@ -49,6 +51,9 @@ export const registerBulkCreateRoute = ( }, }, catchAndReturnBoomErrors(async (context, req, res) => { + logger.warn( + "The bulk create saved object API '/api/saved_objects/_bulk_create' is deprecated." + ); const { overwrite } = req.query; const usageStatsClient = coreUsageData.getClient(); diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_delete.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_delete.ts index 720bd6ecdb7e1..cbd22f827a642 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_delete.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_delete.ts @@ -8,16 +8,18 @@ import { schema } from '@kbn/config-schema'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +import type { Logger } from '@kbn/logging'; import type { InternalSavedObjectRouter } from '../internal_types'; import { catchAndReturnBoomErrors, throwIfAnyTypeNotVisibleByAPI } from './utils'; interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; + logger: Logger; } export const registerBulkDeleteRoute = ( router: InternalSavedObjectRouter, - { coreUsageData }: RouteDependencies + { coreUsageData, logger }: RouteDependencies ) => { router.post( { @@ -35,6 +37,9 @@ export const registerBulkDeleteRoute = ( }, }, catchAndReturnBoomErrors(async (context, req, res) => { + logger.warn( + "The bulk update saved object API '/api/saved_objects/_bulk_update' is deprecated." + ); const { force } = req.query; const usageStatsClient = coreUsageData.getClient(); usageStatsClient.incrementSavedObjectsBulkDelete({ request: req }).catch(() => {}); diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_get.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_get.ts index 30a9a1625c39d..455657d9ca640 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_get.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_get.ts @@ -8,16 +8,18 @@ import { schema } from '@kbn/config-schema'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +import type { Logger } from '@kbn/logging'; import type { InternalSavedObjectRouter } from '../internal_types'; import { catchAndReturnBoomErrors, throwIfAnyTypeNotVisibleByAPI } from './utils'; interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; + logger: Logger; } export const registerBulkGetRoute = ( router: InternalSavedObjectRouter, - { coreUsageData }: RouteDependencies + { coreUsageData, logger }: RouteDependencies ) => { router.post( { @@ -34,6 +36,7 @@ export const registerBulkGetRoute = ( }, }, catchAndReturnBoomErrors(async (context, req, res) => { + logger.warn("The bulk get saved object API '/api/saved_objects/_bulk_get' is deprecated."); const usageStatsClient = coreUsageData.getClient(); usageStatsClient.incrementSavedObjectsBulkGet({ request: req }).catch(() => {}); diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_resolve.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_resolve.ts index 77f3d480bcdef..874df9f59bb8c 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_resolve.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_resolve.ts @@ -8,16 +8,18 @@ import { schema } from '@kbn/config-schema'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +import type { Logger } from '@kbn/logging'; import type { InternalSavedObjectRouter } from '../internal_types'; import { catchAndReturnBoomErrors, throwIfAnyTypeNotVisibleByAPI } from './utils'; interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; + logger: Logger; } export const registerBulkResolveRoute = ( router: InternalSavedObjectRouter, - { coreUsageData }: RouteDependencies + { coreUsageData, logger }: RouteDependencies ) => { router.post( { @@ -32,6 +34,9 @@ export const registerBulkResolveRoute = ( }, }, catchAndReturnBoomErrors(async (context, req, res) => { + logger.warn( + "The bulk resolve saved object API '/api/saved_objects/_bulk_resolve' is deprecated." + ); const usageStatsClient = coreUsageData.getClient(); usageStatsClient.incrementSavedObjectsBulkResolve({ request: req }).catch(() => {}); diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_update.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_update.ts index b198ef2ad5fec..b51e39fd1c6d5 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_update.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_update.ts @@ -8,16 +8,18 @@ import { schema } from '@kbn/config-schema'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +import type { Logger } from '@kbn/logging'; import type { InternalSavedObjectRouter } from '../internal_types'; import { catchAndReturnBoomErrors, throwIfAnyTypeNotVisibleByAPI } from './utils'; interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; + logger: Logger; } export const registerBulkUpdateRoute = ( router: InternalSavedObjectRouter, - { coreUsageData }: RouteDependencies + { coreUsageData, logger }: RouteDependencies ) => { router.put( { @@ -44,6 +46,9 @@ export const registerBulkUpdateRoute = ( }, }, catchAndReturnBoomErrors(async (context, req, res) => { + logger.warn( + "The bulk update saved object API '/api/saved_objects/_bulk_update' is deprecated." + ); const usageStatsClient = coreUsageData.getClient(); usageStatsClient.incrementSavedObjectsBulkUpdate({ request: req }).catch(() => {}); diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/create.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/create.ts index fcdb1fef13889..af0be90481c33 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/create.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/create.ts @@ -8,16 +8,18 @@ import { schema } from '@kbn/config-schema'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +import type { Logger } from '@kbn/logging'; import type { InternalSavedObjectRouter } from '../internal_types'; import { catchAndReturnBoomErrors, throwIfTypeNotVisibleByAPI } from './utils'; interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; + logger: Logger; } export const registerCreateRoute = ( router: InternalSavedObjectRouter, - { coreUsageData }: RouteDependencies + { coreUsageData, logger }: RouteDependencies ) => { router.post( { @@ -48,6 +50,7 @@ export const registerCreateRoute = ( }, }, catchAndReturnBoomErrors(async (context, req, res) => { + logger.warn("The create saved object API '/api/saved_objects/{type}/{id}' is deprecated."); const { type, id } = req.params; const { overwrite } = req.query; const { attributes, migrationVersion, coreMigrationVersion, references, initialNamespaces } = diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/delete.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/delete.ts index eea862e64008b..71124fee2ca38 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/delete.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/delete.ts @@ -8,16 +8,18 @@ import { schema } from '@kbn/config-schema'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +import type { Logger } from '@kbn/logging'; import type { InternalSavedObjectRouter } from '../internal_types'; import { catchAndReturnBoomErrors, throwIfTypeNotVisibleByAPI } from './utils'; interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; + logger: Logger; } export const registerDeleteRoute = ( router: InternalSavedObjectRouter, - { coreUsageData }: RouteDependencies + { coreUsageData, logger }: RouteDependencies ) => { router.delete( { @@ -33,6 +35,7 @@ export const registerDeleteRoute = ( }, }, catchAndReturnBoomErrors(async (context, req, res) => { + logger.warn("The delete saved object API '/api/saved_objects/{type}/{id}' is deprecated."); const { type, id } = req.params; const { force } = req.query; const { getClient, typeRegistry } = (await context.core).savedObjects; diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts index a2d4497dd5f6e..42cf0290b52d2 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts @@ -8,16 +8,18 @@ import { schema } from '@kbn/config-schema'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +import type { Logger } from '@kbn/logging'; import type { InternalSavedObjectRouter } from '../internal_types'; import { catchAndReturnBoomErrors, throwOnHttpHiddenTypes } from './utils'; interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; + logger: Logger; } export const registerFindRoute = ( router: InternalSavedObjectRouter, - { coreUsageData }: RouteDependencies + { coreUsageData, logger }: RouteDependencies ) => { const referenceSchema = schema.object({ type: schema.string(), @@ -59,6 +61,7 @@ export const registerFindRoute = ( }, }, catchAndReturnBoomErrors(async (context, req, res) => { + logger.warn("The find saved object API '/api/saved_objects/_find' is deprecated."); const query = req.query; const namespaces = diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/get.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/get.ts index b8c1ab5614b7a..ecacdc4452c67 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/get.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/get.ts @@ -8,16 +8,18 @@ import { schema } from '@kbn/config-schema'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +import type { Logger } from '@kbn/logging'; import type { InternalSavedObjectRouter } from '../internal_types'; import { catchAndReturnBoomErrors, throwIfTypeNotVisibleByAPI } from './utils'; interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; + logger: Logger; } export const registerGetRoute = ( router: InternalSavedObjectRouter, - { coreUsageData }: RouteDependencies + { coreUsageData, logger }: RouteDependencies ) => { router.get( { @@ -30,6 +32,7 @@ export const registerGetRoute = ( }, }, catchAndReturnBoomErrors(async (context, req, res) => { + logger.warn("The get saved object API '/api/saved_objects/{type}/{id}' is deprecated."); const { type, id } = req.params; const usageStatsClient = coreUsageData.getClient(); diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/index.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/index.ts index 89d5b41dd8885..8c85017064498 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/index.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/index.ts @@ -53,17 +53,17 @@ export function registerRoutes({ const router = http.createRouter('/api/saved_objects/'); - registerGetRoute(router, { coreUsageData }); - registerResolveRoute(router, { coreUsageData }); - registerCreateRoute(router, { coreUsageData }); - registerDeleteRoute(router, { coreUsageData }); - registerFindRoute(router, { coreUsageData }); - registerUpdateRoute(router, { coreUsageData }); - registerBulkGetRoute(router, { coreUsageData }); - registerBulkCreateRoute(router, { coreUsageData }); - registerBulkResolveRoute(router, { coreUsageData }); - registerBulkUpdateRoute(router, { coreUsageData }); - registerBulkDeleteRoute(router, { coreUsageData }); + registerGetRoute(router, { coreUsageData, logger }); + registerResolveRoute(router, { coreUsageData, logger }); + registerCreateRoute(router, { coreUsageData, logger }); + registerDeleteRoute(router, { coreUsageData, logger }); + registerFindRoute(router, { coreUsageData, logger }); + registerUpdateRoute(router, { coreUsageData, logger }); + registerBulkGetRoute(router, { coreUsageData, logger }); + registerBulkCreateRoute(router, { coreUsageData, logger }); + registerBulkResolveRoute(router, { coreUsageData, logger }); + registerBulkUpdateRoute(router, { coreUsageData, logger }); + registerBulkDeleteRoute(router, { coreUsageData, logger }); registerExportRoute(router, { config, coreUsageData }); registerImportRoute(router, { config, coreUsageData }); registerResolveImportErrorsRoute(router, { config, coreUsageData }); diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/resolve.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/resolve.ts index ac8d4fdcc5ba7..be77423cba09b 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/resolve.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/resolve.ts @@ -8,16 +8,18 @@ import { schema } from '@kbn/config-schema'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +import type { Logger } from '@kbn/logging'; import type { InternalSavedObjectRouter } from '../internal_types'; import { throwIfTypeNotVisibleByAPI } from './utils'; interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; + logger: Logger; } export const registerResolveRoute = ( router: InternalSavedObjectRouter, - { coreUsageData }: RouteDependencies + { coreUsageData, logger }: RouteDependencies ) => { router.get( { @@ -30,6 +32,9 @@ export const registerResolveRoute = ( }, }, router.handleLegacyErrors(async (context, req, res) => { + logger.warn( + "The resolve saved object API '/api/saved_objects/resolve/{type}/{id}' is deprecated." + ); const { type, id } = req.params; const { savedObjects } = await context.core; diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/update.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/update.ts index 491add9a182ca..d1b544519efa3 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/update.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/update.ts @@ -8,17 +8,19 @@ import { schema } from '@kbn/config-schema'; import type { SavedObjectsUpdateOptions } from '@kbn/core-saved-objects-api-server'; +import type { Logger } from '@kbn/logging'; import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; import type { InternalSavedObjectRouter } from '../internal_types'; import { catchAndReturnBoomErrors, throwIfTypeNotVisibleByAPI } from './utils'; interface RouteDependencies { coreUsageData: InternalCoreUsageDataSetup; + logger: Logger; } export const registerUpdateRoute = ( router: InternalSavedObjectRouter, - { coreUsageData }: RouteDependencies + { coreUsageData, logger }: RouteDependencies ) => { router.put( { @@ -45,6 +47,7 @@ export const registerUpdateRoute = ( }, }, catchAndReturnBoomErrors(async (context, req, res) => { + logger.warn("The update saved object API '/api/saved_objects/{type}/{id}' is deprecated."); const { type, id } = req.params; const { attributes, version, references, upsert } = req.body; const options: SavedObjectsUpdateOptions = { version, references, upsert }; diff --git a/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts b/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts index 2dd0659f9cc19..0817ea3d0e34f 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import datemath from '@kbn/datemath'; import type { Moment } from 'moment'; import { Interval } from './interval'; @@ -23,12 +23,22 @@ export class Timerange { type DateLike = Date | number | Moment | string; -function getDateFrom(date: DateLike): Date { +function getDateFrom(date: DateLike, now: Date): Date { if (date instanceof Date) return date; + + if (typeof date === 'string') { + const parsed = datemath.parse(date, { forceNow: now }); + if (parsed && parsed.isValid()) { + return parsed.toDate(); + } + } + if (typeof date === 'number' || typeof date === 'string') return new Date(date); + return date.toDate(); } -export function timerange(from: Date | number | Moment, to: Date | number | Moment) { - return new Timerange(getDateFrom(from), getDateFrom(to)); +export function timerange(from: DateLike, to: DateLike) { + const now = new Date(); + return new Timerange(getDateFrom(from, now), getDateFrom(to, now)); } diff --git a/packages/kbn-apm-synthtrace-client/tsconfig.json b/packages/kbn-apm-synthtrace-client/tsconfig.json index 8d1c9cae899a3..8286fda7455b0 100644 --- a/packages/kbn-apm-synthtrace-client/tsconfig.json +++ b/packages/kbn-apm-synthtrace-client/tsconfig.json @@ -11,5 +11,8 @@ "include": ["**/*.ts"], "exclude": [ "target/**/*", + ], + "kbn_references": [ + "@kbn/datemath", ] } diff --git a/packages/kbn-cell-actions/src/components/cell_actions.tsx b/packages/kbn-cell-actions/src/components/cell_actions.tsx index 013fc372fcd9e..3d843a3168f79 100644 --- a/packages/kbn-cell-actions/src/components/cell_actions.tsx +++ b/packages/kbn-cell-actions/src/components/cell_actions.tsx @@ -23,14 +23,12 @@ export const CellActions: React.FC = ({ metadata, className, }) => { - const extraContentNodeRef = useRef(null); const nodeRef = useRef(null); const actionContext: CellActionExecutionContext = useMemo( () => ({ field, trigger: { id: triggerId }, - extraContentNodeRef, nodeRef, metadata, }), @@ -49,8 +47,6 @@ export const CellActions: React.FC = ({ > {children} - -
); } @@ -62,16 +58,17 @@ export const CellActions: React.FC = ({ ref={nodeRef} gutterSize="none" justifyContent="flexStart" + className={className} + data-test-subj={dataTestSubj} > {children} - + -
); diff --git a/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.test.tsx b/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.test.tsx index db6a02b918ca1..7cb321a6a3f67 100644 --- a/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.test.tsx +++ b/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.test.tsx @@ -6,13 +6,14 @@ * Side Public License, v 1. */ -import React, { JSXElementConstructor } from 'react'; +import React, { JSXElementConstructor, MutableRefObject } from 'react'; import { EuiButtonEmpty, EuiDataGridColumnCellActionProps, + EuiDataGridRefProps, type EuiDataGridColumnCellAction, } from '@elastic/eui'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import { act, renderHook } from '@testing-library/react-hooks'; import { makeAction } from '../mocks/helpers'; import { @@ -35,10 +36,14 @@ const field1 = { name: 'column1', values: ['0.0', '0.1', '0.2', '0.3'], type: 't const field2 = { name: 'column2', values: ['1.0', '1.1', '1.2', '1.3'], type: 'keyword' }; const columns = [{ id: field1.name }, { id: field2.name }]; +const mockCloseCellPopover = jest.fn(); const useDataGridColumnsCellActionsProps: UseDataGridColumnsCellActionsProps = { fields: [field1, field2], triggerId: 'testTriggerId', metadata: { some: 'value' }, + dataGridRef: { + current: { closeCellPopover: mockCloseCellPopover }, + } as unknown as MutableRefObject, }; const renderCellAction = ( @@ -115,7 +120,9 @@ describe('useDataGridColumnsCellActions', () => { cellAction.getByTestId(`dataGridColumnCellAction-${action1.id}`).click(); - expect(action1.execute).toHaveBeenCalled(); + waitFor(() => { + expect(action1.execute).toHaveBeenCalled(); + }); }); it('should execute the action with correct context', async () => { @@ -128,23 +135,27 @@ describe('useDataGridColumnsCellActions', () => { cellAction1.getByTestId(`dataGridColumnCellAction-${action1.id}`).click(); - expect(action1.execute).toHaveBeenCalledWith( - expect.objectContaining({ - field: { name: field1.name, type: field1.type, value: field1.values[1] }, - trigger: { id: useDataGridColumnsCellActionsProps.triggerId }, - }) - ); + await waitFor(() => { + expect(action1.execute).toHaveBeenCalledWith( + expect.objectContaining({ + field: { name: field1.name, type: field1.type, value: field1.values[1] }, + trigger: { id: useDataGridColumnsCellActionsProps.triggerId }, + }) + ); + }); const cellAction2 = renderCellAction(result.current[1][1], { rowIndex: 2 }); cellAction2.getByTestId(`dataGridColumnCellAction-${action2.id}`).click(); - expect(action2.execute).toHaveBeenCalledWith( - expect.objectContaining({ - field: { name: field2.name, type: field2.type, value: field2.values[2] }, - trigger: { id: useDataGridColumnsCellActionsProps.triggerId }, - }) - ); + await waitFor(() => { + expect(action2.execute).toHaveBeenCalledWith( + expect.objectContaining({ + field: { name: field2.name, type: field2.type, value: field2.values[2] }, + trigger: { id: useDataGridColumnsCellActionsProps.triggerId }, + }) + ); + }); }); it('should execute the action with correct page value', async () => { @@ -157,10 +168,27 @@ describe('useDataGridColumnsCellActions', () => { cellAction.getByTestId(`dataGridColumnCellAction-${action1.id}`).click(); - expect(action1.execute).toHaveBeenCalledWith( - expect.objectContaining({ - field: { name: field1.name, type: field1.type, value: field1.values[1] }, - }) - ); + await waitFor(() => { + expect(action1.execute).toHaveBeenCalledWith( + expect.objectContaining({ + field: { name: field1.name, type: field1.type, value: field1.values[1] }, + }) + ); + }); + }); + + it('should close popover then action executed', async () => { + const { result, waitForNextUpdate } = renderHook(useDataGridColumnsCellActions, { + initialProps: useDataGridColumnsCellActionsProps, + }); + await waitForNextUpdate(); + + const cellAction = renderCellAction(result.current[0][0], { rowIndex: 25 }); + + cellAction.getByTestId(`dataGridColumnCellAction-${action1.id}`).click(); + + await waitFor(() => { + expect(mockCloseCellPopover).toHaveBeenCalled(); + }); }); }); diff --git a/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.tsx b/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.tsx index 0c78561909fc0..2fe9668097f1b 100644 --- a/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.tsx +++ b/packages/kbn-cell-actions/src/hooks/use_data_grid_column_cell_actions.tsx @@ -6,8 +6,12 @@ * Side Public License, v 1. */ -import React, { useMemo, useRef } from 'react'; -import { EuiLoadingSpinner, type EuiDataGridColumnCellAction } from '@elastic/eui'; +import React, { MutableRefObject, useCallback, useMemo, useRef } from 'react'; +import { + EuiDataGridRefProps, + EuiLoadingSpinner, + type EuiDataGridColumnCellAction, +} from '@elastic/eui'; import type { CellAction, CellActionCompatibilityContext, @@ -27,11 +31,13 @@ interface BulkField extends Pick { export interface UseDataGridColumnsCellActionsProps extends Pick { fields: BulkField[]; + dataGridRef: MutableRefObject; } export const useDataGridColumnsCellActions = ({ fields, triggerId, metadata, + dataGridRef, disabledActions = [], }: UseDataGridColumnsCellActionsProps): EuiDataGridColumnCellAction[][] => { const bulkContexts: CellActionCompatibilityContext[] = useMemo( @@ -57,15 +63,22 @@ export const useDataGridColumnsCellActions = ({ } return columnsActions.map((actions, columnIndex) => actions.map((action) => - createColumnCellAction({ action, metadata, triggerId, field: fields[columnIndex] }) + createColumnCellAction({ + action, + metadata, + triggerId, + field: fields[columnIndex], + dataGridRef, + }) ) ); - }, [columnsActions, fields, loading, metadata, triggerId]); + }, [columnsActions, fields, loading, metadata, triggerId, dataGridRef]); return columnsCellActions; }; -interface CreateColumnCellActionParams extends Pick { +interface CreateColumnCellActionParams + extends Pick { field: BulkField; action: CellAction; } @@ -74,36 +87,76 @@ const createColumnCellAction = ({ action, metadata, triggerId, + dataGridRef, }: CreateColumnCellActionParams): EuiDataGridColumnCellAction => - function ColumnCellAction({ Component, rowIndex }) { + function ColumnCellAction({ Component, rowIndex, isExpanded }) { const nodeRef = useRef(null); - const extraContentNodeRef = useRef(null); + const buttonRef = useRef(null); - const { name, type, values } = field; - // rowIndex refers to all pages, we need to use the row index relative to the page to get the value - const value = values[rowIndex % values.length]; + const actionContext: CellActionExecutionContext = useMemo(() => { + const { name, type, values } = field; + // rowIndex refers to all pages, we need to use the row index relative to the page to get the value + const value = values[rowIndex % values.length]; + return { + field: { name, type, value }, + trigger: { id: triggerId }, + nodeRef, + metadata, + }; + }, [rowIndex]); - const actionContext: CellActionExecutionContext = { - field: { name, type, value }, - trigger: { id: triggerId }, - extraContentNodeRef, - nodeRef, - metadata, - }; + const onClick = useCallback(async () => { + actionContext.nodeRef.current = await closeAndGetCellElement({ + dataGrid: dataGridRef.current, + isExpanded, + buttonRef, + }); + action.execute(actionContext); + }, [actionContext, isExpanded]); return ( { - action.execute(actionContext); - }} + onClick={onClick} > {action.getDisplayName(actionContext)} -
); }; + +const closeAndGetCellElement = ({ + dataGrid, + isExpanded, + buttonRef, +}: { + dataGrid?: EuiDataGridRefProps | null; + isExpanded: boolean; + buttonRef: MutableRefObject; +}): Promise => + new Promise((resolve) => { + const gridCellElement = isExpanded + ? // if actions popover is expanded the button is outside dataGrid, using euiDataGridRowCell--open class + document.querySelector('div[role="gridcell"].euiDataGridRowCell--open') + : // if not expanded the button is inside the cell, get the parent cell from the button + getParentCellElement(buttonRef.current); + // close the popover if needed + dataGrid?.closeCellPopover(); + // closing the popover updates the cell content, get the first child after all updates + setTimeout(() => { + resolve((gridCellElement?.firstElementChild as HTMLElement) ?? null); + }); + }); + +const getParentCellElement = (element?: HTMLElement | null): HTMLElement | null => { + if (element == null) { + return null; + } + if (element.nodeName === 'div' && element.getAttribute('role') === 'gridcell') { + return element; + } + return getParentCellElement(element.parentElement); +}; diff --git a/packages/kbn-cell-actions/src/mocks/helpers.ts b/packages/kbn-cell-actions/src/mocks/helpers.ts index 75e4399199815..acb1afd1bc21e 100644 --- a/packages/kbn-cell-actions/src/mocks/helpers.ts +++ b/packages/kbn-cell-actions/src/mocks/helpers.ts @@ -32,7 +32,6 @@ export const makeActionContext = ( type: 'keyword', value: 'some value', }, - extraContentNodeRef: {} as MutableRefObject, nodeRef: {} as MutableRefObject, ...override, }); diff --git a/packages/kbn-cell-actions/src/types.ts b/packages/kbn-cell-actions/src/types.ts index aaf2f745f7f61..bb58ebdc69f51 100644 --- a/packages/kbn-cell-actions/src/types.ts +++ b/packages/kbn-cell-actions/src/types.ts @@ -95,16 +95,10 @@ type Metadata = Record | undefined; export interface CellActionExecutionContext extends ActionExecutionContext { field: CellActionField; - /** - * Ref to a DOM node where the action can add custom HTML. - */ - extraContentNodeRef: React.MutableRefObject; - /** * Ref to the node where the cell action are rendered. */ nodeRef: React.MutableRefObject; - /** * Extra configurations for actions. */ diff --git a/src/core/CORE_CONVENTIONS.md b/src/core/CORE_CONVENTIONS.md index 1cd997d570b6a..9571be679ce57 100644 --- a/src/core/CORE_CONVENTIONS.md +++ b/src/core/CORE_CONVENTIONS.md @@ -122,7 +122,7 @@ area of Core API's and does not apply to internal types. ```ts // -- good -- - const createMock => { + const createMock = () => { const mocked: jest.Mocked = { start: jest.fn(), }; diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts index 5558e5156324d..be38a8225d0f4 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts @@ -93,6 +93,7 @@ describe('checking migration metadata changes on all registered SO types', () => "file-upload-usage-collection-telemetry": "c6fcb9a7efcf19b2bb66ca6e005bfee8961f6073", "fileShare": "f07d346acbb724eacf139a0fb781c38dc5280115", "fleet-fleet-server-host": "67180a54a689111fb46403c3603c9b3a329c698d", + "fleet-message-signing-keys": "0c6da6a680807e568540b2aa263ae52331ba66db", "fleet-preconfiguration-deletion-record": "3afad160748b430427086985a3445fd8697566d5", "fleet-proxy": "94d0a902a0fd22578d7d3a20873b95d902e25245", "graph-workspace": "565642a208fe7413b487aea979b5b153e4e74abe", diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts index 2c8dbabf878a1..7f4ca3fb70dc2 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts @@ -58,8 +58,9 @@ const previouslyRegisteredTypes = [ 'fleet-agent-events', 'fleet-agents', 'fleet-enrollment-api-keys', - 'fleet-preconfiguration-deletion-record', 'fleet-fleet-server-host', + 'fleet-message-signing-keys', + 'fleet-preconfiguration-deletion-record', 'fleet-proxy', 'graph-workspace', 'guided-setup-state', diff --git a/src/core/server/integration_tests/saved_objects/routes/bulk_create.test.ts b/src/core/server/integration_tests/saved_objects/routes/bulk_create.test.ts index aba1f322f3bd2..096d7f330abca 100644 --- a/src/core/server/integration_tests/saved_objects/routes/bulk_create.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/bulk_create.test.ts @@ -19,6 +19,7 @@ import { type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; import { createHiddenTypeVariants, setupServer } from '@kbn/core-test-helpers-test-utils'; +import { loggerMock } from '@kbn/logging-mocks'; type SetupServerReturn = Awaited>; @@ -33,6 +34,7 @@ describe('POST /api/saved_objects/_bulk_create', () => { let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; + let loggerWarnSpy: jest.SpyInstance; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); @@ -50,7 +52,9 @@ describe('POST /api/saved_objects/_bulk_create', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsBulkCreate.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); - registerBulkCreateRoute(router, { coreUsageData }); + const logger = loggerMock.create(); + loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registerBulkCreateRoute(router, { coreUsageData, logger }); await server.start(); }); @@ -161,4 +165,21 @@ describe('POST /api/saved_objects/_bulk_create', () => { 'Unsupported saved object type(s): hidden-from-http: Bad Request' ); }); + + it('logs a warning message when called', async () => { + await supertest(httpSetup.server.listener) + .post('/api/saved_objects/_bulk_create') + .send([ + { + id: 'abc1234', + type: 'index-pattern', + attributes: { + title: 'foo', + }, + references: [], + }, + ]) + .expect(200); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/bulk_delete.test.ts b/src/core/server/integration_tests/saved_objects/routes/bulk_delete.test.ts index f05780cc0fd65..47559aecf9769 100644 --- a/src/core/server/integration_tests/saved_objects/routes/bulk_delete.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/bulk_delete.test.ts @@ -18,6 +18,7 @@ import { registerBulkDeleteRoute, type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; +import { loggerMock } from '@kbn/logging-mocks'; type SetupServerReturn = Awaited>; @@ -32,6 +33,7 @@ describe('POST /api/saved_objects/_bulk_delete', () => { let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; + let loggerWarnSpy: jest.SpyInstance; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); @@ -52,7 +54,9 @@ describe('POST /api/saved_objects/_bulk_delete', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsBulkDelete.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); - registerBulkDeleteRoute(router, { coreUsageData }); + const logger = loggerMock.create(); + loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registerBulkDeleteRoute(router, { coreUsageData, logger }); await server.start(); }); @@ -133,4 +137,17 @@ describe('POST /api/saved_objects/_bulk_delete', () => { .expect(400); expect(result.body.message).toContain('Unsupported saved object type(s):'); }); + + it('logs a warning message when called', async () => { + await supertest(httpSetup.server.listener) + .post('/api/saved_objects/_bulk_delete') + .send([ + { + id: 'hiddenID', + type: 'hidden-from-http', + }, + ]) + .expect(400); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/bulk_get.test.ts b/src/core/server/integration_tests/saved_objects/routes/bulk_get.test.ts index fdd23217891c4..7c894c250dceb 100644 --- a/src/core/server/integration_tests/saved_objects/routes/bulk_get.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/bulk_get.test.ts @@ -18,6 +18,7 @@ import { registerBulkGetRoute, type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; +import { loggerMock } from '@kbn/logging-mocks'; type SetupServerReturn = Awaited>; @@ -32,6 +33,7 @@ describe('POST /api/saved_objects/_bulk_get', () => { let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; + let loggerWarnSpy: jest.SpyInstance; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); @@ -51,7 +53,9 @@ describe('POST /api/saved_objects/_bulk_get', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsBulkGet.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); - registerBulkGetRoute(router, { coreUsageData }); + const logger = loggerMock.create(); + loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registerBulkGetRoute(router, { coreUsageData, logger }); await server.start(); }); @@ -120,4 +124,17 @@ describe('POST /api/saved_objects/_bulk_get', () => { .expect(400); expect(result.body.message).toContain('Unsupported saved object type(s):'); }); + + it('logs a warning message when called', async () => { + await supertest(httpSetup.server.listener) + .post('/api/saved_objects/_bulk_get') + .send([ + { + id: 'abc123', + type: 'index-pattern', + }, + ]) + .expect(200); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/bulk_resolve.test.ts b/src/core/server/integration_tests/saved_objects/routes/bulk_resolve.test.ts index 3ea1e0c9580ac..98253fabb2fa4 100644 --- a/src/core/server/integration_tests/saved_objects/routes/bulk_resolve.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/bulk_resolve.test.ts @@ -18,6 +18,7 @@ import { registerBulkResolveRoute, type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; +import { loggerMock } from '@kbn/logging-mocks'; type SetupServerReturn = Awaited>; @@ -32,6 +33,7 @@ describe('POST /api/saved_objects/_bulk_resolve', () => { let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; + let loggerWarnSpy: jest.SpyInstance; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); @@ -52,7 +54,9 @@ describe('POST /api/saved_objects/_bulk_resolve', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsBulkResolve.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); - registerBulkResolveRoute(router, { coreUsageData }); + const logger = loggerMock.create(); + loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registerBulkResolveRoute(router, { coreUsageData, logger }); await server.start(); }); @@ -124,4 +128,17 @@ describe('POST /api/saved_objects/_bulk_resolve', () => { .expect(400); expect(result.body.message).toContain('Unsupported saved object type(s):'); }); + + it('logs a warning message when called', async () => { + await supertest(httpSetup.server.listener) + .post('/api/saved_objects/_bulk_resolve') + .send([ + { + id: 'abc123', + type: 'index-pattern', + }, + ]) + .expect(200); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/bulk_update.test.ts b/src/core/server/integration_tests/saved_objects/routes/bulk_update.test.ts index 04ad9dcd40592..eb50fd141e2af 100644 --- a/src/core/server/integration_tests/saved_objects/routes/bulk_update.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/bulk_update.test.ts @@ -18,6 +18,7 @@ import { registerBulkUpdateRoute, type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; +import { loggerMock } from '@kbn/logging-mocks'; type SetupServerReturn = Awaited>; @@ -34,6 +35,7 @@ describe('PUT /api/saved_objects/_bulk_update', () => { let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; + let loggerWarnSpy: jest.SpyInstance; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); @@ -51,7 +53,9 @@ describe('PUT /api/saved_objects/_bulk_update', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsBulkUpdate.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); - registerBulkUpdateRoute(router, { coreUsageData }); + const logger = loggerMock.create(); + loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registerBulkUpdateRoute(router, { coreUsageData, logger }); await server.start(); }); @@ -169,4 +173,27 @@ describe('PUT /api/saved_objects/_bulk_update', () => { .expect(400); expect(result.body.message).toContain('Unsupported saved object type(s):'); }); + + it('logs a warning message when called', async () => { + await supertest(httpSetup.server.listener) + .put('/api/saved_objects/_bulk_update') + .send([ + { + type: 'visualization', + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + attributes: { + title: 'An existing visualization', + }, + }, + { + type: 'dashboard', + id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', + attributes: { + title: 'An existing dashboard', + }, + }, + ]) + .expect(200); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/create.test.ts b/src/core/server/integration_tests/saved_objects/routes/create.test.ts index 50791be209547..bf67325a8e756 100644 --- a/src/core/server/integration_tests/saved_objects/routes/create.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/create.test.ts @@ -18,6 +18,7 @@ import { registerCreateRoute, type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; +import { loggerMock } from '@kbn/logging-mocks'; type SetupServerReturn = Awaited>; @@ -32,6 +33,7 @@ describe('POST /api/saved_objects/{type}', () => { let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; + let loggerWarnSpy: jest.SpyInstance; const clientResponse = { id: 'logstash-*', @@ -52,7 +54,9 @@ describe('POST /api/saved_objects/{type}', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsCreate.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); - registerCreateRoute(router, { coreUsageData }); + const logger = loggerMock.create(); + loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registerCreateRoute(router, { coreUsageData, logger }); handlerContext.savedObjects.typeRegistry.getType.mockImplementation((typename: string) => { return testTypes @@ -145,4 +149,16 @@ describe('POST /api/saved_objects/{type}', () => { expect(result.body.message).toContain("Unsupported saved object type: 'hidden-from-http'"); }); + + it('logs a warning message when called', async () => { + await supertest(httpSetup.server.listener) + .post('/api/saved_objects/index-pattern') + .send({ + attributes: { + title: 'Logging test', + }, + }) + .expect(200); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/delete.test.ts b/src/core/server/integration_tests/saved_objects/routes/delete.test.ts index 42c97c0c565da..538cc2d721485 100644 --- a/src/core/server/integration_tests/saved_objects/routes/delete.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/delete.test.ts @@ -18,6 +18,7 @@ import { registerDeleteRoute, type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; +import { loggerMock } from '@kbn/logging-mocks'; type SetupServerReturn = Awaited>; @@ -33,6 +34,7 @@ describe('DELETE /api/saved_objects/{type}/{id}', () => { let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; + let loggerWarnSpy: jest.SpyInstance; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); @@ -49,7 +51,9 @@ describe('DELETE /api/saved_objects/{type}/{id}', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsDelete.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); - registerDeleteRoute(router, { coreUsageData }); + const logger = loggerMock.create(); + loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registerDeleteRoute(router, { coreUsageData, logger }); await server.start(); }); @@ -104,4 +108,11 @@ describe('DELETE /api/saved_objects/{type}/{id}', () => { .expect(400); expect(result.body.message).toContain("Unsupported saved object type: 'hidden-from-http'"); }); + + it('logs a warning message when called', async () => { + await supertest(httpSetup.server.listener) + .delete('/api/saved_objects/index-pattern/logstash-*') + .expect(200); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/find.test.ts b/src/core/server/integration_tests/saved_objects/routes/find.test.ts index 2185edd9f5e19..25fd8a32fc9ef 100644 --- a/src/core/server/integration_tests/saved_objects/routes/find.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/find.test.ts @@ -16,6 +16,7 @@ import { coreUsageDataServiceMock, } from '@kbn/core-usage-data-server-mocks'; import { createHiddenTypeVariants, setupServer } from '@kbn/core-test-helpers-test-utils'; +import { loggerMock } from '@kbn/logging-mocks'; import { registerFindRoute, type InternalSavedObjectsRequestHandlerContext, @@ -38,6 +39,7 @@ describe('GET /api/saved_objects/_find', () => { let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; + let loggerWarnSpy: jest.SpyInstance; const clientResponse = { total: 0, @@ -64,7 +66,9 @@ describe('GET /api/saved_objects/_find', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsFind.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); - registerFindRoute(router, { coreUsageData }); + const logger = loggerMock.create(); + loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registerFindRoute(router, { coreUsageData, logger }); await server.start(); }); @@ -428,4 +432,11 @@ describe('GET /api/saved_objects/_find', () => { }) ); }); + + it('logs a warning message when called', async () => { + await supertest(httpSetup.server.listener) + .get('/api/saved_objects/_find?type=foo&type=bar') + .expect(200); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/get.test.ts b/src/core/server/integration_tests/saved_objects/routes/get.test.ts index efd7f25938592..363f0406b2148 100644 --- a/src/core/server/integration_tests/saved_objects/routes/get.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/get.test.ts @@ -23,6 +23,7 @@ import { type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; import { createHiddenTypeVariants } from '@kbn/core-test-helpers-test-utils'; +import { loggerMock } from '@kbn/logging-mocks'; const coreId = Symbol('core'); @@ -38,6 +39,7 @@ describe('GET /api/saved_objects/{type}/{id}', () => { let handlerContext: ReturnType; let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; + let loggerWarnSpy: jest.SpyInstance; beforeEach(async () => { const coreContext = createCoreContext({ coreId }); @@ -72,7 +74,9 @@ describe('GET /api/saved_objects/{type}/{id}', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsGet.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); - registerGetRoute(router, { coreUsageData }); + const logger = loggerMock.create(); + loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registerGetRoute(router, { coreUsageData, logger }); await server.start(); }); @@ -121,4 +125,11 @@ describe('GET /api/saved_objects/{type}/{id}', () => { .expect(400); expect(result.body.message).toContain("Unsupported saved object type: 'hidden-from-http'"); }); + + it('logs a warning message when called', async () => { + await supertest(httpSetup.server.listener) + .get('/api/saved_objects/index-pattern/logstash-*') + .expect(200); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/resolve.test.ts b/src/core/server/integration_tests/saved_objects/routes/resolve.test.ts index 6f58e31334764..0ecc6221730ff 100644 --- a/src/core/server/integration_tests/saved_objects/routes/resolve.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/resolve.test.ts @@ -23,6 +23,7 @@ import { type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; import { createHiddenTypeVariants } from '@kbn/core-test-helpers-test-utils'; +import { loggerMock } from '@kbn/logging-mocks'; const coreId = Symbol('core'); @@ -38,6 +39,7 @@ describe('GET /api/saved_objects/resolve/{type}/{id}', () => { let handlerContext: ReturnType; let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; + let loggerWarnSpy: jest.SpyInstance; beforeEach(async () => { const coreContext = createCoreContext({ coreId }); @@ -73,7 +75,9 @@ describe('GET /api/saved_objects/resolve/{type}/{id}', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsResolve.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); - registerResolveRoute(router, { coreUsageData }); + const logger = loggerMock.create(); + loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registerResolveRoute(router, { coreUsageData, logger }); await server.start(); }); @@ -122,4 +126,11 @@ describe('GET /api/saved_objects/resolve/{type}/{id}', () => { .expect(400); expect(result.body.message).toContain("Unsupported saved object type: 'hidden-from-http'"); }); + + it('logs a warning message when called', async () => { + await supertest(httpSetup.server.listener) + .get('/api/saved_objects/resolve/index-pattern/logstash-*') + .expect(200); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/core/server/integration_tests/saved_objects/routes/update.test.ts b/src/core/server/integration_tests/saved_objects/routes/update.test.ts index 639f503d050d7..8333159b8e1c6 100644 --- a/src/core/server/integration_tests/saved_objects/routes/update.test.ts +++ b/src/core/server/integration_tests/saved_objects/routes/update.test.ts @@ -18,6 +18,7 @@ import { registerUpdateRoute, type InternalSavedObjectsRequestHandlerContext, } from '@kbn/core-saved-objects-server-internal'; +import { loggerMock } from '@kbn/logging-mocks'; type SetupServerReturn = Awaited>; @@ -33,6 +34,7 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => { let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; let coreUsageStatsClient: jest.Mocked; + let loggerWarnSpy: jest.SpyInstance; beforeEach(async () => { const clientResponse = { @@ -60,7 +62,9 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => { coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsUpdate.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); - registerUpdateRoute(router, { coreUsageData }); + const logger = loggerMock.create(); + loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + registerUpdateRoute(router, { coreUsageData, logger }); await server.start(); }); @@ -123,4 +127,12 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => { .expect(400); expect(result.body.message).toContain("Unsupported saved object type: 'hidden-from-http'"); }); + + it('logs a warning message when called', async () => { + await supertest(httpSetup.server.listener) + .put('/api/saved_objects/index-pattern/logstash-*') + .send({ attributes: { title: 'Logging test' }, version: 'log' }) + .expect(200); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/x-pack/plugins/alerting/server/routes/get_flapping_settings.test.ts b/x-pack/plugins/alerting/server/routes/get_flapping_settings.test.ts index 156ab604fb905..80354da80b784 100644 --- a/x-pack/plugins/alerting/server/routes/get_flapping_settings.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_flapping_settings.test.ts @@ -58,6 +58,16 @@ describe('getFlappingSettingsRoute', () => { await handler(context, req, res); expect(rulesSettingsClient.flapping().get).toHaveBeenCalledTimes(1); - expect(res.ok).toHaveBeenCalled(); + expect(res.ok).toHaveBeenCalledWith({ + body: expect.objectContaining({ + enabled: true, + look_back_window: 10, + status_change_threshold: 10, + created_by: 'test name', + updated_by: 'test name', + created_at: expect.any(String), + updated_at: expect.any(String), + }), + }); }); }); diff --git a/x-pack/plugins/alerting/server/routes/get_flapping_settings.ts b/x-pack/plugins/alerting/server/routes/get_flapping_settings.ts index 6ae039032994d..5d4795d664ed5 100644 --- a/x-pack/plugins/alerting/server/routes/get_flapping_settings.ts +++ b/x-pack/plugins/alerting/server/routes/get_flapping_settings.ts @@ -8,8 +8,26 @@ import { IRouter } from '@kbn/core/server'; import { ILicenseState } from '../lib'; import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types'; -import { verifyAccessAndContext } from './lib'; -import { API_PRIVILEGES } from '../../common'; +import { verifyAccessAndContext, RewriteResponseCase } from './lib'; +import { API_PRIVILEGES, RulesSettingsFlapping } from '../../common'; + +const rewriteBodyRes: RewriteResponseCase = ({ + lookBackWindow, + statusChangeThreshold, + createdBy, + updatedBy, + createdAt, + updatedAt, + ...rest +}) => ({ + ...rest, + look_back_window: lookBackWindow, + status_change_threshold: statusChangeThreshold, + created_by: createdBy, + updated_by: updatedBy, + created_at: createdAt, + updated_at: updatedAt, +}); export const getFlappingSettingsRoute = ( router: IRouter, @@ -27,7 +45,7 @@ export const getFlappingSettingsRoute = ( verifyAccessAndContext(licenseState, async function (context, req, res) { const rulesSettingsClient = (await context.alerting).getRulesSettingsClient(); const flappingSettings = await rulesSettingsClient.flapping().get(); - return res.ok({ body: flappingSettings }); + return res.ok({ body: rewriteBodyRes(flappingSettings) }); }) ) ); diff --git a/x-pack/plugins/alerting/server/routes/update_flapping_settings.test.ts b/x-pack/plugins/alerting/server/routes/update_flapping_settings.test.ts index 28914e71e7dd3..84fb238b8509b 100644 --- a/x-pack/plugins/alerting/server/routes/update_flapping_settings.test.ts +++ b/x-pack/plugins/alerting/server/routes/update_flapping_settings.test.ts @@ -22,6 +22,16 @@ beforeEach(() => { rulesSettingsClient = rulesSettingsClientMock.create(); }); +const mockFlappingSettings = { + enabled: true, + lookBackWindow: 10, + statusChangeThreshold: 10, + createdBy: 'test name', + updatedBy: 'test name', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), +}; + describe('updateFlappingSettingsRoute', () => { test('updates flapping settings', async () => { const licenseState = licenseStateMock.create(); @@ -40,20 +50,13 @@ describe('updateFlappingSettingsRoute', () => { } `); - (rulesSettingsClient.flapping().get as jest.Mock).mockResolvedValue({ - enabled: true, - lookBackWindow: 10, - statusChangeThreshold: 10, - createdBy: 'test name', - updatedBy: 'test name', - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }); + (rulesSettingsClient.flapping().get as jest.Mock).mockResolvedValue(mockFlappingSettings); + (rulesSettingsClient.flapping().update as jest.Mock).mockResolvedValue(mockFlappingSettings); const updateResult = { enabled: false, - lookBackWindow: 6, - statusChangeThreshold: 5, + look_back_window: 6, + status_change_threshold: 5, }; const [context, req, res] = mockHandlerArguments( @@ -77,6 +80,16 @@ describe('updateFlappingSettingsRoute', () => { }, ] `); - expect(res.ok).toHaveBeenCalled(); + expect(res.ok).toHaveBeenCalledWith({ + body: expect.objectContaining({ + enabled: true, + look_back_window: 10, + status_change_threshold: 10, + created_by: 'test name', + updated_by: 'test name', + created_at: expect.any(String), + updated_at: expect.any(String), + }), + }); }); }); diff --git a/x-pack/plugins/alerting/server/routes/update_flapping_settings.ts b/x-pack/plugins/alerting/server/routes/update_flapping_settings.ts index ede33a7d36a95..6df16434d3833 100644 --- a/x-pack/plugins/alerting/server/routes/update_flapping_settings.ts +++ b/x-pack/plugins/alerting/server/routes/update_flapping_settings.ts @@ -8,14 +8,46 @@ import { IRouter } from '@kbn/core/server'; import { schema } from '@kbn/config-schema'; import { ILicenseState } from '../lib'; -import { verifyAccessAndContext } from './lib'; +import { verifyAccessAndContext, RewriteResponseCase, RewriteRequestCase } from './lib'; import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types'; -import { API_PRIVILEGES } from '../../common'; +import { + API_PRIVILEGES, + RulesSettingsFlapping, + RulesSettingsFlappingProperties, +} from '../../common'; const bodySchema = schema.object({ enabled: schema.boolean(), - lookBackWindow: schema.number(), - statusChangeThreshold: schema.number(), + look_back_window: schema.number(), + status_change_threshold: schema.number(), +}); + +const rewriteQueryReq: RewriteRequestCase = ({ + look_back_window: lookBackWindow, + status_change_threshold: statusChangeThreshold, + ...rest +}) => ({ + ...rest, + lookBackWindow, + statusChangeThreshold, +}); + +const rewriteBodyRes: RewriteResponseCase = ({ + lookBackWindow, + statusChangeThreshold, + createdBy, + updatedBy, + createdAt, + updatedAt, + ...rest +}) => ({ + ...rest, + look_back_window: lookBackWindow, + status_change_threshold: statusChangeThreshold, + created_by: createdBy, + updated_by: updatedBy, + created_at: createdAt, + updated_at: updatedAt, }); export const updateFlappingSettingsRoute = ( @@ -36,10 +68,12 @@ export const updateFlappingSettingsRoute = ( verifyAccessAndContext(licenseState, async function (context, req, res) { const rulesSettingsClient = (await context.alerting).getRulesSettingsClient(); - const updatedFlappingSettings = await rulesSettingsClient.flapping().update(req.body); + const updatedFlappingSettings = await rulesSettingsClient + .flapping() + .update(rewriteQueryReq(req.body)); return res.ok({ - body: updatedFlappingSettings, + body: updatedFlappingSettings && rewriteBodyRes(updatedFlappingSettings), }); }) ) diff --git a/x-pack/plugins/apm/common/rules/default_action_message.ts b/x-pack/plugins/apm/common/rules/default_action_message.ts new file mode 100644 index 0000000000000..503bc1ca3cd26 --- /dev/null +++ b/x-pack/plugins/apm/common/rules/default_action_message.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 { i18n } from '@kbn/i18n'; + +export const errorCountMessage = i18n.translate( + 'xpack.apm.alertTypes.errorCount.defaultActionMessage', + { + defaultMessage: `\\{\\{alertName\\}\\} alert is firing because of the following conditions: + +- Service name: \\{\\{context.serviceName\\}\\} +- Environment: \\{\\{context.environment\\}\\} +- Threshold: \\{\\{context.threshold\\}\\} +- Triggered value: \\{\\{context.triggerValue\\}\\} errors over the last \\{\\{context.interval\\}\\}`, + } +); + +export const transactionDurationMessage = i18n.translate( + 'xpack.apm.alertTypes.transactionDuration.defaultActionMessage', + { + defaultMessage: `\\{\\{alertName\\}\\} alert is firing because of the following conditions: + +- Service name: \\{\\{context.serviceName\\}\\} +- Type: \\{\\{context.transactionType\\}\\} +- Environment: \\{\\{context.environment\\}\\} +- Latency threshold: \\{\\{context.threshold\\}\\}ms +- Latency observed: \\{\\{context.triggerValue\\}\\} over the last \\{\\{context.interval\\}\\}`, + } +); + +export const transactionErrorRateMessage = i18n.translate( + 'xpack.apm.alertTypes.transactionErrorRate.defaultActionMessage', + { + defaultMessage: `\\{\\{alertName\\}\\} alert is firing because of the following conditions: + +- Service name: \\{\\{context.serviceName\\}\\} +- Type: \\{\\{context.transactionType\\}\\} +- Environment: \\{\\{context.environment\\}\\} +- Threshold: \\{\\{context.threshold\\}\\}% +- Triggered value: \\{\\{context.triggerValue\\}\\}% of errors over the last \\{\\{context.interval\\}\\}`, + } +); + +export const anomalyMessage = i18n.translate( + 'xpack.apm.alertTypes.transactionDurationAnomaly.defaultActionMessage', + { + defaultMessage: `\\{\\{alertName\\}\\} alert is firing because of the following conditions: + +- Service name: \\{\\{context.serviceName\\}\\} +- Type: \\{\\{context.transactionType\\}\\} +- Environment: \\{\\{context.environment\\}\\} +- Severity threshold: \\{\\{context.threshold\\}\\} +- Severity value: \\{\\{context.triggerValue\\}\\} +`, + } +); diff --git a/x-pack/plugins/apm/common/rules/schema.ts b/x-pack/plugins/apm/common/rules/schema.ts new file mode 100644 index 0000000000000..58a5b40da41f2 --- /dev/null +++ b/x-pack/plugins/apm/common/rules/schema.ts @@ -0,0 +1,71 @@ +/* + * 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, TypeOf } from '@kbn/config-schema'; +import { ANOMALY_SEVERITY } from '../ml_constants'; +import { AggregationType, ApmRuleType } from './apm_rule_types'; + +export const errorCountParamsSchema = schema.object({ + windowSize: schema.number(), + windowUnit: schema.string(), + threshold: schema.number(), + serviceName: schema.maybe(schema.string()), + environment: schema.string(), +}); + +export const transactionDurationParamsSchema = schema.object({ + serviceName: schema.maybe(schema.string()), + transactionType: schema.maybe(schema.string()), + windowSize: schema.number(), + windowUnit: schema.string(), + threshold: schema.number(), + aggregationType: schema.oneOf([ + schema.literal(AggregationType.Avg), + schema.literal(AggregationType.P95), + schema.literal(AggregationType.P99), + ]), + environment: schema.string(), +}); + +export const anomalyParamsSchema = schema.object({ + serviceName: schema.maybe(schema.string()), + transactionType: schema.maybe(schema.string()), + windowSize: schema.number(), + windowUnit: schema.string(), + environment: schema.string(), + anomalySeverityType: schema.oneOf([ + schema.literal(ANOMALY_SEVERITY.CRITICAL), + schema.literal(ANOMALY_SEVERITY.MAJOR), + schema.literal(ANOMALY_SEVERITY.MINOR), + schema.literal(ANOMALY_SEVERITY.WARNING), + ]), +}); + +export const transactionErrorRateParamsSchema = schema.object({ + windowSize: schema.number(), + windowUnit: schema.string(), + threshold: schema.number(), + transactionType: schema.maybe(schema.string()), + serviceName: schema.maybe(schema.string()), + environment: schema.string(), +}); + +type ErrorCountParamsType = TypeOf; +type TransactionDurationParamsType = TypeOf< + typeof transactionDurationParamsSchema +>; +type AnomalyParamsType = TypeOf; +type TransactionErrorRateParamsType = TypeOf< + typeof transactionErrorRateParamsSchema +>; + +export interface ApmRuleParamsType { + [ApmRuleType.TransactionDuration]: TransactionDurationParamsType; + [ApmRuleType.ErrorCount]: ErrorCountParamsType; + [ApmRuleType.Anomaly]: AnomalyParamsType; + [ApmRuleType.TransactionErrorRate]: TransactionErrorRateParamsType; +} diff --git a/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts b/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts index ed9971307bf64..f355ea4c2f6eb 100644 --- a/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts +++ b/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts @@ -14,6 +14,12 @@ import { getAlertUrlTransaction, } from '../../../../common/utils/formatters'; import { ApmRuleType } from '../../../../common/rules/apm_rule_types'; +import { + anomalyMessage, + errorCountMessage, + transactionDurationMessage, + transactionErrorRateMessage, +} from '../../../../common/rules/default_action_message'; // copied from elasticsearch_fieldnames.ts to limit page load bundle size const SERVICE_ENVIRONMENT = 'service.environment'; @@ -54,17 +60,7 @@ export function registerApmRuleTypes( ) ), requiresAppContext: false, - defaultActionMessage: i18n.translate( - 'xpack.apm.alertTypes.errorCount.defaultActionMessage', - { - defaultMessage: `\\{\\{alertName\\}\\} alert is firing because of the following conditions: - -- Service name: \\{\\{context.serviceName\\}\\} -- Environment: \\{\\{context.environment\\}\\} -- Threshold: \\{\\{context.threshold\\}\\} errors -- Triggered value: \\{\\{context.triggerValue\\}\\} errors over the last \\{\\{context.interval\\}\\}`, - } - ), + defaultActionMessage: errorCountMessage, }); observabilityRuleTypeRegistry.register({ @@ -104,18 +100,7 @@ export function registerApmRuleTypes( ) ), requiresAppContext: false, - defaultActionMessage: i18n.translate( - 'xpack.apm.alertTypes.transactionDuration.defaultActionMessage', - { - defaultMessage: `\\{\\{alertName\\}\\} alert is firing because of the following conditions: - -- Service name: \\{\\{context.serviceName\\}\\} -- Type: \\{\\{context.transactionType\\}\\} -- Environment: \\{\\{context.environment\\}\\} -- Latency threshold: \\{\\{context.threshold\\}\\}ms -- Latency observed: \\{\\{context.triggerValue\\}\\} over the last \\{\\{context.interval\\}\\}`, - } - ), + defaultActionMessage: transactionDurationMessage, }); observabilityRuleTypeRegistry.register({ @@ -153,18 +138,7 @@ export function registerApmRuleTypes( ) ), requiresAppContext: false, - defaultActionMessage: i18n.translate( - 'xpack.apm.alertTypes.transactionErrorRate.defaultActionMessage', - { - defaultMessage: `\\{\\{alertName\\}\\} alert is firing because of the following conditions: - -- Service name: \\{\\{context.serviceName\\}\\} -- Type: \\{\\{context.transactionType\\}\\} -- Environment: \\{\\{context.environment\\}\\} -- Threshold: \\{\\{context.threshold\\}\\}% -- Triggered value: \\{\\{context.triggerValue\\}\\}% of errors over the last \\{\\{context.interval\\}\\}`, - } - ), + defaultActionMessage: transactionErrorRateMessage, }); observabilityRuleTypeRegistry.register({ @@ -199,18 +173,6 @@ export function registerApmRuleTypes( ) ), requiresAppContext: false, - defaultActionMessage: i18n.translate( - 'xpack.apm.alertTypes.transactionDurationAnomaly.defaultActionMessage', - { - defaultMessage: `\\{\\{alertName\\}\\} alert is firing because of the following conditions: - -- Service name: \\{\\{context.serviceName\\}\\} -- Type: \\{\\{context.transactionType\\}\\} -- Environment: \\{\\{context.environment\\}\\} -- Severity threshold: \\{\\{context.threshold\\}\\} -- Severity value: \\{\\{context.triggerValue\\}\\} -`, - } - ), + defaultActionMessage: anomalyMessage, }); } diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts index 529efb583b7dd..a10d803ef6d86 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts @@ -5,7 +5,6 @@ * 2.0. */ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { schema } from '@kbn/config-schema'; import { KibanaRequest } from '@kbn/core/server'; import datemath from '@kbn/datemath'; import type { ESSearchResponse } from '@kbn/es-types'; @@ -36,7 +35,6 @@ import { getEnvironmentEsField, getEnvironmentLabel, } from '../../../../../common/environment_filter_values'; -import { ANOMALY_SEVERITY } from '../../../../../common/ml_constants'; import { ANOMALY_ALERT_SEVERITY_TYPES, ApmRuleType, @@ -49,20 +47,7 @@ import { getMLJobs } from '../../../service_map/get_service_anomalies'; import { apmActionVariables } from '../../action_variables'; import { RegisterRuleDependencies } from '../../register_apm_rule_types'; import { getServiceGroupFieldsForAnomaly } from './get_service_group_fields_for_anomaly'; - -const paramsSchema = schema.object({ - serviceName: schema.maybe(schema.string()), - transactionType: schema.maybe(schema.string()), - windowSize: schema.number(), - windowUnit: schema.string(), - environment: schema.string(), - anomalySeverityType: schema.oneOf([ - schema.literal(ANOMALY_SEVERITY.CRITICAL), - schema.literal(ANOMALY_SEVERITY.MAJOR), - schema.literal(ANOMALY_SEVERITY.MINOR), - schema.literal(ANOMALY_SEVERITY.WARNING), - ]), -}); +import { anomalyParamsSchema } from '../../../../../common/rules/schema'; const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.Anomaly]; @@ -86,9 +71,7 @@ export function registerAnomalyRuleType({ name: ruleTypeConfig.name, actionGroups: ruleTypeConfig.actionGroups, defaultActionGroupId: ruleTypeConfig.defaultActionGroupId, - validate: { - params: paramsSchema, - }, + validate: { params: anomalyParamsSchema }, actionVariables: { context: [ ...(observability.getAlertDetailsConfig()?.apm.enabled diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.test.ts index 705804cfa74f4..6d6752eba0ce5 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.test.ts @@ -146,7 +146,7 @@ describe('Error count alert', () => { threshold: 2, triggerValue: 5, reason: 'Error count is 5 in the last 5 mins for foo. Alert when > 2.', - interval: '5m', + interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo', }); @@ -159,7 +159,7 @@ describe('Error count alert', () => { threshold: 2, triggerValue: 4, reason: 'Error count is 4 in the last 5 mins for foo. Alert when > 2.', - interval: '5m', + interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo-2', }); @@ -172,7 +172,7 @@ describe('Error count alert', () => { reason: 'Error count is 3 in the last 5 mins for bar. Alert when > 2.', threshold: 2, triggerValue: 3, - interval: '5m', + interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/bar/errors?environment=env-bar', }); diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts index 54bfb00d468b0..c811e71fe1f17 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts @@ -5,54 +5,49 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; -import { firstValueFrom } from 'rxjs'; +import { getAlertDetailsUrl } from '@kbn/infra-plugin/server/lib/alerting/common/utils'; +import { + formatDurationFromTimeUnitChar, + ProcessorEvent, + TimeUnitChar, +} from '@kbn/observability-plugin/common'; +import { termQuery } from '@kbn/observability-plugin/server'; import { ALERT_EVALUATION_THRESHOLD, ALERT_EVALUATION_VALUE, ALERT_REASON, } from '@kbn/rule-data-utils'; import { createLifecycleRuleTypeFactory } from '@kbn/rule-registry-plugin/server'; -import { termQuery } from '@kbn/observability-plugin/server'; -import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { getAlertDetailsUrl } from '@kbn/infra-plugin/server/lib/alerting/common/utils'; import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; - +import { firstValueFrom } from 'rxjs'; import { ENVIRONMENT_NOT_DEFINED, getEnvironmentEsField, getEnvironmentLabel, } from '../../../../../common/environment_filter_values'; -import { getAlertUrlErrorCount } from '../../../../../common/utils/formatters'; -import { - ApmRuleType, - APM_SERVER_FEATURE_ID, - RULE_TYPES_CONFIG, - formatErrorCountReason, -} from '../../../../../common/rules/apm_rule_types'; import { PROCESSOR_EVENT, SERVICE_ENVIRONMENT, SERVICE_NAME, } from '../../../../../common/es_fields/apm'; +import { + ApmRuleType, + APM_SERVER_FEATURE_ID, + formatErrorCountReason, + RULE_TYPES_CONFIG, +} from '../../../../../common/rules/apm_rule_types'; +import { errorCountParamsSchema } from '../../../../../common/rules/schema'; import { environmentQuery } from '../../../../../common/utils/environment_query'; +import { getAlertUrlErrorCount } from '../../../../../common/utils/formatters'; import { getApmIndices } from '../../../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from '../../action_variables'; import { alertingEsClient } from '../../alerting_es_client'; import { RegisterRuleDependencies } from '../../register_apm_rule_types'; import { - getServiceGroupFieldsAgg, getServiceGroupFields, + getServiceGroupFieldsAgg, } from '../get_service_group_fields'; -const paramsSchema = schema.object({ - windowSize: schema.number(), - windowUnit: schema.string(), - threshold: schema.number(), - serviceName: schema.maybe(schema.string()), - environment: schema.string(), -}); - const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.ErrorCount]; export function registerErrorCountRuleType({ @@ -74,9 +69,7 @@ export function registerErrorCountRuleType({ name: ruleTypeConfig.name, actionGroups: ruleTypeConfig.actionGroups, defaultActionGroupId: ruleTypeConfig.defaultActionGroupId, - validate: { - params: paramsSchema, - }, + validate: { params: errorCountParamsSchema }, actionVariables: { context: [ ...(observability.getAlertDetailsConfig()?.apm.enabled @@ -214,7 +207,10 @@ export function registerErrorCountRuleType({ .scheduleActions(ruleTypeConfig.defaultActionGroupId, { alertDetailsUrl, environment: getEnvironmentLabel(environment), - interval: `${ruleParams.windowSize}${ruleParams.windowUnit}`, + interval: formatDurationFromTimeUnitChar( + ruleParams.windowSize, + ruleParams.windowUnit as TimeUnitChar + ), reason: alertReason, serviceName, threshold: ruleParams.threshold, diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.test.ts index cebf71f1bad97..dcd8994860ae7 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.test.ts @@ -60,7 +60,7 @@ describe('registerTransactionDurationRuleType', () => { 'http://localhost:5601/eyr/app/observability/alerts/' ), environment: 'Not defined', - interval: `5m`, + interval: `5 mins`, reason: 'Avg. latency is 5,500 ms in the last 5 mins for opbeans-java. Alert when > 3,000 ms.', transactionType: 'request', diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts index 803945568494b..bc11cee03a506 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts @@ -6,41 +6,46 @@ */ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { schema } from '@kbn/config-schema'; +import { getAlertDetailsUrl } from '@kbn/infra-plugin/server/lib/alerting/common/utils'; +import { + formatDurationFromTimeUnitChar, + ProcessorEvent, + TimeUnitChar, +} from '@kbn/observability-plugin/common'; +import { asDuration } from '@kbn/observability-plugin/common/utils/formatters'; +import { termQuery } from '@kbn/observability-plugin/server'; import { ALERT_EVALUATION_THRESHOLD, ALERT_EVALUATION_VALUE, ALERT_REASON, } from '@kbn/rule-data-utils'; -import { firstValueFrom } from 'rxjs'; -import { asDuration } from '@kbn/observability-plugin/common/utils/formatters'; -import { termQuery } from '@kbn/observability-plugin/server'; import { createLifecycleRuleTypeFactory } from '@kbn/rule-registry-plugin/server'; -import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { getAlertDetailsUrl } from '@kbn/infra-plugin/server/lib/alerting/common/utils'; import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; -import { getAlertUrlTransaction } from '../../../../../common/utils/formatters'; +import { firstValueFrom } from 'rxjs'; import { SearchAggregatedTransactionSetting } from '../../../../../common/aggregated_transactions'; import { - ApmRuleType, - AggregationType, - RULE_TYPES_CONFIG, - APM_SERVER_FEATURE_ID, - formatTransactionDurationReason, -} from '../../../../../common/rules/apm_rule_types'; + ENVIRONMENT_NOT_DEFINED, + getEnvironmentEsField, + getEnvironmentLabel, +} from '../../../../../common/environment_filter_values'; import { PROCESSOR_EVENT, + SERVICE_ENVIRONMENT, SERVICE_NAME, TRANSACTION_TYPE, - SERVICE_ENVIRONMENT, } from '../../../../../common/es_fields/apm'; import { - ENVIRONMENT_NOT_DEFINED, - getEnvironmentEsField, - getEnvironmentLabel, -} from '../../../../../common/environment_filter_values'; + ApmRuleType, + APM_SERVER_FEATURE_ID, + formatTransactionDurationReason, + RULE_TYPES_CONFIG, +} from '../../../../../common/rules/apm_rule_types'; +import { transactionDurationParamsSchema } from '../../../../../common/rules/schema'; import { environmentQuery } from '../../../../../common/utils/environment_query'; -import { getDurationFormatter } from '../../../../../common/utils/formatters'; +import { + getAlertUrlTransaction, + getDurationFormatter, +} from '../../../../../common/utils/formatters'; import { getDocumentTypeFilterForTransactions, getDurationFieldForTransactions, @@ -49,28 +54,14 @@ import { getApmIndices } from '../../../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from '../../action_variables'; import { alertingEsClient } from '../../alerting_es_client'; import { RegisterRuleDependencies } from '../../register_apm_rule_types'; -import { - averageOrPercentileAgg, - getMultiTermsSortOrder, -} from './average_or_percentile_agg'; import { getServiceGroupFields, getServiceGroupFieldsAgg, } from '../get_service_group_fields'; - -const paramsSchema = schema.object({ - serviceName: schema.maybe(schema.string()), - transactionType: schema.maybe(schema.string()), - windowSize: schema.number(), - windowUnit: schema.string(), - threshold: schema.number(), - aggregationType: schema.oneOf([ - schema.literal(AggregationType.Avg), - schema.literal(AggregationType.P95), - schema.literal(AggregationType.P99), - ]), - environment: schema.string(), -}); +import { + averageOrPercentileAgg, + getMultiTermsSortOrder, +} from './average_or_percentile_agg'; const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.TransactionDuration]; @@ -92,9 +83,7 @@ export function registerTransactionDurationRuleType({ name: ruleTypeConfig.name, actionGroups: ruleTypeConfig.actionGroups, defaultActionGroupId: ruleTypeConfig.defaultActionGroupId, - validate: { - params: paramsSchema, - }, + validate: { params: transactionDurationParamsSchema }, actionVariables: { context: [ ...(observability.getAlertDetailsConfig()?.apm.enabled @@ -289,7 +278,10 @@ export function registerTransactionDurationRuleType({ .scheduleActions(ruleTypeConfig.defaultActionGroupId, { alertDetailsUrl, environment: environmentLabel, - interval: `${ruleParams.windowSize}${ruleParams.windowUnit}`, + interval: formatDurationFromTimeUnitChar( + ruleParams.windowSize, + ruleParams.windowUnit as TimeUnitChar + ), reason, serviceName, threshold: ruleParams.threshold, diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.test.ts index 38de7d48cce4c..02eb14e782df3 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.test.ts @@ -131,7 +131,7 @@ describe('Transaction error rate alert', () => { 'Failed transactions is 10% in the last 5 mins for foo. Alert when > 10%.', threshold: 10, triggerValue: '10', - interval: '5m', + interval: '5 mins', viewInAppUrl: 'http://localhost:5601/eyr/app/apm/services/foo?transactionType=type-foo&environment=env-foo', }); diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts index 308ed32e3b5a1..df94a3e8a3c75 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts @@ -5,31 +5,28 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; -import { firstValueFrom } from 'rxjs'; +import { getAlertDetailsUrl } from '@kbn/infra-plugin/server/lib/alerting/common/utils'; +import { + formatDurationFromTimeUnitChar, + ProcessorEvent, + TimeUnitChar, +} from '@kbn/observability-plugin/common'; +import { asPercent } from '@kbn/observability-plugin/common/utils/formatters'; +import { termQuery } from '@kbn/observability-plugin/server'; import { ALERT_EVALUATION_THRESHOLD, ALERT_EVALUATION_VALUE, ALERT_REASON, } from '@kbn/rule-data-utils'; import { createLifecycleRuleTypeFactory } from '@kbn/rule-registry-plugin/server'; -import { asPercent } from '@kbn/observability-plugin/common/utils/formatters'; -import { termQuery } from '@kbn/observability-plugin/server'; import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; -import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { getAlertDetailsUrl } from '@kbn/infra-plugin/server/lib/alerting/common/utils'; +import { firstValueFrom } from 'rxjs'; +import { SearchAggregatedTransactionSetting } from '../../../../../common/aggregated_transactions'; import { ENVIRONMENT_NOT_DEFINED, getEnvironmentEsField, getEnvironmentLabel, } from '../../../../../common/environment_filter_values'; -import { getAlertUrlTransaction } from '../../../../../common/utils/formatters'; -import { - ApmRuleType, - RULE_TYPES_CONFIG, - APM_SERVER_FEATURE_ID, - formatTransactionErrorRateReason, -} from '../../../../../common/rules/apm_rule_types'; import { EVENT_OUTCOME, PROCESSOR_EVENT, @@ -38,28 +35,28 @@ import { TRANSACTION_TYPE, } from '../../../../../common/es_fields/apm'; import { EventOutcome } from '../../../../../common/event_outcome'; -import { asDecimalOrInteger } from '../../../../../common/utils/formatters'; +import { + ApmRuleType, + APM_SERVER_FEATURE_ID, + formatTransactionErrorRateReason, + RULE_TYPES_CONFIG, +} from '../../../../../common/rules/apm_rule_types'; +import { transactionErrorRateParamsSchema } from '../../../../../common/rules/schema'; import { environmentQuery } from '../../../../../common/utils/environment_query'; +import { + asDecimalOrInteger, + getAlertUrlTransaction, +} from '../../../../../common/utils/formatters'; +import { getDocumentTypeFilterForTransactions } from '../../../../lib/helpers/transactions'; import { getApmIndices } from '../../../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from '../../action_variables'; import { alertingEsClient } from '../../alerting_es_client'; import { RegisterRuleDependencies } from '../../register_apm_rule_types'; -import { SearchAggregatedTransactionSetting } from '../../../../../common/aggregated_transactions'; -import { getDocumentTypeFilterForTransactions } from '../../../../lib/helpers/transactions'; import { getServiceGroupFields, getServiceGroupFieldsAgg, } from '../get_service_group_fields'; -const paramsSchema = schema.object({ - windowSize: schema.number(), - windowUnit: schema.string(), - threshold: schema.number(), - transactionType: schema.maybe(schema.string()), - serviceName: schema.maybe(schema.string()), - environment: schema.string(), -}); - const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.TransactionErrorRate]; export function registerTransactionErrorRateRuleType({ @@ -81,9 +78,7 @@ export function registerTransactionErrorRateRuleType({ name: ruleTypeConfig.name, actionGroups: ruleTypeConfig.actionGroups, defaultActionGroupId: ruleTypeConfig.defaultActionGroupId, - validate: { - params: paramsSchema, - }, + validate: { params: transactionErrorRateParamsSchema }, actionVariables: { context: [ ...(observability.getAlertDetailsConfig()?.apm.enabled @@ -285,7 +280,10 @@ export function registerTransactionErrorRateRuleType({ .scheduleActions(ruleTypeConfig.defaultActionGroupId, { alertDetailsUrl, environment: getEnvironmentLabel(environment), - interval: `${ruleParams.windowSize}${ruleParams.windowUnit}`, + interval: formatDurationFromTimeUnitChar( + ruleParams.windowSize, + ruleParams.windowUnit as TimeUnitChar + ), reason: reasonMessage, serviceName, threshold: ruleParams.threshold, diff --git a/x-pack/plugins/cloud_security_posture/README.md b/x-pack/plugins/cloud_security_posture/README.md index a655d292c39ee..f9c760fbeb99f 100755 --- a/x-pack/plugins/cloud_security_posture/README.md +++ b/x-pack/plugins/cloud_security_posture/README.md @@ -10,7 +10,15 @@ read [Kibana Contributing Guide](https://github.com/elastic/kibana/blob/main/CON ## Testing -read [Kibana Testing Guide](https://www.elastic.co/guide/en/kibana/current/development-tests.html) for more details +for general guidelines, read [Kibana Testing Guide](https://www.elastic.co/guide/en/kibana/current/development-tests.html) for more details + +### Tests + +1. Unit Tests (Jest) - located in sibling files to the source code +2. [Integration Tests](../../test/api_integration/apis/cloud_security_posture/index.ts) +3. [End-to-End Tests](../../test/cloud_security_posture_functional/pages/index.ts) + +### Tools Run **TypeScript**: @@ -24,7 +32,7 @@ Run **ESLint**: yarn lint:es x-pack/plugins/cloud_security_posture ``` -Run **Unit Tests**: +Run [**Unit Tests**](https://www.elastic.co/guide/en/kibana/current/development-tests.html#_unit_testing): ```bash yarn test:jest --config x-pack/plugins/cloud_security_posture/jest.config.js @@ -33,24 +41,23 @@ yarn test:jest --config x-pack/plugins/cloud_security_posture/jest.config.js > **Note** > for a coverage report, add the `--coverage` flag, and run `open target/kibana-coverage/jest/x-pack/plugins/cloud_security_posture/index.html` -Run **API Integration**: +Run [**Integration Tests**](https://docs.elastic.dev/kibana-dev-docs/tutorials/testing-plugins#): ```bash yarn test:ftr --config x-pack/test/api_integration/config.ts ``` -Run **Functional UI Tests**: +Run [**End-to-End Tests**](https://www.elastic.co/guide/en/kibana/current/development-tests.html#_running_functional_tests): ```bash -yarn test:ftr --config x-pack/test/cloud_security_posture_functional/config.ts +yarn test:ftr --config x-pack/test/cloud_security_posture_functional/config.ts --debug ```
-> **Note** -> in development, run them separately with `ftr:runner` and `ftr:server` +test runner (FTR) can be used separately with `ftr:runner` and `ftr:server`: ```bash yarn test:ftr:server --config x-pack/test/api_integration/config.ts yarn test:ftr:runner --include-tag=cloud_security_posture --config x-pack/test/api_integration/config.ts -``` \ No newline at end of file +``` diff --git a/x-pack/plugins/fleet/common/constants/index.ts b/x-pack/plugins/fleet/common/constants/index.ts index f42b2a372ebb6..4dcc2d58d65ba 100644 --- a/x-pack/plugins/fleet/common/constants/index.ts +++ b/x-pack/plugins/fleet/common/constants/index.ts @@ -19,6 +19,7 @@ export * from './download_source'; export * from './fleet_server_policy_config'; export * from './authz'; export * from './file_storage'; +export * from './message_signing_keys'; // TODO: This is the default `index.max_result_window` ES setting, which dictates // the maximum amount of results allowed to be returned from a search. It's possible diff --git a/x-pack/plugins/fleet/common/constants/message_signing_keys.ts b/x-pack/plugins/fleet/common/constants/message_signing_keys.ts new file mode 100644 index 0000000000000..a51cff3e376c9 --- /dev/null +++ b/x-pack/plugins/fleet/common/constants/message_signing_keys.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 const MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE = 'fleet-message-signing-keys'; diff --git a/x-pack/plugins/fleet/common/index.ts b/x-pack/plugins/fleet/common/index.ts index 3dc816f9f3090..3cdfa354a8c5f 100644 --- a/x-pack/plugins/fleet/common/index.ts +++ b/x-pack/plugins/fleet/common/index.ts @@ -26,6 +26,7 @@ export { OUTPUT_SAVED_OBJECT_TYPE, PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, ASSETS_SAVED_OBJECT_TYPE, + MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, // Fleet server index FLEET_SERVER_SERVERS_INDEX, FLEET_SERVER_ARTIFACTS_INDEX, diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index 93dc93051e98e..ff7c70d8f64e8 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -44,6 +44,7 @@ export { PACKAGES_SAVED_OBJECT_TYPE, ASSETS_SAVED_OBJECT_TYPE, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, // Defaults DEFAULT_OUTPUT, DEFAULT_OUTPUT_ID, diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index cfc063cccbeca..b2540552b62be 100644 --- a/x-pack/plugins/fleet/server/index.ts +++ b/x-pack/plugins/fleet/server/index.ts @@ -20,6 +20,7 @@ export type { ArtifactsClientInterface, Artifact, ListArtifactsProps, + MessageSigningServiceInterface, } from './services'; export { getRegistryUrl } from './services'; diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index d6620fe4c3fb9..75f55a1b353d4 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -58,6 +58,7 @@ export const createAppContextStartContractMock = ( elasticsearch: elasticsearchServiceMock.createStart(), data: dataPluginMock.createStartContract(), encryptedSavedObjectsStart: encryptedSavedObjectsMock.createStart(), + encryptedSavedObjectsSetup: encryptedSavedObjectsMock.createSetup({ canEncrypt: true }), savedObjects: savedObjectsServiceMock.createStartContract(), securitySetup: securityMock.createSetup(), securityStart: securityMock.createStart(), @@ -74,6 +75,11 @@ export const createAppContextStartContractMock = ( kibanaBranch: 'main', telemetryEventsSender: createMockTelemetryEventsSender(), bulkActionsResolver: {} as any, + messageSigningService: { + generateKeyPair: jest.fn(), + sign: jest.fn(), + getPublicKey: jest.fn(), + }, }; }; diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 714ea06d0eefe..265431a87e265 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -54,14 +54,16 @@ import type { FleetConfigType } from '../common/types'; import type { FleetAuthz } from '../common'; import type { ExperimentalFeatures } from '../common/experimental_features'; -import { INTEGRATIONS_PLUGIN_ID } from '../common'; +import { MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, INTEGRATIONS_PLUGIN_ID } from '../common'; import { parseExperimentalConfigValue } from '../common/experimental_features'; +import type { MessageSigningServiceInterface } from './services/security'; import { getRouteRequiredAuthz, makeRouterWithFleetAuthz, calculateRouteAuthz, getAuthzFromRequest, + MessageSigningService, } from './services/security'; import { @@ -151,6 +153,7 @@ export interface FleetAppContext { httpSetup?: HttpServiceSetup; telemetryEventsSender: TelemetryEventsSender; bulkActionsResolver: BulkActionsResolver; + messageSigningService: MessageSigningServiceInterface; } export type FleetSetupContract = void; @@ -198,6 +201,8 @@ export interface FleetStartContract { * @param packageName */ createArtifactsClient: (packageName: string) => FleetArtifactsClient; + + messageSigningService: MessageSigningServiceInterface; } export class FleetPlugin @@ -248,7 +253,7 @@ export class FleetPlugin core.status.set(this.fleetStatus$.asObservable()); - registerSavedObjects(core.savedObjects, deps.encryptedSavedObjects); + registerSavedObjects(core.savedObjects); registerEncryptedSavedObjects(deps.encryptedSavedObjects); // Register feature @@ -422,6 +427,12 @@ export class FleetPlugin } public start(core: CoreStart, plugins: FleetStartDeps): FleetStartContract { + const messageSigningService = new MessageSigningService( + plugins.encryptedSavedObjects.getClient({ + includedHiddenTypes: [MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE], + }) + ); + appContextService.start({ elasticsearch: core.elasticsearch, data: plugins.data, @@ -444,6 +455,7 @@ export class FleetPlugin logger: this.logger, telemetryEventsSender: this.telemetryEventsSender, bulkActionsResolver: this.bulkActionsResolver!, + messageSigningService, }); licenseService.start(plugins.licensing.license$); @@ -530,6 +542,7 @@ export class FleetPlugin createArtifactsClient(packageName: string) { return new FleetArtifactsClient(core.elasticsearch.client.asInternalUser, packageName); }, + messageSigningService, }; } diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index 1a8178890b1dc..16480b0031628 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -20,6 +20,7 @@ import { DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE, FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, FLEET_PROXY_SAVED_OBJECT_TYPE, + MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, } from '../constants'; import { @@ -61,9 +62,7 @@ import { migratePackagePolicyToV870 } from './migrations/security_solution'; * Please update typings in `/common/types` as well as * schemas in `/server/types` if mappings are updated. */ -const getSavedObjectTypes = ( - encryptedSavedObjects: EncryptedSavedObjectsPluginSetup -): { [key: string]: SavedObjectsType } => ({ +const getSavedObjectTypes = (): { [key: string]: SavedObjectsType } => ({ // Deprecated [GLOBAL_SETTINGS_SAVED_OBJECT_TYPE]: { name: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, @@ -366,13 +365,22 @@ const getSavedObjectTypes = ( }, }, }, + [MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE]: { + name: MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, + hidden: true, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + dynamic: false, + properties: {}, + }, + }, }); -export function registerSavedObjects( - savedObjects: SavedObjectsServiceSetup, - encryptedSavedObjects: EncryptedSavedObjectsPluginSetup -) { - const savedObjectTypes = getSavedObjectTypes(encryptedSavedObjects); +export function registerSavedObjects(savedObjects: SavedObjectsServiceSetup) { + const savedObjectTypes = getSavedObjectTypes(); Object.values(savedObjectTypes).forEach((type) => { savedObjects.registerType(type); }); @@ -400,4 +408,8 @@ export function registerEncryptedSavedObjects( ]), }); // Encrypted saved objects + encryptedSavedObjects.registerType({ + type: MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, + attributesToEncrypt: new Set(['private_key', 'passphrase']), + }); } diff --git a/x-pack/plugins/fleet/server/services/app_context.ts b/x-pack/plugins/fleet/server/services/app_context.ts index c8c6c8b4c0e59..71c09621c59a0 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -43,6 +43,7 @@ import type { } from '../types'; import type { FleetAppContext } from '../plugin'; import type { TelemetryEventsSender } from '../telemetry/sender'; +import type { MessageSigningServiceInterface } from '..'; import type { BulkActionsResolver } from './agents'; @@ -67,6 +68,7 @@ class AppContextService { private telemetryEventsSender: TelemetryEventsSender | undefined; private savedObjectsTagging: SavedObjectTaggingStart | undefined; private bulkActionsResolver: BulkActionsResolver | undefined; + private messageSigningService: MessageSigningServiceInterface | undefined; public start(appContext: FleetAppContext) { this.data = appContext.data; @@ -86,6 +88,7 @@ class AppContextService { this.telemetryEventsSender = appContext.telemetryEventsSender; this.savedObjectsTagging = appContext.savedObjectsTagging; this.bulkActionsResolver = appContext.bulkActionsResolver; + this.messageSigningService = appContext.messageSigningService; if (appContext.config$) { this.config$ = appContext.config$; @@ -243,6 +246,10 @@ class AppContextService { public getBulkActionsResolver() { return this.bulkActionsResolver; } + + public getMessageSigningService() { + return this.messageSigningService; + } } export const appContextService = new AppContextService(); diff --git a/x-pack/plugins/fleet/server/services/index.ts b/x-pack/plugins/fleet/server/services/index.ts index cfd8eb95ceb4d..c2abd872d5df9 100644 --- a/x-pack/plugins/fleet/server/services/index.ts +++ b/x-pack/plugins/fleet/server/services/index.ts @@ -67,3 +67,5 @@ export { migrateSettingsToFleetServerHost } from './fleet_server_host'; export { FleetUsageSender } from './telemetry/fleet_usage_sender'; export { checkAllowedPackages } from './check_allowed_packages'; + +export type { MessageSigningServiceInterface } from './security'; diff --git a/x-pack/plugins/fleet/server/services/security/index.ts b/x-pack/plugins/fleet/server/services/security/index.ts index c41c769c58d8d..6b38c6f6c7d5b 100644 --- a/x-pack/plugins/fleet/server/services/security/index.ts +++ b/x-pack/plugins/fleet/server/services/security/index.ts @@ -15,3 +15,5 @@ export { getAuthzFromRequest, doesNotHaveRequiredFleetAuthz, } from './security'; +export type { MessageSigningServiceInterface } from './message_signing_service'; +export { MessageSigningService } from './message_signing_service'; diff --git a/x-pack/plugins/fleet/server/services/security/message_signing_service.test.ts b/x-pack/plugins/fleet/server/services/security/message_signing_service.test.ts new file mode 100644 index 0000000000000..7416e2af49ec5 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/security/message_signing_service.test.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createVerify } from 'crypto'; + +import type { KibanaRequest } from '@kbn/core-http-server'; +import type { SavedObjectsClientContract } from '@kbn/core/server'; +import type { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server'; + +import { MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; +import { createAppContextStartContractMock } from '../../mocks'; +import { appContextService } from '../app_context'; + +import { + type MessageSigningServiceInterface, + MessageSigningService, +} from './message_signing_service'; + +describe('MessageSigningService', () => { + let soClientMock: jest.Mocked; + let esoClientMock: jest.Mocked; + let messageSigningService: MessageSigningServiceInterface; + const keyPairObj = { + id: 'id1', + type: MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, + attributes: { + private_key: + 'MIHsMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAgtNcDFoj07+QICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEELajFPDz2bpD2qfPCRHphAgEgZCq0eUxTOEGrefdeNgHR2VVxXjWRZG+cGn+e8LW4auBCwwMiZsAZPKKvzLdlLi5sQhH+qWPM7Z9/OLbF/0ZKvyDM2/+4/9+5Iwna7vueTZtcdSIuGIFRjqUZbgNLejPSPcBMM9SP1V6I8TjDguGAQ3Nj95t7g7cbl0x48nQZ9bNDJyvy4ytHl+ubzdanLlFkLc=', + public_key: + 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6E5aKP8dAa+TlBuSKrrgl9UtkzHjn6YUQO+72vi3khGfUQIpD9qq9MsjsWz6Bvm6tnSOyyPXv+Koh80lNCKw5A==', + passphrase: 'eb35af2291344a51c9a8bb81e653281c38892d564db617a2cb0bc660f0ae96f2', + }, + }; + + function mockCreatePointInTimeFinderAsInternalUser(savedObjects: unknown[] = []) { + esoClientMock.createPointInTimeFinderDecryptedAsInternalUser = jest.fn().mockResolvedValue({ + close: jest.fn(), + find: function* asyncGenerator() { + yield { saved_objects: savedObjects }; + }, + }); + } + + beforeEach(() => { + const mockContext = createAppContextStartContractMock(); + appContextService.start(mockContext); + esoClientMock = + mockContext.encryptedSavedObjectsStart!.getClient() as jest.Mocked; + soClientMock = appContextService + .getSavedObjects() + .getScopedClient({} as unknown as KibanaRequest) as jest.Mocked; + + messageSigningService = new MessageSigningService(esoClientMock); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('can correctly generate key pair if none exist', async () => { + mockCreatePointInTimeFinderAsInternalUser(); + + await messageSigningService.generateKeyPair(); + expect(soClientMock.create).toBeCalledWith(MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, { + private_key: expect.any(String), + public_key: expect.any(String), + passphrase: expect.any(String), + }); + }); + + it('does not generate key pair if one exists', async () => { + mockCreatePointInTimeFinderAsInternalUser([keyPairObj]); + + await messageSigningService.generateKeyPair(); + expect(soClientMock.create).not.toBeCalled(); + }); + + it('can correctly sign messages', async () => { + mockCreatePointInTimeFinderAsInternalUser([keyPairObj]); + + const message = Buffer.from(JSON.stringify({ message: 'foobar' }), 'utf8'); + const { data, signature } = await messageSigningService.sign(message); + + const verifier = createVerify('SHA256'); + verifier.update(data); + verifier.end(); + + const serializedPublicKey = await messageSigningService.getPublicKey(); + const publicKey = Buffer.from(serializedPublicKey, 'base64'); + const isVerified = verifier.verify( + { key: publicKey, format: 'der', type: 'spki' }, + signature, + 'base64' + ); + expect(isVerified).toBe(true); + expect(data).toBe(message); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/security/message_signing_service.ts b/x-pack/plugins/fleet/server/services/security/message_signing_service.ts new file mode 100644 index 0000000000000..6e323a55fc6f8 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/security/message_signing_service.ts @@ -0,0 +1,177 @@ +/* + * 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 { generateKeyPairSync, createSign, randomBytes } from 'crypto'; + +import type { KibanaRequest } from '@kbn/core-http-server'; +import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; +import type { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server'; +import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server'; + +import { MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; +import { appContextService } from '../app_context'; + +interface MessageSigningKeys { + private_key: string; + public_key: string; + passphrase: string; +} + +export interface MessageSigningServiceInterface { + generateKeyPair(providedPassphrase?: string): Promise; + sign(serializedMessage: Buffer | object): Promise<{ data: Buffer; signature: string }>; + getPublicKey(): Promise; +} + +export class MessageSigningService implements MessageSigningServiceInterface { + private _soClient: SavedObjectsClientContract | undefined; + + constructor(private esoClient: EncryptedSavedObjectsClient) {} + + public async generateKeyPair(providedPassphrase?: string) { + this.checkForEncryptionKey(); + + const currentKeyPair = await this.getCurrentKeyPair(); + if (currentKeyPair.privateKey && currentKeyPair.publicKey && currentKeyPair.passphrase) { + return; + } + + const passphrase = providedPassphrase || this.generatePassphrase(); + + const keyPair = generateKeyPairSync('ec', { + namedCurve: 'prime256v1', + privateKeyEncoding: { + type: 'pkcs8', + format: 'der', + cipher: 'aes-256-cbc', + passphrase, + }, + publicKeyEncoding: { + type: 'spki', + format: 'der', + }, + }); + + await this.soClient.create(MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, { + private_key: keyPair.privateKey.toString('base64'), + public_key: keyPair.publicKey.toString('base64'), + passphrase, + }); + + return; + } + + public async sign( + message: Buffer | Record + ): Promise<{ data: Buffer; signature: string }> { + this.checkForEncryptionKey(); + + const msgBuffer = Buffer.isBuffer(message) + ? message + : Buffer.from(JSON.stringify(message), 'utf8'); + + const signer = createSign('SHA256'); + signer.update(msgBuffer); + signer.end(); + + const { privateKey: serializedPrivateKey, passphrase } = await this.getCurrentKeyPair(); + + if (!serializedPrivateKey) { + throw new Error('unable to find private key'); + } + if (!passphrase) { + throw new Error('unable to find passphrase'); + } + + const privateKey = Buffer.from(serializedPrivateKey, 'base64'); + const signature = signer.sign( + { key: privateKey, passphrase, format: 'der', type: 'pkcs8' }, + 'base64' + ); + return { + data: msgBuffer, + signature, + }; + } + + public async getPublicKey(): Promise { + this.checkForEncryptionKey(); + + const { publicKey } = await this.getCurrentKeyPair(); + + if (!publicKey) { + throw new Error('unable to find public key'); + } + + return publicKey; + } + + private get soClient() { + if (this._soClient) { + return this._soClient; + } + + const fakeRequest = { + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { href: {} }, + raw: { req: { url: '/' } }, + } as unknown as KibanaRequest; + + this._soClient = appContextService.getSavedObjects().getScopedClient(fakeRequest, { + excludedExtensions: [SECURITY_EXTENSION_ID], + includedHiddenTypes: [MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE], + }); + + return this._soClient; + } + + private async getCurrentKeyPair(): Promise<{ + privateKey: string; + publicKey: string; + passphrase: string; + }> { + const finder = + await this.esoClient.createPointInTimeFinderDecryptedAsInternalUser({ + type: MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, + perPage: 1, + sortField: 'created_at', + sortOrder: 'desc', + }); + let keyPair = { + privateKey: '', + publicKey: '', + passphrase: '', + }; + for await (const result of finder.find()) { + const attributes = result.saved_objects[0]?.attributes; + if (!attributes?.private_key) { + break; + } + keyPair = { + privateKey: attributes.private_key, + publicKey: attributes.public_key, + passphrase: attributes.passphrase, + }; + break; + } + + return keyPair; + } + + private generatePassphrase(): string { + return randomBytes(32).toString('hex'); + } + + private checkForEncryptionKey(): void { + if (!appContextService.getEncryptedSavedObjectsSetup()?.canEncrypt) { + throw new Error('encryption key not set, message signing service is disabled'); + } + } +} diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index 2802fd34bc001..7acf46408367b 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -171,6 +171,13 @@ async function createSetupSideEffects( logger.debug('Setting up Fleet enrollment keys'); await ensureDefaultEnrollmentAPIKeysExists(soClient, esClient); + if (appContextService.getEncryptedSavedObjectsSetup()?.canEncrypt) { + logger.debug('Generating key pair for message signing'); + await appContextService.getMessageSigningService()?.generateKeyPair(); + } else { + logger.info('No encryption key set, skipping key pair generation for message signing'); + } + if (nonFatalErrors.length > 0) { logger.info('Encountered non fatal errors during Fleet setup'); formatNonFatalErrors(nonFatalErrors).forEach((error) => logger.info(JSON.stringify(error))); diff --git a/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/url_state_storage_service.ts b/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/url_state_storage_service.ts index c18ee71ed4f8e..fabd26cc03d22 100644 --- a/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/url_state_storage_service.ts +++ b/x-pack/plugins/infra/public/observability_logs/log_stream_query_state/src/url_state_storage_service.ts @@ -165,11 +165,24 @@ const legacyFilterStateInUrlRT = rt.union([ }), ]); +const legacyLegacyFilterStateWithExpressionInUrlRT = rt.type({ + kind: rt.literal('kuery'), + expression: rt.string, +}); + const decodeQueryValueFromUrl = (queryValueFromUrl: unknown) => Either.getAltValidation(Array.getMonoid()).alt( pipe( - legacyFilterStateInUrlRT.decode(queryValueFromUrl), - Either.map((legacyQuery) => ({ query: legacyQuery })) + pipe( + legacyLegacyFilterStateWithExpressionInUrlRT.decode(queryValueFromUrl), + Either.map(({ expression, kind }) => ({ query: { language: kind, query: expression } })) + ), + Either.alt(() => + pipe( + legacyFilterStateInUrlRT.decode(queryValueFromUrl), + Either.map((legacyQuery) => ({ query: legacyQuery })) + ) + ) ), () => filterStateInUrlRT.decode(queryValueFromUrl) ); diff --git a/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.test.tsx b/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.test.tsx index 3691f507d6a4d..c49ef7f349f18 100644 --- a/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.test.tsx @@ -37,12 +37,12 @@ describe('application-level user messages', () => { Object { "displayLocations": Array [ Object { - "id": "visualization", + "id": "visualizationOnEmbeddable", }, ], "fixableInEditor": true, "longMessage": "Visualization type not found.", - "severity": "warning", + "severity": "error", "shortMessage": "", }, ] diff --git a/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx b/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx index f7b5d1eb8a135..f5190ec334157 100644 --- a/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx +++ b/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx @@ -33,9 +33,9 @@ export const getApplicationUserMessages = ({ core, }: { visualizationType: string | null | undefined; - visualization: VisualizationState; + visualization: VisualizationState | undefined; visualizationMap: VisualizationMap; - activeDatasource: Datasource | null; + activeDatasource: Datasource | null | undefined; activeDatasourceState: { state: unknown } | null; dataViews: DataViewsState; core: CoreStart; @@ -46,7 +46,7 @@ export const getApplicationUserMessages = ({ messages.push(getMissingVisTypeError()); } - if (visualization.activeId && !visualizationMap[visualization.activeId]) { + if (visualization?.activeId && !visualizationMap[visualization.activeId]) { messages.push(getUnknownVisualizationTypeError(visualization.activeId)); } @@ -69,8 +69,8 @@ export const getApplicationUserMessages = ({ function getMissingVisTypeError(): UserMessage { return { - severity: 'warning', - displayLocations: [{ id: 'visualization' }], + severity: 'error', + displayLocations: [{ id: 'visualizationOnEmbeddable' }], fixableInEditor: true, shortMessage: '', longMessage: i18n.translate('xpack.lens.editorFrame.expressionMissingVisualizationType', { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index 4cda8d1c7c0bd..02410b6994c34 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -401,7 +401,7 @@ export async function persistedStateToExpression( } export function getMissingIndexPattern( - currentDatasource: Datasource | null, + currentDatasource: Datasource | null | undefined, currentDatasourceState: { state: unknown } | null, indexPatterns: IndexPatternMap ) { diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx index db00db6a87f7f..92bfa95eb2aef 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx @@ -30,23 +30,36 @@ import { OnSaveProps } from '@kbn/saved-objects-plugin/public/save_modal'; import { act } from 'react-dom/test-utils'; import { inspectorPluginMock } from '@kbn/inspector-plugin/public/mocks'; import { Visualization } from '../types'; +import { createMockDatasource, createMockVisualization } from '../mocks'; jest.mock('@kbn/inspector-plugin/public', () => ({ isAvailable: false, open: false, })); +const defaultVisualizationId = 'lnsSomeVisType'; +const defaultDatasourceId = 'someDatasource'; + const savedVis: Document = { state: { - visualization: {}, - datasourceStates: {}, + visualization: { activeId: defaultVisualizationId }, + datasourceStates: { [defaultDatasourceId]: {} }, query: { query: '', language: 'lucene' }, filters: [], }, references: [], title: 'My title', - visualizationType: '', + visualizationType: defaultVisualizationId, +}; + +const defaultVisualizationMap = { + [defaultVisualizationId]: createMockVisualization(), +}; + +const defaultDatasourceMap = { + [defaultDatasourceId]: createMockDatasource(defaultDatasourceId), }; + const defaultSaveMethod = ( testAttributes: LensSavedObjectAttributes, savedObjectId?: string @@ -155,8 +168,8 @@ describe('embeddable', () => { inspector: inspectorPluginMock.createStartContract(), getTrigger, theme: themeServiceMock.createStartContract(), - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), documentToExpression: () => Promise.resolve({ @@ -208,8 +221,8 @@ describe('embeddable', () => { inspector: inspectorPluginMock.createStartContract(), getTrigger, theme: themeServiceMock.createStartContract(), - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), documentToExpression: () => Promise.resolve({ @@ -268,8 +281,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -322,8 +335,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -398,8 +411,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -452,8 +465,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -505,8 +518,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -554,8 +567,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -609,8 +622,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -668,8 +681,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -725,8 +738,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -789,8 +802,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -854,8 +867,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -922,8 +935,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -975,8 +988,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -1030,8 +1043,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -1082,8 +1095,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -1150,8 +1163,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -1236,8 +1249,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -1297,8 +1310,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -1355,8 +1368,8 @@ describe('embeddable', () => { navLinks: {}, }, getTrigger, - visualizationMap: {}, - datasourceMap: {}, + visualizationMap: defaultVisualizationMap, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), theme: themeServiceMock.createStartContract(), documentToExpression: () => @@ -1408,7 +1421,7 @@ describe('embeddable', () => { const visDocument: Document = { state: { visualization: {}, - datasourceStates: {}, + datasourceStates: { [defaultDatasourceId]: {} }, query: { query: '', language: 'lucene' }, filters: [], }, @@ -1443,7 +1456,7 @@ describe('embeddable', () => { initialize: () => {}, } as unknown as Visualization, }, - datasourceMap: {}, + datasourceMap: defaultDatasourceMap, documentToExpression: documentToExpressionMock, }, { id: '123' } as unknown as LensEmbeddableInput @@ -1475,23 +1488,11 @@ describe('embeddable', () => { it('should override noPadding in the display options if noPadding is set in the embeddable input', async () => { expressionRenderer = jest.fn((_) => null); - const visDocument: Document = { - state: { - visualization: {}, - datasourceStates: {}, - query: { query: '', language: 'lucene' }, - filters: [], - }, - references: [], - title: 'My title', - visualizationType: 'testVis', - }; - const createEmbeddable = (displayOptions?: { noPadding: boolean }, noPadding?: boolean) => { return new Embeddable( { timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, - attributeService: attributeServiceMockFromSavedVis(visDocument), + attributeService: attributeServiceMockFromSavedVis(savedVis), data: dataMock, expressionRenderer, coreStart: {} as CoreStart, @@ -1507,12 +1508,12 @@ describe('embeddable', () => { getTrigger, theme: themeServiceMock.createStartContract(), visualizationMap: { - [visDocument.visualizationType as string]: { + [savedVis.visualizationType as string]: { getDisplayOptions: displayOptions ? () => displayOptions : undefined, initialize: () => {}, } as unknown as Visualization, }, - datasourceMap: {}, + datasourceMap: defaultDatasourceMap, injectFilterReferences: jest.fn(mockInjectFilterReferences), documentToExpression: () => Promise.resolve({ diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 1a22222155677..e89e8c61e3185 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -513,25 +513,23 @@ export class Embeddable private loadUserMessages() { const userMessages: UserMessage[] = []; - if (this.activeVisualizationState && this.activeDatasource) { - userMessages.push( - ...getApplicationUserMessages({ - visualizationType: this.savedVis?.visualizationType, - visualization: { - state: this.activeVisualizationState, - activeId: this.activeVisualizationId, - }, - visualizationMap: this.deps.visualizationMap, - activeDatasource: this.activeDatasource, - activeDatasourceState: { state: this.activeDatasourceState }, - dataViews: { - indexPatterns: this.indexPatterns, - indexPatternRefs: this.indexPatternRefs, // TODO - are these actually used? - }, - core: this.deps.coreStart, - }) - ); - } + userMessages.push( + ...getApplicationUserMessages({ + visualizationType: this.savedVis?.visualizationType, + visualization: { + state: this.activeVisualizationState, + activeId: this.activeVisualizationId, + }, + visualizationMap: this.deps.visualizationMap, + activeDatasource: this.activeDatasource, + activeDatasourceState: { state: this.activeDatasourceState }, + dataViews: { + indexPatterns: this.indexPatterns, + indexPatternRefs: this.indexPatternRefs, // TODO - are these actually used? + }, + core: this.deps.coreStart, + }) + ); const mergedSearchContext = this.getMergedSearchContext(); @@ -634,14 +632,18 @@ export class Embeddable savedObjectId: (input as LensByReferenceInput)?.savedObjectId, }; - const { ast, indexPatterns, indexPatternRefs } = await getExpressionFromDocument( - this.savedVis, - this.deps.documentToExpression - ); + try { + const { ast, indexPatterns, indexPatternRefs } = await getExpressionFromDocument( + this.savedVis, + this.deps.documentToExpression + ); - this.expression = ast; - this.indexPatterns = indexPatterns; - this.indexPatternRefs = indexPatternRefs; + this.expression = ast; + this.indexPatterns = indexPatterns; + this.indexPatternRefs = indexPatternRefs; + } catch { + // nothing, errors should be reported via getUserMessages + } if (metaInfo?.sharingSavedObjectProps?.outcome === 'conflict' && !!this.deps.spaces) { this.addUserMessages([ diff --git a/x-pack/plugins/profiling/public/components/primary_and_comparison_search_bar.tsx b/x-pack/plugins/profiling/public/components/primary_and_comparison_search_bar.tsx index 33f5f3e8850ff..5298d43deacf3 100644 --- a/x-pack/plugins/profiling/public/components/primary_and_comparison_search_bar.tsx +++ b/x-pack/plugins/profiling/public/components/primary_and_comparison_search_bar.tsx @@ -61,15 +61,29 @@ export function PrimaryAndComparisonSearchBar() { } } + let baselineTitle: string; + let comparisonTitle: string; + + if (routePath === '/flamegraphs/differential') { + baselineTitle = i18n.translate('xpack.profiling.comparisonSearch.baselineTitleFlamegraph', { + defaultMessage: 'Baseline flamegraph', + }); + comparisonTitle = i18n.translate('xpack.profiling.comparisonSearch.comparisonTitleFlamegraph', { + defaultMessage: 'Comparison flamegraph', + }); + } else { + baselineTitle = i18n.translate('xpack.profiling.comparisonSearch.baselineTitleFunctions', { + defaultMessage: 'Baseline functions', + }); + comparisonTitle = i18n.translate('xpack.profiling.comparisonSearch.comparisonTitleFunctions', { + defaultMessage: 'Comparison functions', + }); + } return ( -

- {i18n.translate('xpack.profiling.comparisonSearch.baselineTitle', { - defaultMessage: 'Baseline flamegraph', - })} -

+

{baselineTitle}

@@ -107,11 +121,7 @@ export function PrimaryAndComparisonSearchBar() {
-

- {i18n.translate('xpack.profiling.comparisonSearch.comparisonTitle', { - defaultMessage: 'Comparison flamegraph', - })} -

+

{comparisonTitle}

& { aggregatable?: boolean; - dataTableCellActions?: DataTableCellAction[]; category?: string; columnHeaderType: ColumnHeaderType; description?: string | null; diff --git a/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts b/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts index f5ca9b28628e1..134b659116ee0 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts @@ -6,7 +6,6 @@ */ import type { EuiDataGridCellValueElementProps } from '@elastic/eui'; -import type { Filter } from '@kbn/es-query'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import type { ColumnHeaderOptions, RowRenderer } from '../..'; import type { BrowserFields, TimelineNonEcsData } from '../../../search_strategy'; @@ -18,7 +17,6 @@ export type CellValueElementProps = EuiDataGridCellValueElementProps & { data: TimelineNonEcsData[]; ecsData?: Ecs; eventId: string; // _id - globalFilters?: Filter[]; header: ColumnHeaderOptions; isDraggable: boolean; isTimeline?: boolean; // Default cell renderer is used for both the alert table and timeline. This allows us to cheaply separate concerns @@ -30,5 +28,4 @@ export type CellValueElementProps = EuiDataGridCellValueElementProps & { truncate?: boolean; key?: string; closeCellPopover?: () => void; - enableActions?: boolean; }; diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_cell_actions.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_cell_actions.cy.ts new file mode 100644 index 0000000000000..b322a87929d52 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/alerts_cell_actions.cy.ts @@ -0,0 +1,130 @@ +/* + * 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 { getNewRule } from '../../objects/rule'; +import { CELL_COPY_BUTTON, FILTER_BADGE, SHOW_TOP_N_HEADER } from '../../screens/alerts'; +import { + ALERT_TABLE_FILE_NAME_HEADER, + ALERT_TABLE_FILE_NAME_VALUES, + ALERT_TABLE_SEVERITY_VALUES, + PROVIDER_BADGE, +} from '../../screens/timeline'; + +import { + scrollAlertTableColumnIntoView, + addAlertPropertyToTimeline, + filterForAlertProperty, + showTopNAlertProperty, + clickExpandActions, +} from '../../tasks/alerts'; +import { createCustomRuleEnabled } from '../../tasks/api_calls/rules'; +import { cleanKibana } from '../../tasks/common'; +import { waitForAlertsToPopulate } from '../../tasks/create_new_rule'; +import { login, visit } from '../../tasks/login'; +import { fillAddFilterForm, openAddFilterPopover } from '../../tasks/search_bar'; +import { openActiveTimeline } from '../../tasks/timeline'; + +import { ALERTS_URL } from '../../urls/navigation'; +describe('Alerts cell actions', () => { + before(() => { + cleanKibana(); + login(); + }); + + context('Opening alerts', () => { + before(() => { + createCustomRuleEnabled(getNewRule()); + }); + + beforeEach(() => { + visit(ALERTS_URL); + waitForAlertsToPopulate(); + }); + + describe('Filter', () => { + it('should filter for a non-empty property', () => { + cy.get(ALERT_TABLE_SEVERITY_VALUES) + .first() + .invoke('text') + .then((severityVal) => { + scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); + filterForAlertProperty(ALERT_TABLE_SEVERITY_VALUES, 0); + cy.get(FILTER_BADGE) + .first() + .should('have.text', `kibana.alert.severity: ${severityVal}`); + }); + }); + + it('should filter for an empty property', () => { + // add condition to make sure the field is empty + openAddFilterPopover(); + fillAddFilterForm({ key: 'file.name', operator: 'does not exist' }); + scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); + filterForAlertProperty(ALERT_TABLE_FILE_NAME_VALUES, 0); + cy.get(FILTER_BADGE).first().should('have.text', 'NOT file.name: exists'); + }); + }); + + describe('Add to timeline', () => { + it('should add a non-empty property to default timeline', () => { + cy.get(ALERT_TABLE_SEVERITY_VALUES) + .first() + .invoke('text') + .then((severityVal) => { + scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); + addAlertPropertyToTimeline(ALERT_TABLE_SEVERITY_VALUES, 0); + openActiveTimeline(); + cy.get(PROVIDER_BADGE) + .first() + .should('have.text', `kibana.alert.severity: "${severityVal}"`); + }); + }); + + it('should add an empty property to default timeline', () => { + // add condition to make sure the field is empty + openAddFilterPopover(); + fillAddFilterForm({ key: 'file.name', operator: 'does not exist' }); + scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); + addAlertPropertyToTimeline(ALERT_TABLE_FILE_NAME_VALUES, 0); + openActiveTimeline(); + cy.get(PROVIDER_BADGE).first().should('have.text', 'NOT file.name exists'); + }); + }); + + describe('Show Top N', () => { + it('should show top for a property', () => { + cy.get(ALERT_TABLE_SEVERITY_VALUES) + .first() + .invoke('text') + .then(() => { + scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); + showTopNAlertProperty(ALERT_TABLE_SEVERITY_VALUES, 0); + cy.get(SHOW_TOP_N_HEADER).first().should('have.text', `Top kibana.alert.severity`); + }); + }); + }); + + describe('Copy to clipboard', () => { + it('should copy to clipboard', () => { + cy.get(ALERT_TABLE_SEVERITY_VALUES) + .first() + .invoke('text') + .then(() => { + scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); + cy.window().then((win) => { + cy.stub(win, 'prompt').returns('DISABLED WINDOW PROMPT'); + }); + clickExpandActions(ALERT_TABLE_SEVERITY_VALUES, 0); + cy.get(CELL_COPY_BUTTON).should('exist'); + // We are not able to test the "copy to clipboard" action execution + // due to browsers security limitation accessing the clipboard services. + // We assume external `copy` library works + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/investigate_in_timeline.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/investigate_in_timeline.cy.ts index 9095b5d83f4ff..5f52041e75d17 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/investigate_in_timeline.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/investigate_in_timeline.cy.ts @@ -6,26 +6,15 @@ */ import { getNewRule } from '../../objects/rule'; -import { - ALERT_TABLE_FILE_NAME_HEADER, - ALERT_TABLE_FILE_NAME_VALUES, - ALERT_TABLE_SEVERITY_VALUES, - PROVIDER_BADGE, -} from '../../screens/timeline'; +import { PROVIDER_BADGE } from '../../screens/timeline'; -import { - addAlertPropertyToTimeline, - investigateFirstAlertInTimeline, - scrollAlertTableColumnIntoView, -} from '../../tasks/alerts'; +import { investigateFirstAlertInTimeline } from '../../tasks/alerts'; import { createCustomRuleEnabled } from '../../tasks/api_calls/rules'; import { cleanKibana } from '../../tasks/common'; import { waitForAlertsToPopulate } from '../../tasks/create_new_rule'; import { login, visit } from '../../tasks/login'; -import { openActiveTimeline } from '../../tasks/timeline'; import { ALERTS_URL } from '../../urls/navigation'; -import { fillAddFilterForm, openAddFilterPopover } from '../../tasks/search_bar'; describe('Alerts timeline', () => { before(() => { @@ -48,28 +37,4 @@ describe('Alerts timeline', () => { cy.get(PROVIDER_BADGE).filter(':visible').should('have.text', eventId); }); }); - - it('Add a non-empty property to default timeline', () => { - cy.get(ALERT_TABLE_SEVERITY_VALUES) - .first() - .invoke('text') - .then((severityVal) => { - scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); - addAlertPropertyToTimeline(ALERT_TABLE_SEVERITY_VALUES, 0); - openActiveTimeline(); - cy.get(PROVIDER_BADGE) - .first() - .should('have.text', `kibana.alert.severity: "${severityVal}"`); - }); - }); - - it('Add an empty property to default timeline', () => { - // add condition to make sure the field is empty - openAddFilterPopover(); - fillAddFilterForm({ key: 'file.name', operator: 'does not exist' }); - scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); - addAlertPropertyToTimeline(ALERT_TABLE_FILE_NAME_VALUES, 0); - openActiveTimeline(); - cy.get(PROVIDER_BADGE).first().should('have.text', 'NOT file.name exists'); - }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts.ts b/x-pack/plugins/security_solution/cypress/screens/alerts.ts index 046962a5d9685..1b5ca92e1eb1c 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts.ts @@ -134,3 +134,21 @@ export const EVENT_CONTAINER_TABLE_LOADING = '[data-test-subj="events-container- export const EVENT_CONTAINER_TABLE_NOT_LOADING = '[data-test-subj="events-container-loading-false"]'; + +export const FILTER_BADGE = '[data-test-subj^="filter-badge"]'; + +export const CELL_FILTER_IN_BUTTON = + '[data-test-subj="dataGridColumnCellAction-security_filterIn"]'; +export const CELL_FILTER_OUT_BUTTON = + '[data-test-subj="dataGridColumnCellAction-security_filterOut"]'; +export const CELL_ADD_TO_TIMELINE_BUTTON = + '[data-test-subj="dataGridColumnCellAction-security_addToTimeline"]'; +export const CELL_SHOW_TOP_FIELD_BUTTON = + '[data-test-subj="dataGridColumnCellAction-security_showTopN"]'; +export const CELL_COPY_BUTTON = + '[data-test-subj="dataGridColumnCellAction-security_copyToClipboard"]'; + +export const ACTIONS_EXPAND_BUTTON = '[data-test-subj="euiDataGridCellExpandButton"]'; + +export const SHOW_TOP_N_HEADER = + '[data-test-subj="topN-container"] [data-test-subj="header-section-title"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts index 9a1ac0b8d08f1..23b15524305ed 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts @@ -50,7 +50,7 @@ export const CELL_EXPAND_VALUE = '[data-test-subj="euiDataGridCellExpandButton"] export const CELL_EXPANSION_POPOVER = '[data-test-subj="euiDataGridExpansionPopover"]'; -export const USER_DETAILS_LINK = '[data-test-subj="data-grid-user-details"]'; +export const USER_DETAILS_LINK = '[data-test-subj="users-link-anchor"]'; export const TABLE_TAB = '[data-test-subj="tableTab"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index 670c709b67bee..a9cdb896cdea4 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -296,7 +296,8 @@ export const ALERT_TABLE_FILE_NAME_HEADER = '[data-gridcell-column-id="file.name export const ALERT_TABLE_FILE_NAME_VALUES = '[data-gridcell-column-id="file.name"][data-test-subj="dataGridRowCell"]'; // empty column for the test data -export const ALERT_TABLE_CELL_ACTIONS_ADD_TO_TIMELINE = '[data-test-subj="add-to-timeline"]'; +export const ALERT_TABLE_CELL_ACTIONS_ADD_TO_TIMELINE = + '[data-test-subj="dataGridColumnCellAction-security_addToTimeline"]'; export const ACTIVE_TIMELINE_BOTTOM_BAR = '[data-test-subj="flyoutBottomBar"] .active-timeline-button'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts index c7810db6ae21d..a17d4bb004671 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts @@ -32,12 +32,13 @@ import { CLOSED_ALERTS_FILTER_BTN, OPENED_ALERTS_FILTER_BTN, ACKNOWLEDGED_ALERTS_FILTER_BTN, + CELL_ADD_TO_TIMELINE_BUTTON, + CELL_FILTER_IN_BUTTON, + CELL_SHOW_TOP_FIELD_BUTTON, + ACTIONS_EXPAND_BUTTON, } from '../screens/alerts'; import { LOADING_INDICATOR, REFRESH_BUTTON } from '../screens/security_header'; -import { - ALERT_TABLE_CELL_ACTIONS_ADD_TO_TIMELINE, - TIMELINE_COLUMN_SPINNER, -} from '../screens/timeline'; +import { TIMELINE_COLUMN_SPINNER } from '../screens/timeline'; import { UPDATE_ENRICHMENT_RANGE_BUTTON, ENRICHMENT_QUERY_END_INPUT, @@ -299,9 +300,22 @@ export const openAnalyzerForFirstAlertInTimeline = () => { cy.get(OPEN_ANALYZER_BTN).first().click({ force: true }); }; -export const addAlertPropertyToTimeline = (propertySelector: string, rowIndex: number) => { +const clickAction = (propertySelector: string, rowIndex: number, actionSelector: string) => { cy.get(propertySelector).eq(rowIndex).trigger('mouseover'); - cy.get(ALERT_TABLE_CELL_ACTIONS_ADD_TO_TIMELINE).first().click({ force: true }); + cy.get(actionSelector).first().click({ force: true }); +}; +export const clickExpandActions = (propertySelector: string, rowIndex: number) => { + clickAction(propertySelector, rowIndex, ACTIONS_EXPAND_BUTTON); +}; +export const addAlertPropertyToTimeline = (propertySelector: string, rowIndex: number) => { + clickAction(propertySelector, rowIndex, CELL_ADD_TO_TIMELINE_BUTTON); +}; +export const filterForAlertProperty = (propertySelector: string, rowIndex: number) => { + clickAction(propertySelector, rowIndex, CELL_FILTER_IN_BUTTON); +}; +export const showTopNAlertProperty = (propertySelector: string, rowIndex: number) => { + clickExpandActions(propertySelector, rowIndex); + cy.get(CELL_SHOW_TOP_FIELD_BUTTON).first().click({ force: true }); }; export const waitForAlerts = () => { diff --git a/x-pack/plugins/security_solution/cypress/tasks/search_bar.ts b/x-pack/plugins/security_solution/cypress/tasks/search_bar.ts index fb5e978befdda..fa3dbbc2589e5 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/search_bar.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/search_bar.ts @@ -14,7 +14,6 @@ import { ADD_FILTER_FORM_FIELD_INPUT, ADD_FILTER_FORM_OPERATOR_OPTION_IS, ADD_FILTER_FORM_OPERATOR_FIELD, - ADD_FILTER_FORM_FIELD_OPTION, ADD_FILTER_FORM_FILTER_VALUE_INPUT, GLOBAL_KQL_INPUT, } from '../screens/search_bar'; @@ -38,9 +37,7 @@ export const fillKqlQueryBar = (query: string) => { export const fillAddFilterForm = ({ key, value, operator }: SearchBarFilter) => { cy.get(ADD_FILTER_FORM_FIELD_INPUT).should('exist'); cy.get(ADD_FILTER_FORM_FIELD_INPUT).should('be.visible'); - cy.get(ADD_FILTER_FORM_FIELD_INPUT).type(`${key}{downarrow}`); - cy.get(ADD_FILTER_FORM_FIELD_INPUT).click(); - cy.get(ADD_FILTER_FORM_FIELD_OPTION(key)).click({ force: true }); + cy.get(ADD_FILTER_FORM_FIELD_INPUT).type(`${key}{downarrow}{enter}`); if (!operator) { cy.get(ADD_FILTER_FORM_OPERATOR_FIELD).click(); cy.get(ADD_FILTER_FORM_OPERATOR_OPTION_IS).click(); diff --git a/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.test.tsx b/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.test.tsx index 26b7ab8e5c052..a2e047bc5e415 100644 --- a/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.test.tsx +++ b/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.test.tsx @@ -53,9 +53,6 @@ describe('createShowTopNAction', () => { const context = { field: { name: 'user.name', value: 'the-value', type: 'keyword', aggregatable: true }, trigger: { id: 'trigger' }, - extraContentNodeRef: { - current: element, - }, nodeRef: { current: element, }, diff --git a/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.tsx b/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.tsx index 4ff35bea5118c..215033409a46f 100644 --- a/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.tsx +++ b/x-pack/plugins/security_solution/public/actions/show_top_n/default/show_top_n.tsx @@ -68,11 +68,14 @@ export const createShowTopNAction = ({ !UNSUPPORTED_FIELD_TYPES.includes(field.type) && !!field.aggregatable, execute: async (context) => { - const node = context.extraContentNodeRef?.current; - if (!node) return; + if (!context.nodeRef.current) return; + + const node = document.createElement('div'); + document.body.appendChild(node); const onClose = () => { unmountComponentAtNode(node); + document.body.removeChild(node); }; const element = ( diff --git a/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx b/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx index 49ddc83563fd7..1737a65596a78 100644 --- a/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx +++ b/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx @@ -35,9 +35,6 @@ const context = { nodeRef: { current: element, }, - extraContentNodeRef: { - current: null, - }, } as CellActionExecutionContext; describe('TopNAction', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.test.tsx index eb0c2379ff609..e4596cd1fb7f3 100644 --- a/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.test.tsx @@ -19,7 +19,6 @@ describe('transformControlColumns', () => { setEventsDeleted: jest.fn(), columnHeaders: [], controlColumns: [], - disabledCellActions: [], selectedEventIds: {}, tabType: '', isSelectAllChecked: false, diff --git a/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.tsx b/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.tsx index 94980890d1530..651310755b183 100644 --- a/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/control_columns/transform_control_columns.tsx @@ -34,7 +34,6 @@ export interface TransformColumnsProps { columnHeaders: ColumnHeaderOptions[]; controlColumns: ControlColumnProps[]; data: TimelineItem[]; - disabledCellActions: string[]; fieldBrowserOptions?: FieldBrowserOptions; loadingEventIds: string[]; onRowSelected: OnRowSelected; diff --git a/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx index b4110c1e78340..527eb6e3d0ab9 100644 --- a/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/data_table/index.test.tsx @@ -14,21 +14,32 @@ import { REMOVE_COLUMN } from './column_headers/translations'; import { useMountAppended } from '../../utils/use_mount_appended'; import type { EuiDataGridColumn } from '@elastic/eui'; import { defaultHeaders, mockGlobalState, mockTimelineData, TestProviders } from '../../mock'; -import { defaultColumnHeaderType } from '../../store/data_table/defaults'; import { mockBrowserFields } from '../../containers/source/mock'; import { getMappedNonEcsValue } from '../../../timelines/components/timeline/body/data_driven_columns'; import type { CellValueElementProps } from '../../../../common/types'; import { TableId } from '../../../../common/types'; +import { CELL_ACTIONS_DEFAULT_TRIGGER } from '../../../../common/constants'; const mockDispatch = jest.fn(); -jest.mock('react-redux', () => { - const original = jest.requireActual('react-redux'); +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useDispatch: () => mockDispatch, +})); - return { - ...original, - useDispatch: () => mockDispatch, - }; -}); +const mockUseDataGridColumnsCellActions = jest.fn( + (_: object): Array JSX.Element>> => [] +); +jest.mock('@kbn/cell-actions', () => ({ + ...jest.requireActual('@kbn/cell-actions'), + useDataGridColumnsCellActions: (params: object) => mockUseDataGridColumnsCellActions(params), +})); + +const headersJustTimestamp = defaultHeaders.filter((h) => h.id === '@timestamp'); +const mockGetColumnHeaders = jest.fn(() => headersJustTimestamp); +jest.mock('./column_headers/helpers', () => ({ + ...jest.requireActual('./column_headers/helpers'), + getColumnHeaders: () => mockGetColumnHeaders(), +})); jest.mock('@kbn/kibana-react-plugin/public', () => { const originalModule = jest.requireActual('@kbn/kibana-react-plugin/public'); @@ -80,8 +91,6 @@ describe('DataTable', () => { const props: DataTableProps = { browserFields: mockBrowserFields, data: mockTimelineData, - defaultCellActions: [], - disabledCellActions: ['signal.rule.risk_score', 'signal.reason'], id: TableId.test, loadPage: jest.fn(), renderCellValue: TestCellRenderer, @@ -98,7 +107,8 @@ describe('DataTable', () => { }; beforeEach(() => { - mockDispatch.mockReset(); + mockDispatch.mockClear(); + mockUseDataGridColumnsCellActions.mockClear(); }); describe('rendering', () => { @@ -142,10 +152,8 @@ describe('DataTable', () => { }); test('it renders cell value', () => { - const headersJustTimestamp = defaultHeaders.filter((h) => h.id === '@timestamp'); const testProps = { ...props, - columnHeaders: headersJustTimestamp, data: mockTimelineData.slice(0, 1), }; const wrapper = mount( @@ -163,49 +171,55 @@ describe('DataTable', () => { .text() ).toEqual(mockTimelineData[0].ecs.timestamp); }); + }); - test('timestamp column renders cell actions', () => { - const headersJustTimestamp = defaultHeaders.filter((h) => h.id === '@timestamp'); - const testProps = { - ...props, - columnHeaders: headersJustTimestamp, - data: mockTimelineData.slice(0, 1), - }; + describe('cellActions', () => { + test('calls useDataGridColumnsCellActions properly', () => { + const data = mockTimelineData.slice(0, 1); const wrapper = mount( - + ); wrapper.update(); - expect( - wrapper - .find('[data-test-subj="body-data-grid"]') - .first() - .prop('columns') - .find((c) => c.id === '@timestamp')?.cellActions - ).toBeDefined(); + expect(mockUseDataGridColumnsCellActions).toHaveBeenCalledWith({ + triggerId: CELL_ACTIONS_DEFAULT_TRIGGER, + fields: [ + { + name: '@timestamp', + values: [data[0]?.data[0]?.value], + type: 'date', + aggregatable: true, + }, + ], + metadata: { + scopeId: 'table-test', + }, + dataGridRef: expect.any(Object), + }); }); - test("signal.rule.risk_score column doesn't render cell actions", () => { - const columnHeaders = [ - { - category: 'signal', - columnHeaderType: defaultColumnHeaderType, - id: 'signal.rule.risk_score', - type: 'number', - aggregatable: true, - initialWidth: 105, - }, - ]; - const testProps = { - ...props, - columnHeaders, - data: mockTimelineData.slice(0, 1), - }; + test('does not render cell actions if disableCellActions is true', () => { const wrapper = mount( - + + + ); + wrapper.update(); + + expect(mockUseDataGridColumnsCellActions).toHaveBeenCalledWith( + expect.objectContaining({ + fields: [], + }) + ); + }); + + test('does not render cell actions if empty actions returned', () => { + mockUseDataGridColumnsCellActions.mockReturnValueOnce([]); + const wrapper = mount( + + ); wrapper.update(); @@ -215,29 +229,15 @@ describe('DataTable', () => { .find('[data-test-subj="body-data-grid"]') .first() .prop('columns') - .find((c) => c.id === 'signal.rule.risk_score')?.cellActions - ).toBeUndefined(); + .find((c) => c.id === '@timestamp')?.cellActions + ).toHaveLength(0); }); - test("signal.reason column doesn't render cell actions", () => { - const columnHeaders = [ - { - category: 'signal', - columnHeaderType: defaultColumnHeaderType, - id: 'signal.reason', - type: 'string', - aggregatable: true, - initialWidth: 450, - }, - ]; - const testProps = { - ...props, - columnHeaders, - data: mockTimelineData.slice(0, 1), - }; + test('renders returned cell actions', () => { + mockUseDataGridColumnsCellActions.mockReturnValueOnce([[() =>
]]); const wrapper = mount( - + ); wrapper.update(); @@ -247,43 +247,11 @@ describe('DataTable', () => { .find('[data-test-subj="body-data-grid"]') .first() .prop('columns') - .find((c) => c.id === 'signal.reason')?.cellActions - ).toBeUndefined(); + .find((c) => c.id === '@timestamp')?.cellActions + ).toHaveLength(1); }); }); - test("signal.rule.risk_score column doesn't render cell actions", () => { - const columnHeaders = [ - { - category: 'signal', - columnHeaderType: defaultColumnHeaderType, - id: 'signal.rule.risk_score', - type: 'number', - aggregatable: true, - initialWidth: 105, - }, - ]; - const testProps = { - ...props, - columnHeaders, - data: mockTimelineData.slice(0, 1), - }; - const wrapper = mount( - - - - ); - wrapper.update(); - - expect( - wrapper - .find('[data-test-subj="body-data-grid"]') - .first() - .prop('columns') - .find((c) => c.id === 'signal.rule.risk_score')?.cellActions - ).toBeUndefined(); - }); - test('it does NOT render switches for hiding columns in the `EuiDataGrid` `Columns` popover', async () => { render( diff --git a/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx index 85189cd221b2f..bfddac06f64b5 100644 --- a/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/data_table/index.tsx @@ -22,11 +22,13 @@ import React, { useCallback, useEffect, useMemo, useContext, useRef } from 'reac import { useDispatch } from 'react-redux'; import styled, { ThemeContext } from 'styled-components'; -import type { Filter } from '@kbn/es-query'; import type { EuiTheme } from '@kbn/kibana-react-plugin/common'; import type { FieldBrowserOptions } from '@kbn/triggers-actions-ui-plugin/public'; import { i18n } from '@kbn/i18n'; -import type { DataTableCellAction } from '../../../../common/types'; +import { + useDataGridColumnsCellActions, + type UseDataGridColumnsCellActionsProps, +} from '@kbn/cell-actions'; import type { CellValueElementProps, ColumnHeaderOptions, @@ -36,12 +38,7 @@ import type { import type { TimelineItem } from '../../../../common/search_strategy/timeline'; import { getColumnHeader, getColumnHeaders } from './column_headers/helpers'; -import { - addBuildingBlockStyle, - hasCellActions, - mapSortDirectionToDirection, - mapSortingColumns, -} from './helpers'; +import { addBuildingBlockStyle, mapSortDirectionToDirection, mapSortingColumns } from './helpers'; import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; import { REMOVE_COLUMN } from './column_headers/translations'; @@ -52,6 +49,7 @@ import { getPageRowIndex } from './pagination'; import { UnitCount } from '../toolbar/unit'; import { useShallowEqualSelector } from '../../hooks/use_selector'; import { tableDefaults } from '../../store/data_table/defaults'; +import { CELL_ACTIONS_DEFAULT_TRIGGER } from '../../../../common/constants'; const DATA_TABLE_ARIA_LABEL = i18n.translate('xpack.securitySolution.dataTable.ariaLabel', { defaultMessage: 'Alerts', @@ -62,10 +60,8 @@ export interface DataTableProps { browserFields: BrowserFields; bulkActions?: BulkActionsProp; data: TimelineItem[]; - defaultCellActions?: DataTableCellAction[]; - disabledCellActions: string[]; + disableCellActions?: boolean; fieldBrowserOptions?: FieldBrowserOptions; - filters?: Filter[]; id: string; leadingControlColumns: EuiDataGridControlColumn[]; loadPage: (newActivePage: number) => void; @@ -107,7 +103,7 @@ const EuiDataGridContainer = styled.div<{ hideLastPage: boolean }>` } `; -const memoizedColumnHeaders: ( +const memoizedGetColumnHeaders: ( headers: ColumnHeaderOptions[], browserFields: BrowserFields, isEventRenderedView: boolean @@ -119,10 +115,8 @@ export const DataTableComponent = React.memo( browserFields, bulkActions = true, data, - defaultCellActions, - disabledCellActions, + disableCellActions = false, fieldBrowserOptions, - filters, hasCrudPermissions, id, leadingControlColumns, @@ -143,7 +137,7 @@ export const DataTableComponent = React.memo( const { columns, selectedEventIds, showCheckboxes, sort, isLoading, defaultColumns } = dataTable; - const columnHeaders = memoizedColumnHeaders(columns, browserFields, isEventRenderedView); + const columnHeaders = memoizedGetColumnHeaders(columns, browserFields, isEventRenderedView); const dataGridRef = useRef(null); @@ -309,57 +303,52 @@ export const DataTableComponent = React.memo( [dispatch, id] ); + const columnsCellActionsProps = useMemo((): UseDataGridColumnsCellActionsProps => { + const fields: UseDataGridColumnsCellActionsProps['fields'] = disableCellActions + ? [] + : columnHeaders.map((column) => ({ + name: column.id, + type: column.type ?? 'keyword', + values: data.map( + ({ data: columnData }) => + columnData.find((rowData) => rowData.field === column.id)?.value + ), + aggregatable: column.aggregatable, + })); + + return { + triggerId: CELL_ACTIONS_DEFAULT_TRIGGER, + fields, + metadata: { + scopeId: id, + }, + dataGridRef, + }; + }, [disableCellActions, columnHeaders, data, id]); + + const columnsCellActions = useDataGridColumnsCellActions(columnsCellActionsProps); + const columnsWithCellActions: EuiDataGridColumn[] = useMemo( () => - columnHeaders.map((header) => { - const buildAction = (dataTableCellAction: DataTableCellAction) => - dataTableCellAction({ - browserFields, - data: data.map((row) => row.data), - ecsData: data.map((row) => row.ecs), - header: columnHeaders.find((h) => h.id === header.id), - pageSize: pagination.pageSize, - scopeId: id, - closeCellPopover: dataGridRef.current?.closeCellPopover, - }); - return { - ...header, - actions: { - ...header.actions, - additional: [ - { - iconType: 'cross', - label: REMOVE_COLUMN, - onClick: () => { - dispatch(dataTableActions.removeColumn({ id, columnId: header.id })); - }, - size: 'xs', + columnHeaders.map((header, columnIndex) => ({ + ...header, + actions: { + ...header.actions, + additional: [ + { + iconType: 'cross', + label: REMOVE_COLUMN, + onClick: () => { + dispatch(dataTableActions.removeColumn({ id, columnId: header.id })); }, - ], - }, - ...(hasCellActions({ - columnId: header.id, - disabledCellActions, - }) - ? { - cellActions: - header.dataTableCellActions?.map(buildAction) ?? - defaultCellActions?.map(buildAction), - visibleCellActions: 3, - } - : {}), - }; - }), - [ - browserFields, - columnHeaders, - data, - defaultCellActions, - disabledCellActions, - dispatch, - id, - pagination.pageSize, - ] + size: 'xs', + }, + ], + }, + cellActions: columnsCellActions[columnIndex] ?? [], + visibleCellActions: 3, + })), + [columnHeaders, columnsCellActions, dispatch, id] ); const renderTableCellValue = useMemo(() => { @@ -392,12 +381,12 @@ export const DataTableComponent = React.memo( } return renderCellValue({ + asPlainText: false, browserFields, columnId: header.id, data: rowData, ecsData: ecs, eventId, - globalFilters: filters, header, isDetails, isDraggable: false, @@ -417,7 +406,6 @@ export const DataTableComponent = React.memo( browserFields, columnHeaders, data, - filters, id, pagination.pageSize, renderCellValue, diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap index 23d6db8adf75e..89d876e5efa88 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap @@ -116,19 +116,18 @@ exports[`Event Details Overview Cards renders rows and spacers correctly 1`] = ` >
-
@@ -163,19 +162,18 @@ exports[`Event Details Overview Cards renders rows and spacers correctly 1`] = ` >
-
@@ -222,19 +220,18 @@ exports[`Event Details Overview Cards renders rows and spacers correctly 1`] = ` >
-
diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx index a72e0aa35f3ad..af8b54f6f5c90 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx @@ -30,7 +30,6 @@ import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell import { SourcererScopeName } from '../../store/sourcerer/model'; import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; -import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions'; import type { GlobalTimeArgs } from '../../containers/use_global_time'; import type { QueryTabBodyProps as UserQueryTabBodyProps } from '../../../explore/users/pages/navigation/types'; import type { QueryTabBodyProps as HostQueryTabBodyProps } from '../../../explore/hosts/pages/navigation/types'; @@ -182,7 +181,6 @@ const EventsQueryTabBodyComponent: React.FC = )} void; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; + renderCellValue: React.FC; rowRenderers: RowRenderer[]; additionalFilters?: React.ReactNode; hasCrudPermissions?: boolean; @@ -105,8 +103,8 @@ export interface EventsViewerProps { * NOTE: As of writting, it is not used in the Case_View component */ const StatefulEventsViewerComponent: React.FC = ({ - defaultCellActions, defaultModel, + disableCellActions, end, entityType = 'events', tableId, @@ -441,7 +439,6 @@ const StatefulEventsViewerComponent: React.FC css` SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly and `EuiPopover`, `EuiToolTip` global styles */ -export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimary: string } } }>` +export const AppGlobalStyle = createGlobalStyle<{ + theme: { eui: { euiColorPrimary: string; euiColorLightShade: string; euiSizeS: string } }; +}>` ${TIMELINE_OVERRIDES_CSS_STYLESHEET} @@ -103,11 +105,16 @@ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimar .euiPopoverFooter { border: 0; - margin-top: 0 !important; + margin-top: 0; .euiFlexGroup { flex-direction: column; } } + + .euiText + .euiPopoverFooter { + border-top: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; + margin-top: ${({ theme }) => theme.eui.euiSizeS}; + } } /* overrides default styling in angular code that was not theme-friendly */ diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/constants.ts b/x-pack/plugins/security_solution/public/common/lib/cell_actions/constants.ts deleted file mode 100644 index bccd4efa8f98c..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/cell_actions/constants.ts +++ /dev/null @@ -1,16 +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 { ALERT_RISK_SCORE } from '@kbn/rule-data-utils'; - -/** actions are disabled for these fields in tables and popovers */ -export const FIELDS_WITHOUT_CELL_ACTIONS = [ - 'signal.rule.risk_score', - 'signal.reason', - ALERT_RISK_SCORE, - 'kibana.alert.reason', -]; diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/expanded_cell_value_actions.test.tsx b/x-pack/plugins/security_solution/public/common/lib/cell_actions/expanded_cell_value_actions.test.tsx deleted file mode 100644 index e20c4887c0df9..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/cell_actions/expanded_cell_value_actions.test.tsx +++ /dev/null @@ -1,33 +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 { shallow } from 'enzyme'; -import React from 'react'; -import { ExpandedCellValueActions } from './expanded_cell_value_actions'; -import type { ColumnHeaderType } from '@kbn/timelines-plugin/common/types'; - -jest.mock('../kibana'); - -describe('ExpandedCellValueActions', () => { - const props = { - field: { - id: 'host.name', - type: 'keyword', - columnHeaderType: 'not-filtered' as ColumnHeaderType, - aggregatable: true, - }, - globalFilters: [], - onFilterAdded: () => {}, - scopeId: 'mockTimelineId', - value: ['mock value'], - }; - const wrapper = shallow(); - - test('renders show topN button', () => { - expect(wrapper.find('[data-test-subj="data-grid-expanded-show-top-n"]').exists()).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/expanded_cell_value_actions.tsx b/x-pack/plugins/security_solution/public/common/lib/cell_actions/expanded_cell_value_actions.tsx deleted file mode 100644 index 4d36c450fd177..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/cell_actions/expanded_cell_value_actions.tsx +++ /dev/null @@ -1,85 +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 { EuiButtonEmpty } from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import React, { useMemo, useState, useCallback } from 'react'; -import styled from 'styled-components'; -import type { Filter } from '@kbn/es-query'; -import type { ColumnHeaderOptions } from '../../../../common/types'; -import { allowTopN } from '../../components/drag_and_drop/helpers'; -import { ShowTopNButton } from '../../components/hover_actions/actions/show_top_n'; -import { SHOW_TOP_VALUES, HIDE_TOP_VALUES } from './translations'; - -interface Props { - field: ColumnHeaderOptions; - globalFilters?: Filter[]; - scopeId: string; - value: string[] | undefined; - onFilterAdded?: () => void; -} - -const StyledContent = styled.div<{ $isDetails: boolean }>` - border-bottom: 1px solid #d3dae6; - padding: ${({ $isDetails }) => ($isDetails ? '0 8px' : undefined)}; -`; - -const ExpandedCellValueActionsComponent: React.FC = ({ - field, - globalFilters, - onFilterAdded, - scopeId, - value, -}) => { - const showButton = useMemo( - () => - allowTopN({ - fieldName: field.id, - fieldType: field.type ?? '', - isAggregatable: field.aggregatable ?? false, - hideTopN: false, - }), - [field] - ); - - const [showTopN, setShowTopN] = useState(false); - const onClick = useCallback(() => setShowTopN(!showTopN), [showTopN]); - - return ( - <> - - {showButton ? ( - - ) : null} - - - ); -}; - -ExpandedCellValueActionsComponent.displayName = 'ExpandedCellValueActionsComponent'; - -export const ExpandedCellValueActions = React.memo(ExpandedCellValueActionsComponent); diff --git a/x-pack/plugins/security_solution/public/common/store/data_table/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/common/store/data_table/epic_local_storage.test.tsx index 4eb160b644904..0ffd6c33aa3ed 100644 --- a/x-pack/plugins/security_solution/public/common/store/data_table/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/common/store/data_table/epic_local_storage.test.tsx @@ -40,7 +40,6 @@ import { addTableInStorage } from '../../../timelines/containers/local_storage'; import { Direction } from '../../../../common/search_strategy'; import { StatefulEventsViewer } from '../../components/events_viewer'; import { eventsDefaultModel } from '../../components/events_viewer/default_model'; -import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions'; import { EntityType } from '@kbn/timelines-plugin/common'; import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns'; import { SourcererScopeName } from '../sourcerer/model'; @@ -64,7 +63,6 @@ describe('epicLocalStorage', () => { const ACTION_BUTTON_COUNT = 4; testProps = { - defaultCellActions, defaultModel: eventsDefaultModel, end: to, entityType: EntityType.ALERTS, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index b6de0e4f80c7b..c191dcf20e98e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -39,7 +39,6 @@ import { StatefulEventsViewer } from '../../../common/components/events_viewer'; import { useSourcererDataView } from '../../../common/containers/sourcerer'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query'; -import { defaultCellActions } from '../../../common/lib/cell_actions/default_cell_actions'; import { useKibana } from '../../../common/lib/kibana'; import type { inputsModel, State } from '../../../common/store'; import { inputsSelectors } from '../../../common/store'; @@ -416,7 +415,6 @@ export const AlertsTableComponent: React.FC = ({ = ({ = (props) => RenderCellValue({ ...props, enableActions: false, asPlainText: true }); +> = (props) => RenderCellValue({ ...props, asPlainText: true }); diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx index 21d0d3a199f55..476e15fce02a3 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx +++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx @@ -91,7 +91,6 @@ export const useRenderCellValue = ({ data, ecsData, eventId, - globalFilters, header, isDetails = false, isDraggable = false, @@ -120,7 +119,6 @@ export const useRenderCellValue = ({ data={data} ecsData={ecsData} eventId={eventId} - globalFilters={globalFilters} header={myHeader} isDetails={isDetails} isDraggable={isDraggable} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx index 34f57d567951c..3487e2770ff45 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx @@ -127,42 +127,6 @@ describe('DefaultCellRenderer', () => { values: ['2018-11-05T19:03:25.937Z'], }); }); - - test('if in tgrid expanded value, it renders ExpandedCellValueActions', () => { - const data = cloneDeep(mockTimelineData[0].data); - const header = cloneDeep(defaultHeaders[1]); - const isDetails = true; - const id = 'event.severity'; - const wrapper = mount( - - - - - - - - ); - - expect( - wrapper.find('[data-test-subj="data-grid-expanded-cell-value-actions"]').exists() - ).toBeTruthy(); - }); }); describe('host link rendering', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx index 1fafa85162ea1..8056a07fb39de 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx @@ -13,12 +13,6 @@ import { columnRenderers } from '../body/renderers'; import { getColumnRenderer } from '../body/renderers/get_column_renderer'; import type { CellValueElementProps } from '.'; import { getLinkColumnDefinition } from '../../../../common/lib/cell_actions/helpers'; -import { FIELDS_WITHOUT_CELL_ACTIONS } from '../../../../common/lib/cell_actions/constants'; -import { ExpandedCellValueActions } from '../../../../common/lib/cell_actions/expanded_cell_value_actions'; - -const hasCellActions = (columnId?: string) => { - return columnId && !FIELDS_WITHOUT_CELL_ACTIONS.includes(columnId); -}; const StyledContent = styled.div<{ $isDetails: boolean }>` padding: ${({ $isDetails }) => ($isDetails ? '0 8px' : undefined)}; @@ -28,7 +22,6 @@ export const DefaultCellRenderer: React.FC = ({ data, ecsData, eventId, - globalFilters, header, isDetails, isDraggable, @@ -37,7 +30,6 @@ export const DefaultCellRenderer: React.FC = ({ rowRenderers, scopeId, truncate, - enableActions = true, asPlainText, }) => { const asPlainTextDefault = useMemo(() => { @@ -54,31 +46,21 @@ export const DefaultCellRenderer: React.FC = ({ ? 'eui-textBreakWord' : 'eui-displayInlineBlock eui-textTruncate'; return ( - <> - - {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ - asPlainText: asPlainText ?? asPlainTextDefault, // we want to render value with links as plain text but keep other formatters like badge. Except rule name for non preview tables - columnName: header.id, - ecsData, - eventId, - field: header, - isDetails, - isDraggable, - linkValues, - rowRenderers, - scopeId, - truncate, - values, - })} - - {enableActions && isDetails && hasCellActions(header.id) && ( - - )} - + + {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ + asPlainText: asPlainText ?? asPlainTextDefault, // we want to render value with links as plain text but keep other formatters like badge. Except rule name for non preview tables + columnName: header.id, + ecsData, + eventId, + field: header, + isDetails, + isDraggable, + linkValues, + rowRenderers, + scopeId, + truncate, + values, + })} + ); }; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts index 7df5030a70091..e0938707ea5a0 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts @@ -8,8 +8,14 @@ import { userInfo } from 'os'; import execa from 'execa'; import nodeFetch from 'node-fetch'; -import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; +import { + AGENT_POLICY_SAVED_OBJECT_TYPE, + packagePolicyRouteService, + type UpdatePackagePolicyResponse, + type UpdatePackagePolicy, +} from '@kbn/fleet-plugin/common'; import chalk from 'chalk'; +import { inspect } from 'util'; import { getEndpointPackageInfo } from '../../../common/endpoint/index_data'; import { indexFleetEndpointPolicy } from '../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; import { @@ -19,6 +25,7 @@ import { waitForHostToEnroll, } from '../common/fleet_services'; import { getRuntimeServices } from './runtime'; +import { type PolicyData, ProtectionModes } from '../../../common/endpoint/types'; interface ElasticArtifactSearchResponse { manifest: { @@ -153,7 +160,7 @@ export const enrollEndpointHost = async () => { Delete VM: ${chalk.bold(`multipass delete -p ${vmName}${await getVmCountNotice()}`)} `); } catch (error) { - log.error(error); + log.error(inspect(error, { depth: 4 })); log.indent(-4); throw error; } @@ -215,9 +222,40 @@ const getOrCreateAgentPolicyId = async (): Promise => { endpointPackageVersion, agentPolicyName ); - const agentPolicy = response.agentPolicies[0]; + // Update the Endpoint integration policy to enable all protections + // eslint-disable-next-line @typescript-eslint/naming-convention + const { created_by, created_at, updated_at, updated_by, id, version, revision, ...restOfPolicy } = + response.integrationPolicies[0]; + const updatedEndpointPolicy: UpdatePackagePolicy = restOfPolicy; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const policy = updatedEndpointPolicy!.inputs[0]!.config!.policy.value; + + policy.mac.malware.mode = ProtectionModes.prevent; + policy.windows.malware.mode = ProtectionModes.prevent; + policy.linux.malware.mode = ProtectionModes.prevent; + + policy.mac.memory_protection.mode = ProtectionModes.prevent; + policy.windows.memory_protection.mode = ProtectionModes.prevent; + policy.linux.memory_protection.mode = ProtectionModes.prevent; + + policy.mac.behavior_protection.mode = ProtectionModes.prevent; + policy.windows.behavior_protection.mode = ProtectionModes.prevent; + policy.linux.behavior_protection.mode = ProtectionModes.prevent; + + policy.windows.ransomware.mode = ProtectionModes.prevent; + + response.integrationPolicies[0] = ( + await kbnClient + .request({ + method: 'PUT', + path: packagePolicyRouteService.getUpdatePath(response.integrationPolicies[0].id), + body: updatedEndpointPolicy, + }) + .then((res) => res.data) + ).item as PolicyData; + log.info(`New agent policy with Endpoint integration created: Name: ${agentPolicy.name} Id: ${agentPolicy.id}`); diff --git a/x-pack/plugins/triggers_actions_ui/.storybook/context/http.ts b/x-pack/plugins/triggers_actions_ui/.storybook/context/http.ts index 69e1cf2f64b91..ba7cc79901801 100644 --- a/x-pack/plugins/triggers_actions_ui/.storybook/context/http.ts +++ b/x-pack/plugins/triggers_actions_ui/.storybook/context/http.ts @@ -279,8 +279,8 @@ const rulesSettingsGetResponse = (path: string) => { if (path.endsWith('/settings/_flapping')) { return { enabled: true, - lookBackWindow: 20, - statusChangeThreshold: 4, + look_back_window: 20, + status_change_threshold: 4, }; } }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_flapping_settings.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_flapping_settings.ts index 68947de984fb4..931b1037ef729 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_flapping_settings.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_flapping_settings.ts @@ -6,11 +6,23 @@ */ import { HttpSetup } from '@kbn/core/public'; +import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common'; import { RulesSettingsFlapping } from '@kbn/alerting-plugin/common'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; -export const getFlappingSettings = ({ http }: { http: HttpSetup }) => { - return http.get( +const rewriteBodyRes: RewriteRequestCase = ({ + look_back_window: lookBackWindow, + status_change_threshold: statusChangeThreshold, + ...rest +}: any) => ({ + ...rest, + lookBackWindow, + statusChangeThreshold, +}); + +export const getFlappingSettings = async ({ http }: { http: HttpSetup }) => { + const res = await http.get>( `${INTERNAL_BASE_ALERTING_API_PATH}/rules/settings/_flapping` ); + return rewriteBodyRes(res); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_flapping_settings.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_flapping_settings.ts index f38393b591d72..9e03da7e6e100 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_flapping_settings.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_flapping_settings.ts @@ -10,9 +10,20 @@ import { RulesSettingsFlapping, RulesSettingsFlappingProperties, } from '@kbn/alerting-plugin/common'; +import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; -export const updateFlappingSettings = ({ +const rewriteBodyRes: RewriteRequestCase = ({ + look_back_window: lookBackWindow, + status_change_threshold: statusChangeThreshold, + ...rest +}: any) => ({ + ...rest, + lookBackWindow, + statusChangeThreshold, +}); + +export const updateFlappingSettings = async ({ http, flappingSettings, }: { @@ -21,14 +32,21 @@ export const updateFlappingSettings = ({ }) => { let body: string; try { - body = JSON.stringify(flappingSettings); + body = JSON.stringify({ + enabled: flappingSettings.enabled, + look_back_window: flappingSettings.lookBackWindow, + status_change_threshold: flappingSettings.statusChangeThreshold, + }); } catch (e) { throw new Error(`Unable to parse flapping settings update params: ${e}`); } - return http.post( + + const res = await http.post>( `${INTERNAL_BASE_ALERTING_API_PATH}/rules/settings/_flapping`, { body, } ); + + return rewriteBodyRes(res); }; diff --git a/x-pack/test/alerting_api_integration/common/lib/reset_rules_settings.ts b/x-pack/test/alerting_api_integration/common/lib/reset_rules_settings.ts index b1b24856e9ef8..17ce4985e1f0c 100644 --- a/x-pack/test/alerting_api_integration/common/lib/reset_rules_settings.ts +++ b/x-pack/test/alerting_api_integration/common/lib/reset_rules_settings.ts @@ -14,6 +14,10 @@ export const resetRulesSettings = (supertest: any, space: string) => { .post(`${getUrlPrefix(space)}/internal/alerting/rules/settings/_flapping`) .set('kbn-xsrf', 'foo') .auth(Superuser.username, Superuser.password) - .send(DEFAULT_FLAPPING_SETTINGS) + .send({ + enabled: DEFAULT_FLAPPING_SETTINGS.enabled, + look_back_window: DEFAULT_FLAPPING_SETTINGS.lookBackWindow, + status_change_threshold: DEFAULT_FLAPPING_SETTINGS.statusChangeThreshold, + }) .expect(200); }; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/get_flapping_settings.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/get_flapping_settings.ts index 7bc307f41e6d4..386e32b8e5778 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/get_flapping_settings.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/get_flapping_settings.ts @@ -51,14 +51,16 @@ export default function getFlappingSettingsTests({ getService }: FtrProviderCont case 'space_1_all at space1': expect(response.statusCode).to.eql(200); expect(response.body.enabled).to.eql(DEFAULT_FLAPPING_SETTINGS.enabled); - expect(response.body.lookBackWindow).to.eql(DEFAULT_FLAPPING_SETTINGS.lookBackWindow); - expect(response.body.statusChangeThreshold).to.eql( + expect(response.body.look_back_window).to.eql( + DEFAULT_FLAPPING_SETTINGS.lookBackWindow + ); + expect(response.body.status_change_threshold).to.eql( DEFAULT_FLAPPING_SETTINGS.statusChangeThreshold ); - expect(response.body.createdBy).to.be.a('string'); - expect(response.body.updatedBy).to.be.a('string'); - expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0); - expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0); + expect(response.body.created_by).to.be.a('string'); + expect(response.body.updated_by).to.be.a('string'); + expect(Date.parse(response.body.created_at)).to.be.greaterThan(0); + expect(Date.parse(response.body.updated_at)).to.be.greaterThan(0); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/update_flapping_settings.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/update_flapping_settings.ts index 93659256d2e97..a4d39ad4be89b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/update_flapping_settings.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/update_flapping_settings.ts @@ -30,8 +30,8 @@ export default function updateFlappingSettingsTest({ getService }: FtrProviderCo .auth(user.username, user.password) .send({ enabled: false, - lookBackWindow: 20, - statusChangeThreshold: 20, + look_back_window: 20, + status_change_threshold: 20, }); switch (scenario.id) { @@ -51,12 +51,12 @@ export default function updateFlappingSettingsTest({ getService }: FtrProviderCo case 'space_1_all at space1': expect(response.statusCode).to.eql(200); expect(response.body.enabled).to.eql(false); - expect(response.body.lookBackWindow).to.eql(20); - expect(response.body.statusChangeThreshold).to.eql(20); - expect(response.body.createdBy).to.eql(user.username); - expect(response.body.updatedBy).to.eql(user.username); - expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0); - expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0); + expect(response.body.look_back_window).to.eql(20); + expect(response.body.status_change_threshold).to.eql(20); + expect(response.body.created_by).to.eql(user.username); + expect(response.body.updated_by).to.eql(user.username); + expect(Date.parse(response.body.created_at)).to.be.greaterThan(0); + expect(Date.parse(response.body.updated_at)).to.be.greaterThan(0); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); @@ -72,8 +72,8 @@ export default function updateFlappingSettingsTest({ getService }: FtrProviderCo .auth(Superuser.username, Superuser.password) .send({ enabled: true, - lookBackWindow: 200, - statusChangeThreshold: 200, + look_back_window: 200, + status_change_threshold: 200, }) .expect(400); @@ -87,8 +87,8 @@ export default function updateFlappingSettingsTest({ getService }: FtrProviderCo .auth(Superuser.username, Superuser.password) .send({ enabled: true, - lookBackWindow: 20, - statusChangeThreshold: 200, + look_back_window: 20, + status_change_threshold: 200, }) .expect(400); @@ -102,8 +102,8 @@ export default function updateFlappingSettingsTest({ getService }: FtrProviderCo .auth(Superuser.username, Superuser.password) .send({ enabled: true, - lookBackWindow: 5, - statusChangeThreshold: 10, + look_back_window: 5, + status_change_threshold: 10, }) .expect(400); @@ -121,14 +121,14 @@ export default function updateFlappingSettingsTest({ getService }: FtrProviderCo .auth(Superuser.username, Superuser.password) .send({ enabled: false, - lookBackWindow: 20, - statusChangeThreshold: 20, + look_back_window: 20, + status_change_threshold: 20, }); expect(postResponse.statusCode).to.eql(200); expect(postResponse.body.enabled).to.eql(false); - expect(postResponse.body.lookBackWindow).to.eql(20); - expect(postResponse.body.statusChangeThreshold).to.eql(20); + expect(postResponse.body.look_back_window).to.eql(20); + expect(postResponse.body.status_change_threshold).to.eql(20); // Get the rules settings in space2 const getResponse = await supertestWithoutAuth @@ -137,8 +137,8 @@ export default function updateFlappingSettingsTest({ getService }: FtrProviderCo expect(getResponse.statusCode).to.eql(200); expect(getResponse.body.enabled).to.eql(true); - expect(getResponse.body.lookBackWindow).to.eql(20); - expect(getResponse.body.statusChangeThreshold).to.eql(4); + expect(getResponse.body.look_back_window).to.eql(20); + expect(getResponse.body.status_change_threshold).to.eql(4); }); }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts index 5525631c2a534..6574c2164b06c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts @@ -546,8 +546,8 @@ export default function eventLogTests({ getService }: FtrProviderContext) { .auth('superuser', 'superuser') .send({ enabled: true, - lookBackWindow: 3, - statusChangeThreshold: 2, + look_back_window: 3, + status_change_threshold: 2, }) .expect(200); const { body: createdAction } = await supertest @@ -630,8 +630,8 @@ export default function eventLogTests({ getService }: FtrProviderContext) { .auth('superuser', 'superuser') .send({ enabled: true, - lookBackWindow: 3, - statusChangeThreshold: 2, + look_back_window: 3, + status_change_threshold: 2, }) .expect(200); const { body: createdAction } = await supertest diff --git a/x-pack/test/apm_api_integration/tests/alerts/alerting_api_helper.ts b/x-pack/test/apm_api_integration/tests/alerts/alerting_api_helper.ts new file mode 100644 index 0000000000000..b6c5b891b8353 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/alerts/alerting_api_helper.ts @@ -0,0 +1,117 @@ +/* + * 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 { SuperTest, Test } from 'supertest'; +import { ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; +import { ApmRuleParamsType } from '@kbn/apm-plugin/common/rules/schema'; +import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; +import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; +import { ApmApiClient } from '../../common/config'; + +export async function createIndexConnector({ + supertest, + name, + indexName, +}: { + supertest: SuperTest; + name: string; + indexName: string; +}) { + const { body } = await supertest + .post(`/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name, + config: { + index: indexName, + refresh: true, + }, + connector_type_id: '.index', + }); + return body.id as string; +} + +export async function createApmRule({ + supertest, + name, + ruleTypeId, + params, + actions = [], +}: { + supertest: SuperTest; + ruleTypeId: T; + name: string; + params: ApmRuleParamsType[T]; + actions?: any[]; +}) { + const { body } = await supertest + .post(`/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send({ + params, + consumer: 'apm', + schedule: { + interval: '1m', + }, + tags: ['apm'], + name, + rule_type_id: ruleTypeId, + actions, + }); + return body; +} + +function getTimerange() { + return { + start: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), + end: new Date(Date.now() + 5 * 60 * 1000).toISOString(), + }; +} + +export async function fetchServiceInventoryAlertCounts(apmApiClient: ApmApiClient) { + const timerange = getTimerange(); + const serviceInventoryResponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services', + params: { + query: { + ...timerange, + environment: 'ENVIRONMENT_ALL', + kuery: '', + probability: 1, + documentType: ApmDocumentType.ServiceTransactionMetric, + rollupInterval: RollupInterval.SixtyMinutes, + }, + }, + }); + return serviceInventoryResponse.body.items.reduce>((acc, item) => { + return { ...acc, [item.serviceName]: item.alertsCount ?? 0 }; + }, {}); +} + +export async function fetchServiceTabAlertCount({ + apmApiClient, + serviceName, +}: { + apmApiClient: ApmApiClient; + serviceName: string; +}) { + const timerange = getTimerange(); + const alertsCountReponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/alerts_count', + params: { + path: { + serviceName, + }, + query: { + ...timerange, + environment: 'ENVIRONMENT_ALL', + }, + }, + }); + + return alertsCountReponse.body.alertsCount; +} diff --git a/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts index 39c81e71948a0..a72760ca339ce 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts @@ -9,8 +9,10 @@ import { ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { range } from 'lodash'; +import { ANOMALY_SEVERITY } from '@kbn/apm-plugin/common/ml_constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createAndRunApmMlJobs } from '../../common/utils/create_and_run_apm_ml_jobs'; +import { createApmRule } from './alerting_api_helper'; import { waitForRuleStatus } from './wait_for_rule_status'; export default function ApiTest({ getService }: FtrProviderContext) { @@ -18,7 +20,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const ml = getService('ml'); - const log = getService('log'); const es = getService('es'); const synthtraceEsClient = getService('synthtraceEsClient'); @@ -81,36 +82,29 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('checks if alert is active', async () => { - const { body: createdRule } = await supertest - .post(`/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - environment: 'production', - windowSize: 99, - windowUnit: 'y', - anomalySeverityType: 'warning', - }, - consumer: 'apm', - schedule: { - interval: '1m', - }, - tags: ['apm', 'service.name:service-a'], - name: 'Latency anomaly | service-a', - rule_type_id: ApmRuleType.Anomaly, - notify_when: 'onActiveAlert', - actions: [], - }); - - ruleId = createdRule.id; - - const executionStatus = await waitForRuleStatus({ - id: ruleId, - expectedStatus: 'active', + const createdRule = await createApmRule({ supertest, - log, + name: 'Latency anomaly | service-a', + params: { + environment: 'production', + windowSize: 99, + windowUnit: 'y', + anomalySeverityType: ANOMALY_SEVERITY.WARNING, + }, + ruleTypeId: ApmRuleType.Anomaly, }); - expect(executionStatus.status).to.be('active'); + + ruleId = createdRule.id; + if (!ruleId) { + expect(ruleId).to.not.eql(undefined); + } else { + const executionStatus = await waitForRuleStatus({ + id: ruleId, + expectedStatus: 'active', + supertest, + }); + expect(executionStatus.status).to.be('active'); + } }); }); } diff --git a/x-pack/test/apm_api_integration/tests/alerts/error_count_threshold.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/error_count_threshold.spec.ts new file mode 100644 index 0000000000000..a19f1ef93503a --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/alerts/error_count_threshold.spec.ts @@ -0,0 +1,165 @@ +/* + * 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 { ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; +import { errorCountMessage } from '@kbn/apm-plugin/common/rules/default_action_message'; +import { apm, timerange } from '@kbn/apm-synthtrace-client'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createApmRule, + createIndexConnector, + fetchServiceInventoryAlertCounts, + fetchServiceTabAlertCount, +} from './alerting_api_helper'; +import { waitForRuleStatus, waitForDocumentInIndex } from './wait_for_rule_status'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const registry = getService('registry'); + + const supertest = getService('supertest'); + const es = getService('es'); + const apmApiClient = getService('apmApiClient'); + const esDeleteAllIndices = getService('esDeleteAllIndices'); + + const synthtraceEsClient = getService('synthtraceEsClient'); + + registry.when('error count threshold alert', { config: 'basic', archives: [] }, () => { + let ruleId: string; + let actionId: string | undefined; + + const INDEX_NAME = 'error-count'; + + before(async () => { + const opbeansJava = apm + .service({ name: 'opbeans-java', environment: 'production', agentName: 'java' }) + .instance('instance'); + const opbeansNode = apm + .service({ name: 'opbeans-node', environment: 'production', agentName: 'node' }) + .instance('instance'); + const events = timerange('now-15m', 'now') + .ratePerMinute(1) + .generator((timestamp) => { + return [ + opbeansJava + .transaction({ transactionName: 'tx-java' }) + .timestamp(timestamp) + .duration(100) + .failure() + .errors( + opbeansJava + .error({ message: '[ResponseError] index_not_found_exception' }) + .timestamp(timestamp + 50) + ), + opbeansNode + .transaction({ transactionName: 'tx-node' }) + .timestamp(timestamp) + .duration(100) + .success(), + ]; + }); + await synthtraceEsClient.index(events); + }); + + after(async () => { + await synthtraceEsClient.clean(); + await supertest.delete(`/api/alerting/rule/${ruleId}`).set('kbn-xsrf', 'foo'); + await supertest.delete(`/api/actions/connector/${actionId}`).set('kbn-xsrf', 'foo'); + await esDeleteAllIndices(['.alerts*', INDEX_NAME]); + await es.deleteByQuery({ + index: '.kibana-event-log-*', + query: { term: { 'kibana.alert.rule.consumer': 'apm' } }, + }); + }); + + describe('create alert', () => { + before(async () => { + actionId = await createIndexConnector({ + supertest, + name: 'Error count API test', + indexName: INDEX_NAME, + }); + const createdRule = await createApmRule({ + supertest, + ruleTypeId: ApmRuleType.ErrorCount, + name: 'Apm error count', + params: { + environment: 'production', + threshold: 1, + windowSize: 1, + windowUnit: 'h', + }, + actions: [ + { + group: 'threshold_met', + id: actionId, + params: { + documents: [{ message: errorCountMessage }], + }, + frequency: { + notify_when: 'onActionGroupChange', + throttle: null, + summary: false, + }, + }, + ], + }); + expect(createdRule.id).to.not.eql(undefined); + ruleId = createdRule.id; + }); + + it('checks if alert is active', async () => { + const executionStatus = await waitForRuleStatus({ + id: ruleId, + expectedStatus: 'active', + supertest, + }); + expect(executionStatus.status).to.be('active'); + }); + + it('returns correct message', async () => { + const resp = await waitForDocumentInIndex<{ message: string }>({ + es, + indexName: INDEX_NAME, + }); + + expect(resp.hits.hits[0]._source?.message).eql( + `Apm error count alert is firing because of the following conditions: + +- Service name: opbeans-java +- Environment: production +- Threshold: 1 +- Triggered value: 15 errors over the last 1 hr` + ); + }); + + it('shows the correct alert count for each service on service inventory', async () => { + const serviceInventoryAlertCounts = await fetchServiceInventoryAlertCounts(apmApiClient); + expect(serviceInventoryAlertCounts).to.eql({ + 'opbeans-node': 0, + 'opbeans-java': 1, + }); + }); + + it('shows the correct alert count in opbeans-java service', async () => { + const serviceTabAlertCount = await fetchServiceTabAlertCount({ + apmApiClient, + serviceName: 'opbeans-java', + }); + expect(serviceTabAlertCount).to.be(1); + }); + + it('shows the correct alert count in opbeans-node service', async () => { + const serviceTabAlertCount = await fetchServiceTabAlertCount({ + apmApiClient, + serviceName: 'opbeans-node', + }); + expect(serviceTabAlertCount).to.be(0); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/alerts/wait_for_rule_status.ts b/x-pack/test/apm_api_integration/tests/alerts/wait_for_rule_status.ts index f4814358f1636..44da2bea36bf2 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/wait_for_rule_status.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/wait_for_rule_status.ts @@ -4,53 +4,52 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { ToolingLog } from '@kbn/tooling-log'; -import expect from '@kbn/expect'; +import type { Client } from '@elastic/elasticsearch'; +import type { + AggregationsAggregate, + SearchResponse, +} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import pRetry from 'p-retry'; import type SuperTest from 'supertest'; -const WAIT_FOR_STATUS_INCREMENT = 500; - export async function waitForRuleStatus({ id, expectedStatus, - waitMillis = 10000, supertest, - log, }: { + id: string; expectedStatus: string; supertest: SuperTest.SuperTest; - log: ToolingLog; - waitMillis?: number; - id?: string; }): Promise> { - if (waitMillis < 0 || !id) { - expect().fail(`waiting for alert ${id} status ${expectedStatus} timed out`); - } - - const response = await supertest.get(`/api/alerting/rule/${id}`); - expect(response.status).to.eql(200); - - const { execution_status: executionStatus } = response.body || {}; - const { status } = executionStatus || {}; - - const message = `waitForStatus(${expectedStatus}): got ${JSON.stringify(executionStatus)}`; - - if (status === expectedStatus) { - return executionStatus; - } - - log.debug(`${message}, retrying`); - - await delay(WAIT_FOR_STATUS_INCREMENT); - return await waitForRuleStatus({ - id, - expectedStatus, - waitMillis: waitMillis - WAIT_FOR_STATUS_INCREMENT, - supertest, - log, - }); + return pRetry( + async () => { + const response = await supertest.get(`/api/alerting/rule/${id}`); + const { execution_status: executionStatus } = response.body || {}; + const { status } = executionStatus || {}; + if (status !== expectedStatus) { + throw new Error(`waitForStatus(${expectedStatus}): got ${status}`); + } + return executionStatus; + }, + { retries: 10 } + ); } -async function delay(millis: number): Promise { - await new Promise((resolve) => setTimeout(resolve, millis)); +export async function waitForDocumentInIndex({ + es, + indexName, +}: { + es: Client; + indexName: string; +}): Promise>> { + return pRetry( + async () => { + const response = await es.search({ index: indexName }); + if (response.hits.hits.length === 0) { + throw new Error('No hits found'); + } + return response; + }, + { retries: 10 } + ); } diff --git a/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts b/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts index 69e3d7678ca9e..abae62f2012a6 100644 --- a/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts @@ -4,10 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; +import { AggregationType, ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { waitForActiveAlert } from '../../../common/utils/wait_for_active_alert'; +import { createApmRule } from '../../alerts/alerting_api_helper'; import { createServiceGroupApi, deleteAllServiceGroups, @@ -26,28 +27,21 @@ export default function ApiTest({ getService }: FtrProviderContext) { const start = Date.now() - 24 * 60 * 60 * 1000; const end = Date.now(); - async function createRule() { - return supertest - .post(`/api/alerting/rule`) - .set('kbn-xsrf', 'true') - .send({ - params: { - serviceName: 'synth-go', - transactionType: '', - windowSize: 99, - windowUnit: 'y', - threshold: 100, - aggregationType: 'avg', - environment: 'testing', - }, - consumer: 'apm', - schedule: { interval: '1m' }, - tags: ['apm'], - name: 'Latency threshold | synth-go', - rule_type_id: ApmRuleType.TransactionDuration, - notify_when: 'onActiveAlert', - actions: [], - }); + function createRule() { + return createApmRule({ + supertest, + name: 'Latency threshold | synth-go', + params: { + serviceName: 'synth-go', + transactionType: '', + windowSize: 99, + windowUnit: 'y', + threshold: 100, + aggregationType: AggregationType.Avg, + environment: 'testing', + }, + ruleTypeId: ApmRuleType.TransactionDuration, + }); } registry.when('Service group counts', { config: 'basic', archives: [] }, () => { @@ -89,7 +83,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { describe('with alerts', () => { let ruleId: string; before(async () => { - const { body: createdRule } = await createRule(); + const createdRule = await createRule(); ruleId = createdRule.id; await waitForActiveAlert({ ruleId, esClient, log }); }); diff --git a/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts b/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts index a8c92dfdd256e..35ee8da8ba39b 100644 --- a/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts @@ -5,10 +5,11 @@ * 2.0. */ import expect from '@kbn/expect'; -import { ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; +import { AggregationType, ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types'; import { apm, timerange } from '@kbn/apm-synthtrace-client'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { waitForActiveAlert } from '../../common/utils/wait_for_active_alert'; +import { createApmRule } from '../alerts/alerting_api_helper'; export default function ServiceAlerts({ getService }: FtrProviderContext) { const registry = getService('registry'); @@ -42,28 +43,21 @@ export default function ServiceAlerts({ getService }: FtrProviderContext) { }); } - async function createRule() { - return supertest - .post(`/api/alerting/rule`) - .set('kbn-xsrf', 'true') - .send({ - params: { - serviceName: goService, - transactionType: '', - windowSize: 99, - windowUnit: 'y', - threshold: 100, - aggregationType: 'avg', - environment: 'testing', - }, - consumer: 'apm', - schedule: { interval: '1m' }, - tags: ['apm'], - name: `Latency threshold | ${goService}`, - rule_type_id: ApmRuleType.TransactionDuration, - notify_when: 'onActiveAlert', - actions: [], - }); + function createRule() { + return createApmRule({ + supertest, + name: `Latency threshold | ${goService}`, + params: { + serviceName: goService, + transactionType: '', + windowSize: 99, + windowUnit: 'y', + threshold: 100, + aggregationType: AggregationType.Avg, + environment: 'testing', + }, + ruleTypeId: ApmRuleType.TransactionDuration, + }); } registry.when('Service alerts', { config: 'basic', archives: [] }, () => { @@ -121,7 +115,7 @@ export default function ServiceAlerts({ getService }: FtrProviderContext) { describe('with alerts', () => { let ruleId: string; before(async () => { - const { body: createdRule } = await createRule(); + const createdRule = await createRule(); ruleId = createdRule.id; await waitForActiveAlert({ ruleId, esClient, log }); }); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/cases/push_case.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/cases/push_case.ts index a18c24c62652d..d6d0919be8b4a 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/cases/push_case.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/cases/push_case.ts @@ -616,7 +616,8 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - describe('alerts', () => { + // FLAKY: https://github.com/elastic/kibana/issues/150962 + describe.skip('alerts', () => { const defaultSignalsIndex = '.siem-signals-default-000001'; const signalID = '4679431ee0ba3209b6fcd60a255a696886fe0a7d18f5375de510ff5b68fa6b78'; const signalID2 = '1023bcfea939643c5e51fd8df53797e0ea693cee547db579ab56d96402365c1e'; diff --git a/x-pack/test/functional/apps/infra/index.ts b/x-pack/test/functional/apps/infra/index.ts index 9e6cb3f74cc96..197481c431394 100644 --- a/x-pack/test/functional/apps/infra/index.ts +++ b/x-pack/test/functional/apps/infra/index.ts @@ -25,6 +25,7 @@ export default ({ loadTestFile }: FtrProviderContext) => { loadTestFile(require.resolve('./log_entry_rate_tab')); loadTestFile(require.resolve('./logs_source_configuration')); loadTestFile(require.resolve('./link_to')); + loadTestFile(require.resolve('./log_stream')); }); }); }; diff --git a/x-pack/test/functional/apps/infra/log_stream.ts b/x-pack/test/functional/apps/infra/log_stream.ts new file mode 100644 index 0000000000000..a0836e4ef57e9 --- /dev/null +++ b/x-pack/test/functional/apps/infra/log_stream.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { URL } from 'url'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +const SERVICE_ID = '49a18510598271e924253ed2581d7ada'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const pageObjects = getPageObjects(['common']); + const retry = getService('retry'); + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + + describe('Log stream', function () { + describe('Legacy URL handling', async () => { + describe('Correctly handles legacy versions of logFilter', async () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/infra/8.0.0/logs_and_metrics'); + }); + after(async () => { + await esArchiver.unload( + 'x-pack/test/functional/es_archives/infra/8.0.0/logs_and_metrics' + ); + }); + it('Expression and kind', async () => { + const location = { + hash: '', + pathname: '/stream', + search: `logFilter=(expression:'service.id:"${SERVICE_ID}"',kind:kuery)`, + state: undefined, + }; + + await pageObjects.common.navigateToUrlWithBrowserHistory( + 'infraLogs', + location.pathname, + location.search, + { + ensureCurrentUrl: false, + } + ); + + await retry.tryForTime(5000, async () => { + const currentUrl = await browser.getCurrentUrl(); + const parsedUrl = new URL(currentUrl); + + expect(parsedUrl.pathname).to.be('/app/logs/stream'); + expect(parsedUrl.searchParams.get('logFilter')).to.be( + `(filters:!(),query:(language:kuery,query:\'service.id:"${SERVICE_ID}"\'))` + ); + }); + }); + it('Top-level query and language', async () => { + const location = { + hash: '', + pathname: '/stream', + search: `logFilter=(query:'service.id:"${SERVICE_ID}"',language:kuery)`, + state: undefined, + }; + + await pageObjects.common.navigateToUrlWithBrowserHistory( + 'infraLogs', + location.pathname, + location.search, + { + ensureCurrentUrl: false, + } + ); + + await retry.tryForTime(5000, async () => { + const currentUrl = await browser.getCurrentUrl(); + const parsedUrl = new URL(currentUrl); + + expect(parsedUrl.pathname).to.be('/app/logs/stream'); + expect(parsedUrl.searchParams.get('logFilter')).to.be( + `(filters:!(),query:(language:kuery,query:\'service.id:"${SERVICE_ID}"\'))` + ); + }); + }); + }); + }); + }); +}; diff --git a/x-pack/test/functional/apps/lens/group3/error_handling.ts b/x-pack/test/functional/apps/lens/group3/error_handling.ts index 85f9cad73992c..997066343509c 100644 --- a/x-pack/test/functional/apps/lens/group3/error_handling.ts +++ b/x-pack/test/functional/apps/lens/group3/error_handling.ts @@ -129,5 +129,27 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'x-pack/test/functional/fixtures/kbn_archiver/lens/missing_fields' ); }); + + it('displays fundamental configuration issues on dashboard', async () => { + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/lens/fundamental_config_errors_on_dashboard' + ); + + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('lens fundamental config errors dash'); + + const failureElements = await testSubjects.findAll('errorMessageMarkdown'); + const errorMessages = await Promise.all(failureElements.map((el) => el.getVisibleText())); + + expect(errorMessages).to.eql([ + 'Visualization type not found.', + 'The visualization type lnsUNKNOWN could not be resolved.', + 'Could not find datasource for the visualization', + ]); + + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/lens/fundamental_config_errors_on_dashboard' + ); + }); }); } diff --git a/x-pack/test/functional/fixtures/kbn_archiver/lens/fundamental_config_errors_on_dashboard.json b/x-pack/test/functional/fixtures/kbn_archiver/lens/fundamental_config_errors_on_dashboard.json new file mode 100644 index 0000000000000..435c5f43eb1b9 --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/lens/fundamental_config_errors_on_dashboard.json @@ -0,0 +1,9 @@ +{"attributes":{"fieldFormatMap":"{\"hour_of_day\":{}}","name":"Kibana Sample Data Logs","runtimeFieldMap":"{\"hour_of_day\":{\"type\":\"long\",\"script\":{\"source\":\"emit(doc['timestamp'].value.getHour());\"}}}","timeFieldName":"timestamp","title":"kibana_sample_data_logs"},"coreMigrationVersion":"8.8.0","created_at":"2023-02-08T22:00:06.671Z","id":"90943e30-9a47-11e8-b64d-95841ca0b247","migrationVersion":{"index-pattern":"8.0.0"},"references":[],"type":"index-pattern","updated_at":"2023-02-08T22:00:06.671Z","version":"WzE1MywxXQ=="} + +{"attributes":{"state":{"adHocDataViews":{},"datasourceStates":{"formBased":{"layers":{"19b05f1b-1187-4c55-abe1-a0bfc45d35f7":{"columnOrder":["bf200ba8-e158-41b4-a701-1121910912e9","b39bfe4c-3699-4f11-a211-aa8cf6d19e1c"],"columns":{"b39bfe4c-3699-4f11-a211-aa8cf6d19e1c":{"dataType":"number","isBucketed":false,"label":"Median of bytes","operationType":"median","params":{"emptyAsNull":true},"scale":"ratio","sourceField":"bytes"},"bf200ba8-e158-41b4-a701-1121910912e9":{"dataType":"date","isBucketed":true,"label":"timestamp","operationType":"date_histogram","params":{"dropPartials":false,"includeEmptyRows":true,"interval":"auto"},"scale":"interval","sourceField":"timestamp"}},"incompleteColumns":{},"sampling":1}}},"textBased":{"layers":{}}},"filters":[],"internalReferences":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"labelsOrientation":{"x":0,"yLeft":0,"yRight":0},"layers":[{"accessors":["b39bfe4c-3699-4f11-a211-aa8cf6d19e1c"],"layerId":"19b05f1b-1187-4c55-abe1-a0bfc45d35f7","layerType":"data","position":"top","seriesType":"bar_stacked","showGridlines":false,"xAccessor":"bf200ba8-e158-41b4-a701-1121910912e9"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"missing visualization type","visualizationType":null},"coreMigrationVersion":"8.8.0","created_at":"2023-02-08T22:00:21.379Z","id":"323d9ca0-a7f8-11ed-a593-e73d16536d96","migrationVersion":{"lens":"8.6.0"},"references":[{"id":"90943e30-9a47-11e8-b64d-95841ca0b247","name":"indexpattern-datasource-layer-19b05f1b-1187-4c55-abe1-a0bfc45d35f7","type":"index-pattern"}],"type":"lens","updated_at":"2023-02-08T22:00:21.379Z","version":"WzE2NywxXQ=="} + +{"attributes":{"state":{"adHocDataViews":{},"datasourceStates":{"formBased":{"layers":{"19b05f1b-1187-4c55-abe1-a0bfc45d35f7":{"columnOrder":["bf200ba8-e158-41b4-a701-1121910912e9","b39bfe4c-3699-4f11-a211-aa8cf6d19e1c"],"columns":{"b39bfe4c-3699-4f11-a211-aa8cf6d19e1c":{"dataType":"number","isBucketed":false,"label":"Median of bytes","operationType":"median","params":{"emptyAsNull":true},"scale":"ratio","sourceField":"bytes"},"bf200ba8-e158-41b4-a701-1121910912e9":{"dataType":"date","isBucketed":true,"label":"timestamp","operationType":"date_histogram","params":{"dropPartials":false,"includeEmptyRows":true,"interval":"auto"},"scale":"interval","sourceField":"timestamp"}},"incompleteColumns":{},"sampling":1}}},"textBased":{"layers":{}}},"filters":[],"internalReferences":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"labelsOrientation":{"x":0,"yLeft":0,"yRight":0},"layers":[{"accessors":["b39bfe4c-3699-4f11-a211-aa8cf6d19e1c"],"layerId":"19b05f1b-1187-4c55-abe1-a0bfc45d35f7","layerType":"data","position":"top","seriesType":"bar_stacked","showGridlines":false,"xAccessor":"bf200ba8-e158-41b4-a701-1121910912e9"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"unknown visualization ID","visualizationType":"lnsUNKNOWN"},"coreMigrationVersion":"8.8.0","created_at":"2023-02-08T22:00:21.379Z","id":"40e08f60-a7f8-11ed-a593-e73d16536d96","migrationVersion":{"lens":"8.6.0"},"references":[{"id":"90943e30-9a47-11e8-b64d-95841ca0b247","name":"indexpattern-datasource-layer-19b05f1b-1187-4c55-abe1-a0bfc45d35f7","type":"index-pattern"}],"type":"lens","updated_at":"2023-02-08T22:00:21.379Z","version":"WzE2OCwxXQ=="} + +{"attributes":{"state":{"adHocDataViews":{},"datasourceStates":{"UNKNOWN":{"layers":{"19b05f1b-1187-4c55-abe1-a0bfc45d35f7":{"columnOrder":["bf200ba8-e158-41b4-a701-1121910912e9","b39bfe4c-3699-4f11-a211-aa8cf6d19e1c"],"columns":{"b39bfe4c-3699-4f11-a211-aa8cf6d19e1c":{"dataType":"number","isBucketed":false,"label":"Median of bytes","operationType":"median","params":{"emptyAsNull":true},"scale":"ratio","sourceField":"bytes"},"bf200ba8-e158-41b4-a701-1121910912e9":{"dataType":"date","isBucketed":true,"label":"timestamp","operationType":"date_histogram","params":{"dropPartials":false,"includeEmptyRows":true,"interval":"auto"},"scale":"interval","sourceField":"timestamp"}},"incompleteColumns":{},"sampling":1}}},"textBased":{"layers":{}}},"filters":[],"internalReferences":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"labelsOrientation":{"x":0,"yLeft":0,"yRight":0},"layers":[{"accessors":["b39bfe4c-3699-4f11-a211-aa8cf6d19e1c"],"layerId":"19b05f1b-1187-4c55-abe1-a0bfc45d35f7","layerType":"data","position":"top","seriesType":"bar_stacked","showGridlines":false,"xAccessor":"bf200ba8-e158-41b4-a701-1121910912e9"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"unknown datasource ID","visualizationType":"lnsXY"},"coreMigrationVersion":"8.8.0","created_at":"2023-02-08T22:00:21.379Z","id":"49600170-a7f8-11ed-a593-e73d16536d96","migrationVersion":{"lens":"8.6.0"},"references":[{"id":"90943e30-9a47-11e8-b64d-95841ca0b247","name":"indexpattern-datasource-layer-19b05f1b-1187-4c55-abe1-a0bfc45d35f7","type":"index-pattern"}],"type":"lens","updated_at":"2023-02-08T22:00:21.379Z","version":"WzE2OSwxXQ=="} + +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"},"optionsJSON":"{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}","panelsJSON":"[{\"version\":\"8.8.0\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"56667b2c-e6c4-49f7-9c07-e27e798d7fea\"},\"panelIndex\":\"56667b2c-e6c4-49f7-9c07-e27e798d7fea\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_56667b2c-e6c4-49f7-9c07-e27e798d7fea\"},{\"version\":\"8.8.0\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"802298ec-2195-460c-a6ca-5b0a46271584\"},\"panelIndex\":\"802298ec-2195-460c-a6ca-5b0a46271584\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_802298ec-2195-460c-a6ca-5b0a46271584\"},{\"version\":\"8.8.0\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"12060b3e-ec82-4c33-9f67-aa713e512211\"},\"panelIndex\":\"12060b3e-ec82-4c33-9f67-aa713e512211\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_12060b3e-ec82-4c33-9f67-aa713e512211\"}]","timeRestore":false,"title":"lens fundamental config errors dash","version":1},"coreMigrationVersion":"8.8.0","created_at":"2023-02-08T22:14:41.552Z","id":"674f1f40-a7f8-11ed-a593-e73d16536d96","migrationVersion":{"dashboard":"8.7.0"},"references":[{"id":"323d9ca0-a7f8-11ed-a593-e73d16536d96","name":"56667b2c-e6c4-49f7-9c07-e27e798d7fea:panel_56667b2c-e6c4-49f7-9c07-e27e798d7fea","type":"lens"},{"id":"40e08f60-a7f8-11ed-a593-e73d16536d96","name":"802298ec-2195-460c-a6ca-5b0a46271584:panel_802298ec-2195-460c-a6ca-5b0a46271584","type":"lens"},{"id":"49600170-a7f8-11ed-a593-e73d16536d96","name":"12060b3e-ec82-4c33-9f67-aa713e512211:panel_12060b3e-ec82-4c33-9f67-aa713e512211","type":"lens"}],"type":"dashboard","updated_at":"2023-02-08T22:14:41.552Z","version":"WzI0MCwxXQ=="} \ No newline at end of file diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_settings.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_settings.ts index 6b4297f2dc153..56b6d08253ec4 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_settings.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_settings.ts @@ -47,8 +47,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { .set('kbn-xsrf', 'foo') .send({ enabled: true, - lookBackWindow: 10, - statusChangeThreshold: 10, + look_back_window: 10, + status_change_threshold: 10, }) .expect(200); }); diff --git a/x-pack/test/security_solution_ftr/page_objects/detections/index.ts b/x-pack/test/security_solution_ftr/page_objects/detections/index.ts index 1b2b6628afe69..e633bd9b62dda 100644 --- a/x-pack/test/security_solution_ftr/page_objects/detections/index.ts +++ b/x-pack/test/security_solution_ftr/page_objects/detections/index.ts @@ -172,7 +172,7 @@ export class DetectionsPageObject extends FtrService { for (const eventRow of allEvents) { const hostNameButton = await this.testSubjects.findDescendant( - 'formatted-field-host.name', + 'host-details-button', eventRow ); const eventRowHostName = (await hostNameButton.getVisibleText()).trim();