diff --git a/package.json b/package.json index 216cfe5576ed6..deeac13134ad7 100644 --- a/package.json +++ b/package.json @@ -452,8 +452,8 @@ "@opentelemetry/semantic-conventions": "^1.4.0", "@reduxjs/toolkit": "1.7.2", "@slack/webhook": "^5.0.4", - "@tanstack/react-query": "^4.19.0", - "@tanstack/react-query-devtools": "^4.19.0", + "@tanstack/react-query": "^4.19.1", + "@tanstack/react-query-devtools": "^4.19.1", "@turf/along": "6.0.1", "@turf/area": "6.0.1", "@turf/bbox": "6.0.1", diff --git a/x-pack/plugins/enterprise_search/common/types/analytics.ts b/x-pack/plugins/enterprise_search/common/types/analytics.ts index 0106a855627a3..49c00830a7fc9 100644 --- a/x-pack/plugins/enterprise_search/common/types/analytics.ts +++ b/x-pack/plugins/enterprise_search/common/types/analytics.ts @@ -13,3 +13,7 @@ export interface AnalyticsCollection { } export type AnalyticsCollectionDocument = Omit; + +export interface AnalyticsEventsIndexExists { + exists: boolean; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/api/check_analytics_events_index/check_analytics_events_index_api_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/api/check_analytics_events_index/check_analytics_events_index_api_logic.test.ts new file mode 100644 index 0000000000000..b7173f20be3be --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/api/check_analytics_events_index/check_analytics_events_index_api_logic.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockHttpValues } from '../../../__mocks__/kea_logic'; + +import { nextTick } from '@kbn/test-jest-helpers'; + +import { checkAnalyticsEventsIndexExists } from './check_analytics_events_index_api_logic'; + +describe('FetchAnalyticsCollectionApiLogic', () => { + const { http } = mockHttpValues; + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('FetchAnalyticsCollectionsApiLogic', () => { + it('calls the analytics collections exists api', async () => { + const promise = Promise.resolve({ exists: true }); + const indexName = 'eventsIndex'; + http.get.mockReturnValue(promise); + const result = checkAnalyticsEventsIndexExists({ indexName }); + await nextTick(); + expect(http.get).toHaveBeenCalledWith( + `/internal/enterprise_search/analytics/events/${indexName}/exists` + ); + await expect(result).resolves.toEqual({ exists: true }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/api/check_analytics_events_index/check_analytics_events_index_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/api/check_analytics_events_index/check_analytics_events_index_api_logic.ts new file mode 100644 index 0000000000000..37a0d1a796179 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/api/check_analytics_events_index/check_analytics_events_index_api_logic.ts @@ -0,0 +1,31 @@ +/* + * 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 { AnalyticsEventsIndexExists } from '../../../../../common/types/analytics'; +import { createApiLogic } from '../../../shared/api_logic/create_api_logic'; +import { HttpLogic } from '../../../shared/http'; + +export interface AnalyticsEventsIndexExistsApiLogicArgs { + indexName: string; +} + +export type AnalyticsEventsIndexExistsApiLogicResponse = AnalyticsEventsIndexExists; + +export const checkAnalyticsEventsIndexExists = async ({ + indexName, +}: AnalyticsEventsIndexExistsApiLogicArgs): Promise => { + const { http } = HttpLogic.values; + const route = `/internal/enterprise_search/analytics/events/${indexName}/exists`; + const response = await http.get(route); + + return response; +}; + +export const AnalyticsEventsIndexExistsAPILogic = createApiLogic( + ['analytics', 'analytics_events_index_exists_api_logic'], + checkAnalyticsEventsIndexExists +); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_events.test.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_events.test.tsx index 740dde01a07f4..372043fd96ed8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_events.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_events.test.tsx @@ -7,31 +7,67 @@ import '../../../__mocks__/shallow_useeffect.mock'; +import { setMockValues, setMockActions } from '../../../__mocks__/kea_logic'; + import React from 'react'; import { shallow } from 'enzyme'; +import { EuiEmptyPrompt } from '@elastic/eui'; + import { AnalyticsCollection } from '../../../../../common/types/analytics'; import { EntSearchLogStream } from '../../../shared/log_stream'; import { AnalyticsCollectionEvents } from './analytics_collection_events'; describe('AnalyticsCollectionEvents', () => { - const analyticsCollections: AnalyticsCollection = { + const analyticsCollection: AnalyticsCollection = { event_retention_day_length: 180, events_datastream: 'logs-elastic_analytics.events-example', id: '1', name: 'example', }; + const mockActions = { + analyticsEventsIndexExists: jest.fn(), + }; + beforeEach(() => { jest.clearAllMocks(); + + setMockActions(mockActions); }); it('renders', () => { + setMockValues({ + isPresent: true, + isLoading: false, + }); const expectedQuery = '_index: logs-elastic_analytics.events-example'; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(EntSearchLogStream).prop('query')).toEqual(expectedQuery); }); + + describe('empty state', () => { + it('renders when analytics events index is not present', () => { + setMockValues({ + isPresent: false, + }); + + const wrapper = shallow(); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + }); + + it('renders when analytics events index check is not performed yet', () => { + setMockValues({ + isLoading: true, + }); + + const wrapper = shallow(); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_events.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_events.tsx index 3a87c2e9d0762..d847f50f3ca3d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_events.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_events.tsx @@ -5,14 +5,23 @@ * 2.0. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; +import { useValues, useActions } from 'kea'; + +import { EuiEmptyPrompt, EuiButton, EuiLink, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; import { ENTERPRISE_SEARCH_ANALYTICS_LOGS_SOURCE_ID } from '../../../../../common/constants'; import { AnalyticsCollection } from '../../../../../common/types/analytics'; +import { generateEncodedPath } from '../../../shared/encode_path_params'; +import { KibanaLogic } from '../../../shared/kibana'; import { EntSearchLogStream } from '../../../shared/log_stream'; +import { COLLECTION_VIEW_PATH } from '../../routes'; + +import { AnalyticsEventsIndexExistsLogic } from './analytics_events_index_exists_logic'; interface AnalyticsCollectionEventsProps { collection: AnalyticsCollection; @@ -23,53 +32,116 @@ const EVENTS_POLLING_INTERVAL = 30 * 1000; export const AnalyticsCollectionEvents: React.FC = ({ collection, }) => { - // Since EntSearchLogStream component doesn't have a poll interval property - // but gets reloaded on every filter or query change, it was decided to introduce - // a mutable filters state and reset it every 30 seconds to trigger polling - - const [filters, setFilters] = useState([]); + const { analyticsEventsIndexExists } = useActions(AnalyticsEventsIndexExistsLogic); + const { isLoading, isPresent } = useValues(AnalyticsEventsIndexExistsLogic); + const { navigateToUrl } = useValues(KibanaLogic); useEffect(() => { + analyticsEventsIndexExists(collection.id); + const interval = setInterval(() => { - setFilters([]); + analyticsEventsIndexExists(collection.id); }, EVENTS_POLLING_INTERVAL); return () => clearInterval(interval); }, []); return ( - + {(isLoading || !isPresent) && ( + +

+ + There are no analytics events for {collection.name} yet + + ), + }} + /> +

+ + } + body={i18n.translate( + 'xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.emptyState.body', { - defaultMessage: 'Event name', + defaultMessage: + "Start tracking events by adding the behavioral analytics client to every page of your website or application that you'd like to track", } - ), - field: 'event.action', - }, - { - type: 'field', - header: i18n.translate( - 'xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.columns.userUuid', + )} + actions={ + + navigateToUrl( + generateEncodedPath(COLLECTION_VIEW_PATH, { + id: collection.id, + section: 'integrate', + }) + ) + } + > + {i18n.translate( + 'xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.emptyState.actions', + { + defaultMessage: 'View integration instructions', + } + )} + + } + footer={ + + {i18n.translate( + 'xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.emptyState.footer', + { + defaultMessage: 'Visit the behavioral analytics documentation', + } + )} + + } + /> + )} + {!isLoading && isPresent && ( + + type: 'timestamp', + }, + { + type: 'field', + header: i18n.translate( + 'xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.columns.eventName', + { + defaultMessage: 'Event name', + } + ), + field: 'event.action', + }, + { + type: 'field', + header: i18n.translate( + 'xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.columns.userUuid', + { + defaultMessage: 'User UUID', + } + ), + field: 'labels.user_uuid', + }, + ]} + query={`_index: ${collection.events_datastream}`} + /> + )} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_events_index_exists_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_events_index_exists_logic.test.ts new file mode 100644 index 0000000000000..e93e5e5f7108c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_events_index_exists_logic.test.ts @@ -0,0 +1,48 @@ +/* + * 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 { LogicMounter } from '../../../__mocks__/kea_logic'; + +import { Status } from '../../../../../common/types/api'; + +import { AnalyticsEventsIndexExistsLogic } from './analytics_events_index_exists_logic'; + +describe('analyticsEventsIndexExistsLogic', () => { + const { mount } = new LogicMounter(AnalyticsEventsIndexExistsLogic); + const indexName = true; + + beforeEach(() => { + jest.clearAllMocks(); + jest.useRealTimers(); + mount(); + }); + + const DEFAULT_VALUES = { + data: undefined, + isLoading: true, + isPresent: false, + status: Status.IDLE, + }; + + it('has expected default values', () => { + expect(AnalyticsEventsIndexExistsLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('selectors', () => { + it('updates when apiSuccess listener triggered', () => { + AnalyticsEventsIndexExistsLogic.actions.apiSuccess({ exists: indexName }); + + expect(AnalyticsEventsIndexExistsLogic.values).toEqual({ + ...DEFAULT_VALUES, + isLoading: false, + isPresent: true, + status: Status.SUCCESS, + data: { exists: indexName }, + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_events_index_exists_logic.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_events_index_exists_logic.tsx new file mode 100644 index 0000000000000..f9449da5172c8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_events_index_exists_logic.tsx @@ -0,0 +1,52 @@ +/* + * 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 { kea, MakeLogicType } from 'kea'; + +import { Status } from '../../../../../common/types/api'; +import { Actions } from '../../../shared/api_logic/create_api_logic'; +import { + AnalyticsEventsIndexExistsAPILogic, + AnalyticsEventsIndexExistsApiLogicResponse, +} from '../../api/check_analytics_events_index/check_analytics_events_index_api_logic'; + +export interface AnalyticsEventsIndexExistsActions { + apiSuccess: Actions<{}, AnalyticsEventsIndexExistsApiLogicResponse>['apiSuccess']; + analyticsEventsIndexExists(indexName: string): { indexName: string }; + makeRequest: Actions<{}, AnalyticsEventsIndexExistsApiLogicResponse>['makeRequest']; +} +export interface AnalyticsEventsIndexExistsValues { + isLoading: boolean; + isPresent: boolean; + status: Status; + data: typeof AnalyticsEventsIndexExistsAPILogic.values.data; +} + +export const AnalyticsEventsIndexExistsLogic = kea< + MakeLogicType +>({ + actions: { + analyticsEventsIndexExists: (indexName) => ({ indexName }), + }, + connect: { + actions: [AnalyticsEventsIndexExistsAPILogic, ['makeRequest', 'apiSuccess', 'apiError']], + values: [AnalyticsEventsIndexExistsAPILogic, ['status', 'data']], + }, + listeners: ({ actions }) => ({ + analyticsEventsIndexExists: ({ indexName }) => { + actions.makeRequest({ indexName }); + }, + }), + path: ['enterprise_search', 'analytics', 'events_index'], + selectors: ({ selectors }) => ({ + isLoading: [ + () => [selectors.status], + (status) => [Status.LOADING, Status.IDLE].includes(status), + ], + isPresent: [() => [selectors.data], (data) => data?.exists === true], + }), +}); diff --git a/x-pack/plugins/enterprise_search/server/lib/analytics/analytics_events_index_exists.test.ts b/x-pack/plugins/enterprise_search/server/lib/analytics/analytics_events_index_exists.test.ts new file mode 100644 index 0000000000000..5a9a64420b8ff --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/lib/analytics/analytics_events_index_exists.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IScopedClusterClient } from '@kbn/core-elasticsearch-server'; + +import { analyticsEventsIndexExists } from './analytics_events_index_exists'; + +// jest.mock('./analytics_events_index_exists', () => ({ +// analyticsEventsIndexExists: jest.fn(), +// })); + +describe('analytics collection events exists function', () => { + const mockClient = { + asCurrentUser: { + indices: { + exists: jest.fn(), + }, + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('checking if analytics events index exists', () => { + it('should call exists endpoint', async () => { + mockClient.asCurrentUser.indices.exists.mockImplementationOnce(() => Promise.resolve(true)); + await expect( + analyticsEventsIndexExists(mockClient as unknown as IScopedClusterClient, 'example') + ).resolves.toEqual(true); + expect(mockClient.asCurrentUser.indices.exists).toHaveBeenCalledWith({ + index: '.ds-logs-elastic_analytics.events-example-*', + allow_no_indices: false, + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/lib/analytics/analytics_events_index_exists.ts b/x-pack/plugins/enterprise_search/server/lib/analytics/analytics_events_index_exists.ts new file mode 100644 index 0000000000000..0d5ffad5a291a --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/lib/analytics/analytics_events_index_exists.ts @@ -0,0 +1,22 @@ +/* + * 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 { IScopedClusterClient } from '@kbn/core-elasticsearch-server'; + +const getFullIndexName = (indexName: string): string => { + return `.ds-logs-elastic_analytics.events-${indexName}-*`; +}; + +export const analyticsEventsIndexExists = async ( + client: IScopedClusterClient, + indexName: string +): Promise => { + return await client.asCurrentUser.indices.exists({ + index: getFullIndexName(indexName), + allow_no_indices: false, + }); +}; diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts index 3cd115183fa9f..1e84a1b81845e 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts @@ -14,6 +14,7 @@ import { i18n } from '@kbn/i18n'; import { ErrorCode } from '../../../common/types/error_codes'; import { addAnalyticsCollection } from '../../lib/analytics/add_analytics_collection'; +import { analyticsEventsIndexExists } from '../../lib/analytics/analytics_events_index_exists'; import { deleteAnalyticsCollectionById } from '../../lib/analytics/delete_analytics_collection'; import { fetchAnalyticsCollectionById, @@ -157,4 +158,26 @@ export function registerAnalyticsRoutes({ } }) ); + + router.get( + { + path: '/internal/enterprise_search/analytics/events/{id}/exists', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + elasticsearchErrorHandler(log, async (context, request, response) => { + const { client } = (await context.core).elasticsearch; + + const eventsIndexExists = await analyticsEventsIndexExists(client, request.params.id); + + if (!eventsIndexExists) { + return response.ok({ body: { exists: false } }); + } + + return response.ok({ body: { exists: true } }); + }) + ); } diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.test.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.test.ts index dfe0828e88bad..5d65909dde50d 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.test.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.test.ts @@ -41,4 +41,16 @@ describe('APM Transaction Duration Transform Generator', () => { expect(transform.source.query).toMatchSnapshot(); }); + + it('uses the provided index params as source index', async () => { + const index = 'my-custom-apm-index*'; + const anSLO = createSLO({ + indicator: createAPMTransactionDurationIndicator({ + index, + }), + }); + const transform = generator.getTransformParams(anSLO); + + expect(transform.source.index).toEqual(index); + }); }); diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts index 6cba9c32d40ac..23ee248a1372f 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts @@ -16,8 +16,7 @@ import { import { getSLOTransformTemplate } from '../../../assets/transform_templates/slo_transform_template'; import { SLO, APMTransactionDurationIndicator } from '../../../domain/models'; import { TransformGenerator } from '.'; - -const APM_SOURCE_INDEX = 'metrics-apm*'; +import { DEFAULT_APM_INDEX } from './constants'; export class ApmTransactionDurationTransformGenerator extends TransformGenerator { public getTransformParams(slo: SLO): TransformPutTransformRequest { @@ -74,7 +73,7 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator } return { - index: APM_SOURCE_INDEX, + index: indicator.params.index ?? DEFAULT_APM_INDEX, runtime_mappings: this.buildCommonRuntimeMappings(slo), query: { bool: { diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.test.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.test.ts index e44f4be531c32..0960fe7c073ca 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.test.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.test.ts @@ -50,4 +50,16 @@ describe('APM Transaction Error Rate Transform Generator', () => { expect(transform.source.query).toMatchSnapshot(); }); + + it('uses the provided index params as source index', async () => { + const index = 'my-custom-apm-index*'; + const anSLO = createSLO({ + indicator: createAPMTransactionErrorRateIndicator({ + index, + }), + }); + const transform = generator.getTransformParams(anSLO); + + expect(transform.source.index).toEqual(index); + }); }); diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts index a68eb490ebde1..f0df8dd54209f 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts @@ -16,8 +16,8 @@ import { getSLOTransformId, } from '../../../assets/constants'; import { APMTransactionErrorRateIndicator, SLO } from '../../../domain/models'; +import { DEFAULT_APM_INDEX } from './constants'; -const APM_SOURCE_INDEX = 'metrics-apm*'; const ALLOWED_STATUS_CODES = ['2xx', '3xx', '4xx', '5xx']; const DEFAULT_GOOD_STATUS_CODES = ['2xx', '3xx', '4xx']; @@ -76,7 +76,7 @@ export class ApmTransactionErrorRateTransformGenerator extends TransformGenerato } return { - index: APM_SOURCE_INDEX, + index: indicator.params.index ?? DEFAULT_APM_INDEX, runtime_mappings: this.buildCommonRuntimeMappings(slo), query: { bool: { diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/constants.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/constants.ts new file mode 100644 index 0000000000000..0bd3f7f31edcc --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/constants.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 DEFAULT_APM_INDEX = 'metrics-apm*'; diff --git a/x-pack/plugins/observability/server/types/schema/indicators.ts b/x-pack/plugins/observability/server/types/schema/indicators.ts index ef7b2d59fcf74..a430ee421a346 100644 --- a/x-pack/plugins/observability/server/types/schema/indicators.ts +++ b/x-pack/plugins/observability/server/types/schema/indicators.ts @@ -11,13 +11,18 @@ import { allOrAnyString, dateRangeSchema } from './common'; const apmTransactionDurationIndicatorTypeSchema = t.literal('sli.apm.transaction_duration'); const apmTransactionDurationIndicatorSchema = t.type({ type: apmTransactionDurationIndicatorTypeSchema, - params: t.type({ - environment: allOrAnyString, - service: allOrAnyString, - transaction_type: allOrAnyString, - transaction_name: allOrAnyString, - 'threshold.us': t.number, - }), + params: t.intersection([ + t.type({ + environment: allOrAnyString, + service: allOrAnyString, + transaction_type: allOrAnyString, + transaction_name: allOrAnyString, + 'threshold.us': t.number, + }), + t.partial({ + index: t.string, + }), + ]), }); const apmTransactionErrorRateIndicatorTypeSchema = t.literal('sli.apm.transaction_error_rate'); @@ -34,6 +39,7 @@ const apmTransactionErrorRateIndicatorSchema = t.type({ good_status_codes: t.array( t.union([t.literal('2xx'), t.literal('3xx'), t.literal('4xx'), t.literal('5xx')]) ), + index: t.string, }), ]), }); diff --git a/x-pack/plugins/osquery/public/results/results_table.tsx b/x-pack/plugins/osquery/public/results/results_table.tsx index 23dca7ea1e019..b7e08dda9a14a 100644 --- a/x-pack/plugins/osquery/public/results/results_table.tsx +++ b/x-pack/plugins/osquery/public/results/results_table.tsx @@ -48,7 +48,9 @@ import { AddToCaseWrapper } from '../cases/add_to_cases'; const DataContext = createContext([]); const StyledEuiDataGrid = styled(EuiDataGrid)` - max-height: 500px; + :not(.euiDataGrid--fullScreen) { + max-height: 500px; + } `; export interface ResultsTableComponentProps { diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/changing_alert_status.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/changing_alert_status.cy.ts index 9500e19be545f..692c81bd58381 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/changing_alert_status.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/changing_alert_status.cy.ts @@ -151,7 +151,7 @@ describe('Changing alert status', () => { context('Closing alerts', () => { beforeEach(() => { deleteAlertsAndRules(); - createCustomRuleEnabled(getNewRule(), '1', '100m', 100); + createCustomRuleEnabled(getNewRule(), '1', 100); visit(ALERTS_URL); waitForAlertsToPopulate(); selectCountTable(); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts index 6a3718baa2261..b897aa3024cda 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts @@ -99,7 +99,6 @@ describe('Detection rules, bulk edit of rule actions', () => { name: ruleNameToAssert, }, '1', - '100m', 500, actions ); diff --git a/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/add_edit_exception.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/add_edit_exception.cy.ts index 64a2e14bbf61f..c7ec942497c50 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/add_edit_exception.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/add_edit_exception.cy.ts @@ -253,9 +253,13 @@ describe('Add/edit exception from rule details', () => { ...getNewRule(), customQuery: 'agent.name:*', dataSource: { index: ['exceptions*'], type: 'indexPatterns' }, + runsEvery: { + interval: '1', + timeType: 'Seconds', + type: 's', + }, }, - 'rule_testing', - '1s' + 'rule_testing' ); visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); goToRuleDetails(); diff --git a/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/add_edit_exception_data_view.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/add_edit_exception_data_view.cy.ts index f17a5e4221a89..b3c807fdf7297 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/add_edit_exception_data_view.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/add_edit_exception_data_view.cy.ts @@ -65,9 +65,13 @@ describe('Add exception using data views from rule details', () => { ...getNewRule(), customQuery: 'agent.name:*', dataSource: { dataView: 'exceptions-*', type: 'dataView' }, + runsEvery: { + interval: '1', + timeType: 'Seconds', + type: 's', + }, }, - 'rule_testing', - '1s' + 'rule_testing' ); visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); goToRuleDetails(); diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index 3bfa4123ac591..d60ef8dd54d51 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -49,7 +49,6 @@ export interface CustomRule { name: string; description: string; dataSource: RuleDataSource; - interval?: string; severity?: string; riskScore?: string; tags?: string[]; @@ -59,6 +58,7 @@ export interface CustomRule { mitre?: Mitre[]; note?: string; runsEvery?: Interval; + interval?: string; lookBack?: Interval; timeline?: CompleteTimeline; maxSignals?: number; @@ -180,16 +180,8 @@ const getSeverityOverride4 = (): SeverityOverride => ({ sourceValue: 'auditbeat', }); -// Default interval is 1m, our tests config overwrite this to 1s -// See https://github.com/elastic/kibana/pull/125396 for details const getRunsEvery = (): Interval => ({ - interval: '1', - timeType: 'Seconds', - type: 's', -}); - -const getRunsEveryFiveMinutes = (): Interval => ({ - interval: '5', + interval: '100', timeType: 'Minutes', type: 'm', }); @@ -212,7 +204,7 @@ export const getDataViewRule = (): CustomRule => ({ falsePositivesExamples: ['False1', 'False2'], mitre: [getMitre1(), getMitre2()], note: '# test markdown', - runsEvery: getRunsEveryFiveMinutes(), + runsEvery: getRunsEvery(), lookBack: getLookBack(), timeline: getTimeline(), maxSignals: 100, @@ -305,7 +297,6 @@ export const getExistingRule = (): CustomRule => ({ name: 'Rule 1', description: 'Description for Rule 1', dataSource: { index: ['auditbeat-*'], type: 'indexPatterns' }, - interval: '100m', severity: 'High', riskScore: '19', tags: ['rule1'], @@ -314,6 +305,7 @@ export const getExistingRule = (): CustomRule => ({ mitre: [], note: 'This is my note', runsEvery: getRunsEvery(), + interval: '100m', lookBack: getLookBack(), timeline: getTimeline(), // Please do not change, or if you do, needs diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/prebuilt_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/prebuilt_rules.ts index f8a48dd9c8239..2d8d8b7bf49e0 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/prebuilt_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/prebuilt_rules.ts @@ -31,6 +31,6 @@ export const waitTillPrebuiltRulesReadyToInstall = () => { return availablePrebuiltRulesCount > 0; }); }, - { interval: 100, timeout: 60000 } + { interval: 2000, timeout: 60000 } ); }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts index 099736e4f1003..b48f86d0e6241 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts @@ -40,12 +40,12 @@ export const createMachineLearningRule = (rule: MachineLearningRule, ruleId = 'm export const createCustomRule = ( rule: CustomRule, - ruleId = 'rule_testing', - interval = '100m' + ruleId = 'rule_testing' ): Cypress.Chainable> => { const riskScore = rule.riskScore != null ? parseInt(rule.riskScore, 10) : undefined; const severity = rule.severity != null ? rule.severity.toLocaleLowerCase() : undefined; const timeline = rule.timeline != null ? rule.timeline : undefined; + const interval = rule.runsEvery ? `${rule.runsEvery.interval}${rule.runsEvery.type}` : '100m'; return cy.request({ method: 'POST', @@ -82,6 +82,7 @@ export const createCustomRule = ( export const createEventCorrelationRule = (rule: CustomRule, ruleId = 'rule_testing') => { const riskScore = rule.riskScore != null ? parseInt(rule.riskScore, 10) : undefined; const severity = rule.severity != null ? rule.severity.toLowerCase() : undefined; + const interval = rule.runsEvery ? `${rule.runsEvery.interval}${rule.runsEvery.type}` : '100m'; cy.request({ method: 'POST', @@ -90,7 +91,7 @@ export const createEventCorrelationRule = (rule: CustomRule, ruleId = 'rule_test rule_id: ruleId, risk_score: riskScore, description: rule.description, - interval: `${rule.runsEvery?.interval}${rule.runsEvery?.type}`, + interval, from: `now-${rule.lookBack?.interval}${rule.lookBack?.type}`, name: rule.name, severity, @@ -109,6 +110,7 @@ export const createEventCorrelationRule = (rule: CustomRule, ruleId = 'rule_test export const createThresholdRule = (rule: ThresholdRule, ruleId = 'rule_testing') => { const riskScore = rule.riskScore != null ? parseInt(rule.riskScore, 10) : undefined; const severity = rule.severity != null ? rule.severity.toLocaleLowerCase() : undefined; + const interval = rule.runsEvery ? `${rule.runsEvery.interval}${rule.runsEvery.type}` : '100m'; cy.request({ method: 'POST', @@ -117,7 +119,7 @@ export const createThresholdRule = (rule: ThresholdRule, ruleId = 'rule_testing' rule_id: ruleId, risk_score: riskScore, description: rule.description, - interval: `${rule.runsEvery?.interval}${rule.runsEvery?.type}`, + interval, from: `now-${rule.lookBack?.interval}${rule.lookBack?.type}`, name: rule.name, severity, @@ -140,6 +142,7 @@ export const createThresholdRule = (rule: ThresholdRule, ruleId = 'rule_testing' export const createNewTermsRule = (rule: NewTermsRule, ruleId = 'rule_testing') => { const riskScore = rule.riskScore != null ? parseInt(rule.riskScore, 10) : undefined; const severity = rule.severity != null ? rule.severity.toLocaleLowerCase() : undefined; + const interval = rule.runsEvery ? `${rule.runsEvery.interval}${rule.runsEvery.type}` : '100m'; cy.request({ method: 'POST', @@ -148,7 +151,7 @@ export const createNewTermsRule = (rule: NewTermsRule, ruleId = 'rule_testing') rule_id: ruleId, risk_score: riskScore, description: rule.description, - interval: `${rule.runsEvery?.interval}${rule.runsEvery?.type}`, + interval, from: `now-${rule.lookBack?.interval}${rule.lookBack?.type}`, name: rule.name, severity, @@ -172,6 +175,7 @@ export const createSavedQueryRule = ( const riskScore = rule.riskScore != null ? parseInt(rule.riskScore, 10) : undefined; const severity = rule.severity != null ? rule.severity.toLocaleLowerCase() : undefined; const timeline = rule.timeline != null ? rule.timeline : undefined; + const interval = rule.runsEvery ? `${rule.runsEvery.interval}${rule.runsEvery.type}` : '100m'; return cy.request({ method: 'POST', @@ -180,7 +184,7 @@ export const createSavedQueryRule = ( rule_id: ruleId, risk_score: riskScore, description: rule.description, - interval: rule.interval, + interval, name: rule.name, severity, type: 'saved_query', @@ -208,6 +212,7 @@ export const createCustomIndicatorRule = (rule: ThreatIndicatorRule, ruleId = 'r const riskScore = rule.riskScore != null ? parseInt(rule.riskScore, 10) : undefined; const severity = rule.severity != null ? rule.severity.toLocaleLowerCase() : undefined; const timeline = rule.timeline != null ? rule.timeline : undefined; + const interval = rule.runsEvery ? `${rule.runsEvery.interval}${rule.runsEvery.type}` : '100m'; cy.request({ method: 'POST', @@ -216,9 +221,7 @@ export const createCustomIndicatorRule = (rule: ThreatIndicatorRule, ruleId = 'r rule_id: ruleId, risk_score: riskScore, description: rule.description, - // Default interval is 1m, our tests config overwrite this to 1s - // See https://github.com/elastic/kibana/pull/125396 for details - interval: '10s', + interval, name: rule.name, severity, type: 'threat_match', @@ -256,12 +259,12 @@ export const createCustomIndicatorRule = (rule: ThreatIndicatorRule, ruleId = 'r export const createCustomRuleEnabled = ( rule: CustomRule, ruleId = '1', - interval = '100m', maxSignals = 500, actions?: RuleActionArray ) => { const riskScore = rule.riskScore != null ? parseInt(rule.riskScore, 10) : undefined; const severity = rule.severity != null ? rule.severity.toLocaleLowerCase() : undefined; + const interval = rule.runsEvery ? `${rule.runsEvery.interval}${rule.runsEvery.type}` : '100m'; if (rule.dataSource.type === 'indexPatterns') { cy.request({ diff --git a/x-pack/plugins/security_solution/cypress/tasks/sourcerer.ts b/x-pack/plugins/security_solution/cypress/tasks/sourcerer.ts index 860e9487d4b3f..b21dd651d2cfc 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/sourcerer.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/sourcerer.ts @@ -148,6 +148,6 @@ const refreshUntilAlertsIndexExists = async () => { }; export const waitForAlertsIndexToExist = () => { - createCustomRuleEnabled(getNewRule(), '1', '100m', 100); + createCustomRuleEnabled(getNewRule(), '1', 100); refreshUntilAlertsIndexExists(); }; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/index.tsx index 57f164ddd3b24..dffb800c213a7 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/index.tsx @@ -39,6 +39,17 @@ export const HostsContainer = React.memo(() => ( + ( + + )} + /> diff --git a/x-pack/plugins/security_solution/public/users/pages/index.tsx b/x-pack/plugins/security_solution/public/users/pages/index.tsx index b75959b5265a9..e71ec531bf9c7 100644 --- a/x-pack/plugins/security_solution/public/users/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/index.tsx @@ -20,7 +20,17 @@ export const UsersContainer = React.memo(() => { - + ( + + )} + /> ( const selectedFieldChange = useCallback( (values: Array>) => { - if (!values || values.length === 0) { - return; + if (values && values.length > 0) { + valueChange(values[0].label); } - - valueChange(values[0].label); setSelectedField(values); }, [valueChange] diff --git a/yarn.lock b/yarn.lock index 5887fef037e47..adde51628d827 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6063,7 +6063,7 @@ resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.19.1.tgz#2e92d9e8a50884eb231c5beb4386e131ebe34306" integrity sha512-Zp0aIose5C8skBzqbVFGk9HJsPtUhRVDVNWIqVzFbGQQgYSeLZMd3Sdb4+EnA5wl1J7X+bre2PJGnQg9x/zHOA== -"@tanstack/react-query-devtools@^4.19.0": +"@tanstack/react-query-devtools@^4.19.1": version "4.19.1" resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-4.19.1.tgz#850058df8dba932362838c17f566bd717044449b" integrity sha512-U63A+ly9JLPJj7ryR9omdXT3n+gS7jlExrHty4klsd/6xdUhC38CKZyZ0Gi3vctaVYRGTU8/vI+uKzBYdFqLaA== @@ -6072,7 +6072,7 @@ superjson "^1.10.0" use-sync-external-store "^1.2.0" -"@tanstack/react-query@^4.19.0": +"@tanstack/react-query@^4.19.1": version "4.19.1" resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.19.1.tgz#43356dd537127e76d75f5a2769eb23dafd9a3690" integrity sha512-5dvHvmc0vrWI03AJugzvKfirxCyCLe+qawrWFCXdu8t7dklIhJ7D5ZhgTypv7mMtIpdHPcECtCiT/+V74wCn2A==