diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index 3fd459ffc64c1..ef56f7bf81b0f 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -123,9 +123,6 @@ enabled: - x-pack/test/api_integration/config_security_basic.ts - x-pack/test/api_integration/config_security_trial.ts - x-pack/test/api_integration/config.ts - - x-pack/test/apm_api_integration/basic/config.ts - - x-pack/test/apm_api_integration/rules/config.ts - - x-pack/test/apm_api_integration/trial/config.ts - x-pack/test/banners_functional/config.ts - x-pack/test/cases_api_integration/security_and_spaces/config_basic.ts - x-pack/test/cases_api_integration/security_and_spaces/config_trial.ts diff --git a/docs/api/saved-objects.asciidoc b/docs/api/saved-objects.asciidoc index 6cabbdc785db4..610f18c38d624 100644 --- a/docs/api/saved-objects.asciidoc +++ b/docs/api/saved-objects.asciidoc @@ -30,6 +30,8 @@ The following saved objects APIs are available: * <> to remove {kib} saved objects +* <> to remove multiple {kib} saved objects + * <> to retrieve sets of saved objects that you want to import into {kib} * <> to create sets of {kib} saved objects from a file created by the export API @@ -46,6 +48,7 @@ include::saved-objects/bulk_create.asciidoc[] include::saved-objects/update.asciidoc[] include::saved-objects/bulk_update.asciidoc[] include::saved-objects/delete.asciidoc[] +include::saved-objects/bulk_delete.asciidoc[] include::saved-objects/export.asciidoc[] include::saved-objects/import.asciidoc[] include::saved-objects/resolve_import_errors.asciidoc[] diff --git a/docs/api/saved-objects/bulk_delete.asciidoc b/docs/api/saved-objects/bulk_delete.asciidoc new file mode 100644 index 0000000000000..d08c5874477bc --- /dev/null +++ b/docs/api/saved-objects/bulk_delete.asciidoc @@ -0,0 +1,107 @@ +[[saved-objects-api-bulk-delete]] +=== Bulk delete object API +++++ +Bulk delete objects +++++ + +experimental[] Remove multiple {kib} saved objects. + +WARNING: Once you delete a saved object, _it cannot be recovered_. + +==== Request + +`POST :/api/saved_objects/_bulk_delete` + +`POST :/s//api/saved_objects/_bulk_delete` + +==== Path parameters + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +==== Request body + +`type`:: + (Required, string) Valid options include `visualization`, `dashboard`, `search`, `index-pattern`, `config`. + +`id`:: + (Required, string) The object ID to remove. + +==== Query parameters + +`force`:: + (Optional, boolean) When true, force delete objects that exist in multiple namespaces. Note that the option applies to the whole request. Use the <> to specify per-object delete behavior. ++ +TIP: Use this if you attempted to delete objects and received an HTTP 400 error with the following message: _"Unable to delete saved object that exists in multiple namespaces, use the `force` option to delete it anyway"_ ++ +WARNING: When you bulk delete objects that exist in multiple namespaces, the API also deletes <> that reference the object. These requests are batched to minimise the impact but they can place a heavy load on {kib}. Make sure you limit the number of objects that exist in multiple namespaces in a single bulk delete operation. + +==== Response code +`200`:: + Indicates a successful call. Note, this HTTP response code indicates that the bulk operation succeeded. Errors pertaining to individual + objects will be returned in the response body. Refer to the example below for details. + +==== Response body + +`statuses`:: + (array) Top-level property that contains objects that represent the response for each of the requested objects. The order of the objects in the response is identical to the order of the objects in the request. + +Saved objects that cannot be removed will include an error object. + +==== Example + +Delete three saved objects, where one of them does not exist and one exists in multiple namespaces: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/saved_objects/_bulk_delete +[ + { + type: 'visualization', + id: 'not an id', + }, + { + type: 'dashboard', + id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', + { + type: 'index-pattern', + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + } +] +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "statuses": [ + { + "success": false, + "id": "not an id", + "type": "visualization", + "error": { + "statusCode": 404, + "error": "Not Found", + "message": "Saved object [visualization/not an id] not found", + }, + }, + { + "success": true, + "id": "be3733a0-9efe-11e7-acb3-3dab96693fab", + "type": "dashboard", + }, + { + "success": false, + "id": "d3d7af60-4c81-11e8-b3d7-01146121b73d", + "type": "index-pattern", + "error": { + "statusCode": 400, + "error": "Bad Request", + "message": "Unable to delete saved object id: d3d7af60-4c81-11e8-b3d7-01146121b73d, type: index-pattern that exists in multiple namespaces, use the \"force\" option to delete all saved objects: Bad Request", + }, + } + ] +} +-------------------------------------------------- diff --git a/package.json b/package.json index 339dd1a7822a9..bcfcd61b5f06e 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ ], "private": true, "version": "8.5.0", - "branch": "main", + "branch": "8.5", "types": "./kibana.d.ts", "tsdocMetadata": "./build/tsdoc-metadata.json", "build": { diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index d8511298f6ac9..41df488839358 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -562,10 +562,6 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, - 'enterpriseSearch:enableIndexTransformsTab': { - type: 'boolean', - _meta: { description: 'Non-default value of setting.' }, - }, 'enterpriseSearch:enableBehavioralAnalyticsSection': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 7ca9ddfeef5b9..2bd59dc69084f 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -150,6 +150,5 @@ export interface UsageStats { 'securitySolution:enableGroupedNav': boolean; 'securitySolution:showRelatedIntegrations': boolean; 'visualization:visualize:legacyGaugeChartsLibrary': boolean; - 'enterpriseSearch:enableIndexTransformsTab': boolean; 'enterpriseSearch:enableBehavioralAnalyticsSection': boolean; } diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 0a4d6c347674d..1a97586dffa62 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -8888,12 +8888,6 @@ "description": "Non-default value of setting." } }, - "enterpriseSearch:enableIndexTransformsTab": { - "type": "boolean", - "_meta": { - "description": "Non-default value of setting." - } - }, "enterpriseSearch:enableBehavioralAnalyticsSection": { "type": "boolean", "_meta": { diff --git a/x-pack/plugins/aiops/public/application/utils/query_utils.test.ts b/x-pack/plugins/aiops/public/application/utils/query_utils.test.ts new file mode 100644 index 0000000000000..59d680d236b15 --- /dev/null +++ b/x-pack/plugins/aiops/public/application/utils/query_utils.test.ts @@ -0,0 +1,254 @@ +/* + * 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 { ChangePoint } from '@kbn/ml-agg-utils'; + +import type { GroupTableItem } from '../../components/spike_analysis_table/spike_analysis_table_groups'; + +import { buildBaseFilterCriteria } from './query_utils'; + +const selectedChangePointMock: ChangePoint = { + doc_count: 53408, + bg_count: 1154, + fieldName: 'meta.cloud.instance_id.keyword', + fieldValue: '1234', + normalizedScore: 1, + pValue: 0.01, + score: 708.3964185322641, + total_bg_count: 179657, + total_doc_count: 114011, +}; + +const selectedGroupMock: GroupTableItem = { + id: '21289599', + docCount: 20468, + pValue: 2.2250738585072626e-308, + group: { + 'error.message': 'rate limit exceeded', + message: 'too many requests', + 'user_agent.original.keyword': 'Mozilla/5.0', + }, + repeatedValues: { + 'beat.hostname.keyword': 'ip-192-168-1-1', + 'beat.name.keyword': 'i-1234', + 'docker.container.id.keyword': 'asdf', + }, + histogram: [], +}; + +describe('query_utils', () => { + describe('buildBaseFilterCriteria', () => { + it('returns range filter based on minimum supplied arguments', () => { + const baseFilterCriteria = buildBaseFilterCriteria('the-time-field-name', 1234, 5678); + + expect(baseFilterCriteria).toEqual([ + { + range: { + 'the-time-field-name': { + format: 'epoch_millis', + gte: 1234, + lte: 5678, + }, + }, + }, + ]); + }); + + it('returns filters including default query with supplied arguments provided via UI', () => { + const baseFilterCriteria = buildBaseFilterCriteria( + '@timestamp', + 1640082000012, + 1640103600906, + { match_all: {} } + ); + + expect(baseFilterCriteria).toEqual([ + { + range: { + '@timestamp': { + format: 'epoch_millis', + gte: 1640082000012, + lte: 1640103600906, + }, + }, + }, + { match_all: {} }, + ]); + }); + + it('includes a term filter when including a selectedChangePoint', () => { + const baseFilterCriteria = buildBaseFilterCriteria( + '@timestamp', + 1640082000012, + 1640103600906, + { match_all: {} }, + selectedChangePointMock + ); + + expect(baseFilterCriteria).toEqual([ + { + range: { + '@timestamp': { + format: 'epoch_millis', + gte: 1640082000012, + lte: 1640103600906, + }, + }, + }, + { match_all: {} }, + { term: { 'meta.cloud.instance_id.keyword': '1234' } }, + ]); + }); + + it('includes a term filter with must_not when excluding a selectedChangePoint', () => { + const baseFilterCriteria = buildBaseFilterCriteria( + '@timestamp', + 1640082000012, + 1640103600906, + { match_all: {} }, + selectedChangePointMock, + false + ); + + expect(baseFilterCriteria).toEqual([ + { + range: { + '@timestamp': { + format: 'epoch_millis', + gte: 1640082000012, + lte: 1640103600906, + }, + }, + }, + { match_all: {} }, + { bool: { must_not: [{ term: { 'meta.cloud.instance_id.keyword': '1234' } }] } }, + ]); + }); + + it('includes multiple term filters when including a selectedGroupMock', () => { + const baseFilterCriteria = buildBaseFilterCriteria( + '@timestamp', + 1640082000012, + 1640103600906, + { match_all: {} }, + undefined, + true, + selectedGroupMock + ); + + expect(baseFilterCriteria).toEqual([ + { + range: { + '@timestamp': { + format: 'epoch_millis', + gte: 1640082000012, + lte: 1640103600906, + }, + }, + }, + { match_all: {} }, + { + term: { + 'error.message': 'rate limit exceeded', + }, + }, + { + term: { + message: 'too many requests', + }, + }, + { + term: { + 'user_agent.original.keyword': 'Mozilla/5.0', + }, + }, + { + term: { + 'beat.hostname.keyword': 'ip-192-168-1-1', + }, + }, + { + term: { + 'beat.name.keyword': 'i-1234', + }, + }, + { + term: { + 'docker.container.id.keyword': 'asdf', + }, + }, + ]); + }); + + it('includes a must_not with nested term filters when excluding a selectedGroup', () => { + const baseFilterCriteria = buildBaseFilterCriteria( + '@timestamp', + 1640082000012, + 1640103600906, + { match_all: {} }, + undefined, + false, + selectedGroupMock + ); + + expect(baseFilterCriteria).toEqual([ + { + range: { + '@timestamp': { + format: 'epoch_millis', + gte: 1640082000012, + lte: 1640103600906, + }, + }, + }, + { match_all: {} }, + { + bool: { + must_not: [ + { + bool: { + filter: [ + { + term: { + 'error.message': 'rate limit exceeded', + }, + }, + { + term: { + message: 'too many requests', + }, + }, + { + term: { + 'user_agent.original.keyword': 'Mozilla/5.0', + }, + }, + { + term: { + 'beat.hostname.keyword': 'ip-192-168-1-1', + }, + }, + { + term: { + 'beat.name.keyword': 'i-1234', + }, + }, + { + term: { + 'docker.container.id.keyword': 'asdf', + }, + }, + ], + }, + }, + ], + }, + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/common/ui_settings_keys.ts b/x-pack/plugins/enterprise_search/common/ui_settings_keys.ts index dc960fb103ddd..d06902fe04fab 100644 --- a/x-pack/plugins/enterprise_search/common/ui_settings_keys.ts +++ b/x-pack/plugins/enterprise_search/common/ui_settings_keys.ts @@ -6,5 +6,4 @@ */ export const enterpriseSearchFeatureId = 'enterpriseSearch'; -export const enableIndexPipelinesTab = 'enterpriseSearch:enableIndexTransformsTab'; export const enableBehavioralAnalyticsSection = 'enterpriseSearch:enableBehavioralAnalyticsSection'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/configure_pipeline.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/configure_pipeline.tsx index 64875e5e6b638..da95bd6d081f4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/configure_pipeline.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/configure_pipeline.tsx @@ -27,7 +27,7 @@ import { MLInferenceLogic } from './ml_inference_logic'; export const ConfigurePipeline: React.FC = () => { const { - addInferencePipelineModal: { configuration, indexName }, + addInferencePipelineModal: { configuration }, formErrors, supportedMLModels, sourceFields, @@ -78,10 +78,7 @@ export const ConfigurePipeline: React.FC = () => { 'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.name.helpText', { defaultMessage: - 'Pipeline names can only contain letters, numbers, underscores, and hyphens. The pipeline name will be automatically prefixed with "ml-inference@{indexName}-".', - values: { - indexName, - }, + 'Pipeline names can only contain letters, numbers, underscores, and hyphens. The pipeline name will be automatically prefixed with "ml-inference-".', } ) } diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.test.ts index 3ea6890c41932..4a9c11faa7f73 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.test.ts @@ -31,11 +31,6 @@ describe('ml inference utils', () => { ner: {}, }, }), - makeFakeModel({ - inference_config: { - classification: {}, - }, - }), makeFakeModel({ inference_config: { text_classification: {}, @@ -53,6 +48,16 @@ describe('ml inference utils', () => { }, }, }), + makeFakeModel({ + inference_config: { + question_answering: {}, + }, + }), + makeFakeModel({ + inference_config: { + fill_mask: {}, + }, + }), ]; for (const model of models) { @@ -61,7 +66,14 @@ describe('ml inference utils', () => { }); it('returns false for expected models', () => { - const models: TrainedModelConfigResponse[] = [makeFakeModel({})]; + const models: TrainedModelConfigResponse[] = [ + makeFakeModel({}), + makeFakeModel({ + inference_config: { + classification: {}, + }, + }), + ]; for (const model of models) { expect(isSupportedMLModel(model)).toBe(false); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.ts index 83cf04585b5ff..b788a522d395f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.ts @@ -11,10 +11,11 @@ import { TrainedModelConfigResponse } from '@kbn/ml-plugin/common/types/trained_ import { AddInferencePipelineFormErrors, InferencePipelineConfiguration } from './types'; const NLP_CONFIG_KEYS = [ + 'fill_mask', 'ner', - 'classification', 'text_classification', 'text_embedding', + 'question_answering', 'zero_shot_classification', ]; export const isSupportedMLModel = (model: TrainedModelConfigResponse): boolean => { diff --git a/x-pack/plugins/enterprise_search/server/ui_settings.ts b/x-pack/plugins/enterprise_search/server/ui_settings.ts index 0be413f8d9c6a..3334e625bc08f 100644 --- a/x-pack/plugins/enterprise_search/server/ui_settings.ts +++ b/x-pack/plugins/enterprise_search/server/ui_settings.ts @@ -11,7 +11,6 @@ import { i18n } from '@kbn/i18n'; import { enterpriseSearchFeatureId, - enableIndexPipelinesTab, enableBehavioralAnalyticsSection, } from '../common/ui_settings_keys'; @@ -31,16 +30,4 @@ export const uiSettings: Record> = { schema: schema.boolean(), value: false, }, - [enableIndexPipelinesTab]: { - category: [enterpriseSearchFeatureId], - description: i18n.translate('xpack.enterpriseSearch.uiSettings.indexPipelines.description', { - defaultMessage: 'Enable the new index pipelines tab in Enterprise Search.', - }), - name: i18n.translate('xpack.enterpriseSearch.uiSettings.indexPipelines.name', { - defaultMessage: 'Enable index pipelines', - }), - requiresPageReload: false, - schema: schema.boolean(), - value: false, - }, }; diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts index ba309a15b04ee..6504b6548f078 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts @@ -7,12 +7,15 @@ import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; -import { PackageNotFoundError } from '../../../errors'; +import { PackageNotFoundError, RegistryResponseError } from '../../../errors'; + +import * as Archive from '../archive'; import { splitPkgKey, fetchFindLatestPackageOrUndefined, fetchFindLatestPackageOrThrow, + fetchInfo, getLicensePath, } from '.'; @@ -21,6 +24,11 @@ const mockLogger = mockLoggerFactory.get('mock logger'); const mockGetBundledPackageByName = jest.fn(); const mockFetchUrl = jest.fn(); + +const MockArchive = Archive as jest.Mocked; + +jest.mock('../archive'); + jest.mock('../..', () => ({ appContextService: { getLogger: () => mockLogger, @@ -150,6 +158,8 @@ describe('fetch package', () => { }); describe('getLicensePath', () => { + MockArchive.getPathParts = jest.requireActual('../archive').getPathParts; + it('returns first license path if found', () => { const path = getLicensePath([ '/package/good-1.0.0/NOTICE.txt', @@ -171,3 +181,40 @@ describe('getLicensePath', () => { expect(path).toEqual(undefined); }); }); + +describe('fetchInfo', () => { + beforeEach(() => { + jest.resetAllMocks(); + + mockFetchUrl.mockRejectedValueOnce(new RegistryResponseError('Not found', 404)); + mockGetBundledPackageByName.mockResolvedValueOnce({ + name: 'test-package', + version: '1.0.0', + buffer: Buffer.from(''), + }); + MockArchive.generatePackageInfoFromArchiveBuffer.mockResolvedValueOnce({ + paths: [], + packageInfo: { + name: 'test-package', + title: 'Test Package', + version: '1.0.0', + description: 'Test package', + owner: { github: 'elastic' }, + format_version: '1.0.0', + }, + }); + }); + + it('falls back to bundled package when one exists', async () => { + const fetchedInfo = await fetchInfo('test-package', '1.0.0'); + expect(fetchedInfo).toBeTruthy(); + }); + + it('throws when no corresponding bundled package exists', async () => { + try { + await fetchInfo('test-package', '1.0.0'); + } catch (e) { + expect(e).toBeInstanceOf(PackageNotFoundError); + } + }); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.ts index fee603f188e29..0b1c1d0bd562f 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.ts @@ -24,6 +24,7 @@ import type { RegistrySearchResults, GetCategoriesRequest, PackageVerificationResult, + ArchivePackage, } from '../../../types'; import { getArchiveFilelist, @@ -159,7 +160,10 @@ export async function fetchFindLatestPackageOrUndefined( } } -export async function fetchInfo(pkgName: string, pkgVersion: string): Promise { +export async function fetchInfo( + pkgName: string, + pkgVersion: string +): Promise { const registryUrl = getRegistryUrl(); try { // Trailing slash avoids 301 redirect / extra hop @@ -168,6 +172,18 @@ export async function fetchInfo(pkgName: string, pkgVersion: string): Promise { const useLocation = () => ({ @@ -45,6 +51,7 @@ describe('workspace_layout', () => { aliasTargetId: '', } as SharingSavedObjectProps, spaces: spacesPluginMock.createStartContract(), + workspace: {} as unknown as Workspace, }; it('should display conflict notification if outcome is conflict', () => { shallow( @@ -60,4 +67,44 @@ describe('workspace_layout', () => { otherObjectPath: '#/workspace/conflictId?query={}', }); }); + it('should not show GraphVisualization when workspaceInitialized is false, savedWorkspace.id is undefined, and savedWorkspace.isSaving is false', () => { + const component = shallow( + + ); + expect(component.find(GraphVisualization).exists()).toBe(false); + }); + it('should show GraphVisualization when workspaceInitialized is true', () => { + const component = shallow( + + ); + expect(component.find(GraphVisualization).exists()).toBe(true); + }); + it('should show GraphVisualization when savedWorkspace.id is defined', () => { + const component = shallow( + + ); + expect(component.find(GraphVisualization).exists()).toBe(true); + }); + it('should show GraphVisualization when savedWorkspace.isSaving is true', () => { + const component = shallow( + + ); + expect(component.find(GraphVisualization).exists()).toBe(true); + }); }); diff --git a/x-pack/plugins/graph/public/components/workspace_layout/workspace_layout.tsx b/x-pack/plugins/graph/public/components/workspace_layout/workspace_layout.tsx index 6cfbbcf7d9105..3eede479bd801 100644 --- a/x-pack/plugins/graph/public/components/workspace_layout/workspace_layout.tsx +++ b/x-pack/plugins/graph/public/components/workspace_layout/workspace_layout.tsx @@ -91,7 +91,11 @@ export const WorkspaceLayoutComponent = ({ const search = useLocation().search; const urlQuery = new URLSearchParams(search).get('query'); - const isInitialized = Boolean(workspaceInitialized || savedWorkspace.id); + // savedWorkspace.id gets set to null while saving a copy of an existing + // workspace, so we need to check for savedWorkspace.isSaving as well + const isInitialized = Boolean( + workspaceInitialized || savedWorkspace.id || savedWorkspace.isSaving + ); const selectSelected = useCallback((node: WorkspaceNode) => { selectedNode.current = node; diff --git a/x-pack/plugins/graph/public/helpers/saved_workspace_utils.test.ts b/x-pack/plugins/graph/public/helpers/saved_workspace_utils.test.ts new file mode 100644 index 0000000000000..7d4027c91858f --- /dev/null +++ b/x-pack/plugins/graph/public/helpers/saved_workspace_utils.test.ts @@ -0,0 +1,47 @@ +/* + * 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 { coreMock } from '@kbn/core/public/mocks'; +import { GraphWorkspaceSavedObject } from '../types'; +import { saveSavedWorkspace } from './saved_workspace_utils'; + +const core = coreMock.createStart(); + +describe('saved_workspace_utils', () => { + describe('saveSavedWorkspace', () => { + it('should delete the savedWorkspace id and set isSaving to true immediately when copyOnSave is true', async () => { + const savedWorkspace = { + id: '123', + title: 'my workspace', + lastSavedTitle: 'my workspace', + migrationVersion: {}, + wsState: '{ "indexPattern": "my-index-pattern" }', + getEsType: () => 'graph-workspace', + copyOnSave: true, + isSaving: false, + } as GraphWorkspaceSavedObject; + const promise = saveSavedWorkspace( + savedWorkspace, + {}, + { + savedObjectsClient: { + ...core.savedObjects.client, + find: jest.fn().mockResolvedValue({ savedObjects: [] }), + create: jest.fn().mockResolvedValue({ id: '456' }), + }, + overlays: core.overlays, + } + ); + expect(savedWorkspace.id).toBe(undefined); + expect(savedWorkspace.isSaving).toBe(true); + const id = await promise; + expect(id).toBe('456'); + expect(savedWorkspace.id).toBe('456'); + expect(savedWorkspace.isSaving).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/graph/public/helpers/saved_workspace_utils.ts b/x-pack/plugins/graph/public/helpers/saved_workspace_utils.ts index 202d13f9cd539..03e377792f1ac 100644 --- a/x-pack/plugins/graph/public/helpers/saved_workspace_utils.ts +++ b/x-pack/plugins/graph/public/helpers/saved_workspace_utils.ts @@ -162,18 +162,6 @@ export async function saveSavedWorkspace( overlays: OverlayStart; } ) { - // Save the original id in case the save fails. - const originalId = savedObject.id; - // Read https://github.com/elastic/kibana/issues/9056 and - // https://github.com/elastic/kibana/issues/9012 for some background into why this copyOnSave variable - // exists. - // The goal is to move towards a better rename flow, but since our users have been conditioned - // to expect a 'save as' flow during a rename, we are keeping the logic the same until a better - // UI/UX can be worked out. - if (savedObject.copyOnSave) { - delete savedObject.id; - } - let attributes: SavedObjectAttributes = {}; forOwn(mapping, (fieldType, fieldName) => { @@ -191,14 +179,28 @@ export async function saveSavedWorkspace( throw new Error('References not returned from extractReferences'); } + // Save the original id in case the save fails. + const originalId = savedObject.id; + try { + // Read https://github.com/elastic/kibana/issues/9056 and + // https://github.com/elastic/kibana/issues/9012 for some background into why this copyOnSave variable + // exists. + // The goal is to move towards a better rename flow, but since our users have been conditioned + // to expect a 'save as' flow during a rename, we are keeping the logic the same until a better + // UI/UX can be worked out. + if (savedObject.copyOnSave) { + delete savedObject.id; + } + + savedObject.isSaving = true; + await checkForDuplicateTitle( savedObject as any, isTitleDuplicateConfirmed, onTitleDuplicate, services ); - savedObject.isSaving = true; const createOpt = { id: savedObject.id, diff --git a/x-pack/test/apm_api_integration/common/authentication.ts b/x-pack/test/apm_api_integration/common/authentication.ts index 88d18c63cfc84..93bae236d5dc5 100644 --- a/x-pack/test/apm_api_integration/common/authentication.ts +++ b/x-pack/test/apm_api_integration/common/authentication.ts @@ -11,7 +11,6 @@ import { ToolingLog } from '@kbn/tooling-log'; import { omit } from 'lodash'; import { KbnClientRequesterError } from '@kbn/test'; import { AxiosError } from 'axios'; -import { ProcessorEvent } from '@kbn/observability-plugin/common'; import { SecurityServiceProvider } from '../../../../test/common/services/security'; type SecurityService = Awaited>; @@ -90,12 +89,7 @@ const customRoles = { elasticsearch: { indices: [ { - names: [ - ProcessorEvent.transaction, - ProcessorEvent.span, - ProcessorEvent.metric, - ProcessorEvent.error, - ], + names: ['traces-apm*', 'logs-apm*', 'metrics-apm*', 'apm-*'], privileges: ['monitor'], }, ], diff --git a/x-pack/test/apm_api_integration/tests/storage_explorer/storage_details.spec.ts b/x-pack/test/apm_api_integration/tests/storage_explorer/storage_details.spec.ts new file mode 100644 index 0000000000000..a9ec2199c8b23 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/storage_explorer/storage_details.spec.ts @@ -0,0 +1,162 @@ +/* + * 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 { IndexLifecyclePhaseSelectOption } from '@kbn/apm-plugin/common/storage_explorer_types'; +import { apm, timerange } from '@kbn/apm-synthtrace'; +import { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { + APIReturnType, + APIClientRequestParamsOf, +} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; +import { keyBy } from 'lodash'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +type StorageDetails = APIReturnType<'GET /internal/apm/services/{serviceName}/storage_details'>; + +export default function ApiTest({ getService }: FtrProviderContext) { + const registry = getService('registry'); + const apmApiClient = getService('apmApiClient'); + const synthtraceEsClient = getService('synthtraceEsClient'); + + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + const serviceName = 'opbeans-go'; + + function areProcessorEventStatsEmpty(storageDetails: StorageDetails) { + return storageDetails.processorEventStats.every(({ docs, size }) => docs === 0 && size === 0); + } + + async function callApi( + overrides?: RecursivePartial< + APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/storage_details'>['params'] + > + ) { + return await apmApiClient.monitorIndicesUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/storage_details', + params: { + path: { + serviceName, + ...overrides?.path, + }, + query: { + indexLifecyclePhase: IndexLifecyclePhaseSelectOption.All, + probability: 1, + environment: 'ENVIRONMENT_ALL', + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + kuery: '', + ...overrides?.query, + }, + }, + }); + } + + registry.when( + 'Storage details when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles empty state', async () => { + const { status, body } = await callApi(); + + expect(status).to.be(200); + expect(body.processorEventStats).to.have.length(4); + expect(areProcessorEventStatsEmpty(body)).to.be(true); + }); + } + ); + + registry.when('Storage details', { config: 'basic', archives: [] }, () => { + describe('when data is loaded', () => { + before(async () => { + const serviceGo = apm + .service({ name: serviceName, environment: 'production', agentName: 'go' }) + .instance('instance'); + + await synthtraceEsClient.index([ + timerange(start, end) + .interval('5m') + .rate(1) + .generator((timestamp) => + serviceGo + .transaction({ transactionName: 'GET /api/product/list' }) + .duration(2000) + .timestamp(timestamp) + .children( + serviceGo + .span({ spanName: '/_search', spanType: 'db', spanSubtype: 'elasticsearch' }) + .destination('elasticsearch') + .duration(100) + .success() + .timestamp(timestamp), + serviceGo + .span({ spanName: '/_search', spanType: 'db', spanSubtype: 'elasticsearch' }) + .destination('elasticsearch') + .duration(300) + .success() + .timestamp(timestamp) + ) + .errors( + serviceGo.error({ message: 'error 1', type: 'foo' }).timestamp(timestamp), + serviceGo.error({ message: 'error 2', type: 'foo' }).timestamp(timestamp), + serviceGo.error({ message: 'error 3', type: 'bar' }).timestamp(timestamp) + ) + ), + ]); + }); + + after(() => synthtraceEsClient.clean()); + + it('returns correct stats for processor events', async () => { + const { status, body } = await callApi(); + expect(status).to.be(200); + expect(body.processorEventStats).to.have.length(4); + + const stats = keyBy(body.processorEventStats, 'processorEvent'); + + expect(stats[ProcessorEvent.transaction]?.docs).to.be(3); + expect(stats[ProcessorEvent.transaction]?.size).to.be.greaterThan(0); + expect(stats[ProcessorEvent.span]?.docs).to.be(6); + expect(stats[ProcessorEvent.span]?.size).to.be.greaterThan(0); + expect(stats[ProcessorEvent.error]?.docs).to.be(9); + expect(stats[ProcessorEvent.error]?.size).to.be.greaterThan(0); + expect(stats[ProcessorEvent.metric]?.docs).to.be.greaterThan(0); + expect(stats[ProcessorEvent.metric]?.size).to.be.greaterThan(0); + }); + + it('returns empty stats when there is no matching environment', async () => { + const { status, body } = await callApi({ + query: { + environment: 'test', + }, + }); + expect(status).to.be(200); + expect(areProcessorEventStatsEmpty(body)).to.be(true); + }); + + it('returns empty stats when there is no matching lifecycle phase', async () => { + const { status, body } = await callApi({ + query: { + indexLifecyclePhase: IndexLifecyclePhaseSelectOption.Warm, + }, + }); + expect(status).to.be(200); + expect(areProcessorEventStatsEmpty(body)).to.be(true); + }); + + it('returns empty stats when there is no matching kql filter', async () => { + const { status, body } = await callApi({ + query: { + kuery: 'service.name : opbeans-node', + }, + }); + expect(status).to.be(200); + expect(areProcessorEventStatsEmpty(body)).to.be(true); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer.spec.ts b/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer.spec.ts new file mode 100644 index 0000000000000..dc2429a5b8b3a --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer.spec.ts @@ -0,0 +1,164 @@ +/* + * 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 { IndexLifecyclePhaseSelectOption } from '@kbn/apm-plugin/common/storage_explorer_types'; +import { apm, timerange } from '@kbn/apm-synthtrace'; +import { APIClientRequestParamsOf } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; +import { keyBy } from 'lodash'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const registry = getService('registry'); + const apmApiClient = getService('apmApiClient'); + const synthtraceEsClient = getService('synthtraceEsClient'); + + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + const goServiceName = 'opbeans-go'; + const nodeServiceName = 'opbeans-node'; + + async function callApi( + overrides?: RecursivePartial< + APIClientRequestParamsOf<'GET /internal/apm/storage_explorer'>['params'] + > + ) { + return await apmApiClient.monitorIndicesUser({ + endpoint: 'GET /internal/apm/storage_explorer', + params: { + query: { + indexLifecyclePhase: IndexLifecyclePhaseSelectOption.All, + probability: 1, + environment: 'ENVIRONMENT_ALL', + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + kuery: '', + ...overrides?.query, + }, + }, + }); + } + + registry.when( + 'Storage explorer when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles empty state', async () => { + const { status, body } = await callApi(); + + expect(status).to.be(200); + expect(body.serviceStatistics).to.be.empty(); + }); + } + ); + + registry.when('Storage explorer', { config: 'basic', archives: [] }, () => { + describe('when data is loaded', () => { + before(async () => { + const serviceGo = apm + .service({ name: goServiceName, environment: 'production', agentName: 'go' }) + .instance('instance-go'); + + const serviceNodeStaging = apm + .service({ name: nodeServiceName, environment: 'staging', agentName: 'node' }) + .instance('instance-node-staging'); + + const serviceNodeDev = apm + .service({ name: nodeServiceName, environment: 'dev', agentName: 'node' }) + .instance('instance-node-dev'); + + await synthtraceEsClient.index([ + timerange(start, end) + .interval('5m') + .rate(1) + .generator((timestamp) => + serviceGo + .transaction({ transactionName: 'GET /api/product/list' }) + .duration(2000) + .timestamp(timestamp) + ), + timerange(start, end) + .interval('5m') + .rate(1) + .generator((timestamp) => + serviceNodeStaging + .transaction({ transactionName: 'GET /api/users/list' }) + .duration(2000) + .timestamp(timestamp) + ), + timerange(start, end) + .interval('5m') + .rate(1) + .generator((timestamp) => + serviceNodeDev + .transaction({ transactionName: 'GET /api/users/list' }) + .duration(2000) + .timestamp(timestamp) + ), + ]); + }); + + after(() => synthtraceEsClient.clean()); + + it('returns correct stats', async () => { + const { status, body } = await callApi(); + expect(status).to.be(200); + + expect(body.serviceStatistics).to.have.length(2); + + const stats = keyBy(body.serviceStatistics, 'serviceName'); + + const goServiceStats = stats[goServiceName]; + expect(goServiceStats?.environments).to.have.length(1); + expect(goServiceStats?.environments).to.contain('production'); + expect(goServiceStats?.agentName).to.be('go'); + expect(goServiceStats?.size).to.be.greaterThan(0); + expect(goServiceStats?.sampling).to.be(1); + + const nodeServiceStats = stats[nodeServiceName]; + expect(nodeServiceStats?.environments).to.have.length(2); + expect(nodeServiceStats?.environments).to.contain('staging'); + expect(nodeServiceStats?.environments).to.contain('dev'); + expect(nodeServiceStats?.agentName).to.be('node'); + expect(nodeServiceStats?.size).to.be.greaterThan(0); + expect(nodeServiceStats?.sampling).to.be(1); + }); + + it('returns only node service stats when there is a matching environment', async () => { + const { status, body } = await callApi({ + query: { + environment: 'dev', + }, + }); + expect(status).to.be(200); + expect(body.serviceStatistics).to.have.length(1); + expect(body.serviceStatistics[0]?.serviceName).to.be(nodeServiceName); + }); + + it('returns empty stats when there is no matching lifecycle phase', async () => { + const { status, body } = await callApi({ + query: { + indexLifecyclePhase: IndexLifecyclePhaseSelectOption.Warm, + }, + }); + expect(status).to.be(200); + expect(body.serviceStatistics).to.be.empty(); + }); + + it('returns only go service stats when there is a matching kql filter', async () => { + const { status, body } = await callApi({ + query: { + kuery: `service.name : ${goServiceName}`, + }, + }); + expect(status).to.be(200); + expect(body.serviceStatistics).to.have.length(1); + expect(body.serviceStatistics[0]?.serviceName).to.be(goServiceName); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer_privileges.spec.ts b/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer_privileges.spec.ts new file mode 100644 index 0000000000000..4d7cc8655840f --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer_privileges.spec.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { ApmApiSupertest } from '../../common/apm_api_supertest'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const registry = getService('registry'); + const apmApiClient = getService('apmApiClient'); + + async function callApi(apiClient: ApmApiSupertest) { + return await apiClient({ + endpoint: 'GET /internal/apm/storage_explorer/privileges', + }); + } + + registry.when('Storage explorer privileges', { config: 'basic', archives: [] }, () => { + it('returns true when the user has the required indices privileges', async () => { + const { status, body } = await callApi(apmApiClient.monitorIndicesUser); + expect(status).to.be(200); + expect(body.hasPrivileges).to.be(true); + }); + + it(`returns false when the user doesn't have the required indices privileges`, async () => { + const { status, body } = await callApi(apmApiClient.writeUser); + expect(status).to.be(200); + expect(body.hasPrivileges).to.be(false); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer_summary_stats.spec.ts b/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer_summary_stats.spec.ts new file mode 100644 index 0000000000000..afde7d243c227 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer_summary_stats.spec.ts @@ -0,0 +1,150 @@ +/* + * 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 { IndexLifecyclePhaseSelectOption } from '@kbn/apm-plugin/common/storage_explorer_types'; +import { apm, timerange } from '@kbn/apm-synthtrace'; +import { APIClientRequestParamsOf } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { roundNumber } from '../../utils'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const registry = getService('registry'); + const apmApiClient = getService('apmApiClient'); + const synthtraceEsClient = getService('synthtraceEsClient'); + + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + const goServiceName = 'opbeans-go'; + const nodeServiceName = 'opbeans-node'; + + async function callApi( + overrides?: RecursivePartial< + APIClientRequestParamsOf<'GET /internal/apm/storage_explorer_summary_stats'>['params'] + > + ) { + return await apmApiClient.monitorIndicesUser({ + endpoint: 'GET /internal/apm/storage_explorer_summary_stats', + params: { + query: { + indexLifecyclePhase: IndexLifecyclePhaseSelectOption.All, + probability: 1, + environment: 'ENVIRONMENT_ALL', + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + kuery: '', + ...overrides?.query, + }, + }, + }); + } + + registry.when( + 'Storage explorer summary stats when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles empty state', async () => { + const { status, body } = await callApi(); + + expect(status).to.be(200); + expect(body.tracesPerMinute).to.be(0); + expect(body.numberOfServices).to.be(0); + expect(body.estimatedSize).to.be(0); + expect(body.dailyDataGeneration).to.be(0); + }); + } + ); + + registry.when('Storage explorer summary stats', { config: 'basic', archives: [] }, () => { + describe('when data is loaded', () => { + before(async () => { + const serviceGo = apm + .service({ name: goServiceName, environment: 'production', agentName: 'go' }) + .instance('instance-go'); + + const serviceNode = apm + .service({ name: nodeServiceName, environment: 'dev', agentName: 'node' }) + .instance('instance-node'); + + await synthtraceEsClient.index([ + timerange(start, end) + .interval('1m') + .rate(1) + .generator((timestamp) => + serviceGo + .transaction({ transactionName: 'GET /api/product/list' }) + .duration(2000) + .timestamp(timestamp) + ), + timerange(start, end) + .interval('1m') + .rate(1) + .generator((timestamp) => + serviceNode + .transaction({ transactionName: 'GET /api/users/list' }) + .duration(2000) + .timestamp(timestamp) + ), + ]); + }); + + after(() => synthtraceEsClient.clean()); + + it('returns correct summary stats', async () => { + const { status, body } = await callApi(); + + expect(status).to.be(200); + expect(body.numberOfServices).to.be(2); + expect(roundNumber(body.tracesPerMinute)).to.be(2); + expect(body.estimatedSize).to.be.greaterThan(0); + expect(body.dailyDataGeneration).to.be.greaterThan(0); + }); + + it('returns only node service summary stats when there is a matching environment', async () => { + const { status, body } = await callApi({ + query: { + environment: 'dev', + }, + }); + + expect(status).to.be(200); + expect(body.numberOfServices).to.be(1); + expect(roundNumber(body.tracesPerMinute)).to.be(1); + expect(body.estimatedSize).to.be.greaterThan(0); + expect(body.dailyDataGeneration).to.be.greaterThan(0); + }); + + it('returns empty summary stats when there is no matching lifecycle phase', async () => { + const { status, body } = await callApi({ + query: { + indexLifecyclePhase: IndexLifecyclePhaseSelectOption.Warm, + }, + }); + + expect(status).to.be(200); + expect(body.tracesPerMinute).to.be(0); + expect(body.numberOfServices).to.be(0); + expect(body.estimatedSize).to.be(0); + expect(body.dailyDataGeneration).to.be(0); + }); + + it('returns only go service summary stats when there is a matching kql filter', async () => { + const { status, body } = await callApi({ + query: { + kuery: `service.name : ${goServiceName}`, + }, + }); + + expect(status).to.be(200); + expect(body.numberOfServices).to.be(1); + expect(roundNumber(body.tracesPerMinute)).to.be(1); + expect(body.estimatedSize).to.be.greaterThan(0); + expect(body.dailyDataGeneration).to.be.greaterThan(0); + }); + }); + }); +} diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts index df0fdaeba6973..3c076d4e35142 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts @@ -55,8 +55,7 @@ export default ({ getService }: FtrProviderContext): void => { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); - // Failing: See https://github.com/elastic/kibana/issues/139626 - describe.skip('find_cases', () => { + describe('find_cases', () => { describe('basic tests', () => { afterEach(async () => { await deleteAllCaseItems(es); @@ -369,6 +368,11 @@ export default ({ getService }: FtrProviderContext): void => { owner: 'securitySolutionFixture', }, }); + + // There is potential for the alert index to not be refreshed by the time the second comment is created + // which could attempt to update the alert status again and will encounter a conflict so this will + // ensure that the index is up to date before we try to update the next alert status + await es.indices.refresh({ index: defaultSignalsIndex }); } const patchedCase = await createComment({ diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index 12ea3e7452500..443170a43215d 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -25,7 +25,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const policyTestResources = getService('policyTestResources'); const endpointTestResources = getService('endpointTestResources'); - describe('When on the Endpoint Policy Details Page', function () { + // Failing: See https://github.com/elastic/kibana/issues/138776 + describe.skip('When on the Endpoint Policy Details Page', function () { let indexedData: IndexedHostsAndAlertsResponse; before(async () => {