From 37d7ea271378818f321059d47aed355d564fe8a8 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 25 Feb 2021 11:11:15 -0700 Subject: [PATCH 01/32] [Maps] fix index pattern references not extracted for tracks source (#92226) * [Maps] fix index pattern references not extracted for tracks source * merge with master * tslint * tslint Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../maps/common/migrations/references.js | 107 ---------------- ...{references.test.js => references.test.ts} | 2 +- .../maps/common/migrations/references.ts | 120 ++++++++++++++++++ .../embeddable/map_embeddable_factory.ts | 5 +- .../maps/public/map_attribute_service.ts | 1 - .../server/maps_telemetry/maps_telemetry.ts | 6 +- 6 files changed, 128 insertions(+), 113 deletions(-) delete mode 100644 x-pack/plugins/maps/common/migrations/references.js rename x-pack/plugins/maps/common/migrations/{references.test.js => references.test.ts} (98%) create mode 100644 x-pack/plugins/maps/common/migrations/references.ts diff --git a/x-pack/plugins/maps/common/migrations/references.js b/x-pack/plugins/maps/common/migrations/references.js deleted file mode 100644 index ab8edbefb27c2..0000000000000 --- a/x-pack/plugins/maps/common/migrations/references.js +++ /dev/null @@ -1,107 +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. - */ - -// Can not use public Layer classes to extract references since this logic must run in both client and server. - -import _ from 'lodash'; -import { SOURCE_TYPES } from '../constants'; - -function doesSourceUseIndexPattern(layerDescriptor) { - const sourceType = _.get(layerDescriptor, 'sourceDescriptor.type'); - return ( - sourceType === SOURCE_TYPES.ES_GEO_GRID || - sourceType === SOURCE_TYPES.ES_SEARCH || - sourceType === SOURCE_TYPES.ES_PEW_PEW - ); -} - -export function extractReferences({ attributes, references = [] }) { - if (!attributes.layerListJSON) { - return { attributes, references }; - } - - const extractedReferences = []; - - const layerList = JSON.parse(attributes.layerListJSON); - layerList.forEach((layer, layerIndex) => { - // Extract index-pattern references from source descriptor - if (doesSourceUseIndexPattern(layer) && _.has(layer, 'sourceDescriptor.indexPatternId')) { - const refName = `layer_${layerIndex}_source_index_pattern`; - extractedReferences.push({ - name: refName, - type: 'index-pattern', - id: layer.sourceDescriptor.indexPatternId, - }); - delete layer.sourceDescriptor.indexPatternId; - layer.sourceDescriptor.indexPatternRefName = refName; - } - - // Extract index-pattern references from join - const joins = _.get(layer, 'joins', []); - joins.forEach((join, joinIndex) => { - if (_.has(join, 'right.indexPatternId')) { - const refName = `layer_${layerIndex}_join_${joinIndex}_index_pattern`; - extractedReferences.push({ - name: refName, - type: 'index-pattern', - id: join.right.indexPatternId, - }); - delete join.right.indexPatternId; - join.right.indexPatternRefName = refName; - } - }); - }); - - return { - attributes: { - ...attributes, - layerListJSON: JSON.stringify(layerList), - }, - references: references.concat(extractedReferences), - }; -} - -function findReference(targetName, references) { - const reference = references.find((reference) => reference.name === targetName); - if (!reference) { - throw new Error(`Could not find reference "${targetName}"`); - } - return reference; -} - -export function injectReferences({ attributes, references }) { - if (!attributes.layerListJSON) { - return { attributes }; - } - - const layerList = JSON.parse(attributes.layerListJSON); - layerList.forEach((layer) => { - // Inject index-pattern references into source descriptor - if (doesSourceUseIndexPattern(layer) && _.has(layer, 'sourceDescriptor.indexPatternRefName')) { - const reference = findReference(layer.sourceDescriptor.indexPatternRefName, references); - layer.sourceDescriptor.indexPatternId = reference.id; - delete layer.sourceDescriptor.indexPatternRefName; - } - - // Inject index-pattern references into join - const joins = _.get(layer, 'joins', []); - joins.forEach((join) => { - if (_.has(join, 'right.indexPatternRefName')) { - const reference = findReference(join.right.indexPatternRefName, references); - join.right.indexPatternId = reference.id; - delete join.right.indexPatternRefName; - } - }); - }); - - return { - attributes: { - ...attributes, - layerListJSON: JSON.stringify(layerList), - }, - }; -} diff --git a/x-pack/plugins/maps/common/migrations/references.test.js b/x-pack/plugins/maps/common/migrations/references.test.ts similarity index 98% rename from x-pack/plugins/maps/common/migrations/references.test.js rename to x-pack/plugins/maps/common/migrations/references.test.ts index 3b8b7de441be4..5b749022bb62b 100644 --- a/x-pack/plugins/maps/common/migrations/references.test.js +++ b/x-pack/plugins/maps/common/migrations/references.test.ts @@ -128,7 +128,7 @@ describe('injectReferences', () => { const attributes = { title: 'my map', }; - expect(injectReferences({ attributes })).toEqual({ + expect(injectReferences({ attributes, references: [] })).toEqual({ attributes: { title: 'my map', }, diff --git a/x-pack/plugins/maps/common/migrations/references.ts b/x-pack/plugins/maps/common/migrations/references.ts new file mode 100644 index 0000000000000..d48be6bd56fbe --- /dev/null +++ b/x-pack/plugins/maps/common/migrations/references.ts @@ -0,0 +1,120 @@ +/* + * 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. + */ + +// Can not use public Layer classes to extract references since this logic must run in both client and server. + +import { SavedObjectReference } from '../../../../../src/core/types'; +import { MapSavedObjectAttributes } from '../map_saved_object_type'; +import { LayerDescriptor } from '../descriptor_types'; + +interface IndexPatternReferenceDescriptor { + indexPatternId?: string; + indexPatternRefName?: string; +} + +export function extractReferences({ + attributes, + references = [], +}: { + attributes: MapSavedObjectAttributes; + references?: SavedObjectReference[]; +}) { + if (!attributes.layerListJSON) { + return { attributes, references }; + } + + const extractedReferences: SavedObjectReference[] = []; + + const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + layerList.forEach((layer, layerIndex) => { + // Extract index-pattern references from source descriptor + if (layer.sourceDescriptor && 'indexPatternId' in layer.sourceDescriptor) { + const sourceDescriptor = layer.sourceDescriptor as IndexPatternReferenceDescriptor; + const refName = `layer_${layerIndex}_source_index_pattern`; + extractedReferences.push({ + name: refName, + type: 'index-pattern', + id: sourceDescriptor.indexPatternId!, + }); + delete sourceDescriptor.indexPatternId; + sourceDescriptor.indexPatternRefName = refName; + } + + // Extract index-pattern references from join + const joins = layer.joins ? layer.joins : []; + joins.forEach((join, joinIndex) => { + if ('indexPatternId' in join.right) { + const sourceDescriptor = join.right as IndexPatternReferenceDescriptor; + const refName = `layer_${layerIndex}_join_${joinIndex}_index_pattern`; + extractedReferences.push({ + name: refName, + type: 'index-pattern', + id: sourceDescriptor.indexPatternId!, + }); + delete sourceDescriptor.indexPatternId; + sourceDescriptor.indexPatternRefName = refName; + } + }); + }); + + return { + attributes: { + ...attributes, + layerListJSON: JSON.stringify(layerList), + }, + references: references.concat(extractedReferences), + }; +} + +function findReference(targetName: string, references: SavedObjectReference[]) { + const reference = references.find(({ name }) => name === targetName); + if (!reference) { + throw new Error(`Could not find reference "${targetName}"`); + } + return reference; +} + +export function injectReferences({ + attributes, + references, +}: { + attributes: MapSavedObjectAttributes; + references: SavedObjectReference[]; +}) { + if (!attributes.layerListJSON) { + return { attributes }; + } + + const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + layerList.forEach((layer) => { + // Inject index-pattern references into source descriptor + if (layer.sourceDescriptor && 'indexPatternRefName' in layer.sourceDescriptor) { + const sourceDescriptor = layer.sourceDescriptor as IndexPatternReferenceDescriptor; + const reference = findReference(sourceDescriptor.indexPatternRefName!, references); + sourceDescriptor.indexPatternId = reference.id; + delete sourceDescriptor.indexPatternRefName; + } + + // Inject index-pattern references into join + const joins = layer.joins ? layer.joins : []; + joins.forEach((join) => { + if ('indexPatternRefName' in join.right) { + const sourceDescriptor = join.right as IndexPatternReferenceDescriptor; + const reference = findReference(sourceDescriptor.indexPatternRefName!, references); + sourceDescriptor.indexPatternId = reference.id; + delete sourceDescriptor.indexPatternRefName; + } + }); + }); + + return { + attributes: { + ...attributes, + layerListJSON: JSON.stringify(layerList), + }, + }; +} diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts index 7e15bfa9a340e..b1944f8136709 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts @@ -16,7 +16,6 @@ import { MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants'; import { getMapEmbeddableDisplayName } from '../../common/i18n_getters'; import { MapByReferenceInput, MapEmbeddableInput, MapByValueInput } from './types'; import { lazyLoadMapModules } from '../lazy_load_bundle'; -// @ts-expect-error import { extractReferences } from '../../common/migrations/references'; export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { @@ -69,7 +68,9 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { const maybeMapByValueInput = state as EmbeddableStateWithType | MapByValueInput; if ((maybeMapByValueInput as MapByValueInput).attributes !== undefined) { - const { references } = extractReferences(maybeMapByValueInput); + const { references } = extractReferences({ + attributes: (maybeMapByValueInput as MapByValueInput).attributes, + }); return { state, references }; } diff --git a/x-pack/plugins/maps/public/map_attribute_service.ts b/x-pack/plugins/maps/public/map_attribute_service.ts index 71b0b7f28a2ff..5f7c45b1b42d7 100644 --- a/x-pack/plugins/maps/public/map_attribute_service.ts +++ b/x-pack/plugins/maps/public/map_attribute_service.ts @@ -12,7 +12,6 @@ import { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; import { getMapEmbeddableDisplayName } from '../common/i18n_getters'; import { checkForDuplicateTitle, OnSaveProps } from '../../../../src/plugins/saved_objects/public'; import { getCoreOverlays, getEmbeddableService, getSavedObjectsClient } from './kibana_services'; -// @ts-expect-error import { extractReferences, injectReferences } from '../common/migrations/references'; import { MapByValueInput, MapByReferenceInput } from './embeddable/types'; diff --git a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts index 8f9b529ae30f5..bf180c514c56f 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts @@ -24,7 +24,6 @@ import { import { MapSavedObject, MapSavedObjectAttributes } from '../../common/map_saved_object_type'; import { getIndexPatternsService, getInternalRepository } from '../kibana_server_services'; import { MapsConfigType } from '../../config'; -// @ts-expect-error import { injectReferences } from '././../../common/migrations/references'; interface Settings { @@ -314,7 +313,10 @@ export async function getMapsTelemetry(config: MapsConfigType): Promise { const savedObjectsWithIndexPatternIds = savedObjects.map((savedObject) => { - return injectReferences(savedObject); + return { + ...savedObject, + ...injectReferences(savedObject), + }; }); return layerLists.push(...getLayerLists(savedObjectsWithIndexPatternIds)); } From 3b66bf7cc553b3516093b5d6f6bb10abd157b61b Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 25 Feb 2021 18:24:39 +0000 Subject: [PATCH 02/32] [ML] Adding ml_capabilities api tests (#92786) * [ML] Adding ml_capabilities api tests * updating test text * changes based on review --- x-pack/test/api_integration/apis/ml/index.ts | 1 + .../apis/ml/system/capabilities.ts | 124 ++++++++++ .../api_integration/apis/ml/system/index.ts | 15 ++ .../apis/ml/system/space_capabilities.ts | 222 ++++++++++++++++++ 4 files changed, 362 insertions(+) create mode 100644 x-pack/test/api_integration/apis/ml/system/capabilities.ts create mode 100644 x-pack/test/api_integration/apis/ml/system/index.ts create mode 100644 x-pack/test/api_integration/apis/ml/system/space_capabilities.ts diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index f03756a2885bb..41e94d69d2e9b 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -70,5 +70,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./calendars')); loadTestFile(require.resolve('./annotations')); loadTestFile(require.resolve('./saved_objects')); + loadTestFile(require.resolve('./system')); }); } diff --git a/x-pack/test/api_integration/apis/ml/system/capabilities.ts b/x-pack/test/api_integration/apis/ml/system/capabilities.ts new file mode 100644 index 0000000000000..d8ab2a30ef7fb --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/system/capabilities.ts @@ -0,0 +1,124 @@ +/* + * 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 '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { MlCapabilitiesResponse } from '../../../../../plugins/ml/common/types/capabilities'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + async function runRequest(user: USER): Promise { + const { body } = await supertest + .get(`/api/ml/ml_capabilities`) + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + return body; + } + + describe('ml_capabilities', () => { + describe('get capabilities', function () { + it('should be enabled in space', async () => { + const { mlFeatureEnabledInSpace } = await runRequest(USER.ML_POWERUSER); + expect(mlFeatureEnabledInSpace).to.eql(true); + }); + + it('should have upgradeInProgress false', async () => { + const { upgradeInProgress } = await runRequest(USER.ML_POWERUSER); + expect(upgradeInProgress).to.eql(false); + }); + + it('should have full license', async () => { + const { isPlatinumOrTrialLicense } = await runRequest(USER.ML_POWERUSER); + expect(isPlatinumOrTrialLicense).to.eql(true); + }); + + it('should have the right number of capabilities', async () => { + const { capabilities } = await runRequest(USER.ML_POWERUSER); + expect(Object.keys(capabilities).length).to.eql(29); + }); + + it('should get viewer capabilities', async () => { + const { capabilities } = await runRequest(USER.ML_VIEWER); + + expect(capabilities).to.eql({ + canCreateJob: false, + canDeleteJob: false, + canOpenJob: false, + canCloseJob: false, + canUpdateJob: false, + canForecastJob: false, + canCreateDatafeed: false, + canDeleteDatafeed: false, + canStartStopDatafeed: false, + canUpdateDatafeed: false, + canPreviewDatafeed: false, + canGetFilters: false, + canCreateCalendar: false, + canDeleteCalendar: false, + canCreateFilter: false, + canDeleteFilter: false, + canCreateDataFrameAnalytics: false, + canDeleteDataFrameAnalytics: false, + canStartStopDataFrameAnalytics: false, + canCreateMlAlerts: false, + canAccessML: true, + canGetJobs: true, + canGetDatafeeds: true, + canGetCalendars: true, + canFindFileStructure: true, + canGetDataFrameAnalytics: true, + canGetAnnotations: true, + canCreateAnnotation: true, + canDeleteAnnotation: true, + }); + }); + + it('should get power user capabilities', async () => { + const { capabilities } = await runRequest(USER.ML_POWERUSER); + + expect(capabilities).to.eql({ + canCreateJob: true, + canDeleteJob: true, + canOpenJob: true, + canCloseJob: true, + canUpdateJob: true, + canForecastJob: true, + canCreateDatafeed: true, + canDeleteDatafeed: true, + canStartStopDatafeed: true, + canUpdateDatafeed: true, + canPreviewDatafeed: true, + canGetFilters: true, + canCreateCalendar: true, + canDeleteCalendar: true, + canCreateFilter: true, + canDeleteFilter: true, + canCreateDataFrameAnalytics: true, + canDeleteDataFrameAnalytics: true, + canStartStopDataFrameAnalytics: true, + canCreateMlAlerts: true, + canAccessML: true, + canGetJobs: true, + canGetDatafeeds: true, + canGetCalendars: true, + canFindFileStructure: true, + canGetDataFrameAnalytics: true, + canGetAnnotations: true, + canCreateAnnotation: true, + canDeleteAnnotation: true, + }); + }); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/system/index.ts b/x-pack/test/api_integration/apis/ml/system/index.ts new file mode 100644 index 0000000000000..68ffd5fa267e9 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/system/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('system', function () { + loadTestFile(require.resolve('./capabilities')); + loadTestFile(require.resolve('./space_capabilities')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/system/space_capabilities.ts b/x-pack/test/api_integration/apis/ml/system/space_capabilities.ts new file mode 100644 index 0000000000000..cd922bf4bae92 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/system/space_capabilities.ts @@ -0,0 +1,222 @@ +/* + * 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 '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { MlCapabilitiesResponse } from '../../../../../plugins/ml/common/types/capabilities'; + +const idSpaceWithMl = 'space_with_ml'; +const idSpaceNoMl = 'space_no_ml'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const spacesService = getService('spaces'); + const ml = getService('ml'); + + async function runRequest(user: USER, space?: string): Promise { + const { body } = await supertest + .get(`${space ? `/s/${space}` : ''}/api/ml/ml_capabilities`) + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + return body; + } + + describe('ml_capabilities in spaces', () => { + before(async () => { + await spacesService.create({ id: idSpaceWithMl, name: 'space_one', disabledFeatures: [] }); + await spacesService.create({ id: idSpaceNoMl, name: 'space_two', disabledFeatures: ['ml'] }); + }); + + after(async () => { + await spacesService.delete(idSpaceWithMl); + await spacesService.delete(idSpaceNoMl); + }); + + describe('get capabilities', function () { + it('should be enabled in space - space with ML', async () => { + const { mlFeatureEnabledInSpace } = await runRequest(USER.ML_POWERUSER, idSpaceWithMl); + expect(mlFeatureEnabledInSpace).to.eql(true); + }); + it('should not be enabled in space - space without ML', async () => { + const { mlFeatureEnabledInSpace } = await runRequest(USER.ML_POWERUSER, idSpaceNoMl); + expect(mlFeatureEnabledInSpace).to.eql(false); + }); + + it('should have upgradeInProgress false - space with ML', async () => { + const { upgradeInProgress } = await runRequest(USER.ML_POWERUSER, idSpaceWithMl); + expect(upgradeInProgress).to.eql(false); + }); + it('should have upgradeInProgress false - space without ML', async () => { + const { upgradeInProgress } = await runRequest(USER.ML_POWERUSER, idSpaceNoMl); + expect(upgradeInProgress).to.eql(false); + }); + + it('should have full license - space with ML', async () => { + const { isPlatinumOrTrialLicense } = await runRequest(USER.ML_POWERUSER, idSpaceWithMl); + expect(isPlatinumOrTrialLicense).to.eql(true); + }); + it('should have full license - space without ML', async () => { + const { isPlatinumOrTrialLicense } = await runRequest(USER.ML_POWERUSER, idSpaceNoMl); + expect(isPlatinumOrTrialLicense).to.eql(true); + }); + + it('should have the right number of capabilities - space with ML', async () => { + const { capabilities } = await runRequest(USER.ML_POWERUSER, idSpaceWithMl); + expect(Object.keys(capabilities).length).to.eql(29); + }); + it('should have the right number of capabilities - space without ML', async () => { + const { capabilities } = await runRequest(USER.ML_POWERUSER, idSpaceNoMl); + expect(Object.keys(capabilities).length).to.eql(29); + }); + + it('should get viewer capabilities - space with ML', async () => { + const { capabilities } = await runRequest(USER.ML_VIEWER, idSpaceWithMl); + expect(capabilities).to.eql({ + canCreateJob: false, + canDeleteJob: false, + canOpenJob: false, + canCloseJob: false, + canUpdateJob: false, + canForecastJob: false, + canCreateDatafeed: false, + canDeleteDatafeed: false, + canStartStopDatafeed: false, + canUpdateDatafeed: false, + canPreviewDatafeed: false, + canGetFilters: false, + canCreateCalendar: false, + canDeleteCalendar: false, + canCreateFilter: false, + canDeleteFilter: false, + canCreateDataFrameAnalytics: false, + canDeleteDataFrameAnalytics: false, + canStartStopDataFrameAnalytics: false, + canCreateMlAlerts: false, + canAccessML: true, + canGetJobs: true, + canGetDatafeeds: true, + canGetCalendars: true, + canFindFileStructure: true, + canGetDataFrameAnalytics: true, + canGetAnnotations: true, + canCreateAnnotation: true, + canDeleteAnnotation: true, + }); + }); + + it('should get viewer capabilities - space without ML', async () => { + const { capabilities } = await runRequest(USER.ML_VIEWER, idSpaceNoMl); + expect(capabilities).to.eql({ + canCreateJob: false, + canDeleteJob: false, + canOpenJob: false, + canCloseJob: false, + canUpdateJob: false, + canForecastJob: false, + canCreateDatafeed: false, + canDeleteDatafeed: false, + canStartStopDatafeed: false, + canUpdateDatafeed: false, + canPreviewDatafeed: false, + canGetFilters: false, + canCreateCalendar: false, + canDeleteCalendar: false, + canCreateFilter: false, + canDeleteFilter: false, + canCreateDataFrameAnalytics: false, + canDeleteDataFrameAnalytics: false, + canStartStopDataFrameAnalytics: false, + canCreateMlAlerts: false, + canAccessML: false, + canGetJobs: false, + canGetDatafeeds: false, + canGetCalendars: false, + canFindFileStructure: false, + canGetDataFrameAnalytics: false, + canGetAnnotations: false, + canCreateAnnotation: false, + canDeleteAnnotation: false, + }); + }); + + it('should get power user capabilities - space with ML', async () => { + const { capabilities } = await runRequest(USER.ML_POWERUSER, idSpaceWithMl); + expect(capabilities).to.eql({ + canCreateJob: true, + canDeleteJob: true, + canOpenJob: true, + canCloseJob: true, + canUpdateJob: true, + canForecastJob: true, + canCreateDatafeed: true, + canDeleteDatafeed: true, + canStartStopDatafeed: true, + canUpdateDatafeed: true, + canPreviewDatafeed: true, + canGetFilters: true, + canCreateCalendar: true, + canDeleteCalendar: true, + canCreateFilter: true, + canDeleteFilter: true, + canCreateDataFrameAnalytics: true, + canDeleteDataFrameAnalytics: true, + canStartStopDataFrameAnalytics: true, + canCreateMlAlerts: true, + canAccessML: true, + canGetJobs: true, + canGetDatafeeds: true, + canGetCalendars: true, + canFindFileStructure: true, + canGetDataFrameAnalytics: true, + canGetAnnotations: true, + canCreateAnnotation: true, + canDeleteAnnotation: true, + }); + }); + + it('should get power user capabilities - space without ML', async () => { + const { capabilities } = await runRequest(USER.ML_POWERUSER, idSpaceNoMl); + expect(capabilities).to.eql({ + canCreateJob: false, + canDeleteJob: false, + canOpenJob: false, + canCloseJob: false, + canUpdateJob: false, + canForecastJob: false, + canCreateDatafeed: false, + canDeleteDatafeed: false, + canStartStopDatafeed: false, + canUpdateDatafeed: false, + canPreviewDatafeed: false, + canGetFilters: false, + canCreateCalendar: false, + canDeleteCalendar: false, + canCreateFilter: false, + canDeleteFilter: false, + canCreateDataFrameAnalytics: false, + canDeleteDataFrameAnalytics: false, + canStartStopDataFrameAnalytics: false, + canCreateMlAlerts: false, + canAccessML: false, + canGetJobs: false, + canGetDatafeeds: false, + canGetCalendars: false, + canFindFileStructure: false, + canGetDataFrameAnalytics: false, + canGetAnnotations: false, + canCreateAnnotation: false, + canDeleteAnnotation: false, + }); + }); + }); + }); +}; From 06d966eca5730d9bcd1166a7c1021c340d9fdb0f Mon Sep 17 00:00:00 2001 From: joxley-elastic <73481302+joxley-elastic@users.noreply.github.com> Date: Thu, 25 Feb 2021 13:30:59 -0500 Subject: [PATCH 03/32] [Console] Autocompletion of component_templates (#91180) --- .../cluster.get_component_template.json | 16 ++++++++++++++++ .../cluster.put_component_template.json | 14 ++++++++++++++ .../cluster.put_component_template.json | 13 +++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_component_template.json create mode 100644 src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_component_template.json create mode 100644 src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.put_component_template.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_component_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_component_template.json new file mode 100644 index 0000000000000..f2ef49f6e13e2 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_component_template.json @@ -0,0 +1,16 @@ +{ + "indices.get_template": { + "url_params": { + "flat_settings": "__flag__", + "master_timeout": "", + "local": "__flag__" + }, + "methods": [ + "GET" + ], + "patterns": [ + "_component_template", + "_component_template/{name}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-component-templates.html" } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_component_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_component_template.json new file mode 100644 index 0000000000000..fbd5a0905d4d8 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_component_template.json @@ -0,0 +1,14 @@ +{ + "indices.put_template": { + "url_params": { + "create": "__flag__", + "master_timeout": "" + }, + "methods": [ + "PUT" + ], + "patterns": [ + "_component_template/{name}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-component-template.html" } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.put_component_template.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.put_component_template.json new file mode 100644 index 0000000000000..7fdd9ff23fd17 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.put_component_template.json @@ -0,0 +1,13 @@ +{ + "indices.put_template": { + "data_autocomplete_rules": { + "template": {}, + "aliases": {}, + "settings": {}, + "mappings": {}, + "version": 0, + "_meta": {}, + "allow_auto_create": false + } + } +} From c561a18b1e61eb5a96b4f35a04d355c254fb71f6 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher <471693+Kerry350@users.noreply.github.com> Date: Thu, 25 Feb 2021 18:44:47 +0000 Subject: [PATCH 04/32] [Logs UI] Hide Create Alert option when user lacks privileges (#92000) * Hide Create Alerts option when lacking permissions --- .../components/alert_dropdown.tsx | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx index f13ce33a44b3d..7cd6295cdcf40 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx @@ -6,12 +6,34 @@ */ import React, { useState, useCallback, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { AlertFlyout } from './alert_flyout'; import { useLinkProps } from '../../../hooks/use_link_props'; +import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; + +const readOnlyUserTooltipContent = i18n.translate( + 'xpack.infra.logs.alertDropdown.readOnlyCreateAlertContent', + { + defaultMessage: 'Creating alerts requires more permissions in this application.', + } +); + +const readOnlyUserTooltipTitle = i18n.translate( + 'xpack.infra.logs.alertDropdown.readOnlyCreateAlertTitle', + { + defaultMessage: 'Read only', + } +); export const AlertDropdown = () => { + const { + services: { + application: { capabilities }, + }, + } = useKibanaContextForPlugin(); + const canCreateAlerts = capabilities?.logs?.save ?? false; const [popoverOpen, setPopoverOpen] = useState(false); const [flyoutVisible, setFlyoutVisible] = useState(false); const manageAlertsLinkProps = useLinkProps( @@ -34,7 +56,14 @@ export const AlertDropdown = () => { const menuItems = useMemo(() => { return [ - setFlyoutVisible(true)}> + setFlyoutVisible(true)} + toolTipContent={!canCreateAlerts ? readOnlyUserTooltipContent : undefined} + toolTipTitle={!canCreateAlerts ? readOnlyUserTooltipTitle : undefined} + > { /> , ]; - }, [manageAlertsLinkProps]); + }, [manageAlertsLinkProps, canCreateAlerts]); return ( <> From 83b22dc568dadb09eb2665bfc0464e4363b5e42e Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 25 Feb 2021 10:56:32 -0800 Subject: [PATCH 05/32] [Actions][Doc] Added user doc for default value for PagerDuty deduplication key. (#92746) * [Actions][Doc] Added user doc for default value for PagerDuty deduplication key. * Apply suggestions from code review Co-authored-by: Gidi Meir Morris Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: Gidi Meir Morris Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- docs/user/alerting/action-types/pagerduty.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/user/alerting/action-types/pagerduty.asciidoc b/docs/user/alerting/action-types/pagerduty.asciidoc index c3185aaad553a..cadf8e0b16a44 100644 --- a/docs/user/alerting/action-types/pagerduty.asciidoc +++ b/docs/user/alerting/action-types/pagerduty.asciidoc @@ -105,7 +105,7 @@ See <> for how to obtain the endpoint and + * The action’s type: Trigger, Resolve, or Acknowledge. * The event’s severity: Info, warning, error, or critical. -* An array of different fields, including the timestamp, group, class, component, and your dedup key. +* An array of different fields, including the timestamp, group, class, component, and your dedup key. By default, the dedup is configured to create a new PagerDuty incident for each alert instance and reuse the incident when a recovered alert instance reactivates. Depending on your custom needs, assign them variables from the alerting context. To see the available context variables, click on the *Add alert variable* icon next to each corresponding field. For more details on these parameters, see the @@ -179,7 +179,7 @@ PagerDuty actions have the following properties: Severity:: The perceived severity of on the affected system. This can be one of `Critical`, `Error`, `Warning` or `Info`(default). Event action:: One of `Trigger` (default), `Resolve`, or `Acknowledge`. See https://v2.developer.pagerduty.com/docs/events-api-v2#event-action[event action] for more details. -Dedup Key:: All actions sharing this key will be associated with the same PagerDuty alert. This value is used to correlate trigger and resolution. This value is *optional*, and if unset defaults to `action:`. The maximum length is *255* characters. See https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication[alert deduplication] for details. +Dedup Key:: All actions sharing this key will be associated with the same PagerDuty alert. This value is used to correlate trigger and resolution. This value is *optional*, and if not set, defaults to `:`. The maximum length is *255* characters. See https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication[alert deduplication] for details. Timestamp:: An *optional* https://v2.developer.pagerduty.com/v2/docs/types#datetime[ISO-8601 format date-time], indicating the time the event was detected or generated. Component:: An *optional* value indicating the component of the source machine that is responsible for the event, for example `mysql` or `eth0`. Group:: An *optional* value indicating the logical grouping of components of a service, for example `app-stack`. From 1f58bc2ebbe28744b9a250d82f03dbfd774b3a64 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 25 Feb 2021 11:59:02 -0700 Subject: [PATCH 06/32] [ts] disable forceConsistentCasingInFileNames, it seems broken (#92849) Co-authored-by: spalger --- tsconfig.base.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.base.json b/tsconfig.base.json index c63d43b4cb6ad..865806cffe5bb 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -39,7 +39,7 @@ // "resolveJsonModule" allows for importing, extracting types from and generating .json files. "resolveJsonModule": true, // Disallow inconsistently-cased references to the same file. - "forceConsistentCasingInFileNames": true, + "forceConsistentCasingInFileNames": false, // Forbid unused local variables as the rule was deprecated by ts-lint "noUnusedLocals": true, // Provide full support for iterables in for..of, spread and destructuring when targeting ES5 or ES3. From 272bf97e311b5a019b267aac74bfe26a767ff927 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Thu, 25 Feb 2021 14:17:47 -0500 Subject: [PATCH 07/32] [Discover] Fix persistence of "hide/show chart" in saved search (#92731) --- .../application/helpers/persist_saved_search.ts | 2 +- .../apps/discover/_discover_histogram.ts | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/plugins/discover/public/application/helpers/persist_saved_search.ts b/src/plugins/discover/public/application/helpers/persist_saved_search.ts index 2e4ab90ee58e5..f44d4650da56a 100644 --- a/src/plugins/discover/public/application/helpers/persist_saved_search.ts +++ b/src/plugins/discover/public/application/helpers/persist_saved_search.ts @@ -49,7 +49,7 @@ export async function persistSavedSearch( if (state.grid) { savedSearch.grid = state.grid; } - if (state.hideChart) { + if (typeof state.hideChart !== 'undefined') { savedSearch.hideChart = state.hideChart; } diff --git a/test/functional/apps/discover/_discover_histogram.ts b/test/functional/apps/discover/_discover_histogram.ts index 9a6692dc793d6..2a6096f8d1a78 100644 --- a/test/functional/apps/discover/_discover_histogram.ts +++ b/test/functional/apps/discover/_discover_histogram.ts @@ -97,15 +97,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(canvasExists).to.be(false); await PageObjects.discover.saveSearch(savedSearch); await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.discover.clickNewSearchButton(); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.discover.loadSavedSearch('persisted hidden histogram'); + await PageObjects.header.waitUntilLoadingHasFinished(); canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(false); await testSubjects.click('discoverChartToggle'); canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(true); - await PageObjects.discover.clickResetSavedSearchButton(); + await PageObjects.discover.saveSearch('persisted hidden histogram'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.discover.clickNewSearchButton(); + await PageObjects.discover.loadSavedSearch('persisted hidden histogram'); await PageObjects.header.waitUntilLoadingHasFinished(); canvasExists = await elasticChart.canvasExists(); - expect(canvasExists).to.be(false); + expect(canvasExists).to.be(true); }); }); } From fa4dda0defb99939bd7aba00a4427f2775bbf06a Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Thu, 25 Feb 2021 14:26:34 -0500 Subject: [PATCH 08/32] [Metrics UI] use global kibana time for metrics explorer in Default View (#92520) * use global kibana time for explorer, stop using local storage for time * fix inventory time --- .../metrics/inventory_view/hooks/use_waffle_view_state.ts | 5 ++++- .../metrics_explorer/hooks/use_metric_explorer_state.ts | 8 +++++++- .../hooks/use_metrics_explorer_options.test.tsx | 2 -- .../hooks/use_metrics_explorer_options.ts | 5 +---- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_view_state.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_view_state.ts index 3a0bc009bcfd0..3fa6bc065e7c1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_view_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_view_state.ts @@ -78,7 +78,9 @@ export const useWaffleViewState = () => { region: newState.region, legend: newState.legend, }); - if (newState.time) { + // if this is the "Default View" view, don't update the time range to the view's time range, + // this way it will use the global Kibana time or the default time already set + if (newState.time && newState.id !== '0') { setWaffleTimeState({ currentTime: newState.time, isAutoReloading: newState.autoReload, @@ -100,4 +102,5 @@ export type WaffleViewState = WaffleOptionsState & { time: number; autoReload: boolean; filterQuery: WaffleFiltersState; + id?: string; }; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts index 424e456aa9dd8..eb5a4633d4fa9 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts @@ -24,6 +24,7 @@ export interface MetricExplorerViewState { chartOptions: MetricsExplorerChartOptions; currentTimerange: MetricsExplorerTimeOptions; options: MetricsExplorerOptions; + id?: string; } export const useMetricsExplorerState = ( @@ -42,6 +43,7 @@ export const useMetricsExplorerState = ( setTimeRange, setOptions, } = useContext(MetricsExplorerOptionsContainer.Context); + const { loading, error, data, loadData } = useMetricsExplorerData( options, source, @@ -121,7 +123,11 @@ export const useMetricsExplorerState = ( setChartOptions(vs.chartOptions); } if (vs.currentTimerange) { - setTimeRange(vs.currentTimerange); + // if this is the "Default View" view, don't update the time range to the view's time range, + // this way it will use the global Kibana time or the default time already set + if (vs.id !== '0') { + setTimeRange(vs.currentTimerange); + } } if (vs.options) { setOptions(vs.options); diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx index 5f182203dd86c..37200f75d109c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx @@ -68,7 +68,6 @@ describe('useMetricExplorerOptions', () => { expect(result.current.currentTimerange).toEqual(DEFAULT_TIMERANGE); expect(result.current.isAutoReloading).toEqual(false); expect(STORE.MetricsExplorerOptions).toEqual(JSON.stringify(DEFAULT_OPTIONS)); - expect(STORE.MetricsExplorerTimeRange).toEqual(JSON.stringify(DEFAULT_TIMERANGE)); }); it('should change the store when options update', () => { @@ -96,7 +95,6 @@ describe('useMetricExplorerOptions', () => { }); rerender(); expect(result.current.currentTimerange).toEqual(newTimeRange); - expect(STORE.MetricsExplorerTimeRange).toEqual(JSON.stringify(newTimeRange)); }); it('should load from store when available', () => { diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts index 06140dd976691..c1e5be94acc03 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts @@ -163,10 +163,7 @@ export const useMetricsExplorerOptions = () => { 'MetricsExplorerOptions', DEFAULT_OPTIONS ); - const [currentTimerange, setTimeRange] = useStateWithLocalStorage( - 'MetricsExplorerTimeRange', - defaultTimeRange - ); + const [currentTimerange, setTimeRange] = useState(defaultTimeRange); useSyncKibanaTimeFilterTime(TIME_DEFAULTS, { from: currentTimerange.from, From 93066631159e3ec6c40c5d5d49702d60fe8e5d02 Mon Sep 17 00:00:00 2001 From: Pete Hampton Date: Thu, 25 Feb 2021 20:15:09 +0000 Subject: [PATCH 09/32] [7.12][Telemetry] Security telemetry allowlist fix. (#92850) * Security telemetry allowlist fix. * Also add process.thread. --- .../server/lib/telemetry/sender.ts | 115 +++++++++--------- 1 file changed, 58 insertions(+), 57 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts index e153c6d42225f..6ce42eabeca5e 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -364,74 +364,75 @@ const allowlistEventFields: AllowlistFields = { pid: true, ppid: true, }, - Target: { - process: { - Ext: { - architecture: true, - code_signature: true, - dll: true, - token: { - integrity_level_name: true, - }, + token: { + integrity_level_name: true, + }, + thread: true, + }, + Target: { + process: { + Ext: { + architecture: true, + code_signature: true, + dll: true, + token: { + integrity_level_name: true, }, - parent: { - process: { - Ext: { - architecture: true, - code_signature: true, - dll: true, - token: { - integrity_level_name: true, - }, + }, + parent: { + process: { + Ext: { + architecture: true, + code_signature: true, + dll: true, + token: { + integrity_level_name: true, }, }, }, - thread: { - Ext: { - call_stack: true, - start_address: true, - start_address_details: { - address_offset: true, - allocation_base: true, - allocation_protection: true, - allocation_size: true, - allocation_type: true, - base_address: true, - bytes_start_address: true, - compressed_bytes: true, - dest_bytes: true, - dest_bytes_disasm: true, - dest_bytes_disasm_hash: true, - pe: { - Ext: { - legal_copyright: true, - product_version: true, - code_signature: { - status: true, - subject_name: true, - trusted: true, - }, + }, + thread: { + Ext: { + call_stack: true, + start_address: true, + start_address_details: { + address_offset: true, + allocation_base: true, + allocation_protection: true, + allocation_size: true, + allocation_type: true, + base_address: true, + bytes_start_address: true, + compressed_bytes: true, + dest_bytes: true, + dest_bytes_disasm: true, + dest_bytes_disasm_hash: true, + pe: { + Ext: { + legal_copyright: true, + product_version: true, + code_signature: { + status: true, + subject_name: true, + trusted: true, }, - company: true, - description: true, - file_version: true, - imphash: true, - original_file_name: true, - product: true, }, - pe_detected: true, - region_protection: true, - region_size: true, - region_state: true, - strings: true, + company: true, + description: true, + file_version: true, + imphash: true, + original_file_name: true, + product: true, }, + pe_detected: true, + region_protection: true, + region_size: true, + region_state: true, + strings: true, }, }, }, }, - token: { - integrity_level_name: true, - }, }, }; From 0627573dbd3be2368dbe37c9c7e7a86e4783589c Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 25 Feb 2021 12:41:48 -0800 Subject: [PATCH 10/32] [Alerts][Docs] Extended README.md and the user docs with the licensing information. (#92564) * [Alerts][Docs] Extended README.md and the user docs with the licensing information. * Apply suggestions from code review Co-authored-by: Lisa Cawley Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * fixed due to comments Co-authored-by: Lisa Cawley Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- docs/user/alerting/alert-types.asciidoc | 8 ++++++++ x-pack/plugins/alerts/README.md | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/docs/user/alerting/alert-types.asciidoc b/docs/user/alerting/alert-types.asciidoc index 993d815c37f71..5983804c5c862 100644 --- a/docs/user/alerting/alert-types.asciidoc +++ b/docs/user/alerting/alert-types.asciidoc @@ -30,6 +30,14 @@ For domain-specific alerts, refer to the documentation for that app. * <> * <> +[NOTE] +============================================== +Some alert types are subscription features, while others are free features. +For a comparison of the Elastic subscription levels, +see {subscriptions}[the subscription page]. +============================================== + + include::stack-alerts/index-threshold.asciidoc[] include::stack-alerts/es-query.asciidoc[] include::maps-alerts/geo-alert-types.asciidoc[] diff --git a/x-pack/plugins/alerts/README.md b/x-pack/plugins/alerts/README.md index aab848d4555d2..83a1ff952cb5d 100644 --- a/x-pack/plugins/alerts/README.md +++ b/x-pack/plugins/alerts/README.md @@ -17,6 +17,9 @@ Table of Contents - [Alert types](#alert-types) - [Methods](#methods) - [Executor](#executor) + - [Licensing](#licensing) + - [Documentation](#documentation) + - [Tests](#tests) - [Example](#example) - [Role Based Access-Control](#role-based-access-control) - [Alert Navigation](#alert-navigation) @@ -124,6 +127,19 @@ For example, if the `context` has one variable `foo` which is an object that has } ``` +## Licensing + +Currently most of the alerts are free features. But some alert types are subscription features, such as the tracking containment alert. + +## Documentation + +You should create documentation for the new alert type. Make an entry in the alert type index [`docs/user/alerting/alert-types.asciidoc`](../../../docs/user/alerting/alert-types.asciidoc) that points to a new document for the alert type that should be in the proper application directory. + +## Tests + +The alert type should have jest tests and optionaly functional tests. +In the the tests we recomend to test the expected alert execution result with a different input params, the structure of the created alert and the params validation. The rest will be guaranteed as a framework functionality. + ### Example This example receives server and threshold as parameters. It will read the CPU usage of the server and schedule actions to be executed (asynchronously by the task manager) if the reading is greater than the threshold. From 3894ecf8629c87f8d17190ea47780fccd003a806 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Thu, 25 Feb 2021 21:45:14 +0100 Subject: [PATCH 11/32] [Discover][docs] Add search for relevance (#90611) Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: Lisa Cawley --- .../images/discover-search-for-relevance.png | Bin 0 -> 282007 bytes docs/discover/search-for-relevance.asciidoc | 24 ++++++++++++++++++ docs/user/discover.asciidoc | 5 +++- 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 docs/discover/images/discover-search-for-relevance.png create mode 100644 docs/discover/search-for-relevance.asciidoc diff --git a/docs/discover/images/discover-search-for-relevance.png b/docs/discover/images/discover-search-for-relevance.png new file mode 100644 index 0000000000000000000000000000000000000000..4d59ad186ded42d4dfede73a9a80fa974e02fe89 GIT binary patch literal 282007 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, and you're a searching for +a flight that arrived or departed from `Warsaw` or `Venice` when the weather was clear. + +. In *Discover*, open the index pattern dropdown, and select `kibana_sample_data_flight`. +. In the query bar, click *KQL*, and switch to the <>. +. Search for `Warsaw OR Venice OR Clear`. +. If you don't see any results, open the time filter and select a time range that contains data. +. From the list of *Available fields*, add `_score` to the document table. +. In the document table, click the header for the `_score` column, and then sort the column by descending scores. ++ +The results are currently sorted by first `Time`, and then by `_score`. +. To sort only by `_score`, remove the `Time` field. ++ +Your table now shows documents with the best matches, from most to least relevant. ++ +[role="screenshot"] +image::images/discover-search-for-relevance.png["Example of a search for relevance"] diff --git a/docs/user/discover.asciidoc b/docs/user/discover.asciidoc index a08713421a7fb..42ac1e22ce167 100644 --- a/docs/user/discover.asciidoc +++ b/docs/user/discover.asciidoc @@ -198,7 +198,6 @@ image:images/visualize-from-discover.png[Visualization that opens from Discover [float] === What’s next? - * <>. * <> to better meet your needs. @@ -209,6 +208,8 @@ the table columns that display by default, and more. * <>. +* <>. + -- include::{kib-repo-dir}/management/index-patterns.asciidoc[] @@ -216,3 +217,5 @@ include::{kib-repo-dir}/management/index-patterns.asciidoc[] include::{kib-repo-dir}/discover/set-time-filter.asciidoc[] include::{kib-repo-dir}/discover/search.asciidoc[] + +include::{kib-repo-dir}/discover/search-for-relevance.asciidoc[] From ad19f821d6fc7ebda203086c8d8cd001adb00762 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 25 Feb 2021 12:49:00 -0800 Subject: [PATCH 12/32] [Alerts][Doc] Added README documentation for API key invalidation configuration options. (#92757) * [Alerts][Doc] Added README documentation for API key invalidation configuration options. * Apply suggestions from code review Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- x-pack/plugins/alerts/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/x-pack/plugins/alerts/README.md b/x-pack/plugins/alerts/README.md index 83a1ff952cb5d..c57216603665d 100644 --- a/x-pack/plugins/alerts/README.md +++ b/x-pack/plugins/alerts/README.md @@ -13,6 +13,7 @@ Table of Contents - [Kibana alerting](#kibana-alerting) - [Terminology](#terminology) - [Usage](#usage) + - [Alerts API keys](#alerts-api-keys) - [Limitations](#limitations) - [Alert types](#alert-types) - [Methods](#methods) @@ -53,6 +54,17 @@ A Kibana alert detects a condition and executes one or more actions when that co 2. Configure feature level privileges using RBAC 3. Create an alert using the RESTful API [Documentation](https://www.elastic.co/guide/en/kibana/master/alerts-api-update.html) (see alerts -> create). +## Alerts API keys + +When we create an alert, we generate a new API key. + +When we update, enable, or disable an alert, we must invalidate the old API key and create a new one. + +To manage the invalidation process for API keys, we use the saved object `api_key_pending_invalidation`. This object stores all API keys that were marked for invalidation when alerts were updated. +For security plugin invalidation, we schedule a task to check if the`api_key_pending_invalidation` saved object contains new API keys that are marked for invalidation earlier than the configured delay. The default value for running the task is 5 mins. +To change the schedule for the invalidation task, use the kibana.yml configuration option `xpack.alerts.invalidateApiKeysTask.interval`. +To change the default delay for the API key invalidation, use the kibana.yml configuration option `xpack.alerts.invalidateApiKeysTask.removalDelay`. + ## Limitations When security is enabled, an SSL connection to Elasticsearch is required in order to use alerting. From cb605845716b1f3a67f62f0568ab40e2e30d625a Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Thu, 25 Feb 2021 16:42:50 -0500 Subject: [PATCH 13/32] [Fleet] Add new index to fleet for artifacts being served out of fleet-server (#92860) * Added index definition for artifacts --- .../plugins/fleet/common/constants/index.ts | 1 + .../fleet_server/elastic_index.test.ts | 53 +++++++------------ .../services/fleet_server/elastic_index.ts | 2 + .../elasticsearch/fleet_artifacts.json | 47 ++++++++++++++++ 4 files changed, 68 insertions(+), 35 deletions(-) create mode 100644 x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_artifacts.json diff --git a/x-pack/plugins/fleet/common/constants/index.ts b/x-pack/plugins/fleet/common/constants/index.ts index d95bc9cf736a6..abf6b3e1cbbd7 100644 --- a/x-pack/plugins/fleet/common/constants/index.ts +++ b/x-pack/plugins/fleet/common/constants/index.ts @@ -27,6 +27,7 @@ export const FLEET_SERVER_INDICES_VERSION = 1; export const FLEET_SERVER_INDICES = [ '.fleet-actions', '.fleet-agents', + '.fleet-artifacts', '.fleet-enrollment-api-keys', '.fleet-policies', '.fleet-policies-leader', diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.test.ts b/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.test.ts index 96e642ba9884e..310db24b8184d 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.test.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.test.ts @@ -14,42 +14,41 @@ import ESFleetPoliciesLeaderIndex from './elasticsearch/fleet_policies_leader.js import ESFleetServersIndex from './elasticsearch/fleet_servers.json'; import ESFleetEnrollmentApiKeysIndex from './elasticsearch/fleet_enrollment_api_keys.json'; import EsFleetActionsIndex from './elasticsearch/fleet_actions.json'; +import EsFleetArtifactsIndex from './elasticsearch/fleet_artifacts.json'; +import { FLEET_SERVER_INDICES } from '../../../common'; -const FLEET_INDEXES_MIGRATION_HASH = { +const FLEET_INDEXES_MIGRATION_HASH: Record = { '.fleet-actions': hash(EsFleetActionsIndex), '.fleet-agents': hash(ESFleetAgentIndex), + '.fleet-artifacts': hash(EsFleetArtifactsIndex), '.fleet-enrollment-apy-keys': hash(ESFleetEnrollmentApiKeysIndex), '.fleet-policies': hash(ESFleetPoliciesIndex), '.fleet-policies-leader': hash(ESFleetPoliciesLeaderIndex), '.fleet-servers': hash(ESFleetServersIndex), }; +const getIndexList = (returnAliases: boolean = false): string[] => { + const response = [...FLEET_SERVER_INDICES]; + + if (returnAliases) { + return response.sort(); + } + + return response.map((index) => `${index}_1`).sort(); +}; + describe('setupFleetServerIndexes ', () => { it('should create all the indices and aliases if nothings exists', async () => { const esMock = elasticsearchServiceMock.createInternalClient(); await setupFleetServerIndexes(esMock); const indexesCreated = esMock.indices.create.mock.calls.map((call) => call[0].index).sort(); - expect(indexesCreated).toEqual([ - '.fleet-actions_1', - '.fleet-agents_1', - '.fleet-enrollment-api-keys_1', - '.fleet-policies-leader_1', - '.fleet-policies_1', - '.fleet-servers_1', - ]); + expect(indexesCreated).toEqual(getIndexList()); const aliasesCreated = esMock.indices.updateAliases.mock.calls .map((call) => (call[0].body as any)?.actions[0].add.alias) .sort(); - expect(aliasesCreated).toEqual([ - '.fleet-actions', - '.fleet-agents', - '.fleet-enrollment-api-keys', - '.fleet-policies', - '.fleet-policies-leader', - '.fleet-servers', - ]); + expect(aliasesCreated).toEqual(getIndexList(true)); }); it('should not create any indices and create aliases if indices exists but not the aliases', async () => { @@ -63,7 +62,6 @@ describe('setupFleetServerIndexes ', () => { [params.index]: { mappings: { _meta: { - // @ts-expect-error migrationHash: FLEET_INDEXES_MIGRATION_HASH[params.index.replace(/_1$/, '')], }, }, @@ -79,14 +77,7 @@ describe('setupFleetServerIndexes ', () => { .map((call) => (call[0].body as any)?.actions[0].add.alias) .sort(); - expect(aliasesCreated).toEqual([ - '.fleet-actions', - '.fleet-agents', - '.fleet-enrollment-api-keys', - '.fleet-policies', - '.fleet-policies-leader', - '.fleet-servers', - ]); + expect(aliasesCreated).toEqual(getIndexList(true)); }); it('should put new indices mapping if the mapping has been updated ', async () => { @@ -115,14 +106,7 @@ describe('setupFleetServerIndexes ', () => { .map((call) => call[0].index) .sort(); - expect(indexesMappingUpdated).toEqual([ - '.fleet-actions_1', - '.fleet-agents_1', - '.fleet-enrollment-api-keys_1', - '.fleet-policies-leader_1', - '.fleet-policies_1', - '.fleet-servers_1', - ]); + expect(indexesMappingUpdated).toEqual(getIndexList()); }); it('should not create any indices or aliases if indices and aliases already exists', async () => { @@ -137,7 +121,6 @@ describe('setupFleetServerIndexes ', () => { [params.index]: { mappings: { _meta: { - // @ts-expect-error migrationHash: FLEET_INDEXES_MIGRATION_HASH[params.index.replace(/_1$/, '')], }, }, diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts b/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts index 15672be756fe2..4b85f753740e3 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts @@ -16,10 +16,12 @@ import ESFleetPoliciesLeaderIndex from './elasticsearch/fleet_policies_leader.js import ESFleetServersIndex from './elasticsearch/fleet_servers.json'; import ESFleetEnrollmentApiKeysIndex from './elasticsearch/fleet_enrollment_api_keys.json'; import EsFleetActionsIndex from './elasticsearch/fleet_actions.json'; +import EsFleetArtifactsIndex from './elasticsearch/fleet_artifacts.json'; const FLEET_INDEXES: Array<[typeof FLEET_SERVER_INDICES[number], any]> = [ ['.fleet-actions', EsFleetActionsIndex], ['.fleet-agents', ESFleetAgentIndex], + ['.fleet-artifacts', EsFleetArtifactsIndex], ['.fleet-enrollment-api-keys', ESFleetEnrollmentApiKeysIndex], ['.fleet-policies', ESFleetPoliciesIndex], ['.fleet-policies-leader', ESFleetPoliciesLeaderIndex], diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_artifacts.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_artifacts.json new file mode 100644 index 0000000000000..01a2c82b71861 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_artifacts.json @@ -0,0 +1,47 @@ +{ + "settings": {}, + "mappings": { + "dynamic": false, + "properties": { + "identifier": { + "type": "keyword" + }, + "compressionAlgorithm": { + "type": "keyword", + "index": false + }, + "encryptionAlgorithm": { + "type": "keyword", + "index": false + }, + "encodedSha256": { + "type": "keyword" + }, + "encodedSize": { + "type": "long", + "index": false + }, + "decodedSha256": { + "type": "keyword", + "index": false + }, + "decodedSize": { + "type": "long", + "index": false + }, + "created": { + "type": "date", + "index": false + }, + "packageName": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "body": { + "type": "binary" + } + } + } +} From fd348d3f827e2b8f6831e89f3d65cf0665768607 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Thu, 25 Feb 2021 14:47:04 -0700 Subject: [PATCH 14/32] [Reporting/Discover] include the document's entire set of fields (#92730) * fix get_sharing_data to provide a list of fields if none are selected as columns in the saved search * add logging for download CSV that fields must be selected as columns * add more functional test coverage * fix ts in test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../application/helpers/get_sharing_data.ts | 5 +- .../csv_from_savedobject/execute_job.ts | 12 ++- .../lib/get_csv_job.test.ts | 18 +++-- .../csv_from_savedobject/lib/get_csv_job.ts | 12 ++- .../discover/__snapshots__/reporting.snap | 40 ++++++++++ .../functional/apps/discover/reporting.ts | 77 ++++++++++++++++++- .../functional/page_objects/reporting_page.ts | 4 +- 7 files changed, 152 insertions(+), 16 deletions(-) create mode 100644 x-pack/test/functional/apps/discover/__snapshots__/reporting.snap diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.ts b/src/plugins/discover/public/application/helpers/get_sharing_data.ts index 31de1f2f6ed66..2455589cf69fc 100644 --- a/src/plugins/discover/public/application/helpers/get_sharing_data.ts +++ b/src/plugins/discover/public/application/helpers/get_sharing_data.ts @@ -19,7 +19,10 @@ const getSharingDataFields = async ( timeFieldName: string, hideTimeColumn: boolean ) => { - if (selectedFields.length === 1 && selectedFields[0] === '_source') { + if ( + selectedFields.length === 0 || + (selectedFields.length === 1 && selectedFields[0] === '_source') + ) { const fieldCounts = await getFieldCounts(); return { searchFields: undefined, diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts index d494a8b529d2c..b037e72699dd6 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts @@ -35,13 +35,19 @@ export const runTaskFnFactory: RunTaskFnFactory = function e return async function runTask(jobId, jobPayload, context, req) { const generateCsv = createGenerateCsv(logger); - const { panel, visType } = jobPayload; + const { panel } = jobPayload; - logger.debug(`Execute job generating [${visType}] csv`); + logger.debug(`Execute job generating saved search CSV`); const savedObjectsClient = context.core.savedObjects.client; const uiSettingsClient = await reporting.getUiSettingsServiceFactory(savedObjectsClient); - const job = await getGenerateCsvParams(jobPayload, panel, savedObjectsClient, uiSettingsClient); + const job = await getGenerateCsvParams( + jobPayload, + panel, + savedObjectsClient, + uiSettingsClient, + logger + ); const elasticsearch = reporting.getElasticsearchService(); const { callAsCurrentUser } = elasticsearch.legacy.client.asScoped(req); diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts index 8c189537ad5dd..fc6e092962d3b 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts @@ -5,9 +5,12 @@ * 2.0. */ +import { createMockLevelLogger } from '../../../test_helpers'; import { JobParamsPanelCsv, SearchPanel } from '../types'; import { getGenerateCsvParams } from './get_csv_job'; +const logger = createMockLevelLogger(); + describe('Get CSV Job', () => { let mockJobParams: JobParamsPanelCsv; let mockSearchPanel: SearchPanel; @@ -42,7 +45,8 @@ describe('Get CSV Job', () => { mockJobParams, mockSearchPanel, mockSavedObjectsClient, - mockUiSettingsClient + mockUiSettingsClient, + logger ); expect(result).toMatchInlineSnapshot(` Object { @@ -94,7 +98,8 @@ describe('Get CSV Job', () => { mockJobParams, mockSearchPanel, mockSavedObjectsClient, - mockUiSettingsClient + mockUiSettingsClient, + logger ); expect(result).toMatchInlineSnapshot(` Object { @@ -149,7 +154,8 @@ describe('Get CSV Job', () => { mockJobParams, mockSearchPanel, mockSavedObjectsClient, - mockUiSettingsClient + mockUiSettingsClient, + logger ); expect(result).toMatchInlineSnapshot(` Object { @@ -203,7 +209,8 @@ describe('Get CSV Job', () => { mockJobParams, mockSearchPanel, mockSavedObjectsClient, - mockUiSettingsClient + mockUiSettingsClient, + logger ); expect(result).toMatchInlineSnapshot(` Object { @@ -275,7 +282,8 @@ describe('Get CSV Job', () => { mockJobParams, mockSearchPanel, mockSavedObjectsClient, - mockUiSettingsClient + mockUiSettingsClient, + logger ); expect(result).toMatchInlineSnapshot(` Object { diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts index b4846ee3b4236..e4570816e26ff 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts @@ -8,6 +8,7 @@ import { IUiSettingsClient, SavedObjectsClientContract } from 'kibana/server'; import { EsQueryConfig } from 'src/plugins/data/server'; import { esQuery, Filter, Query } from '../../../../../../../src/plugins/data/server'; +import { LevelLogger } from '../../../lib'; import { TimeRangeParams } from '../../common'; import { GenerateCsvParams } from '../../csv/generate_csv'; import { @@ -44,7 +45,8 @@ export const getGenerateCsvParams = async ( jobParams: JobParamsPanelCsv, panel: SearchPanel, savedObjectsClient: SavedObjectsClientContract, - uiConfig: IUiSettingsClient + uiConfig: IUiSettingsClient, + logger: LevelLogger ): Promise => { let timerange: TimeRangeParams | null; if (jobParams.post?.timerange) { @@ -75,6 +77,14 @@ export const getGenerateCsvParams = async ( fields: indexPatternFields, } = indexPatternSavedObject; + if (!indexPatternFields || indexPatternFields.length === 0) { + logger.error( + new Error( + `No fields are selected in the saved search! Please select fields as columns in the saved search and try again.` + ) + ); + } + let payloadQuery: QueryFilter | undefined; let payloadSort: any[] = []; let docValueFields: DocValueFields[] | undefined; diff --git a/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap b/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap new file mode 100644 index 0000000000000..43771b00525cc --- /dev/null +++ b/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`discover Discover Generate CSV: archived search generates a report with data 1`] = ` +"\\"order_date\\",category,currency,\\"customer_id\\",\\"order_id\\",\\"day_of_week_i\\",\\"order_date\\",\\"products.created_on\\",sku +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Men's Shoes\\"\\",\\"\\"Men's Clothing\\"\\",\\"\\"Women's Accessories\\"\\",\\"\\"Men's Accessories\\"\\"]\\",EUR,19,716724,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0687606876\\"\\",\\"\\"ZO0290502905\\"\\",\\"\\"ZO0126701267\\"\\",\\"\\"ZO0308503085\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Women's Shoes\\"\\",\\"\\"Women's Clothing\\"\\"]\\",EUR,45,591503,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0006400064\\"\\",\\"\\"ZO0150601506\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,12,591709,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0638206382\\"\\",\\"\\"ZO0038800388\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,52,590937,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0297602976\\"\\",\\"\\"ZO0565605656\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,29,590976,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0561405614\\"\\",\\"\\"ZO0281602816\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Men's Shoes\\"\\",\\"\\"Men's Clothing\\"\\"]\\",EUR,41,591636,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0385003850\\"\\",\\"\\"ZO0408604086\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes\\",EUR,30,591539,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0505605056\\"\\",\\"\\"ZO0513605136\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,41,591598,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0276702767\\"\\",\\"\\"ZO0291702917\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,44,590927,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0046600466\\"\\",\\"\\"ZO0050800508\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Men's Clothing\\"\\",\\"\\"Men's Shoes\\"\\"]\\",EUR,48,590970,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0455604556\\"\\",\\"\\"ZO0680806808\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Women's Clothing\\"\\",\\"\\"Women's Shoes\\"\\"]\\",EUR,46,591299,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0229002290\\"\\",\\"\\"ZO0674406744\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,36,591133,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0529905299\\"\\",\\"\\"ZO0617006170\\"\\"]\\" +" +`; + +exports[`discover Discover Generate CSV: archived search generates a report with filtered data 1`] = ` +"\\"order_date\\",category,currency,\\"customer_id\\",\\"order_id\\",\\"day_of_week_i\\",\\"order_date\\",\\"products.created_on\\",sku +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Men's Shoes\\"\\",\\"\\"Men's Clothing\\"\\",\\"\\"Women's Accessories\\"\\",\\"\\"Men's Accessories\\"\\"]\\",EUR,19,716724,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0687606876\\"\\",\\"\\"ZO0290502905\\"\\",\\"\\"ZO0126701267\\"\\",\\"\\"ZO0308503085\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Women's Shoes\\"\\",\\"\\"Women's Clothing\\"\\"]\\",EUR,45,591503,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0006400064\\"\\",\\"\\"ZO0150601506\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,12,591709,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0638206382\\"\\",\\"\\"ZO0038800388\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,52,590937,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0297602976\\"\\",\\"\\"ZO0565605656\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,29,590976,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0561405614\\"\\",\\"\\"ZO0281602816\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Men's Shoes\\"\\",\\"\\"Men's Clothing\\"\\"]\\",EUR,41,591636,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0385003850\\"\\",\\"\\"ZO0408604086\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes\\",EUR,30,591539,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0505605056\\"\\",\\"\\"ZO0513605136\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,41,591598,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0276702767\\"\\",\\"\\"ZO0291702917\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,44,590927,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0046600466\\"\\",\\"\\"ZO0050800508\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Men's Clothing\\"\\",\\"\\"Men's Shoes\\"\\"]\\",EUR,48,590970,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0455604556\\"\\",\\"\\"ZO0680806808\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Women's Clothing\\"\\",\\"\\"Women's Shoes\\"\\"]\\",EUR,46,591299,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0229002290\\"\\",\\"\\"ZO0674406744\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,36,591133,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0529905299\\"\\",\\"\\"ZO0617006170\\"\\"]\\" +" +`; + +exports[`discover Discover Generate CSV: new search generates a report with data 1`] = ` +"\\"_id\\",\\"_index\\",\\"_score\\",\\"_type\\",category,\\"category.keyword\\",currency,\\"customer_first_name\\",\\"customer_first_name.keyword\\",\\"customer_full_name\\",\\"customer_full_name.keyword\\",\\"customer_gender\\",\\"customer_id\\",\\"customer_last_name\\",\\"customer_last_name.keyword\\",\\"customer_phone\\",\\"day_of_week\\",\\"day_of_week_i\\",email,\\"geoip.city_name\\",\\"geoip.continent_name\\",\\"geoip.country_iso_code\\",\\"geoip.location\\",\\"geoip.region_name\\",manufacturer,\\"manufacturer.keyword\\",\\"order_date\\",\\"order_id\\",\\"products._id\\",\\"products._id.keyword\\",\\"products.base_price\\",\\"products.base_unit_price\\",\\"products.category\\",\\"products.category.keyword\\",\\"products.created_on\\",\\"products.discount_amount\\",\\"products.discount_percentage\\",\\"products.manufacturer\\",\\"products.manufacturer.keyword\\",\\"products.min_price\\",\\"products.price\\",\\"products.product_id\\",\\"products.product_name\\",\\"products.product_name.keyword\\",\\"products.quantity\\",\\"products.sku\\",\\"products.tax_amount\\",\\"products.taxful_price\\",\\"products.taxless_price\\",\\"products.unit_discount_amount\\",sku,\\"taxful_total_price\\",\\"taxless_total_price\\",\\"total_quantity\\",\\"total_unique_products\\",type,user +" +`; diff --git a/x-pack/test/functional/apps/discover/reporting.ts b/x-pack/test/functional/apps/discover/reporting.ts index a0f10774e7fa6..dfc44a8e0e12d 100644 --- a/x-pack/test/functional/apps/discover/reporting.ts +++ b/x-pack/test/functional/apps/discover/reporting.ts @@ -13,7 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const es = getService('es'); const esArchiver = getService('esArchiver'); const browser = getService('browser'); - const PageObjects = getPageObjects(['reporting', 'common', 'discover']); + const PageObjects = getPageObjects(['reporting', 'common', 'discover', 'timePicker']); const filterBar = getService('filterBar'); describe('Discover', () => { @@ -31,7 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe('Generate CSV button', () => { + describe('Generate CSV: new search', () => { beforeEach(() => PageObjects.common.navigateToApp('discover')); it('is not available if new', async () => { @@ -69,14 +69,83 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.reporting.setTimepickerInDataRange(); await PageObjects.discover.saveSearch('my search - with data - expectReportCanBeCreated'); await PageObjects.reporting.openCsvReportingPanel(); - expect(await PageObjects.reporting.canReportBeCreated()).to.be(true); + await PageObjects.reporting.clickGenerateReportButton(); + + const url = await PageObjects.reporting.getReportURL(60000); + const res = await PageObjects.reporting.getResponse(url); + + expect(res.status).to.equal(200); + expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); + expectSnapshot(res.text).toMatch(); }); it('generates a report with no data', async () => { await PageObjects.reporting.setTimepickerInNoDataRange(); await PageObjects.discover.saveSearch('my search - no data - expectReportCanBeCreated'); await PageObjects.reporting.openCsvReportingPanel(); - expect(await PageObjects.reporting.canReportBeCreated()).to.be(true); + await PageObjects.reporting.clickGenerateReportButton(); + + const url = await PageObjects.reporting.getReportURL(60000); + const res = await PageObjects.reporting.getResponse(url); + + expect(res.status).to.equal(200); + expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); + expectSnapshot(res.text).toMatchInline(` + " + " + `); + }); + }); + + describe('Generate CSV: archived search', () => { + before(async () => { + await esArchiver.load('reporting/ecommerce'); + await esArchiver.load('reporting/ecommerce_kibana'); + }); + + after(async () => { + await esArchiver.unload('reporting/ecommerce'); + await esArchiver.unload('reporting/ecommerce_kibana'); + }); + + beforeEach(() => PageObjects.common.navigateToApp('discover')); + + it('generates a report with data', async () => { + await PageObjects.discover.loadSavedSearch('Ecommerce Data'); + const fromTime = 'Apr 27, 2019 @ 23:56:51.374'; + const toTime = 'Aug 23, 2019 @ 16:18:51.821'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + + await PageObjects.reporting.openCsvReportingPanel(); + await PageObjects.reporting.clickGenerateReportButton(); + + const url = await PageObjects.reporting.getReportURL(60000); + const res = await PageObjects.reporting.getResponse(url); + + expect(res.status).to.equal(200); + expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); + expectSnapshot(res.text).toMatch(); + }); + + it('generates a report with filtered data', async () => { + await PageObjects.discover.loadSavedSearch('Ecommerce Data'); + const fromTime = 'Apr 27, 2019 @ 23:56:51.374'; + const toTime = 'Aug 23, 2019 @ 16:18:51.821'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + + // filter and re-save + await filterBar.addFilter('currency', 'is', 'EUR'); + await PageObjects.discover.saveSearch(`Ecommerce Data: EUR Filtered`); + + await PageObjects.reporting.openCsvReportingPanel(); + await PageObjects.reporting.clickGenerateReportButton(); + + const url = await PageObjects.reporting.getReportURL(60000); + const res = await PageObjects.reporting.getResponse(url); + + expect(res.status).to.equal(200); + expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); + expectSnapshot(res.text).toMatch(); }); }); }); diff --git a/x-pack/test/functional/page_objects/reporting_page.ts b/x-pack/test/functional/page_objects/reporting_page.ts index 7d33680f1dc31..746df14d31ac4 100644 --- a/x-pack/test/functional/page_objects/reporting_page.ts +++ b/x-pack/test/functional/page_objects/reporting_page.ts @@ -140,8 +140,8 @@ export function ReportingPageProvider({ getService, getPageObjects }: FtrProvide async setTimepickerInDataRange() { log.debug('Reporting:setTimepickerInDataRange'); - const fromTime = 'Sep 19, 2015 @ 06:31:44.000'; - const toTime = 'Sep 19, 2015 @ 18:01:44.000'; + const fromTime = 'Apr 27, 2019 @ 23:56:51.374'; + const toTime = 'Aug 23, 2019 @ 16:18:51.821'; await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); } From 0198607eb3a972801800d119cb36e8c0c6c72b9f Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 25 Feb 2021 14:07:05 -0800 Subject: [PATCH 15/32] [App Search] Create Curation view/functionality (#92560) * Add server route and logic listener * [Misc] Remove 'Set' from 'deleteCurationSet' - to match createCuration - IMO, this language isn't necessary if we're splitting up Curations and CurationLogic - the context is fairly evident within a smaller and more modular logic file * Add CurationQueries component + accompanying CurationQueriesLogic & CurationQuery row * Add CurationCreation view --- .../curation_queries/curation_queries.scss | 3 + .../curation_queries.test.tsx | 102 ++++++++++++++++++ .../curation_queries/curation_queries.tsx | 72 +++++++++++++ .../curation_queries_logic.test.ts | 98 +++++++++++++++++ .../curation_queries_logic.ts | 53 +++++++++ .../curation_queries/curation_query.test.tsx | 55 ++++++++++ .../curation_queries/curation_query.tsx | 51 +++++++++ .../components/curation_queries/index.ts | 8 ++ .../components/curation_queries/utils.test.ts | 15 +++ .../components/curation_queries/utils.ts | 10 ++ .../components/curations/components/index.ts | 8 ++ .../curations/curations_logic.test.ts | 43 +++++++- .../components/curations/curations_logic.ts | 27 ++++- .../components/curations/curations_router.tsx | 8 +- .../views/curation_creation.test.tsx | 40 +++++++ .../curations/views/curation_creation.tsx | 53 +++++++++ .../curations/views/curations.test.tsx | 8 +- .../components/curations/views/curations.tsx | 4 +- .../components/curations/views/index.ts | 1 + .../routes/app_search/curations.test.ts | 57 ++++++++++ .../server/routes/app_search/curations.ts | 17 +++ 21 files changed, 714 insertions(+), 19 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_query.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_query.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/utils.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/utils.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries.scss new file mode 100644 index 0000000000000..c242cf29fd37d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries.scss @@ -0,0 +1,3 @@ +.curationQueryRow { + margin-bottom: $euiSizeXS; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries.test.tsx new file mode 100644 index 0000000000000..e55b944f7bebc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries.test.tsx @@ -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 { setMockActions, setMockValues } from '../../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { CurationQuery } from './curation_query'; + +import { CurationQueries } from './'; + +describe('CurationQueries', () => { + const props = { + queries: ['a', 'b', 'c'], + onSubmit: jest.fn(), + }; + const values = { + queries: ['a', 'b', 'c'], + hasEmptyQueries: false, + hasOnlyOneQuery: false, + }; + const actions = { + addQuery: jest.fn(), + editQuery: jest.fn(), + deleteQuery: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders a CurationQuery row for each query', () => { + const wrapper = shallow(); + + expect(wrapper.find(CurationQuery)).toHaveLength(3); + expect(wrapper.find(CurationQuery).at(0).prop('queryValue')).toEqual('a'); + expect(wrapper.find(CurationQuery).at(1).prop('queryValue')).toEqual('b'); + expect(wrapper.find(CurationQuery).at(2).prop('queryValue')).toEqual('c'); + }); + + it('calls editQuery when the CurationQuery value changes', () => { + const wrapper = shallow(); + wrapper.find(CurationQuery).at(0).simulate('change', 'new query value'); + + expect(actions.editQuery).toHaveBeenCalledWith(0, 'new query value'); + }); + + it('calls deleteQuery when the CurationQuery calls onDelete', () => { + const wrapper = shallow(); + wrapper.find(CurationQuery).at(2).simulate('delete'); + + expect(actions.deleteQuery).toHaveBeenCalledWith(2); + }); + + it('calls addQuery when the Add Query button is clicked', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="addCurationQueryButton"]').simulate('click'); + + expect(actions.addQuery).toHaveBeenCalled(); + }); + + it('disables the add button if any query fields are empty', () => { + setMockValues({ + ...values, + queries: ['a', '', 'c'], + hasEmptyQueries: true, + }); + const wrapper = shallow(); + const button = wrapper.find('[data-test-subj="addCurationQueryButton"]'); + + expect(button.prop('isDisabled')).toEqual(true); + }); + + it('calls the passed onSubmit callback when the submit button is clicked', () => { + setMockValues({ ...values, queries: ['some query'] }); + const wrapper = shallow(); + wrapper.find('[data-test-subj="submitCurationQueriesButton"]').simulate('click'); + + expect(props.onSubmit).toHaveBeenCalledWith(['some query']); + }); + + it('disables the submit button if no query fields have been filled', () => { + setMockValues({ + ...values, + queries: [''], + hasOnlyOneQuery: true, + hasEmptyQueries: true, + }); + const wrapper = shallow(); + const button = wrapper.find('[data-test-subj="submitCurationQueriesButton"]'); + + expect(button.prop('isDisabled')).toEqual(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries.tsx new file mode 100644 index 0000000000000..ad7872b112408 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues, useActions } from 'kea'; + +import { EuiButton, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { Curation } from '../../types'; + +import { CurationQueriesLogic } from './curation_queries_logic'; +import { CurationQuery } from './curation_query'; +import { filterEmptyQueries } from './utils'; +import './curation_queries.scss'; + +interface Props { + queries: Curation['queries']; + onSubmit(queries: Curation['queries']): void; + submitButtonText?: string; +} + +export const CurationQueries: React.FC = ({ + queries: initialQueries, + onSubmit, + submitButtonText = i18n.translate('xpack.enterpriseSearch.actions.continue', { + defaultMessage: 'Continue', + }), +}) => { + const logic = CurationQueriesLogic({ queries: initialQueries }); + const { queries, hasEmptyQueries, hasOnlyOneQuery } = useValues(logic); + const { addQuery, editQuery, deleteQuery } = useActions(logic); + + return ( + <> + {queries.map((query: string, index) => ( + editQuery(index, newValue)} + onDelete={() => deleteQuery(index)} + disableDelete={hasOnlyOneQuery} + /> + ))} + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.curations.addQueryButtonLabel', { + defaultMessage: 'Add query', + })} + + + onSubmit(filterEmptyQueries(queries))} + data-test-subj="submitCurationQueriesButton" + > + {submitButtonText} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries_logic.test.ts new file mode 100644 index 0000000000000..157e97433d2b6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries_logic.test.ts @@ -0,0 +1,98 @@ +/* + * 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 { resetContext } from 'kea'; + +import { CurationQueriesLogic } from './curation_queries_logic'; + +describe('CurationQueriesLogic', () => { + const MOCK_QUERIES = ['a', 'b', 'c']; + + const DEFAULT_PROPS = { queries: MOCK_QUERIES }; + const DEFAULT_VALUES = { + queries: MOCK_QUERIES, + hasEmptyQueries: false, + hasOnlyOneQuery: false, + }; + + const mount = (props = {}) => { + CurationQueriesLogic({ ...DEFAULT_PROPS, ...props }); + CurationQueriesLogic.mount(); + }; + + beforeEach(() => { + jest.clearAllMocks(); + resetContext({}); + }); + + it('has expected default values passed from props', () => { + mount(); + expect(CurationQueriesLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + afterEach(() => { + // Should not mutate the original array + expect(CurationQueriesLogic.values.queries).not.toBe(MOCK_QUERIES); // Would fail if we did not clone a new array + }); + + describe('addQuery', () => { + it('appends an empty string to the queries array', () => { + mount(); + CurationQueriesLogic.actions.addQuery(); + + expect(CurationQueriesLogic.values).toEqual({ + ...DEFAULT_VALUES, + hasEmptyQueries: true, + queries: ['a', 'b', 'c', ''], + }); + }); + }); + + describe('deleteQuery', () => { + it('deletes the query string at the specified array index', () => { + mount(); + CurationQueriesLogic.actions.deleteQuery(1); + + expect(CurationQueriesLogic.values).toEqual({ + ...DEFAULT_VALUES, + queries: ['a', 'c'], + }); + }); + }); + + describe('editQuery', () => { + it('edits the query string at the specified array index', () => { + mount(); + CurationQueriesLogic.actions.editQuery(2, 'z'); + + expect(CurationQueriesLogic.values).toEqual({ + ...DEFAULT_VALUES, + queries: ['a', 'b', 'z'], + }); + }); + }); + }); + + describe('selectors', () => { + describe('hasEmptyQueries', () => { + it('returns true if queries has any empty strings', () => { + mount({ queries: ['', '', ''] }); + + expect(CurationQueriesLogic.values.hasEmptyQueries).toEqual(true); + }); + }); + + describe('hasOnlyOneQuery', () => { + it('returns true if queries only has one item', () => { + mount({ queries: ['test'] }); + + expect(CurationQueriesLogic.values.hasOnlyOneQuery).toEqual(true); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries_logic.ts new file mode 100644 index 0000000000000..98109657d61a3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries_logic.ts @@ -0,0 +1,53 @@ +/* + * 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'; + +interface CurationQueriesValues { + queries: string[]; + hasEmptyQueries: boolean; + hasOnlyOneQuery: boolean; +} + +interface CurationQueriesActions { + addQuery(): void; + deleteQuery(indexToDelete: number): { indexToDelete: number }; + editQuery(index: number, newQueryValue: string): { index: number; newQueryValue: string }; +} + +export const CurationQueriesLogic = kea< + MakeLogicType +>({ + path: ['enterprise_search', 'app_search', 'curation_queries_logic'], + actions: () => ({ + addQuery: true, + deleteQuery: (indexToDelete) => ({ indexToDelete }), + editQuery: (index, newQueryValue) => ({ index, newQueryValue }), + }), + reducers: ({ props }) => ({ + queries: [ + props.queries, + { + addQuery: (state) => [...state, ''], + deleteQuery: (state, { indexToDelete }) => { + const newState = [...state]; + newState.splice(indexToDelete, 1); + return newState; + }, + editQuery: (state, { index, newQueryValue }) => { + const newState = [...state]; + newState[index] = newQueryValue; + return newState; + }, + }, + ], + }), + selectors: { + hasEmptyQueries: [(selectors) => [selectors.queries], (queries) => queries.indexOf('') >= 0], + hasOnlyOneQuery: [(selectors) => [selectors.queries], (queries) => queries.length <= 1], + }, +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_query.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_query.test.tsx new file mode 100644 index 0000000000000..64fbec59382a0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_query.test.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiFieldText } from '@elastic/eui'; + +import { CurationQuery } from './curation_query'; + +describe('CurationQuery', () => { + const props = { + queryValue: 'some query', + onChange: jest.fn(), + onDelete: jest.fn(), + disableDelete: false, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiFieldText)).toHaveLength(1); + expect(wrapper.find(EuiFieldText).prop('value')).toEqual('some query'); + }); + + it('calls onChange when the input value changes', () => { + const wrapper = shallow(); + wrapper.find(EuiFieldText).simulate('change', { target: { value: 'new query value' } }); + + expect(props.onChange).toHaveBeenCalledWith('new query value'); + }); + + it('calls onDelete when the delete button is clicked', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="deleteCurationQueryButton"]').simulate('click'); + + expect(props.onDelete).toHaveBeenCalled(); + }); + + it('disables the delete button if disableDelete is passed', () => { + const wrapper = shallow(); + const button = wrapper.find('[data-test-subj="deleteCurationQueryButton"]'); + + expect(button.prop('isDisabled')).toEqual(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_query.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_query.tsx new file mode 100644 index 0000000000000..78b32ef12e361 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_query.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiFieldText, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface Props { + queryValue: string; + onChange(newValue: string): void; + onDelete(): void; + disableDelete: boolean; +} + +export const CurationQuery: React.FC = ({ + queryValue, + onChange, + onDelete, + disableDelete, +}) => ( + + + onChange(e.target.value)} + /> + + + + + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/index.ts new file mode 100644 index 0000000000000..4f9136d15d6c3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { CurationQueries } from './curation_queries'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/utils.test.ts new file mode 100644 index 0000000000000..d84649f090691 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/utils.test.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { filterEmptyQueries } from './utils'; + +describe('filterEmptyQueries', () => { + it('filters out all empty strings from a queries array', () => { + const queries = ['', 'a', '', 'b', '', 'c', '']; + expect(filterEmptyQueries(queries)).toEqual(['a', 'b', 'c']); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/utils.ts new file mode 100644 index 0000000000000..505e9641d778e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/utils.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const filterEmptyQueries = (queries: string[]) => { + return queries.filter((query) => query.length); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/index.ts new file mode 100644 index 0000000000000..4f9136d15d6c3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { CurationQueries } from './curation_queries'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts index 1505fe5136bda..c1031fc20bc15 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { LogicMounter, mockHttpValues, mockFlashMessageHelpers } from '../../../__mocks__'; +import { + LogicMounter, + mockHttpValues, + mockKibanaValues, + mockFlashMessageHelpers, +} from '../../../__mocks__'; import '../../__mocks__/engine_logic.mock'; import { nextTick } from '@kbn/test/jest'; @@ -17,6 +22,7 @@ import { CurationsLogic } from './'; describe('CurationsLogic', () => { const { mount } = new LogicMounter(CurationsLogic); const { http } = mockHttpValues; + const { navigateToUrl } = mockKibanaValues; const { clearFlashMessages, setSuccessMessage, flashAPIErrors } = mockFlashMessageHelpers; const MOCK_CURATIONS_RESPONSE = { @@ -128,7 +134,7 @@ describe('CurationsLogic', () => { }); }); - describe('deleteCurationSet', () => { + describe('deleteCuration', () => { const confirmSpy = jest.spyOn(window, 'confirm'); beforeEach(() => { @@ -140,7 +146,7 @@ describe('CurationsLogic', () => { mount(); jest.spyOn(CurationsLogic.actions, 'loadCurations'); - CurationsLogic.actions.deleteCurationSet('some-curation-id'); + CurationsLogic.actions.deleteCuration('some-curation-id'); expect(clearFlashMessages).toHaveBeenCalled(); await nextTick(); @@ -155,7 +161,7 @@ describe('CurationsLogic', () => { http.delete.mockReturnValueOnce(Promise.reject('error')); mount(); - CurationsLogic.actions.deleteCurationSet('some-curation-id'); + CurationsLogic.actions.deleteCuration('some-curation-id'); expect(clearFlashMessages).toHaveBeenCalled(); await nextTick(); @@ -166,12 +172,39 @@ describe('CurationsLogic', () => { confirmSpy.mockImplementationOnce(() => false); mount(); - CurationsLogic.actions.deleteCurationSet('some-curation-id'); + CurationsLogic.actions.deleteCuration('some-curation-id'); expect(clearFlashMessages).toHaveBeenCalled(); await nextTick(); expect(http.delete).not.toHaveBeenCalled(); }); }); + + describe('createCuration', () => { + it('should make an API call and navigate to the new curation', async () => { + http.post.mockReturnValueOnce(Promise.resolve({ id: 'some-cur-id' })); + mount(); + + CurationsLogic.actions.createCuration(['some query']); + expect(clearFlashMessages).toHaveBeenCalled(); + await nextTick(); + + expect(http.post).toHaveBeenCalledWith('/api/app_search/engines/some-engine/curations', { + body: '{"queries":["some query"]}', + }); + expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations/some-cur-id'); + }); + + it('handles errors', async () => { + http.post.mockReturnValueOnce(Promise.reject('error')); + mount(); + + CurationsLogic.actions.createCuration(['some query']); + expect(clearFlashMessages).toHaveBeenCalled(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.ts index 434aff9c3cc4b..f4916f54fbc22 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.ts @@ -15,8 +15,10 @@ import { flashAPIErrors, } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; +import { KibanaLogic } from '../../../shared/kibana'; import { updateMetaPageIndex } from '../../../shared/table_pagination'; -import { EngineLogic } from '../engine'; +import { ENGINE_CURATION_PATH } from '../../routes'; +import { EngineLogic, generateEnginePath } from '../engine'; import { DELETE_MESSAGE, SUCCESS_MESSAGE } from './constants'; import { Curation, CurationsAPIResponse } from './types'; @@ -31,7 +33,8 @@ interface CurationsActions { onCurationsLoad(response: CurationsAPIResponse): CurationsAPIResponse; onPaginate(newPageIndex: number): { newPageIndex: number }; loadCurations(): void; - deleteCurationSet(id: string): string; + deleteCuration(id: string): string; + createCuration(queries: Curation['queries']): Curation['queries']; } export const CurationsLogic = kea>({ @@ -40,7 +43,8 @@ export const CurationsLogic = kea ({ results, meta }), onPaginate: (newPageIndex) => ({ newPageIndex }), loadCurations: true, - deleteCurationSet: (id) => id, + deleteCuration: (id) => id, + createCuration: (queries) => queries, }), reducers: () => ({ dataLoading: [ @@ -82,7 +86,7 @@ export const CurationsLogic = kea { + deleteCuration: async (id) => { const { http } = HttpLogic.values; const { engineName } = EngineLogic.values; clearFlashMessages(); @@ -97,5 +101,20 @@ export const CurationsLogic = kea { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + const { navigateToUrl } = KibanaLogic.values; + clearFlashMessages(); + + try { + const response = await http.post(`/api/app_search/engines/${engineName}/curations`, { + body: JSON.stringify({ queries }), + }); + navigateToUrl(generateEnginePath(ENGINE_CURATION_PATH, { curationId: response.id })); + } catch (e) { + flashAPIErrors(e); + } + }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx index b4479fb145f81..634736bca4c65 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx @@ -19,8 +19,8 @@ import { ENGINE_CURATION_ADD_RESULT_PATH, } from '../../routes'; -import { CURATIONS_TITLE } from './constants'; -import { Curations } from './views'; +import { CURATIONS_TITLE, CREATE_NEW_CURATION_TITLE } from './constants'; +import { Curations, CurationCreation } from './views'; interface Props { engineBreadcrumb: BreadcrumbTrail; @@ -35,8 +35,8 @@ export const CurationsRouter: React.FC = ({ engineBreadcrumb }) => { - - TODO: Curation creation view + + diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx new file mode 100644 index 0000000000000..e6ddbb9c1b7a9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions } from '../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { CurationQueries } from '../components'; + +import { CurationCreation } from './curation_creation'; + +describe('CurationCreation', () => { + const actions = { + createCuration: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(CurationQueries)).toHaveLength(1); + }); + + it('calls createCuration on CurationQueries submit', () => { + const wrapper = shallow(); + wrapper.find(CurationQueries).simulate('submit', ['some query']); + + expect(actions.createCuration).toHaveBeenCalledWith(['some query']); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx new file mode 100644 index 0000000000000..b1bfc6c2ab7fa --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions } from 'kea'; + +import { EuiPageHeader, EuiPageContent, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { FlashMessages } from '../../../../shared/flash_messages'; + +import { CurationQueries } from '../components'; +import { CREATE_NEW_CURATION_TITLE } from '../constants'; +import { CurationsLogic } from '../index'; + +export const CurationCreation: React.FC = () => { + const { createCuration } = useActions(CurationsLogic); + + return ( + <> + + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.create.curationQueriesTitle', + { defaultMessage: 'Curation queries' } + )} +

+ + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.create.curationQueriesDescription', + { + defaultMessage: + 'Add one or multiple queries to curate. You will be able add or remove more queries later.', + } + )} +

+
+ + createCuration(queries)} /> + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx index fd5d5b7ea64a9..d06144023e170 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx @@ -51,7 +51,7 @@ describe('Curations', () => { const actions = { loadCurations: jest.fn(), - deleteCurationSet: jest.fn(), + deleteCuration: jest.fn(), onPaginate: jest.fn(), }; @@ -134,12 +134,12 @@ describe('Curations', () => { expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations/cur-id-2'); }); - it('delete action calls deleteCurationSet', () => { + it('delete action calls deleteCuration', () => { wrapper.find('[data-test-subj="CurationsTableDeleteButton"]').first().simulate('click'); - expect(actions.deleteCurationSet).toHaveBeenCalledWith('cur-id-1'); + expect(actions.deleteCuration).toHaveBeenCalledWith('cur-id-1'); wrapper.find('[data-test-subj="CurationsTableDeleteButton"]').last().simulate('click'); - expect(actions.deleteCurationSet).toHaveBeenCalledWith('cur-id-2'); + expect(actions.deleteCuration).toHaveBeenCalledWith('cur-id-2'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx index 6affef53d71ee..fd0a36dfebec7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx @@ -69,7 +69,7 @@ export const Curations: React.FC = () => { export const CurationsTable: React.FC = () => { const { dataLoading, curations, meta } = useValues(CurationsLogic); - const { onPaginate, deleteCurationSet } = useActions(CurationsLogic); + const { onPaginate, deleteCuration } = useActions(CurationsLogic); const columns: Array> = [ { @@ -141,7 +141,7 @@ export const CurationsTable: React.FC = () => { type: 'icon', icon: 'trash', color: 'danger', - onClick: (curation: Curation) => deleteCurationSet(curation.id), + onClick: (curation: Curation) => deleteCuration(curation.id), 'data-test-subj': 'CurationsTableDeleteButton', }, ], diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/index.ts index d454d24f6c8b5..ca6924879324a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/index.ts @@ -6,3 +6,4 @@ */ export { Curations } from './curations'; +export { CurationCreation } from './curation_creation'; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/curations.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/curations.test.ts index 4ac79068a88f5..28896809bc81a 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/curations.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/curations.test.ts @@ -50,6 +50,63 @@ describe('curations routes', () => { }); }); + describe('POST /api/app_search/engines/{engineName}/curations', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/engines/{engineName}/curations', + }); + + registerCurationsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:engineName/curations/collection', + }); + }); + + describe('validates', () => { + it('with curation queries', () => { + const request = { + body: { + queries: ['a', 'b', 'c'], + }, + }; + mockRouter.shouldValidate(request); + }); + + it('empty queries array', () => { + const request = { + body: { + queries: [], + }, + }; + mockRouter.shouldThrow(request); + }); + + it('empty query strings', () => { + const request = { + body: { + queries: ['', '', ''], + }, + }; + mockRouter.shouldThrow(request); + }); + + it('missing queries', () => { + const request = { body: {} }; + mockRouter.shouldThrow(request); + }); + }); + }); + describe('DELETE /api/app_search/engines/{engineName}/curations/{curationId}', () => { let mockRouter: MockRouter; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/curations.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/curations.ts index 48bb2fc5cb823..2d7f09e1aeb8d 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/curations.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/curations.ts @@ -31,6 +31,23 @@ export function registerCurationsRoutes({ }) ); + router.post( + { + path: '/api/app_search/engines/{engineName}/curations', + validate: { + params: schema.object({ + engineName: schema.string(), + }), + body: schema.object({ + queries: schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:engineName/curations/collection', + }) + ); + router.delete( { path: '/api/app_search/engines/{engineName}/curations/{curationId}', From 9c527347efd45f56432f55a8589b62473b5d134a Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Thu, 25 Feb 2021 14:11:11 -0800 Subject: [PATCH 16/32] Test fix management scripted field filter functional test and unskip it (#92756) * fixes https://github.com/elastic/kibana/issues/74449 * unskipping the functional test, removing the unload and adding an empty kibana in the after method --- test/functional/apps/management/_scripted_fields_filter.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/functional/apps/management/_scripted_fields_filter.js b/test/functional/apps/management/_scripted_fields_filter.js index c7d333bd681d1..7ed15a6cddbca 100644 --- a/test/functional/apps/management/_scripted_fields_filter.js +++ b/test/functional/apps/management/_scripted_fields_filter.js @@ -16,9 +16,7 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['settings']); - // this functionality is no longer functional as of 7.0 but still needs cleanup - // https://github.com/elastic/kibana/issues/74118 - describe.skip('filter scripted fields', function describeIndexTests() { + describe('filter scripted fields', function describeIndexTests() { before(async function () { // delete .kibana index and then wait for Kibana to re-create it await browser.setWindowSize(1200, 800); @@ -29,8 +27,7 @@ export default function ({ getService, getPageObjects }) { }); after(async function () { - await esArchiver.unload('management'); - await kibanaServer.uiSettings.replace({}); + await esArchiver.load('empty_kibana'); }); const scriptedPainlessFieldName = 'ram_pain1'; From 0aabc317ec2a3d624c215c03ce472f236ae2b8bf Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 25 Feb 2021 16:13:27 -0700 Subject: [PATCH 17/32] [kbn/test] add import/export support to KbnClient (#92526) Co-authored-by: Tre' Seymour Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: spalger --- package.json | 1 + packages/kbn-dev-utils/src/index.ts | 1 - .../src/actions/empty_kibana_index.ts | 3 +- packages/kbn-es-archiver/src/actions/load.ts | 3 +- .../kbn-es-archiver/src/actions/unload.ts | 3 +- packages/kbn-es-archiver/src/cli.ts | 4 +- packages/kbn-es-archiver/src/es_archiver.ts | 3 +- .../src/lib/indices/kibana_index.ts | 3 +- .../lib/config/schema.ts | 7 + packages/kbn-test/src/index.ts | 4 + packages/kbn-test/src/kbn_archiver_cli.ts | 149 ++++++++++++++++ .../src/kbn_client/index.ts | 0 .../src/kbn_client/kbn_client.ts | 12 +- .../kbn_client/kbn_client_import_export.ts | 163 ++++++++++++++++++ .../src/kbn_client/kbn_client_plugins.ts | 0 .../src/kbn_client/kbn_client_requester.ts | 9 +- .../kbn_client/kbn_client_saved_objects.ts | 102 ++++++++++- .../src/kbn_client/kbn_client_status.ts | 0 .../src/kbn_client/kbn_client_ui_settings.ts | 2 +- .../src/kbn_client/kbn_client_version.ts | 0 scripts/kbn_archiver.js | 10 ++ .../services/kibana_server/kibana_server.ts | 3 +- test/common/services/security/role.ts | 3 +- .../common/services/security/role_mappings.ts | 3 +- test/common/services/security/user.ts | 3 +- test/functional/apps/discover/_discover.ts | 5 +- .../fixtures/kbn_archiver/discover.json | 51 ++++++ .../case/server/scripts/sub_cases/index.ts | 3 +- .../common/endpoint/index_data.ts | 2 +- .../kbn_client_with_api_key_support.ts | 2 +- .../endpoint/resolver_generator_script.ts | 3 +- .../scripts/endpoint/trusted_apps/index.ts | 3 +- .../detection_engine_api_integration/utils.ts | 2 +- yarn.lock | 9 + 34 files changed, 546 insertions(+), 25 deletions(-) create mode 100644 packages/kbn-test/src/kbn_archiver_cli.ts rename packages/{kbn-dev-utils => kbn-test}/src/kbn_client/index.ts (100%) rename packages/{kbn-dev-utils => kbn-test}/src/kbn_client/kbn_client.ts (87%) create mode 100644 packages/kbn-test/src/kbn_client/kbn_client_import_export.ts rename packages/{kbn-dev-utils => kbn-test}/src/kbn_client/kbn_client_plugins.ts (100%) rename packages/{kbn-dev-utils => kbn-test}/src/kbn_client/kbn_client_requester.ts (93%) rename packages/{kbn-dev-utils => kbn-test}/src/kbn_client/kbn_client_saved_objects.ts (60%) rename packages/{kbn-dev-utils => kbn-test}/src/kbn_client/kbn_client_status.ts (100%) rename packages/{kbn-dev-utils => kbn-test}/src/kbn_client/kbn_client_ui_settings.ts (98%) rename packages/{kbn-dev-utils => kbn-test}/src/kbn_client/kbn_client_version.ts (100%) create mode 100644 scripts/kbn_archiver.js create mode 100644 test/functional/fixtures/kbn_archiver/discover.json diff --git a/package.json b/package.json index 90096bfdf1b80..3dde0c6a17fb5 100644 --- a/package.json +++ b/package.json @@ -655,6 +655,7 @@ "fetch-mock": "^7.3.9", "file-loader": "^4.2.0", "file-saver": "^1.3.8", + "form-data": "^4.0.0", "formsy-react": "^1.1.5", "geckodriver": "^1.21.0", "glob-watcher": "5.0.3", diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 66ad4e7be589b..3ac3927d25c05 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -23,7 +23,6 @@ export { KBN_P12_PATH, KBN_P12_PASSWORD, } from './certs'; -export * from './kbn_client'; export * from './run'; export * from './axios'; export * from './stdio'; diff --git a/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts b/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts index 300c9f4dd66b0..2c36e24453c62 100644 --- a/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts +++ b/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts @@ -7,7 +7,8 @@ */ import { Client } from '@elastic/elasticsearch'; -import { ToolingLog, KbnClient } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; import { migrateKibanaIndex, createStats, cleanKibanaIndices } from '../lib'; diff --git a/packages/kbn-es-archiver/src/actions/load.ts b/packages/kbn-es-archiver/src/actions/load.ts index 60af6b3aa747b..68d5437336023 100644 --- a/packages/kbn-es-archiver/src/actions/load.ts +++ b/packages/kbn-es-archiver/src/actions/load.ts @@ -9,7 +9,8 @@ import { resolve } from 'path'; import { createReadStream } from 'fs'; import { Readable } from 'stream'; -import { ToolingLog, KbnClient } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; import { Client } from '@elastic/elasticsearch'; import { createPromiseFromStreams, concatStreamProviders } from '@kbn/utils'; import { ES_CLIENT_HEADERS } from '../client_headers'; diff --git a/packages/kbn-es-archiver/src/actions/unload.ts b/packages/kbn-es-archiver/src/actions/unload.ts index c12fa935f786a..b5f259a1496bb 100644 --- a/packages/kbn-es-archiver/src/actions/unload.ts +++ b/packages/kbn-es-archiver/src/actions/unload.ts @@ -10,7 +10,8 @@ import { resolve } from 'path'; import { createReadStream } from 'fs'; import { Readable, Writable } from 'stream'; import { Client } from '@elastic/elasticsearch'; -import { ToolingLog, KbnClient } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; import { createPromiseFromStreams } from '@kbn/utils'; import { diff --git a/packages/kbn-es-archiver/src/cli.ts b/packages/kbn-es-archiver/src/cli.ts index 919aff5c3851b..9617457d4573e 100644 --- a/packages/kbn-es-archiver/src/cli.ts +++ b/packages/kbn-es-archiver/src/cli.ts @@ -17,8 +17,8 @@ import Url from 'url'; import readline from 'readline'; import Fs from 'fs'; -import { RunWithCommands, createFlagError, KbnClient, CA_CERT_PATH } from '@kbn/dev-utils'; -import { readConfigFile } from '@kbn/test'; +import { RunWithCommands, createFlagError, CA_CERT_PATH } from '@kbn/dev-utils'; +import { readConfigFile, KbnClient } from '@kbn/test'; import { Client } from '@elastic/elasticsearch'; import { EsArchiver } from './es_archiver'; diff --git a/packages/kbn-es-archiver/src/es_archiver.ts b/packages/kbn-es-archiver/src/es_archiver.ts index b00b9fb8b3f25..68eacb4f3caf2 100644 --- a/packages/kbn-es-archiver/src/es_archiver.ts +++ b/packages/kbn-es-archiver/src/es_archiver.ts @@ -7,7 +7,8 @@ */ import { Client } from '@elastic/elasticsearch'; -import { ToolingLog, KbnClient } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; import { saveAction, diff --git a/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts index 7f0080783ee0a..dc49085cbd458 100644 --- a/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts +++ b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts @@ -9,7 +9,8 @@ import { inspect } from 'util'; import { Client } from '@elastic/elasticsearch'; -import { ToolingLog, KbnClient } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; import { Stats } from '../stats'; import { deleteIndex } from './delete_index'; import { ES_CLIENT_HEADERS } from '../../client_headers'; diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 4fd28678d2653..0694bc4ffdb0f 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -213,6 +213,13 @@ export const schema = Joi.object() }) .default(), + // settings for the saved objects svc + kbnArchiver: Joi.object() + .keys({ + directory: Joi.string().default(defaultRelativeToConfigPath('fixtures/kbn_archiver')), + }) + .default(), + // settings for the kibanaServer.uiSettings module uiSettings: Joi.object() .keys({ diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts index 25a5c6541bf07..919dc8b4477f3 100644 --- a/packages/kbn-test/src/index.ts +++ b/packages/kbn-test/src/index.ts @@ -48,3 +48,7 @@ export { getUrl } from './jest/utils/get_url'; export { runCheckJestConfigsCli } from './jest/run_check_jest_configs_cli'; export { runJest } from './jest/run'; + +export * from './kbn_archiver_cli'; + +export * from './kbn_client'; diff --git a/packages/kbn-test/src/kbn_archiver_cli.ts b/packages/kbn-test/src/kbn_archiver_cli.ts new file mode 100644 index 0000000000000..98bfa6eaa4046 --- /dev/null +++ b/packages/kbn-test/src/kbn_archiver_cli.ts @@ -0,0 +1,149 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; +import Url from 'url'; + +import { RunWithCommands, createFlagError, Flags } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; + +import { readConfigFile } from './functional_test_runner'; + +function getSinglePositionalArg(flags: Flags) { + const positional = flags._; + if (positional.length < 1) { + throw createFlagError('missing name of export to import'); + } + + if (positional.length > 1) { + throw createFlagError(`extra positional arguments, expected 1, got [${positional}]`); + } + + return positional[0]; +} + +function parseTypesFlag(flags: Flags) { + if (!flags.type || (typeof flags.type !== 'string' && !Array.isArray(flags.type))) { + throw createFlagError('--type is a required flag'); + } + + const types = typeof flags.type === 'string' ? [flags.type] : flags.type; + return types.reduce( + (acc: string[], type) => [...acc, ...type.split(',').map((t) => t.trim())], + [] + ); +} + +export function runKbnArchiverCli() { + new RunWithCommands({ + description: 'Import/export saved objects from archives, for testing', + globalFlags: { + string: ['config', 'space', 'kibana-url', 'dir'], + help: ` + --space space id to operate on, defaults to the default space + --config optional path to an FTR config file that will be parsed and used for defaults + --kibana-url set the url that kibana can be reached at, uses the "servers.kibana" setting from --config by default + --dir directory that contains exports to be imported, or where exports will be saved, uses the "kbnArchiver.directory" + setting from --config by default + `, + }, + async extendContext({ log, flags }) { + let config; + if (flags.config) { + if (typeof flags.config !== 'string') { + throw createFlagError('expected --config to be a string'); + } + + config = await readConfigFile(log, Path.resolve(flags.config)); + } + + let kibanaUrl; + if (flags['kibana-url']) { + if (typeof flags['kibana-url'] !== 'string') { + throw createFlagError('expected --kibana-url to be a string'); + } + + kibanaUrl = flags['kibana-url']; + } else if (config) { + kibanaUrl = Url.format(config.get('servers.kibana')); + } + + if (!kibanaUrl) { + throw createFlagError( + 'Either a --config file with `servers.kibana` defined, or a --kibana-url must be passed' + ); + } + + let importExportDir; + if (flags.dir) { + if (typeof flags.dir !== 'string') { + throw createFlagError('expected --dir to be a string'); + } + + importExportDir = flags.dir; + } else if (config) { + importExportDir = config.get('kbnArchiver.directory'); + } + + if (!importExportDir) { + throw createFlagError( + '--config does not include a kbnArchiver.directory, specify it or include --dir flag' + ); + } + + const space = flags.space; + if (!(space === undefined || typeof space === 'string')) { + throw createFlagError('--space must be a string'); + } + + return { + space, + kbnClient: new KbnClient({ + log, + url: kibanaUrl, + importExportDir, + }), + }; + }, + }) + .command({ + name: 'save', + usage: 'save ', + description: 'export saved objects from Kibana to a file', + flags: { + string: ['type'], + help: ` + --type saved object type that should be fetched and stored in the archive, can + be specified multiple times or be a comma-separated list. + `, + }, + async run({ kbnClient, flags, space }) { + await kbnClient.importExport.save(getSinglePositionalArg(flags), { + types: parseTypesFlag(flags), + space, + }); + }, + }) + .command({ + name: 'load', + usage: 'load ', + description: 'import a saved export to Kibana', + async run({ kbnClient, flags, space }) { + await kbnClient.importExport.load(getSinglePositionalArg(flags), { space }); + }, + }) + .command({ + name: 'unload', + usage: 'unload ', + description: 'delete the saved objects saved in the archive from the Kibana index', + async run({ kbnClient, flags, space }) { + await kbnClient.importExport.unload(getSinglePositionalArg(flags), { space }); + }, + }) + .execute(); +} diff --git a/packages/kbn-dev-utils/src/kbn_client/index.ts b/packages/kbn-test/src/kbn_client/index.ts similarity index 100% rename from packages/kbn-dev-utils/src/kbn_client/index.ts rename to packages/kbn-test/src/kbn_client/index.ts diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts b/packages/kbn-test/src/kbn_client/kbn_client.ts similarity index 87% rename from packages/kbn-dev-utils/src/kbn_client/kbn_client.ts rename to packages/kbn-test/src/kbn_client/kbn_client.ts index 963639d47045b..3fa74412c1a8b 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client.ts @@ -6,19 +6,22 @@ * Side Public License, v 1. */ -import { ToolingLog } from '../tooling_log'; +import { ToolingLog } from '@kbn/dev-utils'; + import { KbnClientRequester, ReqOptions } from './kbn_client_requester'; import { KbnClientStatus } from './kbn_client_status'; import { KbnClientPlugins } from './kbn_client_plugins'; import { KbnClientVersion } from './kbn_client_version'; import { KbnClientSavedObjects } from './kbn_client_saved_objects'; import { KbnClientUiSettings, UiSettingValues } from './kbn_client_ui_settings'; +import { KbnClientImportExport } from './kbn_client_import_export'; export interface KbnClientOptions { url: string; certificateAuthorities?: Buffer[]; log: ToolingLog; uiSettingDefaults?: UiSettingValues; + importExportDir?: string; } export class KbnClient { @@ -27,6 +30,7 @@ export class KbnClient { readonly version: KbnClientVersion; readonly savedObjects: KbnClientSavedObjects; readonly uiSettings: KbnClientUiSettings; + readonly importExport: KbnClientImportExport; private readonly requester: KbnClientRequester; private readonly log: ToolingLog; @@ -56,6 +60,12 @@ export class KbnClient { this.version = new KbnClientVersion(this.status); this.savedObjects = new KbnClientSavedObjects(this.log, this.requester); this.uiSettings = new KbnClientUiSettings(this.log, this.requester, this.uiSettingDefaults); + this.importExport = new KbnClientImportExport( + this.log, + this.requester, + this.savedObjects, + options.importExportDir + ); } /** diff --git a/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts new file mode 100644 index 0000000000000..bb5b99fdc4439 --- /dev/null +++ b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts @@ -0,0 +1,163 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { inspect } from 'util'; +import Fs from 'fs/promises'; +import Path from 'path'; + +import FormData from 'form-data'; +import { ToolingLog, isAxiosResponseError, createFailError } from '@kbn/dev-utils'; + +import { KbnClientRequester, uriencode, ReqOptions } from './kbn_client_requester'; +import { KbnClientSavedObjects } from './kbn_client_saved_objects'; + +interface ImportApiResponse { + success: boolean; + [key: string]: unknown; +} + +interface SavedObject { + id: string; + type: string; + [key: string]: unknown; +} + +async function parseArchive(path: string): Promise { + return (await Fs.readFile(path, 'utf-8')) + .split('\n\n') + .filter((line) => !!line) + .map((line) => JSON.parse(line)); +} + +export class KbnClientImportExport { + constructor( + public readonly log: ToolingLog, + public readonly requester: KbnClientRequester, + public readonly savedObjects: KbnClientSavedObjects, + public readonly dir?: string + ) {} + + private resolvePath(path: string) { + if (!Path.extname(path)) { + path = `${path}.json`; + } + + if (!this.dir && !Path.isAbsolute(path)) { + throw new Error( + 'unable to resolve relative path to import/export without a configured dir, either path absolute path or specify --dir' + ); + } + + return this.dir ? Path.resolve(this.dir, path) : path; + } + + async load(name: string, options?: { space?: string }) { + const src = this.resolvePath(name); + this.log.debug('resolved import for', name, 'to', src); + + const objects = await parseArchive(src); + this.log.info('importing', objects.length, 'saved objects', { space: options?.space }); + + const formData = new FormData(); + formData.append('file', objects.map((obj) => JSON.stringify(obj)).join('\n'), 'import.ndjson'); + + // TODO: should we clear out the existing saved objects? + const resp = await this.req(options?.space, { + method: 'POST', + path: '/api/saved_objects/_import', + query: { + overwrite: true, + }, + body: formData, + headers: formData.getHeaders(), + }); + + if (resp.data.success) { + this.log.success('import success'); + } else { + throw createFailError(`failed to import all saved objects: ${inspect(resp.data)}`); + } + } + + async unload(name: string, options?: { space?: string }) { + const src = this.resolvePath(name); + this.log.debug('unloading docs from archive at', src); + + const objects = await parseArchive(src); + this.log.info('deleting', objects.length, 'objects', { space: options?.space }); + + const { deleted, missing } = await this.savedObjects.bulkDelete({ + space: options?.space, + objects, + }); + + if (missing) { + this.log.info(missing, 'saved objects were already deleted'); + } + + this.log.success(deleted, 'saved objects deleted'); + } + + async save(name: string, options: { types: string[]; space?: string }) { + const dest = this.resolvePath(name); + this.log.debug('saving export to', dest); + + const resp = await this.req(options.space, { + method: 'POST', + path: '/api/saved_objects/_export', + body: { + type: options.types, + excludeExportDetails: true, + includeReferencesDeep: true, + }, + }); + + if (typeof resp.data !== 'string') { + throw createFailError(`unexpected response from export API: ${inspect(resp.data)}`); + } + + const objects = resp.data + .split('\n') + .filter((l) => !!l) + .map((line) => JSON.parse(line)); + + const fileContents = objects + .map((obj) => { + const { sort: _, ...nonSortFields } = obj; + return JSON.stringify(nonSortFields, null, 2); + }) + .join('\n\n'); + + await Fs.writeFile(dest, fileContents, 'utf-8'); + + this.log.success('Exported', objects.length, 'saved objects to', dest); + } + + private async req(space: string | undefined, options: ReqOptions) { + if (!options.path.startsWith('/')) { + throw new Error('options.path must start with a /'); + } + + try { + return await this.requester.request({ + ...options, + path: space ? uriencode`/s/${space}` + options.path : options.path, + }); + } catch (error) { + if (!isAxiosResponseError(error)) { + throw error; + } + + throw createFailError( + `${error.response.status} resp: ${inspect(error.response.data)}\nreq: ${inspect( + error.config + )}` + ); + } + } +} diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_plugins.ts b/packages/kbn-test/src/kbn_client/kbn_client_plugins.ts similarity index 100% rename from packages/kbn-dev-utils/src/kbn_client/kbn_client_plugins.ts rename to packages/kbn-test/src/kbn_client/kbn_client_plugins.ts diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts b/packages/kbn-test/src/kbn_client/kbn_client_requester.ts similarity index 93% rename from packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts rename to packages/kbn-test/src/kbn_client/kbn_client_requester.ts index d940525f57e3c..2e1575aee1897 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_requester.ts @@ -8,10 +8,10 @@ import Url from 'url'; import Https from 'https'; -import Axios, { AxiosResponse } from 'axios'; +import Qs from 'querystring'; -import { isAxiosRequestError, isAxiosResponseError } from '../axios'; -import { ToolingLog } from '../tooling_log'; +import Axios, { AxiosResponse } from 'axios'; +import { ToolingLog, isAxiosRequestError, isAxiosResponseError } from '@kbn/dev-utils'; const isConcliftOnGetError = (error: any) => { return ( @@ -52,6 +52,7 @@ export interface ReqOptions { method: 'GET' | 'POST' | 'PUT' | 'DELETE'; body?: any; retries?: number; + headers?: Record; } const delay = (ms: number) => @@ -102,9 +103,11 @@ export class KbnClientRequester { data: options.body, params: options.query, headers: { + ...options.headers, 'kbn-xsrf': 'kbn-client', }, httpsAgent: this.httpsAgent, + paramsSerializer: (params) => Qs.stringify(params), }); return response; diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts b/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts similarity index 60% rename from packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts rename to packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts index 9d616d6f50a88..904ccc385bd7d 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts @@ -6,7 +6,12 @@ * Side Public License, v 1. */ -import { ToolingLog } from '../tooling_log'; +import { inspect } from 'util'; + +import * as Rx from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; +import { lastValueFrom } from '@kbn/std'; +import { ToolingLog, isAxiosResponseError, createFailError } from '@kbn/dev-utils'; import { KbnClientRequester, uriencode } from './kbn_client_requester'; @@ -51,6 +56,38 @@ interface MigrateResponse { result: Array<{ status: string }>; } +interface FindApiResponse { + saved_objects: Array<{ + type: string; + id: string; + [key: string]: unknown; + }>; + total: number; + per_page: number; + page: number; +} + +interface CleanOptions { + space?: string; + types: string[]; +} + +interface DeleteObjectsOptions { + space?: string; + objects: Array<{ + type: string; + id: string; + }>; +} + +async function concurrently(maxConcurrency: number, arr: T[], fn: (item: T) => Promise) { + if (arr.length) { + await lastValueFrom( + Rx.from(arr).pipe(mergeMap(async (item) => await fn(item), maxConcurrency)) + ); + } +} + export class KbnClientSavedObjects { constructor(private readonly log: ToolingLog, private readonly requester: KbnClientRequester) {} @@ -143,4 +180,67 @@ export class KbnClientSavedObjects { return data; } + + public async clean(options: CleanOptions) { + this.log.debug('Cleaning all saved objects', { space: options.space }); + + let deleted = 0; + + while (true) { + const resp = await this.requester.request({ + method: 'GET', + path: options.space + ? uriencode`/s/${options.space}/api/saved_objects/_find` + : '/api/saved_objects/_find', + query: { + per_page: 1000, + type: options.types, + fields: 'none', + }, + }); + + this.log.info('deleting batch of', resp.data.saved_objects.length, 'objects'); + const deletion = await this.bulkDelete({ + space: options.space, + objects: resp.data.saved_objects, + }); + deleted += deletion.deleted; + + if (resp.data.total <= resp.data.per_page) { + break; + } + } + + this.log.success('deleted', deleted, 'objects'); + } + + public async bulkDelete(options: DeleteObjectsOptions) { + let deleted = 0; + let missing = 0; + + await concurrently(20, options.objects, async (obj) => { + try { + await this.requester.request({ + method: 'DELETE', + path: options.space + ? uriencode`/s/${options.space}/api/saved_objects/${obj.type}/${obj.id}` + : uriencode`/api/saved_objects/${obj.type}/${obj.id}`, + }); + deleted++; + } catch (error) { + if (isAxiosResponseError(error)) { + if (error.response.status === 404) { + missing++; + return; + } + + throw createFailError(`${error.response.status} resp: ${inspect(error.response.data)}`); + } + + throw error; + } + }); + + return { deleted, missing }; + } } diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts b/packages/kbn-test/src/kbn_client/kbn_client_status.ts similarity index 100% rename from packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts rename to packages/kbn-test/src/kbn_client/kbn_client_status.ts diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts b/packages/kbn-test/src/kbn_client/kbn_client_ui_settings.ts similarity index 98% rename from packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts rename to packages/kbn-test/src/kbn_client/kbn_client_ui_settings.ts index 75fd7a4c8391e..78155098ef038 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_ui_settings.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ToolingLog } from '../tooling_log'; +import { ToolingLog } from '@kbn/dev-utils'; import { KbnClientRequester, uriencode } from './kbn_client_requester'; diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_version.ts b/packages/kbn-test/src/kbn_client/kbn_client_version.ts similarity index 100% rename from packages/kbn-dev-utils/src/kbn_client/kbn_client_version.ts rename to packages/kbn-test/src/kbn_client/kbn_client_version.ts diff --git a/scripts/kbn_archiver.js b/scripts/kbn_archiver.js new file mode 100644 index 0000000000000..b04b86a0d4eed --- /dev/null +++ b/scripts/kbn_archiver.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +require('../src/setup_node_env'); +require('@kbn/test').runKbnArchiverCli(); diff --git a/test/common/services/kibana_server/kibana_server.ts b/test/common/services/kibana_server/kibana_server.ts index 51f9dc9d00772..f366a864db980 100644 --- a/test/common/services/kibana_server/kibana_server.ts +++ b/test/common/services/kibana_server/kibana_server.ts @@ -7,7 +7,7 @@ */ import Url from 'url'; -import { KbnClient } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -22,6 +22,7 @@ export function KibanaServerProvider({ getService }: FtrProviderContext) { url, certificateAuthorities: config.get('servers.kibana.certificateAuthorities'), uiSettingDefaults: defaults, + importExportDir: config.get('kbnArchiver.directory'), }); if (defaults) { diff --git a/test/common/services/security/role.ts b/test/common/services/security/role.ts index 2aae5b2282940..420bed027f317 100644 --- a/test/common/services/security/role.ts +++ b/test/common/services/security/role.ts @@ -7,7 +7,8 @@ */ import util from 'util'; -import { KbnClient, ToolingLog } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; export class Role { constructor(private log: ToolingLog, private kibanaServer: KbnClient) {} diff --git a/test/common/services/security/role_mappings.ts b/test/common/services/security/role_mappings.ts index c20ff7e327b64..af9204866ad47 100644 --- a/test/common/services/security/role_mappings.ts +++ b/test/common/services/security/role_mappings.ts @@ -7,7 +7,8 @@ */ import util from 'util'; -import { KbnClient, ToolingLog } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; export class RoleMappings { constructor(private log: ToolingLog, private kbnClient: KbnClient) {} diff --git a/test/common/services/security/user.ts b/test/common/services/security/user.ts index 0d12a0dae2e46..3bd31bb5ed186 100644 --- a/test/common/services/security/user.ts +++ b/test/common/services/security/user.ts @@ -7,7 +7,8 @@ */ import util from 'util'; -import { KbnClient, ToolingLog } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; export class User { constructor(private log: ToolingLog, private kbnClient: KbnClient) {} diff --git a/test/functional/apps/discover/_discover.ts b/test/functional/apps/discover/_discover.ts index 9323c9e2fe70b..aeb02e5c30eb8 100644 --- a/test/functional/apps/discover/_discover.ts +++ b/test/functional/apps/discover/_discover.ts @@ -20,6 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const inspector = getService('inspector'); const elasticChart = getService('elasticChart'); const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); + const defaultSettings = { defaultIndex: 'logstash-*', }; @@ -27,7 +28,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('discover test', function describeIndexTests() { before(async function () { log.debug('load kibana index with default index pattern'); - await esArchiver.load('discover'); + + await kibanaServer.savedObjects.clean({ types: ['search'] }); + await kibanaServer.importExport.load('discover'); // and load a set of makelogs data await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/test/functional/fixtures/kbn_archiver/discover.json b/test/functional/fixtures/kbn_archiver/discover.json new file mode 100644 index 0000000000000..e861f875a2d9e --- /dev/null +++ b/test/functional/fixtures/kbn_archiver/discover.json @@ -0,0 +1,51 @@ +{ + "attributes": { + "fieldAttrs": "{\"referer\":{\"customLabel\":\"Referer custom\"}}", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@message\"}}},{\"name\":\"@tags\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@tags\"}}},{\"name\":\"@timestamp\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"agent\"}}},{\"name\":\"bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"extension\"}}},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"headings\"}}},{\"name\":\"host\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"host\"}}},{\"name\":\"id\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"index\"}}},{\"name\":\"ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"links\"}}},{\"name\":\"machine.os\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"machine.os\"}}},{\"name\":\"machine.ram\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"esTypes\":[\"double\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nestedField.child\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"nested\":{\"path\":\"nestedField\"}}},{\"name\":\"phpmemory\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:section\"}}},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:tag\"}}},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:description\"}}},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image\"}}},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:height\"}}},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:width\"}}},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:site_name\"}}},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:title\"}}},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:type\"}}},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:url\"}}},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:card\"}}},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:description\"}}},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:image\"}}},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:site\"}}},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:title\"}}},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.url\"}}},{\"name\":\"request\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"request\"}}},{\"name\":\"response\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"response\"}}},{\"name\":\"spaces\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"spaces\"}}},{\"name\":\"type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"url\"}}},{\"name\":\"utc_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"xss\"}}}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "coreMigrationVersion": "8.0.0", + "id": "logstash-*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzQsMl0=" +} + +{ + "attributes": { + "columns": [ + "_source" + ], + "description": "A Saved Search Description", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"filter\":[],\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "@timestamp", + "desc" + ] + ], + "title": "A Saved Search", + "version": 1 + }, + "coreMigrationVersion": "8.0.0", + "id": "ab12e3c0-f231-11e6-9486-733b1ac9221a", + "migrationVersion": { + "search": "7.9.3" + }, + "references": [ + { + "id": "logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "search", + "version": "WzUsMl0=" +} \ No newline at end of file diff --git a/x-pack/plugins/case/server/scripts/sub_cases/index.ts b/x-pack/plugins/case/server/scripts/sub_cases/index.ts index 9dd577c40c74e..f6ec1f44076e4 100644 --- a/x-pack/plugins/case/server/scripts/sub_cases/index.ts +++ b/x-pack/plugins/case/server/scripts/sub_cases/index.ts @@ -6,7 +6,8 @@ */ /* eslint-disable no-console */ import yargs from 'yargs'; -import { KbnClient, ToolingLog } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; import { CaseResponse, CaseType, diff --git a/x-pack/plugins/security_solution/common/endpoint/index_data.ts b/x-pack/plugins/security_solution/common/endpoint/index_data.ts index 3ff719d267f40..96f83f1073fcc 100644 --- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts @@ -7,7 +7,7 @@ import { Client } from '@elastic/elasticsearch'; import seedrandom from 'seedrandom'; -import { KbnClient } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; import { AxiosResponse } from 'axios'; import { EndpointDocGenerator, TreeOptions, Event } from './generate_data'; import { firstNonNullValue } from './models/ecs_safety_helpers'; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/kbn_client_with_api_key_support.ts b/x-pack/plugins/security_solution/scripts/endpoint/kbn_client_with_api_key_support.ts index 557b485682e15..a8ed7b77a5d9e 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/kbn_client_with_api_key_support.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/kbn_client_with_api_key_support.ts @@ -7,7 +7,7 @@ import { URL } from 'url'; -import { KbnClient, KbnClientOptions } from '@kbn/dev-utils'; +import { KbnClient, KbnClientOptions } from '@kbn/test'; import fetch, { RequestInit } from 'node-fetch'; export class KbnClientWithApiKeySupport extends KbnClient { diff --git a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts index a4664e4a51ca2..72f56f13eaddf 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts @@ -10,7 +10,8 @@ import yargs from 'yargs'; import fs from 'fs'; import { Client, ClientOptions } from '@elastic/elasticsearch'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; -import { KbnClient, ToolingLog, CA_CERT_PATH } from '@kbn/dev-utils'; +import { ToolingLog, CA_CERT_PATH } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; import { AxiosResponse } from 'axios'; import { indexHostsAndAlerts } from '../../common/endpoint/index_data'; import { ANCESTRY_LIMIT, EndpointDocGenerator } from '../../common/endpoint/generate_data'; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts index 3b914f987456a..582969f1dcd45 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts @@ -7,7 +7,8 @@ // @ts-ignore import minimist from 'minimist'; -import { KbnClient, ToolingLog } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; import bluebird from 'bluebird'; import { basename } from 'path'; import { TRUSTED_APPS_CREATE_API, TRUSTED_APPS_LIST_API } from '../../../common/endpoint/constants'; diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 7711338b44697..683d57081a267 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { KbnClient } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; import { ApiResponse, Client } from '@elastic/elasticsearch'; import { SuperTest } from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; diff --git a/yarn.lock b/yarn.lock index 92c67cda974c3..6c706cffebaaf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14954,6 +14954,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" From 3df61f760232fed506db282846908e9b5c640974 Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Thu, 25 Feb 2021 18:36:47 -0500 Subject: [PATCH 18/32] API docs (#92827) --- .../build_api_declaration.ts | 1 - .../build_arrow_fn_dec.ts | 3 - .../build_function_dec.ts | 1 - .../build_variable_dec.ts | 1 - .../extract_import_refs.test.ts | 39 +- .../extract_import_refs.ts | 14 +- .../api_docs/build_api_declarations/utils.ts | 2 +- .../src/api_docs/mdx/write_plugin_mdx_docs.ts | 2 +- .../api_docs/tests/snapshots/plugin_a.json | 1567 ++++++++++++++++- .../tests/snapshots/plugin_a_foo.json | 78 +- packages/kbn-docs-utils/src/api_docs/types.ts | 2 +- packages/kbn-docs-utils/src/api_docs/utils.ts | 3 - src/dev/ci_setup/setup.sh | 16 + 13 files changed, 1710 insertions(+), 19 deletions(-) diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_api_declaration.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_api_declaration.ts index 3ee6676cf5e32..2d1cd7b9f97ca 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_api_declaration.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_api_declaration.ts @@ -50,7 +50,6 @@ export function buildApiDeclaration( name?: string ): ApiDeclaration { const apiName = name ? name : isNamedNode(node) ? node.getName() : 'Unnamed'; - log.debug(`Building API Declaration for ${apiName} of kind ${node.getKindName()}`); const apiId = parentApiId ? parentApiId + '.' + apiName : apiName; const anchorLink: AnchorLink = { scope, pluginName, apiName: apiId }; diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_arrow_fn_dec.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_arrow_fn_dec.ts index 146fcf4fa4d0a..2f041c8d42b4b 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_arrow_fn_dec.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_arrow_fn_dec.ts @@ -47,9 +47,6 @@ export function getArrowFunctionDec( anchorLink: AnchorLink, log: ToolingLog ) { - log.debug( - `Getting Arrow Function doc def for node ${node.getName()} of kind ${node.getKindName()}` - ); return { id: getApiSectionId(anchorLink), type: TypeKind.FunctionKind, diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_function_dec.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_function_dec.ts index 2936699152a83..89050430085fd 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_function_dec.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_function_dec.ts @@ -39,7 +39,6 @@ export function buildFunctionDec( const label = Node.isConstructorDeclaration(node) ? 'Constructor' : node.getName() || '(WARN: Missing name)'; - log.debug(`Getting function doc def for node ${label} of kind ${node.getKindName()}`); return { id: getApiSectionId(anchorLink), type: TypeKind.FunctionKind, diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_variable_dec.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_variable_dec.ts index 3e0b48de1e18b..86e8e5078b6fb 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_variable_dec.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_variable_dec.ts @@ -45,7 +45,6 @@ export function buildVariableDec( anchorLink: AnchorLink, log: ToolingLog ): ApiDeclaration { - log.debug('buildVariableDec for ' + node.getName()); const initializer = node.getInitializer(); // Recusively list object properties as children. if (initializer && Node.isObjectLiteralExpression(initializer)) { diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/extract_import_refs.test.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/extract_import_refs.test.ts index a757df2ece366..f3fafca4c1e82 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/extract_import_refs.test.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/extract_import_refs.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { REPO_ROOT } from '@kbn/utils'; import { KibanaPlatformPlugin, ToolingLog } from '@kbn/dev-utils'; import { getPluginApiDocId } from '../utils'; import { extractImportReferences } from './extract_import_refs'; @@ -82,7 +83,43 @@ it('test extractImportReference with unknown imports', () => { expect(results.length).toBe(3); expect(results[0]).toBe(''); +}); + +it('test full file imports with no matching plugin', () => { + const refs = extractImportReferences( + `typeof import("${REPO_ROOT}/src/plugins/data/common/es_query/kuery/node_types/function")`, + plugins, + log + ); + expect(refs).toMatchInlineSnapshot(` + Array [ + "typeof ", + "src/plugins/data/common/es_query/kuery/node_types/function", + ] + `); + expect(refs.length).toBe(2); +}); + +it('test full file imports with a matching plugin', () => { + const refs = extractImportReferences( + `typeof import("${plugin.directory}/public/foo/index") something`, + plugins, + log + ); + expect(refs).toMatchInlineSnapshot(` + Array [ + "typeof ", + Object { + "docId": "kibPluginAPluginApi", + "pluginId": "pluginA", + "scope": "public", + "section": undefined, + "text": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index", + }, + " something", + ] + `); + expect(refs.length).toBe(3); }); it('test single link', () => { diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/extract_import_refs.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/extract_import_refs.ts index 1147e15a1acb6..92f191197472d 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/extract_import_refs.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/extract_import_refs.ts @@ -9,6 +9,7 @@ import { KibanaPlatformPlugin, ToolingLog } from '@kbn/dev-utils'; import { getApiSectionId, getPluginApiDocId, getPluginForPath } from '../utils'; import { ApiScope, TextWithLinks } from '../types'; +import { getRelativePath } from './utils'; /** * @@ -54,6 +55,9 @@ export function extractImportReferences( const str = textSegment.substr(index + length - name.length, name.length); if (str && str !== '') { texts.push(str); + } else { + // If there is no ".Name" then use the full path. You can see things like "typeof import("file")" + texts.push(getRelativePath(path)); } } else { const section = getApiSectionId({ @@ -69,10 +73,12 @@ export function extractImportReferences( apiPath: path, directory: plugin.directory, }), - section, - text: name, + section: name && name !== '' ? section : undefined, + text: name && name !== '' ? name : getRelativePath(path), }); } + + // Prep textSegment to skip past the `import`, then check for more. textSegment = textSegment.substr(index + length); } else { if (textSegment && textSegment !== '') { @@ -87,10 +93,10 @@ export function extractImportReferences( function extractImportRef( str: string ): { path: string; name: string; index: number; length: number } | undefined { - const groups = str.match(/import\("(.*?)"\)\.(\w*)/); + const groups = str.match(/import\("(.*?)"\)\.?(\w*)/); if (groups) { const path = groups[1]; - const name = groups[2]; + const name = groups.length > 2 ? groups[2] : ''; const index = groups.index!; const length = groups[0].length; return { path, name, index, length }; diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/utils.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/utils.ts index 9efa96b6e9676..76683d968b9c2 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/utils.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/utils.ts @@ -17,7 +17,7 @@ export function isPrivate(node: ParameterDeclaration | ClassMemberTypes): boolea /** * Change the absolute path into a relative one. */ -function getRelativePath(fullPath: string): string { +export function getRelativePath(fullPath: string): string { return Path.relative(REPO_ROOT, fullPath); } diff --git a/packages/kbn-docs-utils/src/api_docs/mdx/write_plugin_mdx_docs.ts b/packages/kbn-docs-utils/src/api_docs/mdx/write_plugin_mdx_docs.ts index b35515eb9d209..608c30f068357 100644 --- a/packages/kbn-docs-utils/src/api_docs/mdx/write_plugin_mdx_docs.ts +++ b/packages/kbn-docs-utils/src/api_docs/mdx/write_plugin_mdx_docs.ts @@ -84,7 +84,7 @@ import ${json} from './${fileName}.json'; common: groupPluginApi(doc.common), server: groupPluginApi(doc.server), }; - fs.writeFileSync(Path.resolve(folder, fileName + '.json'), JSON.stringify(scopedDoc)); + fs.writeFileSync(Path.resolve(folder, fileName + '.json'), JSON.stringify(scopedDoc, null, 2)); mdx += scopApiToMdx(scopedDoc.client, 'Client', json, 'client'); mdx += scopApiToMdx(scopedDoc.server, 'Server', json, 'server'); diff --git a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.json b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.json index db25b8c4f021e..ab605006d7e3d 100644 --- a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.json +++ b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.json @@ -1 +1,1566 @@ -{"id":"pluginA","client":{"classes":[{"id":"def-public.ExampleClass","type":"Class","label":"ExampleClass","description":[],"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ExampleClass","text":"ExampleClass"}," implements ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.AnotherInterface","text":"AnotherInterface"},""],"children":[{"id":"def-public.ExampleClass.component","type":"CompoundType","label":"component","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":30,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L30"},"signature":["React.ComponentClass<{}, any> | React.FunctionComponent<{}> | undefined"]},{"id":"def-public.ExampleClass.Unnamed","type":"Function","label":"Constructor","signature":["any"],"description":[],"children":[{"type":"Uncategorized","label":"t","isRequired":true,"signature":["T"],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":32,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L32"}}],"tags":[],"returnComment":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":32,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L32"}},{"id":"def-public.ExampleClass.arrowFn","type":"Function","children":[{"type":"CompoundType","label":"a","isRequired":true,"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"}],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":40,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L40"}}],"signature":["(a: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"},") => ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"}],"description":["\nan arrow fn on a class."],"label":"arrowFn","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":40,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L40"},"returnComment":[]},{"id":"def-public.ExampleClass.getVar","type":"Function","label":"getVar","signature":["(a: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"},") => string"],"description":["\nA function on a class."],"children":[{"type":"CompoundType","label":"a","isRequired":true,"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"}],"description":["a param"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":46,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L46"}}],"tags":[],"returnComment":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":46,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L46"}}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":24,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L24"},"initialIsOpen":false},{"id":"def-public.CrazyClass","type":"Class","label":"CrazyClass","description":[],"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.CrazyClass","text":"CrazyClass"},"

extends ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ExampleClass","text":"ExampleClass"},"<",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.WithGen","text":"WithGen"},"

>"],"children":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":51,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L51"},"initialIsOpen":false}],"functions":[{"id":"def-public.notAnArrowFn","type":"Function","label":"notAnArrowFn","signature":["(a: string, b: number | undefined, c: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},", d: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"},", e: string | undefined) => ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},""],"description":["\nThis is a non arrow function.\n"],"children":[{"type":"string","label":"a","isRequired":true,"signature":["string"],"description":["The letter A"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":22,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L22"}},{"type":"number","label":"b","isRequired":false,"signature":["number | undefined"],"description":["Feed me to the function"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":23,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L23"}},{"type":"Array","label":"c","isRequired":true,"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},""],"description":["So many params"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":24,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L24"}},{"type":"CompoundType","label":"d","isRequired":true,"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"}],"description":["a great param"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":25,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L25"}},{"type":"string","label":"e","isRequired":false,"signature":["string | undefined"],"description":["Another comment"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":26,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L26"}}],"tags":[],"returnComment":["something!"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":21,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L21"},"initialIsOpen":false},{"id":"def-public.arrowFn","type":"Function","children":[{"type":"string","label":"a","isRequired":true,"signature":["string"],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":42,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L42"}},{"type":"number","label":"b","isRequired":false,"signature":["number | undefined"],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":43,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L43"}},{"type":"Array","label":"c","isRequired":true,"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},""],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":44,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L44"}},{"type":"CompoundType","label":"d","isRequired":true,"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"}],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":45,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L45"}},{"type":"string","label":"e","isRequired":false,"signature":["string | undefined"],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":46,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L46"}}],"signature":["(a: string, b: number | undefined, c: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},", d: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"},", e?: string | undefined) => ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},""],"description":["\nThis is an arrow function.\n"],"label":"arrowFn","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":41,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L41"},"returnComment":["something!"],"initialIsOpen":false},{"id":"def-public.crazyFunction","type":"Function","children":[{"id":"def-public.crazyFunction.obj","type":"Object","label":"obj","description":[],"children":[{"id":"def-public.crazyFunction.obj.hi","type":"string","label":"hi","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":67,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L67"}}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":67,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L67"}},{"id":"def-public.crazyFunction.{-fn }","type":"Object","label":"{ fn }","description":[],"children":[{"id":"def-public.crazyFunction.{-fn }.fn","type":"Function","label":"fn","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":68,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L68"},"signature":["(foo: { param: string; }) => number"]}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":68,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L68"}},{"id":"def-public.crazyFunction.{-str }","type":"Object","label":"{ str }","description":[],"children":[{"id":"def-public.crazyFunction.{-str }.str","type":"string","label":"str","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":69,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L69"}}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":69,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L69"}}],"signature":["(obj: { hi: string; }, { fn }: { fn: (foo: { param: string; }) => number; }, { str }: { str: string; }) => () => () => number"],"description":["\nWho would write such a complicated function?? Ewwww.\n\nAccording to https://jsdoc.app/tags-param.html#parameters-with-properties,\nthis is how destructured arguements should be commented.\n"],"label":"crazyFunction","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":66,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L66"},"returnComment":["I have no idea."],"initialIsOpen":false},{"id":"def-public.fnWithNonExportedRef","type":"Function","children":[{"type":"Object","label":"a","isRequired":true,"signature":["ImNotExported"],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":76,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L76"}}],"signature":["(a: ImNotExported) => string"],"description":[],"label":"fnWithNonExportedRef","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":76,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L76"},"returnComment":[],"initialIsOpen":false}],"interfaces":[{"id":"def-public.SearchSpec","type":"Interface","label":"SearchSpec","description":["\nThe SearchSpec interface contains settings for creating a new SearchService, like\nusername and password."],"children":[{"id":"def-public.SearchSpec.username","type":"string","label":"username","description":["\nStores the username. Duh,"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":26,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L26"}},{"id":"def-public.SearchSpec.password","type":"string","label":"password","description":["\nStores the password. I hope it's encrypted!"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":30,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L30"}}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":22,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L22"},"initialIsOpen":false},{"id":"def-public.WithGen","type":"Interface","label":"WithGen","signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.WithGen","text":"WithGen"},""],"description":["\nAn interface with a generic."],"children":[{"id":"def-public.WithGen.t","type":"Uncategorized","label":"t","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":17,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L17"},"signature":["T"]}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":16,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L16"},"initialIsOpen":false},{"id":"def-public.AnotherInterface","type":"Interface","label":"AnotherInterface","signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.AnotherInterface","text":"AnotherInterface"},""],"description":[],"children":[{"id":"def-public.AnotherInterface.t","type":"Uncategorized","label":"t","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":21,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L21"},"signature":["T"]}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":20,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L20"},"initialIsOpen":false},{"id":"def-public.ExampleInterface","type":"Interface","label":"ExampleInterface","signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ExampleInterface","text":"ExampleInterface"}," extends ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.AnotherInterface","text":"AnotherInterface"},""],"description":["\nThis is an example interface so we can see how it appears inside the API\ndocumentation system."],"children":[{"id":"def-public.ExampleInterface.getAPromiseThatResolvesToString","type":"Function","label":"getAPromiseThatResolvesToString","description":["\nThis gets a promise that resolves to a string."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":61,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L61"},"signature":["() => Promise"]},{"id":"def-public.ExampleInterface.aFnWithGen","type":"Function","label":"aFnWithGen","description":["\nThis function takes a generic. It was sometimes being tripped on\nand returned as an unknown type with no signature."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":67,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L67"},"signature":["(t: T) => void"]},{"id":"def-public.ExampleInterface.aFn","type":"Function","label":"aFn","signature":["() => void"],"description":["\nThese are not coming back properly."],"children":[],"tags":[],"returnComment":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":72,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L72"}}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":57,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L57"},"initialIsOpen":false},{"id":"def-public.IReturnAReactComponent","type":"Interface","label":"IReturnAReactComponent","description":["\nAn interface that has a react component."],"children":[{"id":"def-public.IReturnAReactComponent.component","type":"CompoundType","label":"component","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":79,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L79"},"signature":["React.ComponentType<{}>"]}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":78,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L78"},"initialIsOpen":false},{"id":"def-public.ImAnObject","type":"Interface","label":"ImAnObject","description":[],"children":[{"id":"def-public.ImAnObject.foo","type":"Function","label":"foo","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":44,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L44"},"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.FnWithGeneric","text":"FnWithGeneric"}]}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":43,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L43"},"initialIsOpen":false}],"enums":[{"id":"def-public.DayOfWeek","type":"Enum","label":"DayOfWeek","description":["\nComments on enums."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":31,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L31"},"initialIsOpen":false}],"misc":[{"id":"def-public.imAnAny","type":"Any","label":"imAnAny","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts","lineNumber":19,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts#L19"},"signature":["any"],"initialIsOpen":false},{"id":"def-public.imAnUnknown","type":"Unknown","label":"imAnUnknown","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts","lineNumber":20,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts#L20"},"signature":["unknown"],"initialIsOpen":false},{"id":"def-public.NotAnArrowFnType","type":"Type","label":"NotAnArrowFnType","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":78,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L78"},"signature":["(a: string, b: number | undefined, c: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},", d: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"},", e: string | undefined) => ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},""],"initialIsOpen":false},{"id":"def-public.aUnionProperty","type":"CompoundType","label":"aUnionProperty","description":["\nThis is a complicated union type"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":51,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L51"},"signature":["string | number | (() => string) | ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.CrazyClass","text":"CrazyClass"},""],"initialIsOpen":false},{"id":"def-public.aStrArray","type":"Array","label":"aStrArray","description":["\nThis is an array of strings. The type is explicit."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":56,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L56"},"signature":["string[]"],"initialIsOpen":false},{"id":"def-public.aNumArray","type":"Array","label":"aNumArray","description":["\nThis is an array of numbers. The type is implied."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":61,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L61"},"signature":["number[]"],"initialIsOpen":false},{"id":"def-public.aStr","type":"string","label":"aStr","description":["\nA string that says hi to you!"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":66,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L66"},"initialIsOpen":false},{"id":"def-public.aNum","type":"number","label":"aNum","description":["\nIt's a number. A special number."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":71,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L71"},"signature":["10"],"initialIsOpen":false},{"id":"def-public.literalString","type":"string","label":"literalString","description":["\nI'm a type of string, but more specifically, a literal string type."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":76,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L76"},"signature":["\"HI\""],"initialIsOpen":false},{"id":"def-public.StringOrUndefinedType","type":"Type","label":"StringOrUndefinedType","description":["\nHow should a potentially undefined type show up."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":15,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L15"},"signature":["undefined | string"],"initialIsOpen":false},{"id":"def-public.TypeWithGeneric","type":"Type","label":"TypeWithGeneric","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":17,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L17"},"signature":["T[]"],"initialIsOpen":false},{"id":"def-public.ImAType","type":"Type","label":"ImAType","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":19,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L19"},"signature":["string | number | ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAFooPluginApi","section":"def-public.FooType","text":"FooType"}," | ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"}," | ",{"pluginId":"pluginA","scope":"common","docId":"kibPluginAPluginApi","section":"def-common.ImACommonType","text":"ImACommonType"}],"initialIsOpen":false},{"id":"def-public.FnWithGeneric","type":"Type","label":"FnWithGeneric","description":["\nThis is a type that defines a function.\n"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":26,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L26"},"signature":["(t: T) => ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},""],"initialIsOpen":false},{"id":"def-public.MultipleDeclarationsType","type":"Type","label":"MultipleDeclarationsType","description":["\nCalling node.getSymbol().getDeclarations() will return > 1 declaration."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":40,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L40"},"signature":["(typeof DayOfWeek)[]"],"initialIsOpen":false},{"id":"def-public.IRefANotExportedType","type":"Type","label":"IRefANotExportedType","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":42,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L42"},"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAFooPluginApi","section":"def-public.ImNotExportedFromIndex","text":"ImNotExportedFromIndex"}," | { zed: \"hi\"; }"],"initialIsOpen":false}],"objects":[{"id":"def-public.aPretendNamespaceObj","type":"Object","children":[{"id":"def-public.aPretendNamespaceObj.notAnArrowFn","type":"Function","label":"notAnArrowFn","description":["/**\n * The docs should show this inline comment.\n */"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":21,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L21"},"signature":["typeof ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.notAnArrowFn","text":"notAnArrowFn"}]},{"id":"def-public.aPretendNamespaceObj.aPropertyMisdirection","type":"Function","label":"aPropertyMisdirection","description":["/**\n * Should this comment show up?\n */"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":26,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L26"},"signature":["typeof ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.notAnArrowFn","text":"notAnArrowFn"}]},{"id":"def-public.aPretendNamespaceObj.aPropertyInlineFn","type":"Function","children":[{"type":"CompoundType","label":"a","isRequired":true,"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"}],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":31,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L31"}}],"signature":["(a: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"},") => ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"}],"description":["/**\n * I'm a property inline fun.\n */"],"label":"aPropertyInlineFn","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":31,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L31"},"returnComment":[]},{"id":"def-public.aPretendNamespaceObj.aPropertyStr","type":"string","label":"aPropertyStr","description":["/**\n * The only way for this to have a comment is to grab this.\n */"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":38,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L38"}},{"id":"def-public.aPretendNamespaceObj.nestedObj","type":"Object","children":[{"id":"def-public.aPretendNamespaceObj.nestedObj.foo","type":"string","label":"foo","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":44,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L44"}}],"description":["/**\n * Will this nested object have it's children extracted appropriately?\n */"],"label":"nestedObj","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":43,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L43"}}],"description":["\nSome of the plugins wrap static exports in an object to create\na namespace like this."],"label":"aPretendNamespaceObj","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":17,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L17"},"initialIsOpen":false}],"setup":{"id":"def-public.Setup","type":"Interface","label":"Setup","description":["\nAccess setup functionality from your plugin's setup function by adding the example\nplugin as a dependency.\n\n```ts\nClass MyPlugin {\n setup(core: CoreDependencies, { example }: PluginDependencies) {\n // Here you can access this functionality.\n example.getSearchService();\n }\n}\n```"],"children":[{"id":"def-public.Setup.getSearchService","type":"Function","label":"getSearchService","description":["\nA factory function that returns a new instance of Foo based\non the spec. We aren't sure if this is a good function so it's marked\nbeta. That should be clear in the docs because of the js doc tag.\n"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":96,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L96"},"signature":["(searchSpec: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.SearchSpec","text":"SearchSpec"},") => string"]},{"id":"def-public.Setup.getSearchService2","type":"Function","label":"getSearchService2","description":["\nThis uses an inlined object type rather than referencing an exported type, which is discouraged.\nprefer the way {@link getSearchService} is typed.\n"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":104,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L104"},"signature":["(searchSpec: { username: string; password: string; }) => string"]},{"id":"def-public.Setup.doTheThing","type":"Function","label":"doTheThing","description":["\nThis function does the thing and it's so good at it! But we decided to deprecate it\nanyway. I hope that's clear to developers in the docs!\n"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":117,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L117"},"signature":["(thingOne: number, thingTwo: string, thingThree: { nestedVar: number; }) => void"]},{"id":"def-public.Setup.fnWithInlineParams","type":"Function","label":"fnWithInlineParams","description":["\nWho would write such a complicated function?? Ew, how will the obj parameter appear in docs?\n"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":128,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L128"},"signature":["(obj: { fn: (foo: { param: string; }) => number; }) => () => { retFoo: () => string; }"]},{"id":"def-public.Setup.id","type":"string","label":"id","description":["\nHi, I'm a comment for an id string!"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":135,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L135"}}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":84,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L84"},"lifecycle":"setup","initialIsOpen":true},"start":{"id":"def-public.Start","type":"Interface","label":"Start","description":["\nAccess start functionality from your plugin's start function by adding the example\nplugin as a dependency.\n\n```ts\nClass MyPlugin {\n start(core: CoreDependencies, { example }: PluginDependencies) {\n // Here you can access this functionality.\n example.getSearchLanguage();\n }\n}\n```"],"children":[{"id":"def-public.Start.getSearchLanguage","type":"Function","label":"getSearchLanguage","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":68,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L68"},"signature":["() => ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.SearchLanguage","text":"SearchLanguage"}]}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":64,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L64"},"lifecycle":"start","initialIsOpen":true}},"server":{"classes":[],"functions":[],"interfaces":[],"enums":[],"misc":[],"objects":[]},"common":{"classes":[],"functions":[],"interfaces":[{"id":"def-common.ImACommonType","type":"Interface","label":"ImACommonType","description":[],"children":[{"id":"def-common.ImACommonType.goo","type":"number","label":"goo","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts","lineNumber":12,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts#L12"}}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts","lineNumber":11,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts#L11"},"initialIsOpen":false}],"enums":[],"misc":[],"objects":[]}} \ No newline at end of file +{ + "id": "pluginA", + "client": { + "classes": [ + { + "id": "def-public.ExampleClass", + "type": "Class", + "label": "ExampleClass", + "description": [], + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ExampleClass", + "text": "ExampleClass" + }, + " implements ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.AnotherInterface", + "text": "AnotherInterface" + }, + "" + ], + "children": [ + { + "id": "def-public.ExampleClass.component", + "type": "CompoundType", + "label": "component", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 30, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L30" + }, + "signature": [ + "React.ComponentClass<{}, any> | React.FunctionComponent<{}> | undefined" + ] + }, + { + "id": "def-public.ExampleClass.Unnamed", + "type": "Function", + "label": "Constructor", + "signature": [ + "any" + ], + "description": [], + "children": [ + { + "type": "Uncategorized", + "label": "t", + "isRequired": true, + "signature": [ + "T" + ], + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 32, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L32" + } + } + ], + "tags": [], + "returnComment": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 32, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L32" + } + }, + { + "id": "def-public.ExampleClass.arrowFn", + "type": "Function", + "children": [ + { + "type": "CompoundType", + "label": "a", + "isRequired": true, + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + } + ], + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 40, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L40" + } + } + ], + "signature": [ + "(a: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + }, + ") => ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + } + ], + "description": [ + "\nan arrow fn on a class." + ], + "label": "arrowFn", + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 40, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L40" + }, + "returnComment": [] + }, + { + "id": "def-public.ExampleClass.getVar", + "type": "Function", + "label": "getVar", + "signature": [ + "(a: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + }, + ") => string" + ], + "description": [ + "\nA function on a class." + ], + "children": [ + { + "type": "CompoundType", + "label": "a", + "isRequired": true, + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + } + ], + "description": [ + "a param" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 46, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L46" + } + } + ], + "tags": [], + "returnComment": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 46, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L46" + } + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 24, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L24" + }, + "initialIsOpen": false + }, + { + "id": "def-public.CrazyClass", + "type": "Class", + "label": "CrazyClass", + "description": [], + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.CrazyClass", + "text": "CrazyClass" + }, + "

extends ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ExampleClass", + "text": "ExampleClass" + }, + "<", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.WithGen", + "text": "WithGen" + }, + "

>" + ], + "children": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 51, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L51" + }, + "initialIsOpen": false + } + ], + "functions": [ + { + "id": "def-public.notAnArrowFn", + "type": "Function", + "label": "notAnArrowFn", + "signature": [ + "(a: string, b: number | undefined, c: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + ", d: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + }, + ", e: string | undefined) => ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + "" + ], + "description": [ + "\nThis is a non arrow function.\n" + ], + "children": [ + { + "type": "string", + "label": "a", + "isRequired": true, + "signature": [ + "string" + ], + "description": [ + "The letter A" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 22, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L22" + } + }, + { + "type": "number", + "label": "b", + "isRequired": false, + "signature": [ + "number | undefined" + ], + "description": [ + "Feed me to the function" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 23, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L23" + } + }, + { + "type": "Array", + "label": "c", + "isRequired": true, + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + "" + ], + "description": [ + "So many params" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 24, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L24" + } + }, + { + "type": "CompoundType", + "label": "d", + "isRequired": true, + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + } + ], + "description": [ + "a great param" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 25, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L25" + } + }, + { + "type": "string", + "label": "e", + "isRequired": false, + "signature": [ + "string | undefined" + ], + "description": [ + "Another comment" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 26, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L26" + } + } + ], + "tags": [], + "returnComment": [ + "something!" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 21, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L21" + }, + "initialIsOpen": false + }, + { + "id": "def-public.arrowFn", + "type": "Function", + "children": [ + { + "type": "string", + "label": "a", + "isRequired": true, + "signature": [ + "string" + ], + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 42, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L42" + } + }, + { + "type": "number", + "label": "b", + "isRequired": false, + "signature": [ + "number | undefined" + ], + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 43, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L43" + } + }, + { + "type": "Array", + "label": "c", + "isRequired": true, + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + "" + ], + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 44, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L44" + } + }, + { + "type": "CompoundType", + "label": "d", + "isRequired": true, + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + } + ], + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 45, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L45" + } + }, + { + "type": "string", + "label": "e", + "isRequired": false, + "signature": [ + "string | undefined" + ], + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 46, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L46" + } + } + ], + "signature": [ + "(a: string, b: number | undefined, c: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + ", d: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + }, + ", e?: string | undefined) => ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + "" + ], + "description": [ + "\nThis is an arrow function.\n" + ], + "label": "arrowFn", + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 41, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L41" + }, + "returnComment": [ + "something!" + ], + "initialIsOpen": false + }, + { + "id": "def-public.crazyFunction", + "type": "Function", + "children": [ + { + "id": "def-public.crazyFunction.obj", + "type": "Object", + "label": "obj", + "description": [], + "children": [ + { + "id": "def-public.crazyFunction.obj.hi", + "type": "string", + "label": "hi", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 67, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L67" + } + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 67, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L67" + } + }, + { + "id": "def-public.crazyFunction.{-fn }", + "type": "Object", + "label": "{ fn }", + "description": [], + "children": [ + { + "id": "def-public.crazyFunction.{-fn }.fn", + "type": "Function", + "label": "fn", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 68, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L68" + }, + "signature": [ + "(foo: { param: string; }) => number" + ] + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 68, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L68" + } + }, + { + "id": "def-public.crazyFunction.{-str }", + "type": "Object", + "label": "{ str }", + "description": [], + "children": [ + { + "id": "def-public.crazyFunction.{-str }.str", + "type": "string", + "label": "str", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 69, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L69" + } + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 69, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L69" + } + } + ], + "signature": [ + "(obj: { hi: string; }, { fn }: { fn: (foo: { param: string; }) => number; }, { str }: { str: string; }) => () => () => number" + ], + "description": [ + "\nWho would write such a complicated function?? Ewwww.\n\nAccording to https://jsdoc.app/tags-param.html#parameters-with-properties,\nthis is how destructured arguements should be commented.\n" + ], + "label": "crazyFunction", + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 66, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L66" + }, + "returnComment": [ + "I have no idea." + ], + "initialIsOpen": false + }, + { + "id": "def-public.fnWithNonExportedRef", + "type": "Function", + "children": [ + { + "type": "Object", + "label": "a", + "isRequired": true, + "signature": [ + "ImNotExported" + ], + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 76, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L76" + } + } + ], + "signature": [ + "(a: ImNotExported) => string" + ], + "description": [], + "label": "fnWithNonExportedRef", + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 76, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L76" + }, + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [ + { + "id": "def-public.SearchSpec", + "type": "Interface", + "label": "SearchSpec", + "description": [ + "\nThe SearchSpec interface contains settings for creating a new SearchService, like\nusername and password." + ], + "children": [ + { + "id": "def-public.SearchSpec.username", + "type": "string", + "label": "username", + "description": [ + "\nStores the username. Duh," + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 26, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L26" + } + }, + { + "id": "def-public.SearchSpec.password", + "type": "string", + "label": "password", + "description": [ + "\nStores the password. I hope it's encrypted!" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 30, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L30" + } + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 22, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L22" + }, + "initialIsOpen": false + }, + { + "id": "def-public.WithGen", + "type": "Interface", + "label": "WithGen", + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.WithGen", + "text": "WithGen" + }, + "" + ], + "description": [ + "\nAn interface with a generic." + ], + "children": [ + { + "id": "def-public.WithGen.t", + "type": "Uncategorized", + "label": "t", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 17, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L17" + }, + "signature": [ + "T" + ] + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 16, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L16" + }, + "initialIsOpen": false + }, + { + "id": "def-public.AnotherInterface", + "type": "Interface", + "label": "AnotherInterface", + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.AnotherInterface", + "text": "AnotherInterface" + }, + "" + ], + "description": [], + "children": [ + { + "id": "def-public.AnotherInterface.t", + "type": "Uncategorized", + "label": "t", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 21, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L21" + }, + "signature": [ + "T" + ] + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 20, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L20" + }, + "initialIsOpen": false + }, + { + "id": "def-public.ExampleInterface", + "type": "Interface", + "label": "ExampleInterface", + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ExampleInterface", + "text": "ExampleInterface" + }, + " extends ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.AnotherInterface", + "text": "AnotherInterface" + }, + "" + ], + "description": [ + "\nThis is an example interface so we can see how it appears inside the API\ndocumentation system." + ], + "children": [ + { + "id": "def-public.ExampleInterface.getAPromiseThatResolvesToString", + "type": "Function", + "label": "getAPromiseThatResolvesToString", + "description": [ + "\nThis gets a promise that resolves to a string." + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 61, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L61" + }, + "signature": [ + "() => Promise" + ] + }, + { + "id": "def-public.ExampleInterface.aFnWithGen", + "type": "Function", + "label": "aFnWithGen", + "description": [ + "\nThis function takes a generic. It was sometimes being tripped on\nand returned as an unknown type with no signature." + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 67, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L67" + }, + "signature": [ + "(t: T) => void" + ] + }, + { + "id": "def-public.ExampleInterface.aFn", + "type": "Function", + "label": "aFn", + "signature": [ + "() => void" + ], + "description": [ + "\nThese are not coming back properly." + ], + "children": [], + "tags": [], + "returnComment": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 72, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L72" + } + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 57, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L57" + }, + "initialIsOpen": false + }, + { + "id": "def-public.IReturnAReactComponent", + "type": "Interface", + "label": "IReturnAReactComponent", + "description": [ + "\nAn interface that has a react component." + ], + "children": [ + { + "id": "def-public.IReturnAReactComponent.component", + "type": "CompoundType", + "label": "component", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 79, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L79" + }, + "signature": [ + "React.ComponentType<{}>" + ] + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 78, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L78" + }, + "initialIsOpen": false + }, + { + "id": "def-public.ImAnObject", + "type": "Interface", + "label": "ImAnObject", + "description": [], + "children": [ + { + "id": "def-public.ImAnObject.foo", + "type": "Function", + "label": "foo", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 44, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L44" + }, + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.FnWithGeneric", + "text": "FnWithGeneric" + } + ] + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 43, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L43" + }, + "initialIsOpen": false + } + ], + "enums": [ + { + "id": "def-public.DayOfWeek", + "type": "Enum", + "label": "DayOfWeek", + "description": [ + "\nComments on enums." + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 31, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L31" + }, + "initialIsOpen": false + } + ], + "misc": [ + { + "id": "def-public.imAnAny", + "type": "Any", + "label": "imAnAny", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts", + "lineNumber": 19, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts#L19" + }, + "signature": [ + "any" + ], + "initialIsOpen": false + }, + { + "id": "def-public.imAnUnknown", + "type": "Unknown", + "label": "imAnUnknown", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts", + "lineNumber": 20, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts#L20" + }, + "signature": [ + "unknown" + ], + "initialIsOpen": false + }, + { + "id": "def-public.NotAnArrowFnType", + "type": "Type", + "label": "NotAnArrowFnType", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 78, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L78" + }, + "signature": [ + "(a: string, b: number | undefined, c: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + ", d: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + }, + ", e: string | undefined) => ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + "" + ], + "initialIsOpen": false + }, + { + "id": "def-public.aUnionProperty", + "type": "CompoundType", + "label": "aUnionProperty", + "description": [ + "\nThis is a complicated union type" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 51, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L51" + }, + "signature": [ + "string | number | (() => string) | ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.CrazyClass", + "text": "CrazyClass" + }, + "" + ], + "initialIsOpen": false + }, + { + "id": "def-public.aStrArray", + "type": "Array", + "label": "aStrArray", + "description": [ + "\nThis is an array of strings. The type is explicit." + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 56, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L56" + }, + "signature": [ + "string[]" + ], + "initialIsOpen": false + }, + { + "id": "def-public.aNumArray", + "type": "Array", + "label": "aNumArray", + "description": [ + "\nThis is an array of numbers. The type is implied." + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 61, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L61" + }, + "signature": [ + "number[]" + ], + "initialIsOpen": false + }, + { + "id": "def-public.aStr", + "type": "string", + "label": "aStr", + "description": [ + "\nA string that says hi to you!" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 66, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L66" + }, + "initialIsOpen": false + }, + { + "id": "def-public.aNum", + "type": "number", + "label": "aNum", + "description": [ + "\nIt's a number. A special number." + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 71, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L71" + }, + "signature": [ + "10" + ], + "initialIsOpen": false + }, + { + "id": "def-public.literalString", + "type": "string", + "label": "literalString", + "description": [ + "\nI'm a type of string, but more specifically, a literal string type." + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 76, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L76" + }, + "signature": [ + "\"HI\"" + ], + "initialIsOpen": false + }, + { + "id": "def-public.StringOrUndefinedType", + "type": "Type", + "label": "StringOrUndefinedType", + "description": [ + "\nHow should a potentially undefined type show up." + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 15, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L15" + }, + "signature": [ + "undefined | string" + ], + "initialIsOpen": false + }, + { + "id": "def-public.TypeWithGeneric", + "type": "Type", + "label": "TypeWithGeneric", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 17, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L17" + }, + "signature": [ + "T[]" + ], + "initialIsOpen": false + }, + { + "id": "def-public.ImAType", + "type": "Type", + "label": "ImAType", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 19, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L19" + }, + "signature": [ + "string | number | ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAFooPluginApi", + "section": "def-public.FooType", + "text": "FooType" + }, + " | ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + " | ", + { + "pluginId": "pluginA", + "scope": "common", + "docId": "kibPluginAPluginApi", + "section": "def-common.ImACommonType", + "text": "ImACommonType" + } + ], + "initialIsOpen": false + }, + { + "id": "def-public.FnWithGeneric", + "type": "Type", + "label": "FnWithGeneric", + "description": [ + "\nThis is a type that defines a function.\n" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 26, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L26" + }, + "signature": [ + "(t: T) => ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + "" + ], + "initialIsOpen": false + }, + { + "id": "def-public.MultipleDeclarationsType", + "type": "Type", + "label": "MultipleDeclarationsType", + "description": [ + "\nCalling node.getSymbol().getDeclarations() will return > 1 declaration." + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 40, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L40" + }, + "signature": [ + "(typeof DayOfWeek)[]" + ], + "initialIsOpen": false + }, + { + "id": "def-public.IRefANotExportedType", + "type": "Type", + "label": "IRefANotExportedType", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 42, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L42" + }, + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAFooPluginApi", + "section": "def-public.ImNotExportedFromIndex", + "text": "ImNotExportedFromIndex" + }, + " | { zed: \"hi\"; }" + ], + "initialIsOpen": false + } + ], + "objects": [ + { + "id": "def-public.aPretendNamespaceObj", + "type": "Object", + "children": [ + { + "id": "def-public.aPretendNamespaceObj.notAnArrowFn", + "type": "Function", + "label": "notAnArrowFn", + "description": [ + "/**\n * The docs should show this inline comment.\n */" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 21, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L21" + }, + "signature": [ + "typeof ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.notAnArrowFn", + "text": "notAnArrowFn" + } + ] + }, + { + "id": "def-public.aPretendNamespaceObj.aPropertyMisdirection", + "type": "Function", + "label": "aPropertyMisdirection", + "description": [ + "/**\n * Should this comment show up?\n */" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 26, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L26" + }, + "signature": [ + "typeof ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.notAnArrowFn", + "text": "notAnArrowFn" + } + ] + }, + { + "id": "def-public.aPretendNamespaceObj.aPropertyInlineFn", + "type": "Function", + "children": [ + { + "type": "CompoundType", + "label": "a", + "isRequired": true, + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + } + ], + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 31, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L31" + } + } + ], + "signature": [ + "(a: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + }, + ") => ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + } + ], + "description": [ + "/**\n * I'm a property inline fun.\n */" + ], + "label": "aPropertyInlineFn", + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 31, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L31" + }, + "returnComment": [] + }, + { + "id": "def-public.aPretendNamespaceObj.aPropertyStr", + "type": "string", + "label": "aPropertyStr", + "description": [ + "/**\n * The only way for this to have a comment is to grab this.\n */" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 38, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L38" + } + }, + { + "id": "def-public.aPretendNamespaceObj.nestedObj", + "type": "Object", + "children": [ + { + "id": "def-public.aPretendNamespaceObj.nestedObj.foo", + "type": "string", + "label": "foo", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 44, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L44" + } + } + ], + "description": [ + "/**\n * Will this nested object have it's children extracted appropriately?\n */" + ], + "label": "nestedObj", + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 43, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L43" + } + } + ], + "description": [ + "\nSome of the plugins wrap static exports in an object to create\na namespace like this." + ], + "label": "aPretendNamespaceObj", + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 17, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L17" + }, + "initialIsOpen": false + } + ], + "setup": { + "id": "def-public.Setup", + "type": "Interface", + "label": "Setup", + "description": [ + "\nAccess setup functionality from your plugin's setup function by adding the example\nplugin as a dependency.\n\n```ts\nClass MyPlugin {\n setup(core: CoreDependencies, { example }: PluginDependencies) {\n // Here you can access this functionality.\n example.getSearchService();\n }\n}\n```" + ], + "children": [ + { + "id": "def-public.Setup.getSearchService", + "type": "Function", + "label": "getSearchService", + "description": [ + "\nA factory function that returns a new instance of Foo based\non the spec. We aren't sure if this is a good function so it's marked\nbeta. That should be clear in the docs because of the js doc tag.\n" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 96, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L96" + }, + "signature": [ + "(searchSpec: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.SearchSpec", + "text": "SearchSpec" + }, + ") => string" + ] + }, + { + "id": "def-public.Setup.getSearchService2", + "type": "Function", + "label": "getSearchService2", + "description": [ + "\nThis uses an inlined object type rather than referencing an exported type, which is discouraged.\nprefer the way {@link getSearchService} is typed.\n" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 104, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L104" + }, + "signature": [ + "(searchSpec: { username: string; password: string; }) => string" + ] + }, + { + "id": "def-public.Setup.doTheThing", + "type": "Function", + "label": "doTheThing", + "description": [ + "\nThis function does the thing and it's so good at it! But we decided to deprecate it\nanyway. I hope that's clear to developers in the docs!\n" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 117, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L117" + }, + "signature": [ + "(thingOne: number, thingTwo: string, thingThree: { nestedVar: number; }) => void" + ] + }, + { + "id": "def-public.Setup.fnWithInlineParams", + "type": "Function", + "label": "fnWithInlineParams", + "description": [ + "\nWho would write such a complicated function?? Ew, how will the obj parameter appear in docs?\n" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 128, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L128" + }, + "signature": [ + "(obj: { fn: (foo: { param: string; }) => number; }) => () => { retFoo: () => string; }" + ] + }, + { + "id": "def-public.Setup.id", + "type": "string", + "label": "id", + "description": [ + "\nHi, I'm a comment for an id string!" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 135, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L135" + } + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 84, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L84" + }, + "lifecycle": "setup", + "initialIsOpen": true + }, + "start": { + "id": "def-public.Start", + "type": "Interface", + "label": "Start", + "description": [ + "\nAccess start functionality from your plugin's start function by adding the example\nplugin as a dependency.\n\n```ts\nClass MyPlugin {\n start(core: CoreDependencies, { example }: PluginDependencies) {\n // Here you can access this functionality.\n example.getSearchLanguage();\n }\n}\n```" + ], + "children": [ + { + "id": "def-public.Start.getSearchLanguage", + "type": "Function", + "label": "getSearchLanguage", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 68, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L68" + }, + "signature": [ + "() => ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.SearchLanguage", + "text": "SearchLanguage" + } + ] + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 64, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L64" + }, + "lifecycle": "start", + "initialIsOpen": true + } + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [ + { + "id": "def-common.ImACommonType", + "type": "Interface", + "label": "ImACommonType", + "description": [], + "children": [ + { + "id": "def-common.ImACommonType.goo", + "type": "number", + "label": "goo", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts", + "lineNumber": 12, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts#L12" + } + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts", + "lineNumber": 11, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts#L11" + }, + "initialIsOpen": false + } + ], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a_foo.json b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a_foo.json index 8b5ec5f3da960..2589948b54ff0 100644 --- a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a_foo.json +++ b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a_foo.json @@ -1 +1,77 @@ -{"id":"pluginA.foo","client":{"classes":[],"functions":[{"id":"def-public.doTheFooFnThing","type":"Function","children":[],"signature":["() => void"],"description":[],"label":"doTheFooFnThing","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts","lineNumber":9,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts#L9"},"returnComment":[],"initialIsOpen":false}],"interfaces":[],"enums":[],"misc":[{"id":"def-public.FooType","type":"Type","label":"FooType","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts","lineNumber":11,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts#L11"},"signature":["() => \"foo\""],"initialIsOpen":false}],"objects":[]},"server":{"classes":[],"functions":[],"interfaces":[],"enums":[],"misc":[],"objects":[]},"common":{"classes":[],"functions":[],"interfaces":[],"enums":[],"misc":[{"id":"def-common.commonFoo","type":"string","label":"commonFoo","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/foo/index.ts","lineNumber":9,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/foo/index.ts#L9"},"signature":["\"COMMON VAR!\""],"initialIsOpen":false}],"objects":[]}} \ No newline at end of file +{ + "id": "pluginA.foo", + "client": { + "classes": [], + "functions": [ + { + "id": "def-public.doTheFooFnThing", + "type": "Function", + "children": [], + "signature": [ + "() => void" + ], + "description": [], + "label": "doTheFooFnThing", + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts", + "lineNumber": 9, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts#L9" + }, + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [], + "enums": [], + "misc": [ + { + "id": "def-public.FooType", + "type": "Type", + "label": "FooType", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts", + "lineNumber": 11, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts#L11" + }, + "signature": [ + "() => \"foo\"" + ], + "initialIsOpen": false + } + ], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [ + { + "id": "def-common.commonFoo", + "type": "string", + "label": "commonFoo", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/foo/index.ts", + "lineNumber": 9, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/foo/index.ts#L9" + }, + "signature": [ + "\"COMMON VAR!\"" + ], + "initialIsOpen": false + } + ], + "objects": [] + } +} \ No newline at end of file diff --git a/packages/kbn-docs-utils/src/api_docs/types.ts b/packages/kbn-docs-utils/src/api_docs/types.ts index c41cd42e6b424..5468709206eec 100644 --- a/packages/kbn-docs-utils/src/api_docs/types.ts +++ b/packages/kbn-docs-utils/src/api_docs/types.ts @@ -97,7 +97,7 @@ export interface Reference { pluginId: string; scope: ApiScope; docId: string; - section: string; + section?: string; text: string; } diff --git a/packages/kbn-docs-utils/src/api_docs/utils.ts b/packages/kbn-docs-utils/src/api_docs/utils.ts index 34162aa330911..66cdfee8f233b 100644 --- a/packages/kbn-docs-utils/src/api_docs/utils.ts +++ b/packages/kbn-docs-utils/src/api_docs/utils.ts @@ -91,9 +91,6 @@ export function getPluginApiDocId( const cleanName = id.replace('.', '_'); if (serviceInfo) { const serviceName = getServiceForPath(serviceInfo.apiPath, serviceInfo.directory); - log.debug( - `Service for path ${serviceInfo.apiPath} and ${serviceInfo.directory} is ${serviceName}` - ); const serviceFolder = serviceInfo.serviceFolders?.find((f) => f === serviceName); if (serviceFolder) { diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh index f9c1e67c0540d..b685b32038f8e 100755 --- a/src/dev/ci_setup/setup.sh +++ b/src/dev/ci_setup/setup.sh @@ -87,3 +87,19 @@ if [ "$GIT_CHANGES" ]; then echo -e "$GIT_CHANGES\n" exit 1 fi + +### +### rebuild plugin api docs to ensure it's not out of date +### +echo " -- building api docs" +node scripts/build_api_docs + +### +### verify no api changes +### +GIT_CHANGES="$(git ls-files --modified)" +if [ "$GIT_CHANGES" ]; then + echo -e "\n${RED}ERROR: 'node scripts/build_api_docs' caused changes to the following files:${C_RESET}\n" + echo -e "$GIT_CHANGES\n" + exit 1 +fi \ No newline at end of file From e409405ccbcf22ebc33523d83be25d5ea8c1f259 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 25 Feb 2021 17:09:52 -0700 Subject: [PATCH 19/32] [Maps] fix selecting EMS basemap does not populate input (#92711) * [Maps] fix selecting EMS basemap does not populate input * tslint --- .../ems_tms_source/create_source_editor.tsx | 39 +++++++++++++++++++ .../ems_base_map_layer_wizard.tsx | 13 ++----- ...vice_select.js => tile_service_select.tsx} | 33 ++++++++++++---- 3 files changed, 68 insertions(+), 17 deletions(-) create mode 100644 x-pack/plugins/maps/public/classes/sources/ems_tms_source/create_source_editor.tsx rename x-pack/plugins/maps/public/classes/sources/ems_tms_source/{tile_service_select.js => tile_service_select.tsx} (78%) diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/create_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/create_source_editor.tsx new file mode 100644 index 0000000000000..16e4986f4d8a6 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/create_source_editor.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Component } from 'react'; +import { EuiPanel } from '@elastic/eui'; +import { EmsTmsSourceConfig, TileServiceSelect } from './tile_service_select'; + +interface Props { + onTileSelect: (sourceConfig: EmsTmsSourceConfig) => void; +} + +interface State { + config?: EmsTmsSourceConfig; +} + +export class CreateSourceEditor extends Component { + state: State = {}; + + componentDidMount() { + this._onTileSelect({ id: null, isAutoSelect: true }); + } + + _onTileSelect = (config: EmsTmsSourceConfig) => { + this.setState({ config }); + this.props.onTileSelect(config); + }; + + render() { + return ( + + + + ); + } +} diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx index 3cb707f377b70..859d8b95cef3f 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx @@ -7,14 +7,13 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiPanel } from '@elastic/eui'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; // @ts-ignore import { EMSTMSSource, getSourceTitle } from './ems_tms_source'; // @ts-ignore import { VectorTileLayer } from '../../layers/vector_tile_layer/vector_tile_layer'; -// @ts-ignore -import { TileServiceSelect } from './tile_service_select'; +import { EmsTmsSourceConfig } from './tile_service_select'; +import { CreateSourceEditor } from './create_source_editor'; import { getEMSSettings } from '../../../kibana_services'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; import { WorldMapLayerIcon } from '../../layers/icons/world_map_layer_icon'; @@ -45,18 +44,14 @@ export const emsBaseMapLayerWizardConfig: LayerWizard = { }, icon: WorldMapLayerIcon, renderWizard: ({ previewLayers }: RenderWizardArguments) => { - const onSourceConfigChange = (sourceConfig: unknown) => { + const onSourceConfigChange = (sourceConfig: EmsTmsSourceConfig) => { const layerDescriptor = VectorTileLayer.createDescriptor({ sourceDescriptor: EMSTMSSource.createDescriptor(sourceConfig), }); previewLayers([layerDescriptor]); }; - return ( - - - - ); + return ; }, title: getSourceTitle(), }; diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/tile_service_select.js b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/tile_service_select.tsx similarity index 78% rename from x-pack/plugins/maps/public/classes/sources/ems_tms_source/tile_service_select.js rename to x-pack/plugins/maps/public/classes/sources/ems_tms_source/tile_service_select.tsx index 42ff1789df8f4..5f0f406d53e86 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/tile_service_select.js +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/tile_service_select.tsx @@ -5,17 +5,34 @@ * 2.0. */ -import React from 'react'; -import { EuiSelect, EuiFormRow } from '@elastic/eui'; +import React, { ChangeEvent, Component } from 'react'; +import { EuiSelect, EuiSelectOption, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { getEmsTmsServices } from '../../../meta'; import { getEmsUnavailableMessage } from '../../../components/ems_unavailable_message'; -import { i18n } from '@kbn/i18n'; export const AUTO_SELECT = 'auto_select'; -export class TileServiceSelect extends React.Component { - state = { +export interface EmsTmsSourceConfig { + id: string | null; + isAutoSelect: boolean; +} + +interface Props { + config?: EmsTmsSourceConfig; + onTileSelect: (sourceConfig: EmsTmsSourceConfig) => void; +} + +interface State { + emsTmsOptions: EuiSelectOption[]; + hasLoaded: boolean; +} + +export class TileServiceSelect extends Component { + private _isMounted = false; + + state: State = { emsTmsOptions: [], hasLoaded: false, }; @@ -51,7 +68,7 @@ export class TileServiceSelect extends React.Component { this.setState({ emsTmsOptions, hasLoaded: true }); }; - _onChange = (e) => { + _onChange = (e: ChangeEvent) => { const value = e.target.value; const isAutoSelect = value === AUTO_SELECT; this.props.onTileSelect({ @@ -63,9 +80,9 @@ export class TileServiceSelect extends React.Component { render() { const helpText = this.state.emsTmsOptions.length === 0 ? getEmsUnavailableMessage() : null; - let selectedId; + let selectedId: string | undefined; if (this.props.config) { - selectedId = this.props.config.isAutoSelect ? AUTO_SELECT : this.props.config.id; + selectedId = this.props.config.isAutoSelect ? AUTO_SELECT : this.props.config.id!; } return ( From d0e9b5133c26a16aa90a3b50c6bce19e772e64c2 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 26 Feb 2021 00:48:47 +0000 Subject: [PATCH 20/32] chore(NA): bump bazelisk to v1.7.5 (#92905) --- .bazeliskversion | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.bazeliskversion b/.bazeliskversion index 661e7aeadf36f..6a126f402d53d 100644 --- a/.bazeliskversion +++ b/.bazeliskversion @@ -1 +1 @@ -1.7.3 +1.7.5 From 7aae6c5f50ea74a085e4321dfdea389c6d79d815 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Thu, 25 Feb 2021 21:56:06 -0500 Subject: [PATCH 21/32] [APM] Fix for default fields in correlations view (#91868) (#92090) * [APM] Fix for default fields in correlations view (#91868) * removes useCallback hook Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../apm/public/hooks/useLocalStorage.ts | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/apm/public/hooks/useLocalStorage.ts b/x-pack/plugins/apm/public/hooks/useLocalStorage.ts index dc07b89ec7807..502824135db2a 100644 --- a/x-pack/plugins/apm/public/hooks/useLocalStorage.ts +++ b/x-pack/plugins/apm/public/hooks/useLocalStorage.ts @@ -8,28 +8,10 @@ import { useState, useEffect } from 'react'; export function useLocalStorage(key: string, defaultValue: T) { - const [item, setItem] = useState(getFromStorage()); - - function getFromStorage() { - const storedItem = window.localStorage.getItem(key); - - let toStore: T = defaultValue; - - if (storedItem !== null) { - try { - toStore = JSON.parse(storedItem) as T; - } catch (err) { - window.localStorage.removeItem(key); - // eslint-disable-next-line no-console - console.log(`Unable to decode: ${key}`); - } - } - - return toStore; - } + const [item, setItem] = useState(getFromStorage(key, defaultValue)); const updateFromStorage = () => { - const storedItem = getFromStorage(); + const storedItem = getFromStorage(key, defaultValue); setItem(storedItem); }; @@ -51,5 +33,25 @@ export function useLocalStorage(key: string, defaultValue: T) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // item state must be updated with a new key or default value + useEffect(() => { + setItem(getFromStorage(key, defaultValue)); + }, [key, defaultValue]); + return [item, saveToStorage] as const; } + +function getFromStorage(keyName: string, defaultValue: T) { + const storedItem = window.localStorage.getItem(keyName); + + if (storedItem !== null) { + try { + return JSON.parse(storedItem) as T; + } catch (err) { + window.localStorage.removeItem(keyName); + // eslint-disable-next-line no-console + console.log(`Unable to decode: ${keyName}`); + } + } + return defaultValue; +} From 3d9139089f5652af6894f7cbf409068c2bc4a8bb Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 26 Feb 2021 02:58:55 +0000 Subject: [PATCH 22/32] skip flaky suite (#92114) --- x-pack/test/accessibility/apps/dashboard_edit_panel.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/accessibility/apps/dashboard_edit_panel.ts b/x-pack/test/accessibility/apps/dashboard_edit_panel.ts index c318c2d1c26a0..466eab6b6b336 100644 --- a/x-pack/test/accessibility/apps/dashboard_edit_panel.ts +++ b/x-pack/test/accessibility/apps/dashboard_edit_panel.ts @@ -20,7 +20,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PANEL_TITLE = 'Visualization PieChart'; - describe('Dashboard Edit Panel', () => { + // FLAKY: https://github.com/elastic/kibana/issues/92114 + describe.skip('Dashboard Edit Panel', () => { before(async () => { await esArchiver.load('dashboard/drilldowns'); await esArchiver.loadIfNeeded('logstash_functional'); From 4192ea71c33bdcb25e18e39f942b1e34edf582fc Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Fri, 26 Feb 2021 10:04:44 +0300 Subject: [PATCH 23/32] [Vega] Allow image loading without CORS policy by changing the default to crossOrigin=null (#91991) * changing the default to crossOrigin=null in Vega * Fix eslint Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../vis_type_vega/public/vega_view/vega_base_view.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index 14e4d6034c1c2..353273d1372e6 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -172,7 +172,7 @@ export class VegaBaseView { // Override URL sanitizer to prevent external data loading (if disabled) const vegaLoader = loader(); const originalSanitize = vegaLoader.sanitize.bind(vegaLoader); - vegaLoader.sanitize = (uri, options) => { + vegaLoader.sanitize = async (uri, options) => { if (uri.bypassToken === bypassToken) { // If uri has a bypass token, the uri was encoded by bypassExternalUrlCheck() above. // because user can only supply pure JSON data structure. @@ -189,7 +189,11 @@ export class VegaBaseView { }) ); } - return originalSanitize(uri, options); + const result = await originalSanitize(uri, options); + // This will allow Vega users to load images from any domain. + result.crossOrigin = null; + + return result; }; config.loader = vegaLoader; From a1d2d870d3afab10abc9404823495fea0263fb68 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 26 Feb 2021 10:37:24 +0100 Subject: [PATCH 24/32] [APM] Always allow access to Profiling via URL (#92889) --- .../service_details/service_detail_tabs.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx index 5c9d79f37cc57..b86f0d40de137 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx @@ -37,6 +37,7 @@ interface Tab { key: string; href: string; text: ReactNode; + hidden?: boolean; render: () => ReactNode; } @@ -126,6 +127,7 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) { const profilingTab = { key: 'profiling', href: useServiceProfilingHref({ serviceName }), + hidden: !config.profilingEnabled, text: ( @@ -167,22 +169,20 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) { tabs.push(metricsTab); } - tabs.push(serviceMapTab); - - if (config.profilingEnabled) { - tabs.push(profilingTab); - } + tabs.push(serviceMapTab, profilingTab); const selectedTab = tabs.find((serviceTab) => serviceTab.key === tab); return ( <> - {tabs.map(({ href, key, text }) => ( - - {text} - - ))} + {tabs + .filter((t) => !t.hidden) + .map(({ href, key, text }) => ( + + {text} + + ))}

From c2877a6d96791a9dd5498de80a75945b9e1c70fc Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 26 Feb 2021 15:35:43 +0200 Subject: [PATCH 25/32] [Security Solution][Case] Fix subcases bugs on detections and case view (#91836) Co-authored-by: Jonathan Buttner --- .../case/common/api/cases/commentable_case.ts | 35 -------- x-pack/plugins/case/common/api/cases/index.ts | 1 - x-pack/plugins/case/common/api/helpers.ts | 8 +- .../plugins/case/common/api/runtime_types.ts | 27 +++++- .../plugins/case/server/client/cases/types.ts | 2 +- .../case/server/client/cases/utils.test.ts | 2 + .../plugins/case/server/client/cases/utils.ts | 1 + .../case/server/client/comments/add.ts | 6 +- x-pack/plugins/case/server/client/types.ts | 3 +- .../server/common/models/commentable_case.ts | 63 +++++++++----- .../case/server/connectors/case/index.test.ts | 4 +- .../case/server/connectors/case/types.ts | 4 +- .../api/cases/comments/delete_all_comments.ts | 8 +- .../api/cases/comments/delete_comment.ts | 8 +- .../api/cases/comments/find_comments.ts | 6 +- .../api/cases/comments/get_all_comment.ts | 6 +- .../api/cases/comments/patch_comment.ts | 16 ++-- .../routes/api/cases/comments/post_comment.ts | 4 +- .../api/cases/sub_case/patch_sub_cases.ts | 4 +- .../case/server/scripts/sub_cases/index.ts | 11 +-- .../components/all_cases/expanded_row.tsx | 14 ++- .../cases/components/all_cases/index.tsx | 49 ++++++++--- .../all_cases/status_filter.test.tsx | 20 +++++ .../components/all_cases/status_filter.tsx | 9 +- .../components/all_cases/table_filters.tsx | 3 + .../components/case_action_bar/index.tsx | 24 +++--- .../cases/components/case_view/index.test.tsx | 80 ++++++++++++++++- .../cases/components/case_view/index.tsx | 69 ++++++++------- .../connectors/case/alert_fields.tsx | 4 +- .../connectors/case/existing_case.tsx | 8 +- .../cases/components/create/flyout.test.tsx | 14 +-- .../public/cases/components/create/flyout.tsx | 8 +- .../components/create/form_context.test.tsx | 86 +++++++++++++++++++ .../cases/components/create/form_context.tsx | 12 ++- .../public/cases/components/create/index.tsx | 2 +- .../cases/components/status/button.test.tsx | 2 +- .../public/cases/components/status/config.ts | 2 +- .../add_to_case_action.test.tsx | 69 ++++++++++++--- .../timeline_actions/add_to_case_action.tsx | 34 +++++--- .../use_all_cases_modal/all_cases_modal.tsx | 26 ++++-- .../use_all_cases_modal/index.test.tsx | 2 +- .../components/use_all_cases_modal/index.tsx | 12 ++- .../create_case_modal.test.tsx | 6 +- .../create_case_modal.tsx | 2 +- .../use_create_case_modal/index.test.tsx | 6 +- .../use_create_case_modal/index.tsx | 2 +- .../user_action_tree/index.test.tsx | 6 +- .../components/user_action_tree/index.tsx | 15 ++-- .../cases/containers/use_get_case.test.tsx | 8 +- .../public/cases/containers/use_get_case.tsx | 34 +------- .../cases/containers/use_post_comment.tsx | 2 +- .../public/cases/translations.ts | 7 ++ .../alerts/use_query.test.tsx | 2 +- .../detection_engine/alerts/use_query.tsx | 2 +- .../flyout/add_to_case_button/index.tsx | 4 +- .../tests/cases/comments/delete_comment.ts | 14 +-- .../tests/cases/comments/find_comments.ts | 4 +- .../tests/cases/comments/get_all_comments.ts | 4 +- .../basic/tests/cases/comments/get_comment.ts | 2 +- .../tests/cases/comments/patch_comment.ts | 39 ++++----- .../tests/cases/comments/post_comment.ts | 4 +- .../basic/tests/cases/delete_cases.ts | 16 ++-- .../basic/tests/cases/find_cases.ts | 6 +- .../tests/cases/sub_cases/delete_sub_cases.ts | 20 ++--- .../tests/cases/sub_cases/find_sub_cases.ts | 12 +-- .../tests/cases/sub_cases/get_sub_case.ts | 16 ++-- .../tests/cases/sub_cases/patch_sub_cases.ts | 26 +++--- .../case_api_integration/common/lib/mock.ts | 18 ++-- .../case_api_integration/common/lib/utils.ts | 4 +- 69 files changed, 683 insertions(+), 366 deletions(-) delete mode 100644 x-pack/plugins/case/common/api/cases/commentable_case.ts diff --git a/x-pack/plugins/case/common/api/cases/commentable_case.ts b/x-pack/plugins/case/common/api/cases/commentable_case.ts deleted file mode 100644 index 023229a90d352..0000000000000 --- a/x-pack/plugins/case/common/api/cases/commentable_case.ts +++ /dev/null @@ -1,35 +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 * as rt from 'io-ts'; -import { CaseAttributesRt } from './case'; -import { CommentResponseRt } from './comment'; -import { SubCaseAttributesRt, SubCaseResponseRt } from './sub_case'; - -export const CollectionSubCaseAttributesRt = rt.intersection([ - rt.partial({ subCase: SubCaseAttributesRt }), - rt.type({ - case: CaseAttributesRt, - }), -]); - -export const CollectWithSubCaseResponseRt = rt.intersection([ - CaseAttributesRt, - rt.type({ - id: rt.string, - totalComment: rt.number, - version: rt.string, - }), - rt.partial({ - subCase: SubCaseResponseRt, - totalAlerts: rt.number, - comments: rt.array(CommentResponseRt), - }), -]); - -export type CollectionWithSubCaseResponse = rt.TypeOf; -export type CollectionWithSubCaseAttributes = rt.TypeOf; diff --git a/x-pack/plugins/case/common/api/cases/index.ts b/x-pack/plugins/case/common/api/cases/index.ts index 4d1fc68109ddb..6e7fb818cb2b5 100644 --- a/x-pack/plugins/case/common/api/cases/index.ts +++ b/x-pack/plugins/case/common/api/cases/index.ts @@ -11,4 +11,3 @@ export * from './comment'; export * from './status'; export * from './user_actions'; export * from './sub_case'; -export * from './commentable_case'; diff --git a/x-pack/plugins/case/common/api/helpers.ts b/x-pack/plugins/case/common/api/helpers.ts index 00c8ff402c802..43e292b91db4b 100644 --- a/x-pack/plugins/case/common/api/helpers.ts +++ b/x-pack/plugins/case/common/api/helpers.ts @@ -24,8 +24,8 @@ export const getSubCasesUrl = (caseID: string): string => { return SUB_CASES_URL.replace('{case_id}', caseID); }; -export const getSubCaseDetailsUrl = (caseID: string, subCaseID: string): string => { - return SUB_CASE_DETAILS_URL.replace('{case_id}', caseID).replace('{sub_case_id}', subCaseID); +export const getSubCaseDetailsUrl = (caseID: string, subCaseId: string): string => { + return SUB_CASE_DETAILS_URL.replace('{case_id}', caseID).replace('{sub_case_id}', subCaseId); }; export const getCaseCommentsUrl = (id: string): string => { @@ -40,8 +40,8 @@ export const getCaseUserActionUrl = (id: string): string => { return CASE_USER_ACTIONS_URL.replace('{case_id}', id); }; -export const getSubCaseUserActionUrl = (caseID: string, subCaseID: string): string => { - return SUB_CASE_USER_ACTIONS_URL.replace('{case_id}', caseID).replace('{sub_case_id}', subCaseID); +export const getSubCaseUserActionUrl = (caseID: string, subCaseId: string): string => { + return SUB_CASE_USER_ACTIONS_URL.replace('{case_id}', caseID).replace('{sub_case_id}', subCaseId); }; export const getCasePushUrl = (caseId: string, connectorId: string): string => { diff --git a/x-pack/plugins/case/common/api/runtime_types.ts b/x-pack/plugins/case/common/api/runtime_types.ts index 43e3be04d10e5..b2ff763838287 100644 --- a/x-pack/plugins/case/common/api/runtime_types.ts +++ b/x-pack/plugins/case/common/api/runtime_types.ts @@ -9,14 +9,37 @@ import { either, fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import * as rt from 'io-ts'; -import { failure } from 'io-ts/lib/PathReporter'; +import { isObject } from 'lodash/fp'; type ErrorFactory = (message: string) => Error; +export const formatErrors = (errors: rt.Errors): string[] => { + const err = errors.map((error) => { + if (error.message != null) { + return error.message; + } else { + const keyContext = error.context + .filter( + (entry) => entry.key != null && !Number.isInteger(+entry.key) && entry.key.trim() !== '' + ) + .map((entry) => entry.key) + .join(','); + + const nameContext = error.context.find((entry) => entry.type?.name?.length > 0); + const suppliedValue = + keyContext !== '' ? keyContext : nameContext != null ? nameContext.type.name : ''; + const value = isObject(error.value) ? JSON.stringify(error.value) : error.value; + return `Invalid value "${value}" supplied to "${suppliedValue}"`; + } + }); + + return [...new Set(err)]; +}; + export const createPlainError = (message: string) => new Error(message); export const throwErrors = (createError: ErrorFactory) => (errors: rt.Errors) => { - throw createError(failure(errors).join('\n')); + throw createError(formatErrors(errors).join()); }; export const decodeOrThrow = ( diff --git a/x-pack/plugins/case/server/client/cases/types.ts b/x-pack/plugins/case/server/client/cases/types.ts index 2dd2caf9fe73a..f1d56e7132bd1 100644 --- a/x-pack/plugins/case/server/client/cases/types.ts +++ b/x-pack/plugins/case/server/client/cases/types.ts @@ -72,7 +72,7 @@ export interface TransformFieldsArgs { export interface ExternalServiceComment { comment: string; - commentId?: string; + commentId: string; } export interface MapIncident { diff --git a/x-pack/plugins/case/server/client/cases/utils.test.ts b/x-pack/plugins/case/server/client/cases/utils.test.ts index 44e7a682aa7ed..859114a5e8fb0 100644 --- a/x-pack/plugins/case/server/client/cases/utils.test.ts +++ b/x-pack/plugins/case/server/client/cases/utils.test.ts @@ -540,6 +540,7 @@ describe('utils', () => { }, { comment: 'Elastic Security Alerts attached to the case: 3', + commentId: 'mock-id-1-total-alerts', }, ]); }); @@ -569,6 +570,7 @@ describe('utils', () => { }, { comment: 'Elastic Security Alerts attached to the case: 4', + commentId: 'mock-id-1-total-alerts', }, ]); }); diff --git a/x-pack/plugins/case/server/client/cases/utils.ts b/x-pack/plugins/case/server/client/cases/utils.ts index a5013d9b93982..67d5ef55f83c3 100644 --- a/x-pack/plugins/case/server/client/cases/utils.ts +++ b/x-pack/plugins/case/server/client/cases/utils.ts @@ -185,6 +185,7 @@ export const createIncident = async ({ if (totalAlerts > 0) { comments.push({ comment: `Elastic Security Alerts attached to the case: ${totalAlerts}`, + commentId: `${theCase.id}-total-alerts`, }); } diff --git a/x-pack/plugins/case/server/client/comments/add.ts b/x-pack/plugins/case/server/client/comments/add.ts index 0a86c1825fedc..4c1cc59a95750 100644 --- a/x-pack/plugins/case/server/client/comments/add.ts +++ b/x-pack/plugins/case/server/client/comments/add.ts @@ -25,7 +25,7 @@ import { CaseType, SubCaseAttributes, CommentRequest, - CollectionWithSubCaseResponse, + CaseResponse, User, CommentRequestAlertType, AlertCommentRequestRt, @@ -113,7 +113,7 @@ const addGeneratedAlerts = async ({ caseClient, caseId, comment, -}: AddCommentFromRuleArgs): Promise => { +}: AddCommentFromRuleArgs): Promise => { const query = pipe( AlertCommentRequestRt.decode(comment), fold(throwErrors(Boom.badRequest), identity) @@ -260,7 +260,7 @@ export const addComment = async ({ caseId, comment, user, -}: AddCommentArgs): Promise => { +}: AddCommentArgs): Promise => { const query = pipe( CommentRequestRt.decode(comment), fold(throwErrors(Boom.badRequest), identity) diff --git a/x-pack/plugins/case/server/client/types.ts b/x-pack/plugins/case/server/client/types.ts index ba5677426c222..d6a8f6b5d706c 100644 --- a/x-pack/plugins/case/server/client/types.ts +++ b/x-pack/plugins/case/server/client/types.ts @@ -13,7 +13,6 @@ import { CasesPatchRequest, CasesResponse, CaseStatuses, - CollectionWithSubCaseResponse, CommentRequest, ConnectorMappingsAttributes, GetFieldsResponse, @@ -89,7 +88,7 @@ export interface ConfigureFields { * This represents the interface that other plugins can access. */ export interface CaseClient { - addComment(args: CaseClientAddComment): Promise; + addComment(args: CaseClientAddComment): Promise; create(theCase: CasePostRequest): Promise; get(args: CaseClientGet): Promise; getAlerts(args: CaseClientGetAlerts): Promise; diff --git a/x-pack/plugins/case/server/common/models/commentable_case.ts b/x-pack/plugins/case/server/common/models/commentable_case.ts index 9827118ee8e29..3ae225999db4e 100644 --- a/x-pack/plugins/case/server/common/models/commentable_case.ts +++ b/x-pack/plugins/case/server/common/models/commentable_case.ts @@ -17,8 +17,8 @@ import { CaseSettings, CaseStatuses, CaseType, - CollectionWithSubCaseResponse, - CollectWithSubCaseResponseRt, + CaseResponse, + CaseResponseRt, CommentAttributes, CommentPatchRequest, CommentRequest, @@ -254,7 +254,7 @@ export class CommentableCase { }; } - public async encode(): Promise { + public async encode(): Promise { const collectionCommentStats = await this.service.getAllCaseComments({ client: this.soClient, id: this.collection.id, @@ -265,22 +265,6 @@ export class CommentableCase { }, }); - if (this.subCase) { - const subCaseComments = await this.service.getAllSubCaseComments({ - client: this.soClient, - id: this.subCase.id, - }); - - return CollectWithSubCaseResponseRt.encode({ - subCase: flattenSubCaseSavedObject({ - savedObject: this.subCase, - comments: subCaseComments.saved_objects, - totalAlerts: countAlertsForID({ comments: subCaseComments, id: this.subCase.id }), - }), - ...this.formatCollectionForEncoding(collectionCommentStats.total), - }); - } - const collectionComments = await this.service.getAllCaseComments({ client: this.soClient, id: this.collection.id, @@ -291,10 +275,45 @@ export class CommentableCase { }, }); - return CollectWithSubCaseResponseRt.encode({ + const collectionTotalAlerts = + countAlertsForID({ comments: collectionComments, id: this.collection.id }) ?? 0; + + const caseResponse = { comments: flattenCommentSavedObjects(collectionComments.saved_objects), - totalAlerts: countAlertsForID({ comments: collectionComments, id: this.collection.id }), + totalAlerts: collectionTotalAlerts, ...this.formatCollectionForEncoding(collectionCommentStats.total), - }); + }; + + if (this.subCase) { + const subCaseComments = await this.service.getAllSubCaseComments({ + client: this.soClient, + id: this.subCase.id, + }); + const totalAlerts = countAlertsForID({ comments: subCaseComments, id: this.subCase.id }) ?? 0; + + return CaseResponseRt.encode({ + ...caseResponse, + /** + * For now we need the sub case comments and totals to be exposed on the top level of the response so that the UI + * functionality can stay the same. Ideally in the future we can refactor this so that the UI will look for the + * comments either in the top level for a case or a collection or in the subCases field if it is a sub case. + * + * If we ever need to return both the collection's comments and the sub case comments we'll need to refactor it then + * as well. + */ + comments: flattenCommentSavedObjects(subCaseComments.saved_objects), + totalComment: subCaseComments.saved_objects.length, + totalAlerts, + subCases: [ + flattenSubCaseSavedObject({ + savedObject: this.subCase, + totalComment: subCaseComments.saved_objects.length, + totalAlerts, + }), + ], + }); + } + + return CaseResponseRt.encode(caseResponse); } } diff --git a/x-pack/plugins/case/server/connectors/case/index.test.ts b/x-pack/plugins/case/server/connectors/case/index.test.ts index 4be519858db18..e4c29bb099f0e 100644 --- a/x-pack/plugins/case/server/connectors/case/index.test.ts +++ b/x-pack/plugins/case/server/connectors/case/index.test.ts @@ -18,7 +18,6 @@ import { AssociationType, CaseResponse, CasesResponse, - CollectionWithSubCaseResponse, } from '../../../common/api'; import { connectorMappingsServiceMock, @@ -1018,9 +1017,10 @@ describe('case connector', () => { describe('addComment', () => { it('executes correctly', async () => { - const commentReturn: CollectionWithSubCaseResponse = { + const commentReturn: CaseResponse = { id: 'mock-it', totalComment: 0, + totalAlerts: 0, version: 'WzksMV0=', closed_at: null, diff --git a/x-pack/plugins/case/server/connectors/case/types.ts b/x-pack/plugins/case/server/connectors/case/types.ts index 50ff104d7bad0..6a7dfd9c2e687 100644 --- a/x-pack/plugins/case/server/connectors/case/types.ts +++ b/x-pack/plugins/case/server/connectors/case/types.ts @@ -16,7 +16,7 @@ import { ConnectorSchema, CommentSchema, } from './schema'; -import { CaseResponse, CasesResponse, CollectionWithSubCaseResponse } from '../../../common/api'; +import { CaseResponse, CasesResponse } from '../../../common/api'; export type CaseConfiguration = TypeOf; export type Connector = TypeOf; @@ -29,7 +29,7 @@ export type ExecutorSubActionAddCommentParams = TypeOf< >; export type CaseExecutorParams = TypeOf; -export type CaseExecutorResponse = CaseResponse | CasesResponse | CollectionWithSubCaseResponse; +export type CaseExecutorResponse = CaseResponse | CasesResponse; export type CaseActionType = ActionType< CaseConfiguration, diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts index bcbf1828e1fde..e0b3a4420f4b5 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts @@ -23,7 +23,7 @@ export function initDeleteAllCommentsApi({ caseService, router, userActionServic }), query: schema.maybe( schema.object({ - subCaseID: schema.maybe(schema.string()), + subCaseId: schema.maybe(schema.string()), }) ), }, @@ -35,11 +35,11 @@ export function initDeleteAllCommentsApi({ caseService, router, userActionServic const { username, full_name, email } = await caseService.getUser({ request }); const deleteDate = new Date().toISOString(); - const id = request.query?.subCaseID ?? request.params.case_id; + const id = request.query?.subCaseId ?? request.params.case_id; const comments = await caseService.getCommentsByAssociation({ client, id, - associationType: request.query?.subCaseID + associationType: request.query?.subCaseId ? AssociationType.subCase : AssociationType.case, }); @@ -61,7 +61,7 @@ export function initDeleteAllCommentsApi({ caseService, router, userActionServic actionAt: deleteDate, actionBy: { username, full_name, email }, caseId: request.params.case_id, - subCaseId: request.query?.subCaseID, + subCaseId: request.query?.subCaseId, commentId: comment.id, fields: ['comment'], }) diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts index 73307753a550d..cae0809ea5f0b 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts @@ -25,7 +25,7 @@ export function initDeleteCommentApi({ caseService, router, userActionService }: }), query: schema.maybe( schema.object({ - subCaseID: schema.maybe(schema.string()), + subCaseId: schema.maybe(schema.string()), }) ), }, @@ -46,8 +46,8 @@ export function initDeleteCommentApi({ caseService, router, userActionService }: throw Boom.notFound(`This comment ${request.params.comment_id} does not exist anymore.`); } - const type = request.query?.subCaseID ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; - const id = request.query?.subCaseID ?? request.params.case_id; + const type = request.query?.subCaseId ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; + const id = request.query?.subCaseId ?? request.params.case_id; const caseRef = myComment.references.find((c) => c.type === type); if (caseRef == null || (caseRef != null && caseRef.id !== id)) { @@ -69,7 +69,7 @@ export function initDeleteCommentApi({ caseService, router, userActionService }: actionAt: deleteDate, actionBy: { username, full_name, email }, caseId: id, - subCaseId: request.query?.subCaseID, + subCaseId: request.query?.subCaseId, commentId: request.params.comment_id, fields: ['comment'], }), diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts b/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts index 3431c340c791e..0ec0f1871c7ad 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts @@ -27,7 +27,7 @@ import { defaultPage, defaultPerPage } from '../..'; const FindQueryParamsRt = rt.partial({ ...SavedObjectFindOptionsRt.props, - subCaseID: rt.string, + subCaseId: rt.string, }); export function initFindCaseCommentsApi({ caseService, router }: RouteDeps) { @@ -49,8 +49,8 @@ export function initFindCaseCommentsApi({ caseService, router }: RouteDeps) { fold(throwErrors(Boom.badRequest), identity) ); - const id = query.subCaseID ?? request.params.case_id; - const associationType = query.subCaseID ? AssociationType.subCase : AssociationType.case; + const id = query.subCaseId ?? request.params.case_id; + const associationType = query.subCaseId ? AssociationType.subCase : AssociationType.case; const args = query ? { caseService, diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts index 730b1b92a8a07..8bf49ec3e27a1 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts @@ -25,7 +25,7 @@ export function initGetAllCommentsApi({ caseService, router }: RouteDeps) { query: schema.maybe( schema.object({ includeSubCaseComments: schema.maybe(schema.boolean()), - subCaseID: schema.maybe(schema.string()), + subCaseId: schema.maybe(schema.string()), }) ), }, @@ -35,10 +35,10 @@ export function initGetAllCommentsApi({ caseService, router }: RouteDeps) { const client = context.core.savedObjects.client; let comments: SavedObjectsFindResponse; - if (request.query?.subCaseID) { + if (request.query?.subCaseId) { comments = await caseService.getAllSubCaseComments({ client, - id: request.query.subCaseID, + id: request.query.subCaseId, options: { sortField: defaultSortField, }, diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts index e8b6f7bc957eb..01b0e17464053 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts @@ -26,11 +26,11 @@ interface CombinedCaseParams { service: CaseServiceSetup; client: SavedObjectsClientContract; caseID: string; - subCaseID?: string; + subCaseId?: string; } -async function getCommentableCase({ service, client, caseID, subCaseID }: CombinedCaseParams) { - if (subCaseID) { +async function getCommentableCase({ service, client, caseID, subCaseId }: CombinedCaseParams) { + if (subCaseId) { const [caseInfo, subCase] = await Promise.all([ service.getCase({ client, @@ -38,7 +38,7 @@ async function getCommentableCase({ service, client, caseID, subCaseID }: Combin }), service.getSubCase({ client, - id: subCaseID, + id: subCaseId, }), ]); return new CommentableCase({ collection: caseInfo, service, subCase, soClient: client }); @@ -66,7 +66,7 @@ export function initPatchCommentApi({ }), query: schema.maybe( schema.object({ - subCaseID: schema.maybe(schema.string()), + subCaseId: schema.maybe(schema.string()), }) ), body: escapeHatch, @@ -87,7 +87,7 @@ export function initPatchCommentApi({ service: caseService, client, caseID: request.params.case_id, - subCaseID: request.query?.subCaseID, + subCaseId: request.query?.subCaseId, }); const myComment = await caseService.getComment({ @@ -103,7 +103,7 @@ export function initPatchCommentApi({ throw Boom.badRequest(`You cannot change the type of the comment.`); } - const saveObjType = request.query?.subCaseID ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; + const saveObjType = request.query?.subCaseId ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; const caseRef = myComment.references.find((c) => c.type === saveObjType); if (caseRef == null || (caseRef != null && caseRef.id !== commentableCase.id)) { @@ -144,7 +144,7 @@ export function initPatchCommentApi({ actionAt: updatedDate, actionBy: { username, full_name, email }, caseId: request.params.case_id, - subCaseId: request.query?.subCaseID, + subCaseId: request.query?.subCaseId, commentId: updatedComment.id, fields: ['comment'], newValue: JSON.stringify(queryRestAttributes), diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts index 95b611950bd41..607f3f381f067 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts @@ -21,7 +21,7 @@ export function initPostCommentApi({ router }: RouteDeps) { }), query: schema.maybe( schema.object({ - subCaseID: schema.maybe(schema.string()), + subCaseId: schema.maybe(schema.string()), }) ), body: escapeHatch, @@ -33,7 +33,7 @@ export function initPostCommentApi({ router }: RouteDeps) { } const caseClient = context.case.getCaseClient(); - const caseId = request.query?.subCaseID ?? request.params.case_id; + const caseId = request.query?.subCaseId ?? request.params.case_id; const comment = request.body as CommentRequest; try { diff --git a/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts b/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts index ca5cd657a39f3..4b8e4920852c2 100644 --- a/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts @@ -153,8 +153,8 @@ async function getParentCases({ return parentCases.saved_objects.reduce((acc, so) => { const subCaseIDsWithParent = parentIDInfo.parentIDToSubID.get(so.id); - subCaseIDsWithParent?.forEach((subCaseID) => { - acc.set(subCaseID, so); + subCaseIDsWithParent?.forEach((subCaseId) => { + acc.set(subCaseId, so); }); return acc; }, new Map>()); diff --git a/x-pack/plugins/case/server/scripts/sub_cases/index.ts b/x-pack/plugins/case/server/scripts/sub_cases/index.ts index f6ec1f44076e4..ba3bcaa65091c 100644 --- a/x-pack/plugins/case/server/scripts/sub_cases/index.ts +++ b/x-pack/plugins/case/server/scripts/sub_cases/index.ts @@ -8,12 +8,7 @@ import yargs from 'yargs'; import { ToolingLog } from '@kbn/dev-utils'; import { KbnClient } from '@kbn/test'; -import { - CaseResponse, - CaseType, - CollectionWithSubCaseResponse, - ConnectorTypes, -} from '../../../common/api'; +import { CaseResponse, CaseType, ConnectorTypes } from '../../../common/api'; import { CommentType } from '../../../common/api/cases/comment'; import { CASES_URL } from '../../../common/constants'; import { ActionResult, ActionTypeExecutorResult } from '../../../../actions/common'; @@ -119,9 +114,7 @@ async function handleGenGroupAlerts(argv: any) { ), }; - const executeResp = await client.request< - ActionTypeExecutorResult - >({ + const executeResp = await client.request>({ path: `/api/actions/action/${createdAction.data.id}/_execute`, method: 'POST', body: { diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/expanded_row.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/expanded_row.tsx index bb4bd0f98949d..1e1e925a20ada 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/expanded_row.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/expanded_row.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiBasicTable as _EuiBasicTable } from '@elastic/eui'; import styled from 'styled-components'; -import { Case } from '../../containers/types'; +import { Case, SubCase } from '../../containers/types'; import { CasesColumns } from './columns'; import { AssociationType } from '../../../../../case/common/api'; @@ -34,14 +34,25 @@ BasicTable.displayName = 'BasicTable'; export const getExpandedRowMap = ({ data, columns, + isModal, + onSubCaseClick, }: { data: Case[] | null; columns: CasesColumns[]; + isModal: boolean; + onSubCaseClick?: (theSubCase: SubCase) => void; }): ExpandedRowMap => { if (data == null) { return {}; } + const rowProps = (theSubCase: SubCase) => { + return { + ...(isModal && onSubCaseClick ? { onClick: () => onSubCaseClick(theSubCase) } : {}), + className: 'subCase', + }; + }; + return data.reduce((acc, curr) => { if (curr.subCases != null) { const subCases = curr.subCases.map((subCase, index) => ({ @@ -58,6 +69,7 @@ export const getExpandedRowMap = ({ data-test-subj={`sub-cases-table-${curr.id}`} itemId="id" items={subCases} + rowProps={rowProps} /> ), }; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx index ce0fea07bf473..56dcf3bc28757 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx @@ -19,11 +19,12 @@ import { import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; import { isEmpty, memoize } from 'lodash/fp'; import styled, { css } from 'styled-components'; -import * as i18n from './translations'; +import classnames from 'classnames'; -import { CaseStatuses } from '../../../../../case/common/api'; +import * as i18n from './translations'; +import { CaseStatuses, CaseType } from '../../../../../case/common/api'; import { getCasesColumns } from './columns'; -import { Case, DeleteCase, FilterOptions, SortFieldCase } from '../../containers/types'; +import { Case, DeleteCase, FilterOptions, SortFieldCase, SubCase } from '../../containers/types'; import { useGetCases, UpdateCase } from '../../containers/use_get_cases'; import { useGetCasesStatus } from '../../containers/use_get_cases_status'; import { useDeleteCases } from '../../containers/use_delete_cases'; @@ -58,6 +59,7 @@ import { getExpandedRowMap } from './expanded_row'; const Div = styled.div` margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; `; + const FlexItemDivider = styled(EuiFlexItem)` ${({ theme }) => css` .euiFlexGroup--gutterMedium > &.euiFlexItem { @@ -75,6 +77,7 @@ const ProgressLoader = styled(EuiProgress)` z-index: ${theme.eui.euiZHeader}; `} `; + const getSortField = (field: string): SortFieldCase => { if (field === SortFieldCase.createdAt) { return SortFieldCase.createdAt; @@ -86,19 +89,39 @@ const getSortField = (field: string): SortFieldCase => { const EuiBasicTable: any = _EuiBasicTable; // eslint-disable-line @typescript-eslint/no-explicit-any const BasicTable = styled(EuiBasicTable)` - .euiTableRow-isExpandedRow.euiTableRow-isSelectable .euiTableCellContent { - padding: 8px 0 8px 32px; - } + ${({ theme }) => ` + .euiTableRow-isExpandedRow.euiTableRow-isSelectable .euiTableCellContent { + padding: 8px 0 8px 32px; + } + + &.isModal .euiTableRow.isDisabled { + cursor: not-allowed; + background-color: ${theme.eui.euiTableHoverClickableColor}; + } + + &.isModal .euiTableRow.euiTableRow-isExpandedRow .euiTableRowCell, + &.isModal .euiTableRow.euiTableRow-isExpandedRow:hover { + background-color: transparent; + } + + &.isModal .euiTableRow.euiTableRow-isExpandedRow { + .subCase:hover { + background-color: ${theme.eui.euiTableHoverClickableColor}; + } + } + `} `; BasicTable.displayName = 'BasicTable'; interface AllCasesProps { - onRowClick?: (theCase?: Case) => void; + onRowClick?: (theCase?: Case | SubCase) => void; isModal?: boolean; userCanCrud: boolean; + disabledStatuses?: CaseStatuses[]; + disabledCases?: CaseType[]; } export const AllCases = React.memo( - ({ onRowClick, isModal = false, userCanCrud }) => { + ({ onRowClick, isModal = false, userCanCrud, disabledStatuses, disabledCases = [] }) => { const { navigateToApp } = useKibana().services.application; const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.case); const { actionLicense } = useGetActionLicense(); @@ -334,8 +357,10 @@ export const AllCases = React.memo( getExpandedRowMap({ columns: memoizedGetCasesColumns, data: data.cases, + isModal, + onSubCaseClick: onRowClick, }), - [data.cases, memoizedGetCasesColumns] + [data.cases, isModal, memoizedGetCasesColumns, onRowClick] ); const memoizedPagination = useMemo( @@ -356,6 +381,7 @@ export const AllCases = React.memo( () => ({ selectable: (theCase) => isEmpty(theCase.subCases), onSelectionChange: setSelectedCases, + selectableMessage: (selectable) => (!selectable ? i18n.SELECTABLE_MESSAGE_COLLECTIONS : ''), }), [setSelectedCases] ); @@ -377,7 +403,8 @@ export const AllCases = React.memo( return { 'data-test-subj': `cases-table-row-${theCase.id}`, - ...(isModal ? { onClick: onTableRowClick } : {}), + className: classnames({ isDisabled: theCase.type === CaseType.collection }), + ...(isModal && theCase.type !== CaseType.collection ? { onClick: onTableRowClick } : {}), }; }, [isModal, onRowClick] @@ -462,6 +489,7 @@ export const AllCases = React.memo( status: filterOptions.status, }} setFilterRefetch={setFilterRefetch} + disabledStatuses={disabledStatuses} /> {isCasesLoading && isDataEmpty ? (
@@ -530,6 +558,7 @@ export const AllCases = React.memo( rowProps={tableRowProps} selection={userCanCrud && !isModal ? euiBasicTableSelectionProps : undefined} sorting={sorting} + className={classnames({ isModal })} />
)} diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.test.tsx index 785d4447c0acf..11d53b6609e74 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.test.tsx @@ -61,4 +61,24 @@ describe('StatusFilter', () => { expect(onStatusChanged).toBeCalledWith('closed'); }); }); + + it('should disabled selected statuses', () => { + const wrapper = mount( + + ); + + wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click'); + + expect( + wrapper.find('button[data-test-subj="case-status-filter-open"]').prop('disabled') + ).toBeFalsy(); + + expect( + wrapper.find('button[data-test-subj="case-status-filter-in-progress"]').prop('disabled') + ).toBeFalsy(); + + expect( + wrapper.find('button[data-test-subj="case-status-filter-closed"]').prop('disabled') + ).toBeTruthy(); + }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.tsx index 7fa0625229b48..41997d6f38421 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.tsx @@ -14,9 +14,15 @@ interface Props { stats: Record; selectedStatus: CaseStatuses; onStatusChanged: (status: CaseStatuses) => void; + disabledStatuses?: CaseStatuses[]; } -const StatusFilterComponent: React.FC = ({ stats, selectedStatus, onStatusChanged }) => { +const StatusFilterComponent: React.FC = ({ + stats, + selectedStatus, + onStatusChanged, + disabledStatuses = [], +}) => { const caseStatuses = Object.keys(statuses) as CaseStatuses[]; const options: Array> = caseStatuses.map((status) => ({ value: status, @@ -28,6 +34,7 @@ const StatusFilterComponent: React.FC = ({ stats, selectedStatus, onStatu {` (${stats[status]})`} ), + disabled: disabledStatuses.includes(status), 'data-test-subj': `case-status-filter-${status}`, })); diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx index 1f7f1d1e0d487..61bbbac5a1e84 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx @@ -25,6 +25,7 @@ interface CasesTableFiltersProps { onFilterChanged: (filterOptions: Partial) => void; initial: FilterOptions; setFilterRefetch: (val: () => void) => void; + disabledStatuses?: CaseStatuses[]; } // Fix the width of the status dropdown to prevent hiding long text items @@ -50,6 +51,7 @@ const CasesTableFiltersComponent = ({ onFilterChanged, initial = defaultInitial, setFilterRefetch, + disabledStatuses, }: CasesTableFiltersProps) => { const [selectedReporters, setSelectedReporters] = useState( initial.reporters.map((r) => r.full_name ?? r.username ?? '') @@ -158,6 +160,7 @@ const CasesTableFiltersComponent = ({ selectedStatus={initial.status} onStatusChanged={onStatusChanged} stats={stats} + disabledStatuses={disabledStatuses} /> diff --git a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx index 5e33736ce9c3a..95c534f7c1ede 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx @@ -16,7 +16,7 @@ import { EuiFlexItem, EuiIconTip, } from '@elastic/eui'; -import { CaseStatuses } from '../../../../../case/common/api'; +import { CaseStatuses, CaseType } from '../../../../../case/common/api'; import * as i18n from '../case_view/translations'; import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; import { Actions } from './actions'; @@ -73,19 +73,21 @@ const CaseActionBarComponent: React.FC = ({ ); return ( - + - - {i18n.STATUS} - - - - + {caseData.type !== CaseType.collection && ( + + {i18n.STATUS} + + + + + )} {title} diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx index 7a5f6647a8dcf..5f9fb5b63d6eb 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx @@ -29,6 +29,7 @@ import { connectorsMock } from '../../containers/configure/mock'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; import { useQueryAlerts } from '../../../detections/containers/detection_engine/alerts/use_query'; import { ConnectorTypes } from '../../../../../case/common/api/connectors'; +import { CaseType } from '../../../../../case/common/api'; const mockDispatch = jest.fn(); jest.mock('react-redux', () => { @@ -201,6 +202,10 @@ describe('CaseView ', () => { .first() .text() ).toBe(data.description); + + expect( + wrapper.find('button[data-test-subj="case-view-status-action-button"]').first().text() + ).toBe('Mark in progress'); }); }); @@ -464,7 +469,7 @@ describe('CaseView ', () => { ); await waitFor(() => { wrapper.find('[data-test-subj="case-refresh"]').first().simulate('click'); - expect(fetchCaseUserActions).toBeCalledWith(caseProps.caseData.id); + expect(fetchCaseUserActions).toBeCalledWith('1234', undefined); expect(fetchCase).toBeCalled(); }); }); @@ -547,8 +552,7 @@ describe('CaseView ', () => { }); }); - // TO DO fix when the useEffects in edit_connector are cleaned up - it.skip('should update connector', async () => { + it('should update connector', async () => { const wrapper = mount( @@ -752,4 +756,74 @@ describe('CaseView ', () => { }); }); }); + + describe('Collections', () => { + it('it does not allow the user to update the status', async () => { + const wrapper = mount( + + + + + + ); + + await waitFor(() => { + expect(wrapper.find('[data-test-subj="case-action-bar-wrapper"]').exists()).toBe(true); + expect(wrapper.find('button[data-test-subj="case-view-status"]').exists()).toBe(false); + expect(wrapper.find('[data-test-subj="user-actions"]').exists()).toBe(true); + expect( + wrapper.find('button[data-test-subj="case-view-status-action-button"]').exists() + ).toBe(false); + }); + }); + + it('it shows the push button when has data to push', async () => { + useGetCaseUserActionsMock.mockImplementation(() => ({ + ...defaultUseGetCaseUserActions, + hasDataToPush: true, + })); + + const wrapper = mount( + + + + + + ); + + await waitFor(() => { + expect(wrapper.find('[data-test-subj="has-data-to-push-button"]').exists()).toBe(true); + }); + }); + + it('it does not show the horizontal rule when does NOT has data to push', async () => { + useGetCaseUserActionsMock.mockImplementation(() => ({ + ...defaultUseGetCaseUserActions, + hasDataToPush: false, + })); + + const wrapper = mount( + + + + + + ); + + await waitFor(() => { + expect( + wrapper.find('[data-test-subj="case-view-bottom-actions-horizontal-rule"]').exists() + ).toBe(false); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index e42431e55ee29..d0b7c34ab84fd 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -17,7 +17,7 @@ import { EuiHorizontalRule, } from '@elastic/eui'; -import { CaseStatuses, CaseAttributes } from '../../../../../case/common/api'; +import { CaseStatuses, CaseAttributes, CaseType } from '../../../../../case/common/api'; import { Case, CaseConnector } from '../../containers/types'; import { getCaseDetailsUrl, getCaseUrl, useFormatUrl } from '../../../common/components/link_to'; import { gutterTimeline } from '../../../common/lib/helpers'; @@ -213,9 +213,9 @@ export const CaseComponent = React.memo( const handleUpdateCase = useCallback( (newCase: Case) => { updateCase(newCase); - fetchCaseUserActions(newCase.id); + fetchCaseUserActions(caseId, subCaseId); }, - [updateCase, fetchCaseUserActions] + [updateCase, fetchCaseUserActions, caseId, subCaseId] ); const { loading: isLoadingConnectors, connectors } = useConnectors(); @@ -283,9 +283,9 @@ export const CaseComponent = React.memo( ); const handleRefresh = useCallback(() => { - fetchCaseUserActions(caseData.id); + fetchCaseUserActions(caseId, subCaseId); fetchCase(); - }, [caseData.id, fetchCase, fetchCaseUserActions]); + }, [caseId, fetchCase, fetchCaseUserActions, subCaseId]); const spyState = useMemo(() => ({ caseTitle: caseData.title }), [caseData.title]); @@ -345,6 +345,7 @@ export const CaseComponent = React.memo( ); } }, [dispatch]); + return ( <> @@ -387,7 +388,7 @@ export const CaseComponent = React.memo( caseUserActions={caseUserActions} connectors={connectors} data={caseData} - fetchUserActions={fetchCaseUserActions.bind(null, caseData.id)} + fetchUserActions={fetchCaseUserActions.bind(null, caseId, subCaseId)} isLoadingDescription={isLoading && updateKey === 'description'} isLoadingUserActions={isLoadingUserActions} onShowAlertDetails={showAlert} @@ -395,22 +396,29 @@ export const CaseComponent = React.memo( updateCase={updateCase} userCanCrud={userCanCrud} /> - - - - + - - {hasDataToPush && ( - - {pushButton} - - )} - + {caseData.type !== CaseType.collection && ( + + + + )} + {hasDataToPush && ( + + {pushButton} + + )} + + )} )} @@ -465,6 +473,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = if (isError) { return null; } + if (isLoading) { return ( @@ -476,14 +485,16 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = } return ( - + data && ( + + ) ); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/case/alert_fields.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/case/alert_fields.tsx index d5c90bd09a6db..b7fbaff288a2a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/case/alert_fields.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/alert_fields.tsx @@ -47,7 +47,9 @@ const CaseParamsFields: React.FunctionComponent = ({ onCaseChanged, sel onCaseChanged(''); dispatchResetIsDeleted(); } - }, [isDeleted, dispatchResetIsDeleted, onCaseChanged]); + // onCaseChanged and/or dispatchResetIsDeleted causes re-renders + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isDeleted]); useEffect(() => { if (!isLoading && !isError && data != null) { setCreatedCase(data); onCaseChanged(data.id); } - }, [data, isLoading, isError, onCaseChanged]); + // onCaseChanged causes re-renders + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data, isLoading, isError]); return ( <> diff --git a/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx index 842fe9e00ab39..d5883b7b88cd0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx @@ -20,14 +20,16 @@ jest.mock('../create/form_context', () => { onSuccess, }: { children: ReactNode; - onSuccess: ({ id }: { id: string }) => void; + onSuccess: ({ id }: { id: string }) => Promise; }) => { return ( <> @@ -55,10 +57,10 @@ jest.mock('../create/submit_button', () => { }); const onCloseFlyout = jest.fn(); -const onCaseCreated = jest.fn(); +const onSuccess = jest.fn(); const defaultProps = { onCloseFlyout, - onCaseCreated, + onSuccess, }; describe('CreateCaseFlyout', () => { @@ -97,7 +99,7 @@ describe('CreateCaseFlyout', () => { const props = wrapper.find('FormContext').props(); expect(props).toEqual( expect.objectContaining({ - onSuccess: onCaseCreated, + onSuccess, }) ); }); @@ -110,6 +112,6 @@ describe('CreateCaseFlyout', () => { ); wrapper.find(`[data-test-subj='form-context-on-success']`).first().simulate('click'); - expect(onCaseCreated).toHaveBeenCalledWith({ id: 'case-id' }); + expect(onSuccess).toHaveBeenCalledWith({ id: 'case-id' }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx b/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx index cb3436f6ba3bc..e7bb0b25f391f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx @@ -17,7 +17,8 @@ import * as i18n from '../../translations'; export interface CreateCaseModalProps { onCloseFlyout: () => void; - onCaseCreated: (theCase: Case) => void; + onSuccess: (theCase: Case) => Promise; + afterCaseCreated?: (theCase: Case) => Promise; } const Container = styled.div` @@ -40,7 +41,8 @@ const FormWrapper = styled.div` `; const CreateCaseFlyoutComponent: React.FC = ({ - onCaseCreated, + onSuccess, + afterCaseCreated, onCloseFlyout, }) => { return ( @@ -52,7 +54,7 @@ const CreateCaseFlyoutComponent: React.FC = ({ - + diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form_context.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form_context.test.tsx index 8236ab7b19d27..1e512ef5ffabd 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/form_context.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/form_context.test.tsx @@ -98,6 +98,7 @@ const fillForm = (wrapper: ReactWrapper) => { describe('Create case', () => { const fetchTags = jest.fn(); const onFormSubmitSuccess = jest.fn(); + const afterCaseCreated = jest.fn(); beforeEach(() => { jest.resetAllMocks(); @@ -593,4 +594,89 @@ describe('Create case', () => { }); }); }); + + it(`it should call afterCaseCreated`, async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + + const wrapper = mount( + + + + + + + ); + + fillForm(wrapper); + expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeFalsy(); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find(`button[data-test-subj="dropdown-connector-jira-1"]`).simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeTruthy(); + }); + + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + await waitFor(() => { + expect(afterCaseCreated).toHaveBeenCalledWith({ + id: sampleId, + ...sampleData, + }); + }); + }); + + it(`it should call callbacks in correct order`, async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + + const wrapper = mount( + + + + + + + ); + + fillForm(wrapper); + expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeFalsy(); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find(`button[data-test-subj="dropdown-connector-jira-1"]`).simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeTruthy(); + }); + + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + await waitFor(() => { + expect(postCase).toHaveBeenCalled(); + expect(afterCaseCreated).toHaveBeenCalled(); + expect(pushCaseToExternalService).toHaveBeenCalled(); + expect(onFormSubmitSuccess).toHaveBeenCalled(); + }); + + const postCaseOrder = postCase.mock.invocationCallOrder[0]; + const afterCaseOrder = afterCaseCreated.mock.invocationCallOrder[0]; + const pushCaseToExternalServiceOrder = pushCaseToExternalService.mock.invocationCallOrder[0]; + const onFormSubmitSuccessOrder = onFormSubmitSuccess.mock.invocationCallOrder[0]; + + expect( + postCaseOrder < afterCaseOrder && + postCaseOrder < pushCaseToExternalServiceOrder && + postCaseOrder < onFormSubmitSuccessOrder + ).toBe(true); + + expect( + afterCaseOrder < pushCaseToExternalServiceOrder && afterCaseOrder < onFormSubmitSuccessOrder + ).toBe(true); + + expect(pushCaseToExternalServiceOrder < onFormSubmitSuccessOrder).toBe(true); + }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx index 83b8870ab597d..26203d7268fd3 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx @@ -32,13 +32,15 @@ const initialCaseValue: FormProps = { interface Props { caseType?: CaseType; - onSuccess?: (theCase: Case) => void; + onSuccess?: (theCase: Case) => Promise; + afterCaseCreated?: (theCase: Case) => Promise; } export const FormContext: React.FC = ({ caseType = CaseType.individual, children, onSuccess, + afterCaseCreated, }) => { const { connectors } = useConnectors(); const { connector: configurationConnector } = useCaseConfigure(); @@ -72,6 +74,10 @@ export const FormContext: React.FC = ({ settings: { syncAlerts }, }); + if (afterCaseCreated && updatedCase) { + await afterCaseCreated(updatedCase); + } + if (updatedCase?.id && dataConnectorId !== 'none') { await pushCaseToExternalService({ caseId: updatedCase.id, @@ -80,11 +86,11 @@ export const FormContext: React.FC = ({ } if (onSuccess && updatedCase) { - onSuccess(updatedCase); + await onSuccess(updatedCase); } } }, - [caseType, connectors, postCase, onSuccess, pushCaseToExternalService] + [caseType, connectors, postCase, onSuccess, pushCaseToExternalService, afterCaseCreated] ); const { form } = useForm({ diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx index b7d162bd92761..9f904350b772e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx @@ -41,7 +41,7 @@ const InsertTimeline = () => { export const Create = React.memo(() => { const history = useHistory(); const onSuccess = useCallback( - ({ id }) => { + async ({ id }) => { history.push(getCaseDetailsUrl({ id })); }, [history] diff --git a/x-pack/plugins/security_solution/public/cases/components/status/button.test.tsx b/x-pack/plugins/security_solution/public/cases/components/status/button.test.tsx index ab30fe2979b9e..22d72429836de 100644 --- a/x-pack/plugins/security_solution/public/cases/components/status/button.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/status/button.test.tsx @@ -42,7 +42,7 @@ describe('StatusActionButton', () => { expect( wrapper.find(`[data-test-subj="case-view-status-action-button"]`).first().prop('iconType') - ).toBe('folderClosed'); + ).toBe('folderCheck'); }); it('it renders the correct button icon: status closed', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/status/config.ts b/x-pack/plugins/security_solution/public/cases/components/status/config.ts index d811db43df814..0eebef39859c7 100644 --- a/x-pack/plugins/security_solution/public/cases/components/status/config.ts +++ b/x-pack/plugins/security_solution/public/cases/components/status/config.ts @@ -83,7 +83,7 @@ export const statuses: Statuses = { [CaseStatuses.closed]: { color: 'default', label: i18n.CLOSED, - icon: 'folderClosed' as const, + icon: 'folderCheck' as const, actions: { bulk: { title: i18n.BULK_ACTION_CLOSE_SELECTED, diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx index aa1305f1f655c..b3302a05cfcb2 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx @@ -30,12 +30,18 @@ jest.mock('../../../common/components/toasters', () => { jest.mock('../all_cases', () => { return { - AllCases: ({ onRowClick }: { onRowClick: ({ id }: { id: string }) => void }) => { + AllCases: ({ onRowClick }: { onRowClick: (theCase: Partial) => void }) => { return ( @@ -49,18 +55,25 @@ jest.mock('../create/form_context', () => { FormContext: ({ children, onSuccess, + afterCaseCreated, }: { children: ReactNode; - onSuccess: (theCase: Partial) => void; + onSuccess: (theCase: Partial) => Promise; + afterCaseCreated: (theCase: Partial) => Promise; }) => { return ( <> @@ -212,11 +225,43 @@ describe('AddToCaseAction', () => { }); }); - it('navigates to case view', async () => { + it('navigates to case view when attach to a new case', async () => { + const wrapper = mount( + + + + ); + + wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click'); + wrapper.find(`[data-test-subj="add-new-case-item"]`).first().simulate('click'); + wrapper.find(`[data-test-subj="form-context-on-success"]`).first().simulate('click'); + + expect(mockDispatchToaster).toHaveBeenCalled(); + const toast = mockDispatchToaster.mock.calls[0][0].toast; + + const toastWrapper = mount( + {}} /> + ); + + toastWrapper + .find('[data-test-subj="toaster-content-case-view-link"]') + .first() + .simulate('click'); + + expect(mockNavigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/new-case' }); + }); + + it('navigates to case view when attach to an existing case', async () => { usePostCommentMock.mockImplementation(() => { return { ...defaultPostComment, - postComment: jest.fn().mockImplementation(({ caseId, data, updateCase }) => updateCase()), + postComment: jest.fn().mockImplementation(({ caseId, data, updateCase }) => { + updateCase({ + id: 'selected-case', + title: 'the selected case', + settings: { syncAlerts: true }, + }); + }), }; }); @@ -227,8 +272,8 @@ describe('AddToCaseAction', () => { ); wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click'); - wrapper.find(`[data-test-subj="add-new-case-item"]`).first().simulate('click'); - wrapper.find(`[data-test-subj="form-context-on-success"]`).first().simulate('click'); + wrapper.find(`[data-test-subj="add-existing-case-menu-item"]`).first().simulate('click'); + wrapper.find(`[data-test-subj="all-cases-modal-button"]`).first().simulate('click'); expect(mockDispatchToaster).toHaveBeenCalled(); const toast = mockDispatchToaster.mock.calls[0][0].toast; @@ -242,6 +287,8 @@ describe('AddToCaseAction', () => { .first() .simulate('click'); - expect(mockNavigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/new-case' }); + expect(mockNavigateToApp).toHaveBeenCalledWith('securitySolution:case', { + path: '/selected-case', + }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx index aa9cec2d6b5b1..3000551dd3c07 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx @@ -15,7 +15,7 @@ import { EuiToolTip, } from '@elastic/eui'; -import { CommentType } from '../../../../../case/common/api'; +import { CommentType, CaseStatuses } from '../../../../../case/common/api'; import { Ecs } from '../../../../common/ecs'; import { ActionIconItem } from '../../../timelines/components/timeline/body/actions/action_icon_item'; import { usePostComment } from '../../containers/use_post_comment'; @@ -70,9 +70,9 @@ const AddToCaseActionComponent: React.FC = ({ } = useControl(); const attachAlertToCase = useCallback( - (theCase: Case) => { + async (theCase: Case, updateCase?: (newCase: Case) => void) => { closeCaseFlyoutOpen(); - postComment({ + await postComment({ caseId: theCase.id, data: { type: CommentType.alert, @@ -83,14 +83,19 @@ const AddToCaseActionComponent: React.FC = ({ name: rule?.name != null ? rule.name[0] : null, }, }, - updateCase: () => - dispatchToaster({ - type: 'addToaster', - toast: createUpdateSuccessToaster(theCase, onViewCaseClick), - }), + updateCase, }); }, - [closeCaseFlyoutOpen, postComment, eventId, eventIndex, rule, dispatchToaster, onViewCaseClick] + [closeCaseFlyoutOpen, postComment, eventId, eventIndex, rule] + ); + + const onCaseSuccess = useCallback( + async (theCase: Case) => + dispatchToaster({ + type: 'addToaster', + toast: createUpdateSuccessToaster(theCase, onViewCaseClick), + }), + [dispatchToaster, onViewCaseClick] ); const onCaseClicked = useCallback( @@ -105,12 +110,13 @@ const AddToCaseActionComponent: React.FC = ({ return; } - attachAlertToCase(theCase); + attachAlertToCase(theCase, onCaseSuccess); }, - [attachAlertToCase, openCaseFlyoutOpen] + [attachAlertToCase, onCaseSuccess, openCaseFlyoutOpen] ); const { modal: allCasesModal, openModal: openAllCaseModal } = useAllCasesModal({ + disabledStatuses: [CaseStatuses.closed], onRowClick: onCaseClicked, }); @@ -183,7 +189,11 @@ const AddToCaseActionComponent: React.FC = ({ {isCreateCaseFlyoutOpen && ( - + )} {allCasesModal} diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx index eda8ed8cdfbcd..e1d6baa6e630a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx @@ -6,36 +6,52 @@ */ import React, { memo } from 'react'; +import styled from 'styled-components'; import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui'; import { useGetUserSavedObjectPermissions } from '../../../common/lib/kibana'; -import { Case } from '../../containers/types'; +import { CaseStatuses } from '../../../../../case/common/api'; +import { Case, SubCase } from '../../containers/types'; import { AllCases } from '../all_cases'; import * as i18n from './translations'; export interface AllCasesModalProps { isModalOpen: boolean; onCloseCaseModal: () => void; - onRowClick: (theCase?: Case) => void; + onRowClick: (theCase?: Case | SubCase) => void; + disabledStatuses?: CaseStatuses[]; } +const Modal = styled(EuiModal)` + ${({ theme }) => ` + width: ${theme.eui.euiBreakpoints.l}; + max-width: ${theme.eui.euiBreakpoints.l}; + `} +`; + const AllCasesModalComponent: React.FC = ({ isModalOpen, onCloseCaseModal, onRowClick, + disabledStatuses, }) => { const userPermissions = useGetUserSavedObjectPermissions(); const userCanCrud = userPermissions?.crud ?? false; return isModalOpen ? ( - + {i18n.SELECT_CASE_TITLE} - + - + ) : null; }; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx index 576f942a36a8f..57bb39a1ab50f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx @@ -123,7 +123,7 @@ describe('useAllCasesModal', () => { }); const modal = result.current.modal; - render(<>{modal}); + render({modal}); act(() => { userEvent.click(screen.getByText('case-row')); diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx index 79b490c1962da..52b8ebe0210c0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx @@ -6,11 +6,13 @@ */ import React, { useState, useCallback, useMemo } from 'react'; -import { Case } from '../../containers/types'; +import { CaseStatuses } from '../../../../../case/common/api'; +import { Case, SubCase } from '../../containers/types'; import { AllCasesModal } from './all_cases_modal'; export interface UseAllCasesModalProps { - onRowClick: (theCase?: Case) => void; + onRowClick: (theCase?: Case | SubCase) => void; + disabledStatuses?: CaseStatuses[]; } export interface UseAllCasesModalReturnedValues { @@ -22,12 +24,13 @@ export interface UseAllCasesModalReturnedValues { export const useAllCasesModal = ({ onRowClick, + disabledStatuses, }: UseAllCasesModalProps): UseAllCasesModalReturnedValues => { const [isModalOpen, setIsModalOpen] = useState(false); const closeModal = useCallback(() => setIsModalOpen(false), []); const openModal = useCallback(() => setIsModalOpen(true), []); const onClick = useCallback( - (theCase?: Case) => { + (theCase?: Case | SubCase) => { closeModal(); onRowClick(theCase); }, @@ -41,6 +44,7 @@ export const useAllCasesModal = ({ isModalOpen={isModalOpen} onCloseCaseModal={closeModal} onRowClick={onClick} + disabledStatuses={disabledStatuses} /> ), isModalOpen, @@ -48,7 +52,7 @@ export const useAllCasesModal = ({ openModal, onRowClick, }), - [isModalOpen, closeModal, onClick, openModal, onRowClick] + [isModalOpen, closeModal, onClick, disabledStatuses, openModal, onRowClick] ); return state; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.test.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.test.tsx index 0e04acb013b2d..08fca0cc6e009 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.test.tsx @@ -20,14 +20,16 @@ jest.mock('../create/form_context', () => { onSuccess, }: { children: ReactNode; - onSuccess: ({ id }: { id: string }) => void; + onSuccess: ({ id }: { id: string }) => Promise; }) => { return ( <> diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx index 2806e358fceee..3e11ee526839c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx @@ -19,7 +19,7 @@ import { CaseType } from '../../../../../case/common/api'; export interface CreateCaseModalProps { isModalOpen: boolean; onCloseCaseModal: () => void; - onSuccess: (theCase: Case) => void; + onSuccess: (theCase: Case) => Promise; caseType?: CaseType; } diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.test.tsx index 9966cf75351dd..5174c03e56e0b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.test.tsx @@ -25,14 +25,16 @@ jest.mock('../create/form_context', () => { onSuccess, }: { children: ReactNode; - onSuccess: ({ id }: { id: string }) => void; + onSuccess: ({ id }: { id: string }) => Promise; }) => { return ( <> diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx index 3dc852a19e73f..1cef63ae9cfbf 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx @@ -29,7 +29,7 @@ export const useCreateCaseModal = ({ const closeModal = useCallback(() => setIsModalOpen(false), []); const openModal = useCallback(() => setIsModalOpen(true), []); const onSuccess = useCallback( - (theCase) => { + async (theCase) => { onCaseCreated(theCase); closeModal(); }, diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx index c5d3ef1893ad7..056add32add82 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx @@ -55,6 +55,9 @@ describe('UserActionTree ', () => { useFormMock.mockImplementation(() => ({ form: formHookMock })); useFormDataMock.mockImplementation(() => [{ content: sampleData.content, comment: '' }]); jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); + jest + .spyOn(routeData, 'useParams') + .mockReturnValue({ detailName: 'case-id', subCaseId: 'sub-case-id' }); }); it('Loading spinner when user actions loading and displays fullName/username', () => { @@ -289,7 +292,8 @@ describe('UserActionTree ', () => { ).toEqual(false); expect(patchComment).toBeCalledWith({ commentUpdate: sampleData.content, - caseId: props.data.id, + caseId: 'case-id', + subCaseId: 'sub-case-id', commentId: props.data.comments[0].id, fetchUserActions, updateCase, diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx index 2a9f99465251b..cf68d07859ced 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx @@ -122,7 +122,11 @@ export const UserActionTree = React.memo( userCanCrud, onShowAlertDetails, }: UserActionTreeProps) => { - const { commentId, subCaseId } = useParams<{ commentId?: string; subCaseId?: string }>(); + const { detailName: caseId, commentId, subCaseId } = useParams<{ + detailName: string; + commentId?: string; + subCaseId?: string; + }>(); const handlerTimeoutId = useRef(0); const addCommentRef = useRef(null); const [initLoading, setInitLoading] = useState(true); @@ -149,15 +153,16 @@ export const UserActionTree = React.memo( const handleSaveComment = useCallback( ({ id, version }: { id: string; version: string }, content: string) => { patchComment({ - caseId: caseData.id, + caseId, commentId: id, commentUpdate: content, fetchUserActions, version, updateCase, + subCaseId, }); }, - [caseData.id, fetchUserActions, patchComment, updateCase] + [caseId, fetchUserActions, patchComment, subCaseId, updateCase] ); const handleOutlineComment = useCallback( @@ -223,7 +228,7 @@ export const UserActionTree = React.memo( const MarkdownNewComment = useMemo( () => ( ), - [caseData.id, handleUpdate, userCanCrud, handleManageMarkdownEditId, subCaseId] + [caseId, userCanCrud, handleUpdate, handleManageMarkdownEditId, subCaseId] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_case.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.test.tsx index a157be2dc1353..a3d64a17727e5 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_case.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.test.tsx @@ -6,7 +6,7 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; -import { initialData, useGetCase, UseGetCase } from './use_get_case'; +import { useGetCase, UseGetCase } from './use_get_case'; import { basicCase } from './mock'; import * as api from './api'; @@ -26,8 +26,8 @@ describe('useGetCase', () => { ); await waitForNextUpdate(); expect(result.current).toEqual({ - data: initialData, - isLoading: true, + data: null, + isLoading: false, isError: false, fetchCase: result.current.fetchCase, updateCase: result.current.updateCase, @@ -102,7 +102,7 @@ describe('useGetCase', () => { await waitForNextUpdate(); expect(result.current).toEqual({ - data: initialData, + data: null, isLoading: false, isError: true, fetchCase: result.current.fetchCase, diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx index fb8da8d0663ee..70e202b5d6bdf 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx @@ -6,16 +6,14 @@ */ import { useEffect, useReducer, useCallback, useRef } from 'react'; -import { CaseStatuses, CaseType } from '../../../../case/common/api'; import { Case } from './types'; import * as i18n from './translations'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { getCase, getSubCase } from './api'; -import { getNoneConnector } from '../components/configure_cases/utils'; interface CaseState { - data: Case; + data: Case | null; isLoading: boolean; isError: boolean; } @@ -56,32 +54,6 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => { return state; } }; -export const initialData: Case = { - id: '', - closedAt: null, - closedBy: null, - createdAt: '', - comments: [], - connector: { ...getNoneConnector(), fields: null }, - createdBy: { - username: '', - }, - description: '', - externalService: null, - status: CaseStatuses.open, - tags: [], - title: '', - totalAlerts: 0, - totalComment: 0, - type: CaseType.individual, - updatedAt: null, - updatedBy: null, - version: '', - subCaseIds: [], - settings: { - syncAlerts: true, - }, -}; export interface UseGetCase extends CaseState { fetchCase: () => void; @@ -90,9 +62,9 @@ export interface UseGetCase extends CaseState { export const useGetCase = (caseId: string, subCaseId?: string): UseGetCase => { const [state, dispatch] = useReducer(dataFetchReducer, { - isLoading: true, + isLoading: false, isError: false, - data: initialData, + data: null, }); const [, dispatchToaster] = useStateToaster(); const isCancelledRef = useRef(false); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx index 5eb875287ba88..75d3047bc828e 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx @@ -48,7 +48,7 @@ interface PostComment { subCaseId?: string; } export interface UsePostComment extends NewCommentState { - postComment: (args: PostComment) => void; + postComment: (args: PostComment) => Promise; } export const usePostComment = (): UsePostComment => { diff --git a/x-pack/plugins/security_solution/public/cases/translations.ts b/x-pack/plugins/security_solution/public/cases/translations.ts index caaa1f6e248ea..b7cfe11aafda0 100644 --- a/x-pack/plugins/security_solution/public/cases/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/translations.ts @@ -294,3 +294,10 @@ export const ALERT_ADDED_TO_CASE = i18n.translate( defaultMessage: 'added to case', } ); + +export const SELECTABLE_MESSAGE_COLLECTIONS = i18n.translate( + 'xpack.securitySolution.common.allCases.table.selectableMessageCollections', + { + defaultMessage: 'Cases with sub-cases cannot be selected', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.test.tsx index ff358b6ab0e1d..f7466b183e18a 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.test.tsx @@ -25,7 +25,7 @@ describe('useQueryAlerts', () => { >(() => useQueryAlerts(mockAlertsQuery, indexName)); await waitForNextUpdate(); expect(result.current).toEqual({ - loading: true, + loading: false, data: null, response: '', request: '', diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx index 8557e1082c1cb..3736c8593daa9 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx @@ -42,7 +42,7 @@ export const useQueryAlerts = ( setQuery, refetch: null, }); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(false); useEffect(() => { let isSubscribed = true; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx index 487d42aac4840..5cba64299ee9d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx @@ -20,7 +20,7 @@ import { TimelineStatus, TimelineId, TimelineType } from '../../../../../common/ import { getCreateCaseUrl, getCaseDetailsUrl } from '../../../../common/components/link_to'; import { SecurityPageName } from '../../../../app/types'; import { timelineDefaults } from '../../../../timelines/store/timeline/defaults'; -import { Case } from '../../../../cases/containers/types'; +import { Case, SubCase } from '../../../../cases/containers/types'; import * as i18n from '../../timeline/properties/translations'; interface Props { @@ -46,7 +46,7 @@ const AddToCaseButtonComponent: React.FC = ({ timelineId }) => { const [isPopoverOpen, setPopover] = useState(false); const onRowClick = useCallback( - async (theCase?: Case) => { + async (theCase?: Case | SubCase) => { await navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { path: theCase != null ? getCaseDetailsUrl({ id: theCase.id }) : getCreateCaseUrl(), }); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts index f908a369b46d7..c58ca0242a5b5 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts @@ -101,15 +101,15 @@ export default ({ getService }: FtrProviderContext): void => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); await supertest .delete( - `${CASES_URL}/${caseInfo.id}/comments/${caseInfo.subCase!.comments![0].id}?subCaseID=${ - caseInfo.subCase!.id + `${CASES_URL}/${caseInfo.id}/comments/${caseInfo.comments![0].id}?subCaseId=${ + caseInfo.subCases![0].id }` ) .set('kbn-xsrf', 'true') .send() .expect(204); const { body } = await supertest.get( - `${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}` + `${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}` ); expect(body.length).to.eql(0); }); @@ -117,24 +117,24 @@ export default ({ getService }: FtrProviderContext): void => { it('deletes all comments from a sub case', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`) + .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) .set('kbn-xsrf', 'true') .send(postCommentUserReq) .expect(200); let { body: allComments } = await supertest.get( - `${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}` + `${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}` ); expect(allComments.length).to.eql(2); await supertest - .delete(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`) + .delete(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) .set('kbn-xsrf', 'true') .send() .expect(204); ({ body: allComments } = await supertest.get( - `${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}` + `${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}` )); // no comments for the sub case diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts index 585333291111e..2d8e4c44e023e 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts @@ -126,13 +126,13 @@ export default ({ getService }: FtrProviderContext): void => { it('finds comments for a sub case', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`) + .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) .set('kbn-xsrf', 'true') .send(postCommentUserReq) .expect(200); const { body: subCaseComments }: { body: CommentsResponse } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments/_find?subCaseID=${caseInfo.subCase!.id}`) + .get(`${CASES_URL}/${caseInfo.id}/comments/_find?subCaseId=${caseInfo.subCases![0].id}`) .send() .expect(200); expect(subCaseComments.total).to.be(2); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_all_comments.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_all_comments.ts index 1af16f9e54563..264103a2052e5 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_all_comments.ts @@ -83,13 +83,13 @@ export default ({ getService }: FtrProviderContext): void => { it('should get comments from a sub cases', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); await supertest - .post(`${CASES_URL}/${caseInfo.subCase!.id}/comments`) + .post(`${CASES_URL}/${caseInfo.subCases![0].id}/comments`) .set('kbn-xsrf', 'true') .send(postCommentUserReq) .expect(200); const { body: comments } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`) + .get(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) .expect(200); expect(comments.length).to.eql(2); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts index 389ec3f088f95..bf63c55938dfe 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts @@ -60,7 +60,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should get a sub case comment', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); const { body: comment }: { body: CommentResponse } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments/${caseInfo.subCase!.comments![0].id}`) + .get(`${CASES_URL}/${caseInfo.id}/comments/${caseInfo.comments![0].id}`) .expect(200); expect(comment.type).to.be(CommentType.generatedAlert); }); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts index 86b1c3031cbef..6d9962e938249 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts @@ -10,10 +10,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/case/common/constants'; -import { - CollectionWithSubCaseResponse, - CommentType, -} from '../../../../../../plugins/case/common/api'; +import { CaseResponse, CommentType } from '../../../../../../plugins/case/common/api'; import { defaultUser, postCaseReq, @@ -56,42 +53,38 @@ export default ({ getService }: FtrProviderContext): void => { it('patches a comment for a sub case', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - const { - body: patchedSubCase, - }: { body: CollectionWithSubCaseResponse } = await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`) + const { body: patchedSubCase }: { body: CaseResponse } = await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) .set('kbn-xsrf', 'true') .send(postCommentUserReq) .expect(200); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; const { body: patchedSubCaseUpdatedComment } = await supertest - .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`) + .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) .set('kbn-xsrf', 'true') .send({ - id: patchedSubCase.subCase!.comments![1].id, - version: patchedSubCase.subCase!.comments![1].version, + id: patchedSubCase.comments![1].id, + version: patchedSubCase.comments![1].version, comment: newComment, type: CommentType.user, }) .expect(200); - expect(patchedSubCaseUpdatedComment.subCase.comments.length).to.be(2); - expect(patchedSubCaseUpdatedComment.subCase.comments[0].type).to.be( - CommentType.generatedAlert - ); - expect(patchedSubCaseUpdatedComment.subCase.comments[1].type).to.be(CommentType.user); - expect(patchedSubCaseUpdatedComment.subCase.comments[1].comment).to.be(newComment); + expect(patchedSubCaseUpdatedComment.comments.length).to.be(2); + expect(patchedSubCaseUpdatedComment.comments[0].type).to.be(CommentType.generatedAlert); + expect(patchedSubCaseUpdatedComment.comments[1].type).to.be(CommentType.user); + expect(patchedSubCaseUpdatedComment.comments[1].comment).to.be(newComment); }); it('fails to update the generated alert comment type', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); await supertest - .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`) + .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) .set('kbn-xsrf', 'true') .send({ - id: caseInfo.subCase!.comments![0].id, - version: caseInfo.subCase!.comments![0].version, + id: caseInfo.comments![0].id, + version: caseInfo.comments![0].version, type: CommentType.alert, alertId: 'test-id', index: 'test-index', @@ -106,11 +99,11 @@ export default ({ getService }: FtrProviderContext): void => { it('fails to update the generated alert comment by using another generated alert comment', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); await supertest - .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`) + .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) .set('kbn-xsrf', 'true') .send({ - id: caseInfo.subCase!.comments![0].id, - version: caseInfo.subCase!.comments![0].version, + id: caseInfo.comments![0].id, + version: caseInfo.comments![0].version, type: CommentType.generatedAlert, alerts: [{ _id: 'id1' }], index: 'test-index', diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts index fb095c117cdfb..9447f7ad3613c 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts @@ -393,13 +393,13 @@ export default ({ getService }: FtrProviderContext): void => { // create another sub case just to make sure we get the right comments await createSubCase({ supertest, actionID }); await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`) + .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) .set('kbn-xsrf', 'true') .send(postCommentUserReq) .expect(200); const { body: subCaseComments }: { body: CommentsResponse } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments/_find?subCaseID=${caseInfo.subCase!.id}`) + .get(`${CASES_URL}/${caseInfo.id}/comments/_find?subCaseId=${caseInfo.subCases![0].id}`) .send() .expect(200); expect(subCaseComments.total).to.be(2); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts index 8edc3b0d08113..5e761e4d7e33a 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts @@ -20,7 +20,7 @@ import { deleteComments, } from '../../../common/lib/utils'; import { getSubCaseDetailsUrl } from '../../../../../plugins/case/common/api/helpers'; -import { CollectionWithSubCaseResponse } from '../../../../../plugins/case/common/api'; +import { CaseResponse } from '../../../../../plugins/case/common/api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -104,7 +104,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should delete the sub cases when deleting a collection', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(caseInfo.subCase?.id).to.not.eql(undefined); + expect(caseInfo.subCases![0].id).to.not.eql(undefined); const { body } = await supertest .delete(`${CASES_URL}?ids=["${caseInfo.id}"]`) @@ -114,27 +114,25 @@ export default ({ getService }: FtrProviderContext): void => { expect(body).to.eql({}); await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCase!.id)) + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) .send() .expect(404); }); it(`should delete a sub case's comments when that case gets deleted`, async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(caseInfo.subCase?.id).to.not.eql(undefined); + expect(caseInfo.subCases![0].id).to.not.eql(undefined); // there should be two comments on the sub case now - const { - body: patchedCaseWithSubCase, - }: { body: CollectionWithSubCaseResponse } = await supertest + const { body: patchedCaseWithSubCase }: { body: CaseResponse } = await supertest .post(`${CASES_URL}/${caseInfo.id}/comments`) .set('kbn-xsrf', 'true') - .query({ subCaseID: caseInfo.subCase!.id }) + .query({ subCaseId: caseInfo.subCases![0].id }) .send(postCommentUserReq) .expect(200); const subCaseCommentUrl = `${CASES_URL}/${patchedCaseWithSubCase.id}/comments/${ - patchedCaseWithSubCase.subCase!.comments![1].id + patchedCaseWithSubCase.comments![1].id }`; // make sure we can get the second comment await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts index a2bc0acbcf17c..7514044d376ca 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts @@ -265,8 +265,8 @@ export default ({ getService }: FtrProviderContext): void => { supertest, cases: [ { - id: collection.newSubCaseInfo.subCase!.id, - version: collection.newSubCaseInfo.subCase!.version, + id: collection.newSubCaseInfo.subCases![0].id, + version: collection.newSubCaseInfo.subCases![0].version, status: CaseStatuses['in-progress'], }, ], @@ -356,7 +356,7 @@ export default ({ getService }: FtrProviderContext): void => { it('correctly counts stats including a collection without sub cases', async () => { // delete the sub case on the collection so that it doesn't have any sub cases await supertest - .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${collection.newSubCaseInfo.subCase!.id}"]`) + .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${collection.newSubCaseInfo.subCases![0].id}"]`) .set('kbn-xsrf', 'true') .send() .expect(204); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/delete_sub_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/delete_sub_cases.ts index 537afbe825068..1d8216ded8b7c 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/delete_sub_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/delete_sub_cases.ts @@ -19,7 +19,7 @@ import { deleteCaseAction, } from '../../../../common/lib/utils'; import { getSubCaseDetailsUrl } from '../../../../../../plugins/case/common/api/helpers'; -import { CollectionWithSubCaseResponse } from '../../../../../../plugins/case/common/api'; +import { CaseResponse } from '../../../../../../plugins/case/common/api'; // eslint-disable-next-line import/no-default-export export default function ({ getService }: FtrProviderContext) { @@ -40,10 +40,10 @@ export default function ({ getService }: FtrProviderContext) { it('should delete a sub case', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(caseInfo.subCase?.id).to.not.eql(undefined); + expect(caseInfo.subCases![0].id).to.not.eql(undefined); const { body: subCase } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCase!.id)) + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) .send() .expect(200); @@ -57,33 +57,31 @@ export default function ({ getService }: FtrProviderContext) { expect(body).to.eql({}); await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCase!.id)) + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) .send() .expect(404); }); it(`should delete a sub case's comments when that case gets deleted`, async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(caseInfo.subCase?.id).to.not.eql(undefined); + expect(caseInfo.subCases![0].id).to.not.eql(undefined); // there should be two comments on the sub case now - const { - body: patchedCaseWithSubCase, - }: { body: CollectionWithSubCaseResponse } = await supertest + const { body: patchedCaseWithSubCase }: { body: CaseResponse } = await supertest .post(`${CASES_URL}/${caseInfo.id}/comments`) .set('kbn-xsrf', 'true') - .query({ subCaseID: caseInfo.subCase!.id }) + .query({ subCaseId: caseInfo.subCases![0].id }) .send(postCommentUserReq) .expect(200); const subCaseCommentUrl = `${CASES_URL}/${patchedCaseWithSubCase.id}/comments/${ - patchedCaseWithSubCase.subCase!.comments![1].id + patchedCaseWithSubCase.comments![1].id }`; // make sure we can get the second comment await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200); await supertest - .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${patchedCaseWithSubCase.subCase!.id}"]`) + .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${patchedCaseWithSubCase.subCases![0].id}"]`) .set('kbn-xsrf', 'true') .send() .expect(204); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/find_sub_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/find_sub_cases.ts index 3463b37250980..4fd4cd6ec7542 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/find_sub_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/find_sub_cases.ts @@ -74,7 +74,7 @@ export default ({ getService }: FtrProviderContext): void => { ...findSubCasesResp, total: 1, // find should not return the comments themselves only the stats - subCases: [{ ...caseInfo.subCase!, comments: [], totalComment: 1, totalAlerts: 2 }], + subCases: [{ ...caseInfo.subCases![0], comments: [], totalComment: 1, totalAlerts: 2 }], count_open_cases: 1, }); }); @@ -101,7 +101,7 @@ export default ({ getService }: FtrProviderContext): void => { status: CaseStatuses.closed, }, { - ...subCase2Resp.newSubCaseInfo.subCase, + ...subCase2Resp.newSubCaseInfo.subCases![0], comments: [], totalComment: 1, totalAlerts: 2, @@ -157,8 +157,8 @@ export default ({ getService }: FtrProviderContext): void => { supertest, cases: [ { - id: secondSub.subCase!.id, - version: secondSub.subCase!.version, + id: secondSub.subCases![0].id, + version: secondSub.subCases![0].version, status: CaseStatuses['in-progress'], }, ], @@ -231,8 +231,8 @@ export default ({ getService }: FtrProviderContext): void => { supertest, cases: [ { - id: secondSub.subCase!.id, - version: secondSub.subCase!.version, + id: secondSub.subCases![0].id, + version: secondSub.subCases![0].version, status: CaseStatuses['in-progress'], }, ], diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/get_sub_case.ts b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/get_sub_case.ts index cd5a1ed85742f..dff462d78ba82 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/get_sub_case.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/get_sub_case.ts @@ -28,7 +28,7 @@ import { } from '../../../../../../plugins/case/common/api/helpers'; import { AssociationType, - CollectionWithSubCaseResponse, + CaseResponse, SubCaseResponse, } from '../../../../../../plugins/case/common/api'; @@ -53,14 +53,14 @@ export default ({ getService }: FtrProviderContext): void => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); const { body }: { body: SubCaseResponse } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCase!.id)) + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) .set('kbn-xsrf', 'true') .send() .expect(200); expect(removeServerGeneratedPropertiesFromComments(body.comments)).to.eql( commentsResp({ - comments: [{ comment: defaultCreateSubComment, id: caseInfo.subCase!.comments![0].id }], + comments: [{ comment: defaultCreateSubComment, id: caseInfo.comments![0].id }], associationType: AssociationType.subCase, }) ); @@ -73,15 +73,15 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the correct number of alerts with multiple types of alerts', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - const { body: singleAlert }: { body: CollectionWithSubCaseResponse } = await supertest + const { body: singleAlert }: { body: CaseResponse } = await supertest .post(getCaseCommentsUrl(caseInfo.id)) - .query({ subCaseID: caseInfo.subCase!.id }) + .query({ subCaseId: caseInfo.subCases![0].id }) .set('kbn-xsrf', 'true') .send(postCommentAlertReq) .expect(200); const { body }: { body: SubCaseResponse } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCase!.id)) + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) .set('kbn-xsrf', 'true') .send() .expect(200); @@ -89,10 +89,10 @@ export default ({ getService }: FtrProviderContext): void => { expect(removeServerGeneratedPropertiesFromComments(body.comments)).to.eql( commentsResp({ comments: [ - { comment: defaultCreateSubComment, id: caseInfo.subCase!.comments![0].id }, + { comment: defaultCreateSubComment, id: caseInfo.comments![0].id }, { comment: postCommentAlertReq, - id: singleAlert.subCase!.comments![1].id, + id: singleAlert.comments![1].id, }, ], associationType: AssociationType.subCase, diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts index 49b3c0b1f465b..5a1da194a721f 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts @@ -59,15 +59,15 @@ export default function ({ getService }: FtrProviderContext) { supertest, cases: [ { - id: caseInfo.subCase!.id, - version: caseInfo.subCase!.version, + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, status: CaseStatuses['in-progress'], }, ], type: 'sub_case', }); const { body: subCase }: { body: SubCaseResponse } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCase!.id)) + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) .expect(200); expect(subCase.status).to.eql(CaseStatuses['in-progress']); @@ -102,8 +102,8 @@ export default function ({ getService }: FtrProviderContext) { supertest, cases: [ { - id: caseInfo.subCase!.id, - version: caseInfo.subCase!.version, + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, status: CaseStatuses['in-progress'], }, ], @@ -159,8 +159,8 @@ export default function ({ getService }: FtrProviderContext) { supertest, cases: [ { - id: caseInfo.subCase!.id, - version: caseInfo.subCase!.version, + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, status: CaseStatuses['in-progress'], }, ], @@ -239,8 +239,8 @@ export default function ({ getService }: FtrProviderContext) { supertest, cases: [ { - id: collectionWithSecondSub.subCase!.id, - version: collectionWithSecondSub.subCase!.version, + id: collectionWithSecondSub.subCases![0].id, + version: collectionWithSecondSub.subCases![0].version, status: CaseStatuses['in-progress'], }, ], @@ -349,8 +349,8 @@ export default function ({ getService }: FtrProviderContext) { supertest, cases: [ { - id: caseInfo.subCase!.id, - version: caseInfo.subCase!.version, + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, status: CaseStatuses['in-progress'], }, ], @@ -450,8 +450,8 @@ export default function ({ getService }: FtrProviderContext) { .send({ subCases: [ { - id: caseInfo.subCase!.id, - version: caseInfo.subCase!.version, + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, type: 'blah', }, ], diff --git a/x-pack/test/case_api_integration/common/lib/mock.ts b/x-pack/test/case_api_integration/common/lib/mock.ts index f6fd2b1a6b3be..c3c37bd20f140 100644 --- a/x-pack/test/case_api_integration/common/lib/mock.ts +++ b/x-pack/test/case_api_integration/common/lib/mock.ts @@ -26,7 +26,6 @@ import { CaseClientPostRequest, SubCaseResponse, AssociationType, - CollectionWithSubCaseResponse, SubCasesFindResponse, CommentRequest, } from '../../../../plugins/case/common/api'; @@ -159,18 +158,17 @@ export const subCaseResp = ({ interface FormattedCollectionResponse { caseInfo: Partial; - subCase?: Partial; + subCases?: Array>; comments?: Array>; } -export const formatCollectionResponse = ( - caseInfo: CollectionWithSubCaseResponse -): FormattedCollectionResponse => { +export const formatCollectionResponse = (caseInfo: CaseResponse): FormattedCollectionResponse => { + const subCase = removeServerGeneratedPropertiesFromSubCase(caseInfo.subCases?.[0]); return { caseInfo: removeServerGeneratedPropertiesFromCaseCollection(caseInfo), - subCase: removeServerGeneratedPropertiesFromSubCase(caseInfo.subCase), + subCases: subCase ? [subCase] : undefined, comments: removeServerGeneratedPropertiesFromComments( - caseInfo.subCase?.comments ?? caseInfo.comments + caseInfo.subCases?.[0].comments ?? caseInfo.comments ), }; }; @@ -187,10 +185,10 @@ export const removeServerGeneratedPropertiesFromSubCase = ( }; export const removeServerGeneratedPropertiesFromCaseCollection = ( - config: Partial -): Partial => { + config: Partial +): Partial => { // eslint-disable-next-line @typescript-eslint/naming-convention - const { closed_at, created_at, updated_at, version, subCase, ...rest } = config; + const { closed_at, created_at, updated_at, version, subCases, ...rest } = config; return rest; }; diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 7aee6170c3d5a..3ade7ef96f9dd 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -17,7 +17,7 @@ import { CaseConnector, ConnectorTypes, CasePostRequest, - CollectionWithSubCaseResponse, + CaseResponse, SubCasesFindResponse, CaseStatuses, SubCasesResponse, @@ -120,7 +120,7 @@ export const defaultCreateSubPost = postCollectionReq; * Response structure for the createSubCase and createSubCaseComment functions. */ export interface CreateSubCaseResp { - newSubCaseInfo: CollectionWithSubCaseResponse; + newSubCaseInfo: CaseResponse; modifiedSubCases?: SubCasesResponse; } From 910a19f3c418cdae2d0c3d607595844844098324 Mon Sep 17 00:00:00 2001 From: Marshall Main <55718608+marshallmain@users.noreply.github.com> Date: Fri, 26 Feb 2021 07:25:06 -0800 Subject: [PATCH 26/32] Add warning for EQL and Threshold rules if exception list contains value list items (#92914) --- .../common/detection_engine/utils.ts | 12 ++++++- .../routes/__mocks__/request_responses.ts | 29 +++++++++++++++++ .../signals/signal_rule_alert_type.test.ts | 32 ++++++++++++++++++- .../signals/signal_rule_alert_type.ts | 13 ++++++++ 4 files changed, 84 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index 725a2eb9fea7b..79b912e082fdb 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -5,9 +5,19 @@ * 2.0. */ -import { EntriesArray } from '../shared_imports'; +import { + CreateExceptionListItemSchema, + EntriesArray, + ExceptionListItemSchema, +} from '../shared_imports'; import { Type } from './schemas/common/schemas'; +export const hasLargeValueItem = ( + exceptionItems: Array +) => { + return exceptionItems.some((exceptionItem) => hasLargeValueList(exceptionItem.entries)); +}; + export const hasLargeValueList = (entries: EntriesArray): boolean => { const found = entries.filter(({ type }) => type === 'list'); return found.length > 0; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index cf6ea572aa856..649ce9ed64365 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -440,6 +440,35 @@ export const getMlResult = (): RuleAlertType => { }; }; +export const getThresholdResult = (): RuleAlertType => { + const result = getResult(); + + return { + ...result, + params: { + ...result.params, + type: 'threshold', + threshold: { + field: 'host.ip', + value: 5, + }, + }, + }; +}; + +export const getEqlResult = (): RuleAlertType => { + const result = getResult(); + + return { + ...result, + params: { + ...result.params, + type: 'eql', + query: 'process where true', + }, + }; +}; + export const updateActionResult = (): ActionResult => ({ id: 'result-1', actionTypeId: 'action-id-1', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index cadc6d0c5b7c0..d3d82682cbb4a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -7,7 +7,12 @@ import moment from 'moment'; import { loggingSystemMock } from 'src/core/server/mocks'; -import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; +import { + getResult, + getMlResult, + getThresholdResult, + getEqlResult, +} from '../routes/__mocks__/request_responses'; import { signalRulesAlertType } from './signal_rule_alert_type'; import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; import { ruleStatusServiceFactory } from './rule_status_service'; @@ -24,6 +29,7 @@ import { getListClientMock } from '../../../../../lists/server/services/lists/li import { getExceptionListClientMock } from '../../../../../lists/server/services/exception_lists/exception_list_client.mock'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; +import { getEntryListMock } from '../../../../../lists/common/schemas/types/entry_list.mock'; jest.mock('./rule_status_saved_objects_client'); jest.mock('./rule_status_service'); @@ -211,6 +217,30 @@ describe('rules_notification_alert_type', () => { ); }); + it('should set a warning when exception list for threshold rule contains value list exceptions', async () => { + (getExceptions as jest.Mock).mockReturnValue([ + getExceptionListItemSchemaMock({ entries: [getEntryListMock()] }), + ]); + payload = getPayload(getThresholdResult(), alertServices); + await alert.executor(payload); + expect(ruleStatusService.warning).toHaveBeenCalled(); + expect(ruleStatusService.warning.mock.calls[0][0]).toContain( + 'Exceptions that use "is in list" or "is not in list" operators are not applied to Threshold rules' + ); + }); + + it('should set a warning when exception list for EQL rule contains value list exceptions', async () => { + (getExceptions as jest.Mock).mockReturnValue([ + getExceptionListItemSchemaMock({ entries: [getEntryListMock()] }), + ]); + payload = getPayload(getEqlResult(), alertServices); + await alert.executor(payload); + expect(ruleStatusService.warning).toHaveBeenCalled(); + expect(ruleStatusService.warning.mock.calls[0][0]).toContain( + 'Exceptions that use "is in list" or "is not in list" operators are not applied to EQL rules' + ); + }); + it('should set a failure status for when rules cannot read ANY provided indices', async () => { (checkPrivileges as jest.Mock).mockResolvedValueOnce({ username: 'elastic', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 2025ba512cb65..14a65bc1eeb7b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -24,6 +24,7 @@ import { isThresholdRule, isEqlRule, isThreatMatchRule, + hasLargeValueItem, } from '../../../../common/detection_engine/utils'; import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; import { SetupPlugins } from '../../../plugin'; @@ -365,6 +366,12 @@ export const signalRulesAlertType = ({ }), ]); } else if (isThresholdRule(type) && threshold) { + if (hasLargeValueItem(exceptionItems ?? [])) { + await ruleStatusService.warning( + 'Exceptions that use "is in list" or "is not in list" operators are not applied to Threshold rules' + ); + wroteWarningStatus = true; + } const inputIndex = await getInputIndex(services, version, index); const thresholdFields = Array.isArray(threshold.field) @@ -552,6 +559,12 @@ export const signalRulesAlertType = ({ if (query === undefined) { throw new Error('EQL query rule must have a query defined'); } + if (hasLargeValueItem(exceptionItems ?? [])) { + await ruleStatusService.warning( + 'Exceptions that use "is in list" or "is not in list" operators are not applied to EQL rules' + ); + wroteWarningStatus = true; + } try { const signalIndexVersion = await getIndexVersion(services.callCluster, outputIndex); if (isOutdated({ current: signalIndexVersion, target: MIN_EQL_RULE_INDEX_VERSION })) { From 83234aad2d460a757c7929a3adcaf03f7df50fbc Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Fri, 26 Feb 2021 08:48:51 -0800 Subject: [PATCH 27/32] [Alerts][Doc] Added README documentation for alerts plugin status and framework health checks configuration options. (#92761) * [Alerts][Doc] Added README documentation for alerts plugin status and framework health checks configuration options. * Apply suggestions from code review Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- x-pack/plugins/alerts/README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/x-pack/plugins/alerts/README.md b/x-pack/plugins/alerts/README.md index c57216603665d..07bad42a3bfa3 100644 --- a/x-pack/plugins/alerts/README.md +++ b/x-pack/plugins/alerts/README.md @@ -15,6 +15,7 @@ Table of Contents - [Usage](#usage) - [Alerts API keys](#alerts-api-keys) - [Limitations](#limitations) + - [Plugin status](#plugin-status) - [Alert types](#alert-types) - [Methods](#methods) - [Executor](#executor) @@ -79,6 +80,27 @@ Note that the `manage_own_api_key` cluster privilege is not enough - it can be u is unauthorized for user [user-name-here] ``` +## Plugin status + +The plugin status of an alert is customized by including information about checking failures for the framework decryption: +``` +core.status.set( + combineLatest([ + core.status.derivedStatus$, + getHealthStatusStream(startPlugins.taskManager), + ]).pipe( + map(([derivedStatus, healthStatus]) => { + if (healthStatus.level > derivedStatus.level) { + return healthStatus as ServiceStatus; + } else { + return derivedStatus; + } + }) + ) + ); +``` +To check for framework decryption failures, we use the task `alerting_health_check`, which runs every 60 minutes by default. To change the default schedule, use the kibana.yml configuration option `xpack.alerts.healthCheck.interval`. + ## Alert types ### Methods From 993ac501053dc18607c9a006aaeb3f104c994ff8 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Fri, 26 Feb 2021 12:48:09 -0500 Subject: [PATCH 28/32] [Security Solution][Case][Bug] Improve case logging (#91924) * First pass at bringing in more logging * Adding more logging to routes * Adding more logging fixing tests * Removing duplicate case string in logs * Removing unneeded export * Fixing type error * Adding line breaks to make the messages more readable * Fixing type errors Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/case/server/client/alerts/get.ts | 6 +- .../client/alerts/update_status.test.ts | 1 + .../server/client/alerts/update_status.ts | 6 +- .../case/server/client/cases/create.test.ts | 101 +++--- .../case/server/client/cases/create.ts | 80 +++-- .../plugins/case/server/client/cases/get.ts | 66 ++-- .../plugins/case/server/client/cases/push.ts | 32 +- .../case/server/client/cases/update.test.ts | 73 ++-- .../case/server/client/cases/update.ts | 313 +++++++++--------- x-pack/plugins/case/server/client/client.ts | 220 ++++++++---- .../case/server/client/comments/add.test.ts | 41 ++- .../case/server/client/comments/add.ts | 275 ++++++++------- .../server/client/configure/get_mappings.ts | 75 +++-- .../plugins/case/server/client/index.test.ts | 3 + x-pack/plugins/case/server/client/mocks.ts | 2 +- x-pack/plugins/case/server/client/types.ts | 3 +- x-pack/plugins/case/server/common/error.ts | 73 ++++ .../server/common/models/commentable_case.ts | 310 +++++++++-------- .../case/server/connectors/case/index.ts | 60 +++- x-pack/plugins/case/server/plugin.ts | 6 + .../routes/api/__fixtures__/mock_router.ts | 1 + .../routes/api/__fixtures__/route_contexts.ts | 1 + .../api/cases/comments/delete_all_comments.ts | 10 +- .../api/cases/comments/delete_comment.ts | 10 +- .../api/cases/comments/find_comments.ts | 5 +- .../api/cases/comments/get_all_comment.ts | 5 +- .../routes/api/cases/comments/get_comment.ts | 5 +- .../api/cases/comments/patch_comment.ts | 32 +- .../routes/api/cases/comments/post_comment.ts | 5 +- .../api/cases/configure/get_configure.ts | 3 +- .../api/cases/configure/get_connectors.ts | 3 +- .../api/cases/configure/patch_configure.ts | 8 +- .../api/cases/configure/post_configure.ts | 8 +- .../server/routes/api/cases/delete_cases.ts | 5 +- .../server/routes/api/cases/find_cases.ts | 3 +- .../case/server/routes/api/cases/get_case.ts | 11 +- .../server/routes/api/cases/patch_cases.ts | 15 +- .../case/server/routes/api/cases/post_case.ts | 15 +- .../case/server/routes/api/cases/push_case.ts | 21 +- .../api/cases/reporters/get_reporters.ts | 3 +- .../routes/api/cases/status/get_status.ts | 3 +- .../api/cases/sub_case/delete_sub_cases.ts | 10 +- .../api/cases/sub_case/find_sub_cases.ts | 5 +- .../routes/api/cases/sub_case/get_sub_case.ts | 5 +- .../api/cases/sub_case/patch_sub_cases.ts | 308 +++++++++-------- .../user_actions/get_all_user_actions.ts | 36 +- .../plugins/case/server/routes/api/types.ts | 3 + .../plugins/case/server/routes/api/utils.ts | 17 +- .../case/server/services/alerts/index.test.ts | 5 +- .../case/server/services/alerts/index.ts | 80 +++-- .../services/connector_mappings/index.ts | 4 +- x-pack/plugins/case/server/services/index.ts | 60 ++-- .../server/services/user_actions/index.ts | 4 +- 53 files changed, 1501 insertions(+), 954 deletions(-) create mode 100644 x-pack/plugins/case/server/common/error.ts diff --git a/x-pack/plugins/case/server/client/alerts/get.ts b/x-pack/plugins/case/server/client/alerts/get.ts index a7ca5d9742c6b..0b2663b737204 100644 --- a/x-pack/plugins/case/server/client/alerts/get.ts +++ b/x-pack/plugins/case/server/client/alerts/get.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ElasticsearchClient } from 'kibana/server'; +import { ElasticsearchClient, Logger } from 'kibana/server'; import { AlertServiceContract } from '../../services'; import { CaseClientGetAlertsResponse } from './types'; @@ -14,6 +14,7 @@ interface GetParams { ids: string[]; indices: Set; scopedClusterClient: ElasticsearchClient; + logger: Logger; } export const get = async ({ @@ -21,12 +22,13 @@ export const get = async ({ ids, indices, scopedClusterClient, + logger, }: GetParams): Promise => { if (ids.length === 0 || indices.size <= 0) { return []; } - const alerts = await alertsService.getAlerts({ ids, indices, scopedClusterClient }); + const alerts = await alertsService.getAlerts({ ids, indices, scopedClusterClient, logger }); if (!alerts) { return []; } diff --git a/x-pack/plugins/case/server/client/alerts/update_status.test.ts b/x-pack/plugins/case/server/client/alerts/update_status.test.ts index c8df1c8ab74f3..b3ed3c2b84a99 100644 --- a/x-pack/plugins/case/server/client/alerts/update_status.test.ts +++ b/x-pack/plugins/case/server/client/alerts/update_status.test.ts @@ -22,6 +22,7 @@ describe('updateAlertsStatus', () => { expect(caseClient.services.alertsService.updateAlertsStatus).toHaveBeenCalledWith({ scopedClusterClient: expect.anything(), + logger: expect.anything(), ids: ['alert-id-1'], indices: new Set(['.siem-signals']), status: CaseStatuses.closed, diff --git a/x-pack/plugins/case/server/client/alerts/update_status.ts b/x-pack/plugins/case/server/client/alerts/update_status.ts index cb18bd4fc16e3..2194c3a18afdd 100644 --- a/x-pack/plugins/case/server/client/alerts/update_status.ts +++ b/x-pack/plugins/case/server/client/alerts/update_status.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ElasticsearchClient } from 'src/core/server'; +import { ElasticsearchClient, Logger } from 'src/core/server'; import { CaseStatuses } from '../../../common/api'; import { AlertServiceContract } from '../../services'; @@ -15,6 +15,7 @@ interface UpdateAlertsStatusArgs { status: CaseStatuses; indices: Set; scopedClusterClient: ElasticsearchClient; + logger: Logger; } export const updateAlertsStatus = async ({ @@ -23,6 +24,7 @@ export const updateAlertsStatus = async ({ status, indices, scopedClusterClient, + logger, }: UpdateAlertsStatusArgs): Promise => { - await alertsService.updateAlertsStatus({ ids, status, indices, scopedClusterClient }); + await alertsService.updateAlertsStatus({ ids, status, indices, scopedClusterClient, logger }); }; diff --git a/x-pack/plugins/case/server/client/cases/create.test.ts b/x-pack/plugins/case/server/client/cases/create.test.ts index 3016a57f21875..e8cc1a8898f04 100644 --- a/x-pack/plugins/case/server/client/cases/create.test.ts +++ b/x-pack/plugins/case/server/client/cases/create.test.ts @@ -6,6 +6,7 @@ */ import { ConnectorTypes, CaseStatuses, CaseType, CaseClientPostRequest } from '../../../common/api'; +import { isCaseError } from '../../common/error'; import { createMockSavedObjectsRepository, @@ -276,14 +277,16 @@ describe('create', () => { caseSavedObject: mockCases, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client - // @ts-expect-error - .create({ theCase: postCase }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); + return ( + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }) + ); }); test('it throws when missing description', async () => { @@ -303,14 +306,16 @@ describe('create', () => { caseSavedObject: mockCases, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client - // @ts-expect-error - .create({ theCase: postCase }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); + return ( + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }) + ); }); test('it throws when missing tags', async () => { @@ -330,14 +335,16 @@ describe('create', () => { caseSavedObject: mockCases, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client - // @ts-expect-error - .create({ theCase: postCase }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); + return ( + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }) + ); }); test('it throws when missing connector ', async () => { @@ -352,14 +359,16 @@ describe('create', () => { caseSavedObject: mockCases, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client - // @ts-expect-error - .create({ theCase: postCase }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); + return ( + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }) + ); }); test('it throws when connector missing the right fields', async () => { @@ -380,14 +389,16 @@ describe('create', () => { caseSavedObject: mockCases, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client - // @ts-expect-error - .create({ theCase: postCase }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); + return ( + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }) + ); }); test('it throws if you passing status for a new case', async () => { @@ -413,7 +424,7 @@ describe('create', () => { caseSavedObject: mockCases, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client.create(postCase).catch((e) => { + return caseClient.client.create(postCase).catch((e) => { expect(e).not.toBeNull(); expect(e.isBoom).toBe(true); expect(e.output.statusCode).toBe(400); @@ -441,10 +452,12 @@ describe('create', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client.create(postCase).catch((e) => { + return caseClient.client.create(postCase).catch((e) => { expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); + expect(isCaseError(e)).toBeTruthy(); + const boomErr = e.boomify(); + expect(boomErr.isBoom).toBe(true); + expect(boomErr.output.statusCode).toBe(400); }); }); }); diff --git a/x-pack/plugins/case/server/client/cases/create.ts b/x-pack/plugins/case/server/client/cases/create.ts index ee47c59072fdd..f88924483e0b8 100644 --- a/x-pack/plugins/case/server/client/cases/create.ts +++ b/x-pack/plugins/case/server/client/cases/create.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsClientContract, Logger } from 'src/core/server'; import { flattenCaseSavedObject, transformNewCase } from '../../routes/api/utils'; import { @@ -34,6 +34,7 @@ import { CaseServiceSetup, CaseUserActionServiceSetup, } from '../../services'; +import { createCaseError } from '../../common/error'; interface CreateCaseArgs { caseConfigureService: CaseConfigureServiceSetup; @@ -42,8 +43,12 @@ interface CreateCaseArgs { savedObjectsClient: SavedObjectsClientContract; userActionService: CaseUserActionServiceSetup; theCase: CasePostRequest; + logger: Logger; } +/** + * Creates a new case. + */ export const create = async ({ savedObjectsClient, caseService, @@ -51,6 +56,7 @@ export const create = async ({ userActionService, user, theCase, + logger, }: CreateCaseArgs): Promise => { // default to an individual case if the type is not defined. const { type = CaseType.individual, ...nonTypeCaseFields } = theCase; @@ -60,41 +66,45 @@ export const create = async ({ fold(throwErrors(Boom.badRequest), identity) ); - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = user; - const createdDate = new Date().toISOString(); - const myCaseConfigure = await caseConfigureService.find({ client: savedObjectsClient }); - const caseConfigureConnector = getConnectorFromConfiguration(myCaseConfigure); - - const newCase = await caseService.postNewCase({ - client: savedObjectsClient, - attributes: transformNewCase({ - createdDate, - newCase: query, - username, - full_name, - email, - connector: transformCaseConnectorToEsConnector(query.connector ?? caseConfigureConnector), - }), - }); + try { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { username, full_name, email } = user; + const createdDate = new Date().toISOString(); + const myCaseConfigure = await caseConfigureService.find({ client: savedObjectsClient }); + const caseConfigureConnector = getConnectorFromConfiguration(myCaseConfigure); - await userActionService.postUserActions({ - client: savedObjectsClient, - actions: [ - buildCaseUserActionItem({ - action: 'create', - actionAt: createdDate, - actionBy: { username, full_name, email }, - caseId: newCase.id, - fields: ['description', 'status', 'tags', 'title', 'connector', 'settings'], - newValue: JSON.stringify(query), + const newCase = await caseService.postNewCase({ + client: savedObjectsClient, + attributes: transformNewCase({ + createdDate, + newCase: query, + username, + full_name, + email, + connector: transformCaseConnectorToEsConnector(query.connector ?? caseConfigureConnector), }), - ], - }); + }); - return CaseResponseRt.encode( - flattenCaseSavedObject({ - savedObject: newCase, - }) - ); + await userActionService.postUserActions({ + client: savedObjectsClient, + actions: [ + buildCaseUserActionItem({ + action: 'create', + actionAt: createdDate, + actionBy: { username, full_name, email }, + caseId: newCase.id, + fields: ['description', 'status', 'tags', 'title', 'connector', 'settings'], + newValue: JSON.stringify(query), + }), + ], + }); + + return CaseResponseRt.encode( + flattenCaseSavedObject({ + savedObject: newCase, + }) + ); + } catch (error) { + throw createCaseError({ message: `Failed to create case: ${error}`, error, logger }); + } }; diff --git a/x-pack/plugins/case/server/client/cases/get.ts b/x-pack/plugins/case/server/client/cases/get.ts index ab0b97abbcb76..fa556986ee8d3 100644 --- a/x-pack/plugins/case/server/client/cases/get.ts +++ b/x-pack/plugins/case/server/client/cases/get.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract, Logger } from 'kibana/server'; import { flattenCaseSavedObject } from '../../routes/api/utils'; import { CaseResponseRt, CaseResponse } from '../../../common/api'; import { CaseServiceSetup } from '../../services'; import { countAlertsForID } from '../../common'; +import { createCaseError } from '../../common/error'; interface GetParams { savedObjectsClient: SavedObjectsClientContract; @@ -17,50 +18,59 @@ interface GetParams { id: string; includeComments?: boolean; includeSubCaseComments?: boolean; + logger: Logger; } +/** + * Retrieves a case and optionally its comments and sub case comments. + */ export const get = async ({ savedObjectsClient, caseService, id, + logger, includeComments = false, includeSubCaseComments = false, }: GetParams): Promise => { - const [theCase, subCasesForCaseId] = await Promise.all([ - caseService.getCase({ + try { + const [theCase, subCasesForCaseId] = await Promise.all([ + caseService.getCase({ + client: savedObjectsClient, + id, + }), + caseService.findSubCasesByCaseId({ client: savedObjectsClient, ids: [id] }), + ]); + + const subCaseIds = subCasesForCaseId.saved_objects.map((so) => so.id); + + if (!includeComments) { + return CaseResponseRt.encode( + flattenCaseSavedObject({ + savedObject: theCase, + subCaseIds, + }) + ); + } + const theComments = await caseService.getAllCaseComments({ client: savedObjectsClient, id, - }), - caseService.findSubCasesByCaseId({ client: savedObjectsClient, ids: [id] }), - ]); + options: { + sortField: 'created_at', + sortOrder: 'asc', + }, + includeSubCaseComments, + }); - const subCaseIds = subCasesForCaseId.saved_objects.map((so) => so.id); - - if (!includeComments) { return CaseResponseRt.encode( flattenCaseSavedObject({ savedObject: theCase, + comments: theComments.saved_objects, subCaseIds, + totalComment: theComments.total, + totalAlerts: countAlertsForID({ comments: theComments, id }), }) ); + } catch (error) { + throw createCaseError({ message: `Failed to get case id: ${id}: ${error}`, error, logger }); } - const theComments = await caseService.getAllCaseComments({ - client: savedObjectsClient, - id, - options: { - sortField: 'created_at', - sortOrder: 'asc', - }, - includeSubCaseComments, - }); - - return CaseResponseRt.encode( - flattenCaseSavedObject({ - savedObject: theCase, - comments: theComments.saved_objects, - subCaseIds, - totalComment: theComments.total, - totalAlerts: countAlertsForID({ comments: theComments, id }), - }) - ); }; diff --git a/x-pack/plugins/case/server/client/cases/push.ts b/x-pack/plugins/case/server/client/cases/push.ts index 1e0c246855d88..352328ed1dd40 100644 --- a/x-pack/plugins/case/server/client/cases/push.ts +++ b/x-pack/plugins/case/server/client/cases/push.ts @@ -5,11 +5,12 @@ * 2.0. */ -import Boom, { isBoom, Boom as BoomType } from '@hapi/boom'; +import Boom from '@hapi/boom'; import { SavedObjectsBulkUpdateResponse, SavedObjectsClientContract, SavedObjectsUpdateResponse, + Logger, } from 'kibana/server'; import { ActionResult, ActionsClient } from '../../../../actions/server'; import { flattenCaseSavedObject, getAlertIndicesAndIDs } from '../../routes/api/utils'; @@ -34,16 +35,7 @@ import { CaseUserActionServiceSetup, } from '../../services'; import { CaseClientHandler } from '../client'; - -const createError = (e: Error | BoomType, message: string): Error | BoomType => { - if (isBoom(e)) { - e.message = message; - e.output.payload.message = message; - return e; - } - - return Error(message); -}; +import { createCaseError } from '../../common/error'; interface PushParams { savedObjectsClient: SavedObjectsClientContract; @@ -55,6 +47,7 @@ interface PushParams { connectorId: string; caseClient: CaseClientHandler; actionsClient: ActionsClient; + logger: Logger; } export const push = async ({ @@ -67,6 +60,7 @@ export const push = async ({ connectorId, caseId, user, + logger, }: PushParams): Promise => { /* Start of push to external service */ let theCase: CaseResponse; @@ -84,7 +78,7 @@ export const push = async ({ ]); } catch (e) { const message = `Error getting case and/or connector and/or user actions: ${e.message}`; - throw createError(e, message); + throw createCaseError({ message, error: e, logger }); } // We need to change the logic when we support subcases @@ -102,7 +96,11 @@ export const push = async ({ indices, }); } catch (e) { - throw new Error(`Error getting alerts for case with id ${theCase.id}: ${e.message}`); + throw createCaseError({ + message: `Error getting alerts for case with id ${theCase.id}: ${e.message}`, + logger, + error: e, + }); } try { @@ -113,7 +111,7 @@ export const push = async ({ }); } catch (e) { const message = `Error getting mapping for connector with id ${connector.id}: ${e.message}`; - throw createError(e, message); + throw createCaseError({ message, error: e, logger }); } try { @@ -127,7 +125,7 @@ export const push = async ({ }); } catch (e) { const message = `Error creating incident for case with id ${theCase.id}: ${e.message}`; - throw createError(e, message); + throw createCaseError({ error: e, message, logger }); } const pushRes = await actionsClient.execute({ @@ -171,7 +169,7 @@ export const push = async ({ ]); } catch (e) { const message = `Error getting user and/or case and/or case configuration and/or case comments: ${e.message}`; - throw createError(e, message); + throw createCaseError({ error: e, message, logger }); } // eslint-disable-next-line @typescript-eslint/naming-convention @@ -257,7 +255,7 @@ export const push = async ({ ]); } catch (e) { const message = `Error updating case and/or comments and/or creating user action: ${e.message}`; - throw createError(e, message); + throw createCaseError({ error: e, message, logger }); } /* End of update case with push information */ diff --git a/x-pack/plugins/case/server/client/cases/update.test.ts b/x-pack/plugins/case/server/client/cases/update.test.ts index 7a3e4458f25c5..752b0ab369de0 100644 --- a/x-pack/plugins/case/server/client/cases/update.test.ts +++ b/x-pack/plugins/case/server/client/cases/update.test.ts @@ -6,6 +6,7 @@ */ import { ConnectorTypes, CasesPatchRequest, CaseStatuses } from '../../../common/api'; +import { isCaseError } from '../../common/error'; import { createMockSavedObjectsRepository, mockCaseNoConnectorId, @@ -640,14 +641,16 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client - // @ts-expect-error - .update({ cases: patchCases }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); + return ( + caseClient.client + // @ts-expect-error + .update({ cases: patchCases }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }) + ); }); test('it throws when missing version', async () => { @@ -671,18 +674,20 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client - // @ts-expect-error - .update({ cases: patchCases }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); + return ( + caseClient.client + // @ts-expect-error + .update({ cases: patchCases }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }) + ); }); test('it throws when fields are identical', async () => { - expect.assertions(4); + expect.assertions(5); const patchCases = { cases: [ { @@ -698,16 +703,18 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client.update(patchCases).catch((e) => { + return caseClient.client.update(patchCases).catch((e) => { expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(406); - expect(e.message).toBe('All update fields are identical to current version.'); + expect(isCaseError(e)).toBeTruthy(); + const boomErr = e.boomify(); + expect(boomErr.isBoom).toBe(true); + expect(boomErr.output.statusCode).toBe(406); + expect(boomErr.message).toContain('All update fields are identical to current version.'); }); }); test('it throws when case does not exist', async () => { - expect.assertions(4); + expect.assertions(5); const patchCases = { cases: [ { @@ -728,18 +735,20 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client.update(patchCases).catch((e) => { + return caseClient.client.update(patchCases).catch((e) => { expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(404); - expect(e.message).toBe( + expect(isCaseError(e)).toBeTruthy(); + const boomErr = e.boomify(); + expect(boomErr.isBoom).toBe(true); + expect(boomErr.output.statusCode).toBe(404); + expect(boomErr.message).toContain( 'These cases not-exists do not exist. Please check you have the correct ids.' ); }); }); test('it throws when cases conflicts', async () => { - expect.assertions(4); + expect.assertions(5); const patchCases = { cases: [ { @@ -755,11 +764,13 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client.update(patchCases).catch((e) => { + return caseClient.client.update(patchCases).catch((e) => { expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(409); - expect(e.message).toBe( + expect(isCaseError(e)).toBeTruthy(); + const boomErr = e.boomify(); + expect(boomErr.isBoom).toBe(true); + expect(boomErr.output.statusCode).toBe(409); + expect(boomErr.message).toContain( 'These cases mock-id-1 has been updated. Please refresh before saving additional updates.' ); }); diff --git a/x-pack/plugins/case/server/client/cases/update.ts b/x-pack/plugins/case/server/client/cases/update.ts index a4ca2b4cbdef9..36318f03bd33f 100644 --- a/x-pack/plugins/case/server/client/cases/update.ts +++ b/x-pack/plugins/case/server/client/cases/update.ts @@ -15,6 +15,7 @@ import { SavedObjectsClientContract, SavedObjectsFindResponse, SavedObjectsFindResult, + Logger, } from 'kibana/server'; import { AlertInfo, @@ -53,6 +54,7 @@ import { } from '../../saved_object_types'; import { CaseClientHandler } from '..'; import { addAlertInfoToStatusMap } from '../../common'; +import { createCaseError } from '../../common/error'; /** * Throws an error if any of the requests attempt to update a collection style cases' status field. @@ -325,6 +327,7 @@ interface UpdateArgs { user: User; caseClient: CaseClientHandler; cases: CasesPatchRequest; + logger: Logger; } export const update = async ({ @@ -334,175 +337,189 @@ export const update = async ({ user, caseClient, cases, + logger, }: UpdateArgs): Promise => { const query = pipe( excess(CasesPatchRequestRt).decode(cases), fold(throwErrors(Boom.badRequest), identity) ); - const myCases = await caseService.getCases({ - client: savedObjectsClient, - caseIds: query.cases.map((q) => q.id), - }); - - let nonExistingCases: CasePatchRequest[] = []; - const conflictedCases = query.cases.filter((q) => { - const myCase = myCases.saved_objects.find((c) => c.id === q.id); + try { + const myCases = await caseService.getCases({ + client: savedObjectsClient, + caseIds: query.cases.map((q) => q.id), + }); - if (myCase && myCase.error) { - nonExistingCases = [...nonExistingCases, q]; - return false; - } - return myCase == null || myCase?.version !== q.version; - }); + let nonExistingCases: CasePatchRequest[] = []; + const conflictedCases = query.cases.filter((q) => { + const myCase = myCases.saved_objects.find((c) => c.id === q.id); - if (nonExistingCases.length > 0) { - throw Boom.notFound( - `These cases ${nonExistingCases - .map((c) => c.id) - .join(', ')} do not exist. Please check you have the correct ids.` - ); - } + if (myCase && myCase.error) { + nonExistingCases = [...nonExistingCases, q]; + return false; + } + return myCase == null || myCase?.version !== q.version; + }); - if (conflictedCases.length > 0) { - throw Boom.conflict( - `These cases ${conflictedCases - .map((c) => c.id) - .join(', ')} has been updated. Please refresh before saving additional updates.` - ); - } + if (nonExistingCases.length > 0) { + throw Boom.notFound( + `These cases ${nonExistingCases + .map((c) => c.id) + .join(', ')} do not exist. Please check you have the correct ids.` + ); + } - const updateCases: ESCasePatchRequest[] = query.cases.map((updateCase) => { - const currentCase = myCases.saved_objects.find((c) => c.id === updateCase.id); - const { connector, ...thisCase } = updateCase; - return currentCase != null - ? getCaseToUpdate(currentCase.attributes, { - ...thisCase, - ...(connector != null - ? { connector: transformCaseConnectorToEsConnector(connector) } - : {}), - }) - : { id: thisCase.id, version: thisCase.version }; - }); + if (conflictedCases.length > 0) { + throw Boom.conflict( + `These cases ${conflictedCases + .map((c) => c.id) + .join(', ')} has been updated. Please refresh before saving additional updates.` + ); + } - const updateFilterCases = updateCases.filter((updateCase) => { - const { id, version, ...updateCaseAttributes } = updateCase; - return Object.keys(updateCaseAttributes).length > 0; - }); + const updateCases: ESCasePatchRequest[] = query.cases.map((updateCase) => { + const currentCase = myCases.saved_objects.find((c) => c.id === updateCase.id); + const { connector, ...thisCase } = updateCase; + return currentCase != null + ? getCaseToUpdate(currentCase.attributes, { + ...thisCase, + ...(connector != null + ? { connector: transformCaseConnectorToEsConnector(connector) } + : {}), + }) + : { id: thisCase.id, version: thisCase.version }; + }); - if (updateFilterCases.length <= 0) { - throw Boom.notAcceptable('All update fields are identical to current version.'); - } + const updateFilterCases = updateCases.filter((updateCase) => { + const { id, version, ...updateCaseAttributes } = updateCase; + return Object.keys(updateCaseAttributes).length > 0; + }); - const casesMap = myCases.saved_objects.reduce((acc, so) => { - acc.set(so.id, so); - return acc; - }, new Map>()); + if (updateFilterCases.length <= 0) { + throw Boom.notAcceptable('All update fields are identical to current version.'); + } - throwIfUpdateStatusOfCollection(updateFilterCases, casesMap); - throwIfUpdateTypeCollectionToIndividual(updateFilterCases, casesMap); - await throwIfInvalidUpdateOfTypeWithAlerts({ - requests: updateFilterCases, - caseService, - client: savedObjectsClient, - }); + const casesMap = myCases.saved_objects.reduce((acc, so) => { + acc.set(so.id, so); + return acc; + }, new Map>()); + + throwIfUpdateStatusOfCollection(updateFilterCases, casesMap); + throwIfUpdateTypeCollectionToIndividual(updateFilterCases, casesMap); + await throwIfInvalidUpdateOfTypeWithAlerts({ + requests: updateFilterCases, + caseService, + client: savedObjectsClient, + }); - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = user; - const updatedDt = new Date().toISOString(); - const updatedCases = await caseService.patchCases({ - client: savedObjectsClient, - cases: updateFilterCases.map((thisCase) => { - const { id: caseId, version, ...updateCaseAttributes } = thisCase; - let closedInfo = {}; - if (updateCaseAttributes.status && updateCaseAttributes.status === CaseStatuses.closed) { - closedInfo = { - closed_at: updatedDt, - closed_by: { email, full_name, username }, - }; - } else if ( - updateCaseAttributes.status && - (updateCaseAttributes.status === CaseStatuses.open || - updateCaseAttributes.status === CaseStatuses['in-progress']) - ) { - closedInfo = { - closed_at: null, - closed_by: null, + // eslint-disable-next-line @typescript-eslint/naming-convention + const { username, full_name, email } = user; + const updatedDt = new Date().toISOString(); + const updatedCases = await caseService.patchCases({ + client: savedObjectsClient, + cases: updateFilterCases.map((thisCase) => { + const { id: caseId, version, ...updateCaseAttributes } = thisCase; + let closedInfo = {}; + if (updateCaseAttributes.status && updateCaseAttributes.status === CaseStatuses.closed) { + closedInfo = { + closed_at: updatedDt, + closed_by: { email, full_name, username }, + }; + } else if ( + updateCaseAttributes.status && + (updateCaseAttributes.status === CaseStatuses.open || + updateCaseAttributes.status === CaseStatuses['in-progress']) + ) { + closedInfo = { + closed_at: null, + closed_by: null, + }; + } + return { + caseId, + updatedAttributes: { + ...updateCaseAttributes, + ...closedInfo, + updated_at: updatedDt, + updated_by: { email, full_name, username }, + }, + version, }; - } - return { - caseId, - updatedAttributes: { - ...updateCaseAttributes, - ...closedInfo, - updated_at: updatedDt, - updated_by: { email, full_name, username }, - }, - version, - }; - }), - }); + }), + }); - // If a status update occurred and the case is synced then we need to update all alerts' status - // attached to the case to the new status. - const casesWithStatusChangedAndSynced = updateFilterCases.filter((caseToUpdate) => { - const currentCase = myCases.saved_objects.find((c) => c.id === caseToUpdate.id); - return ( - currentCase != null && - caseToUpdate.status != null && - currentCase.attributes.status !== caseToUpdate.status && - currentCase.attributes.settings.syncAlerts - ); - }); + // If a status update occurred and the case is synced then we need to update all alerts' status + // attached to the case to the new status. + const casesWithStatusChangedAndSynced = updateFilterCases.filter((caseToUpdate) => { + const currentCase = myCases.saved_objects.find((c) => c.id === caseToUpdate.id); + return ( + currentCase != null && + caseToUpdate.status != null && + currentCase.attributes.status !== caseToUpdate.status && + currentCase.attributes.settings.syncAlerts + ); + }); - // If syncAlerts setting turned on we need to update all alerts' status - // attached to the case to the current status. - const casesWithSyncSettingChangedToOn = updateFilterCases.filter((caseToUpdate) => { - const currentCase = myCases.saved_objects.find((c) => c.id === caseToUpdate.id); - return ( - currentCase != null && - caseToUpdate.settings?.syncAlerts != null && - currentCase.attributes.settings.syncAlerts !== caseToUpdate.settings.syncAlerts && - caseToUpdate.settings.syncAlerts - ); - }); + // If syncAlerts setting turned on we need to update all alerts' status + // attached to the case to the current status. + const casesWithSyncSettingChangedToOn = updateFilterCases.filter((caseToUpdate) => { + const currentCase = myCases.saved_objects.find((c) => c.id === caseToUpdate.id); + return ( + currentCase != null && + caseToUpdate.settings?.syncAlerts != null && + currentCase.attributes.settings.syncAlerts !== caseToUpdate.settings.syncAlerts && + caseToUpdate.settings.syncAlerts + ); + }); - // Update the alert's status to match any case status or sync settings changes - await updateAlerts({ - casesWithStatusChangedAndSynced, - casesWithSyncSettingChangedToOn, - caseService, - client: savedObjectsClient, - caseClient, - casesMap, - }); + // Update the alert's status to match any case status or sync settings changes + await updateAlerts({ + casesWithStatusChangedAndSynced, + casesWithSyncSettingChangedToOn, + caseService, + client: savedObjectsClient, + caseClient, + casesMap, + }); - const returnUpdatedCase = myCases.saved_objects - .filter((myCase) => - updatedCases.saved_objects.some((updatedCase) => updatedCase.id === myCase.id) - ) - .map((myCase) => { - const updatedCase = updatedCases.saved_objects.find((c) => c.id === myCase.id); - return flattenCaseSavedObject({ - savedObject: { - ...myCase, - ...updatedCase, - attributes: { ...myCase.attributes, ...updatedCase?.attributes }, - references: myCase.references, - version: updatedCase?.version ?? myCase.version, - }, + const returnUpdatedCase = myCases.saved_objects + .filter((myCase) => + updatedCases.saved_objects.some((updatedCase) => updatedCase.id === myCase.id) + ) + .map((myCase) => { + const updatedCase = updatedCases.saved_objects.find((c) => c.id === myCase.id); + return flattenCaseSavedObject({ + savedObject: { + ...myCase, + ...updatedCase, + attributes: { ...myCase.attributes, ...updatedCase?.attributes }, + references: myCase.references, + version: updatedCase?.version ?? myCase.version, + }, + }); }); - }); - await userActionService.postUserActions({ - client: savedObjectsClient, - actions: buildCaseUserActions({ - originalCases: myCases.saved_objects, - updatedCases: updatedCases.saved_objects, - actionDate: updatedDt, - actionBy: { email, full_name, username }, - }), - }); + await userActionService.postUserActions({ + client: savedObjectsClient, + actions: buildCaseUserActions({ + originalCases: myCases.saved_objects, + updatedCases: updatedCases.saved_objects, + actionDate: updatedDt, + actionBy: { email, full_name, username }, + }), + }); - return CasesResponseRt.encode(returnUpdatedCase); + return CasesResponseRt.encode(returnUpdatedCase); + } catch (error) { + const idVersions = cases.cases.map((caseInfo) => ({ + id: caseInfo.id, + version: caseInfo.version, + })); + + throw createCaseError({ + message: `Failed to update case, ids: ${JSON.stringify(idVersions)}: ${error}`, + error, + logger, + }); + } }; diff --git a/x-pack/plugins/case/server/client/client.ts b/x-pack/plugins/case/server/client/client.ts index c684548decbe6..c34c3942b18d0 100644 --- a/x-pack/plugins/case/server/client/client.ts +++ b/x-pack/plugins/case/server/client/client.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'src/core/server'; import { CaseClientFactoryArguments, CaseClient, @@ -36,6 +36,7 @@ import { get } from './cases/get'; import { get as getUserActions } from './user_actions/get'; import { get as getAlerts } from './alerts/get'; import { push } from './cases/push'; +import { createCaseError } from '../common/error'; /** * This class is a pass through for common case functionality (like creating, get a case). @@ -49,6 +50,7 @@ export class CaseClientHandler implements CaseClient { private readonly _savedObjectsClient: SavedObjectsClientContract; private readonly _userActionService: CaseUserActionServiceSetup; private readonly _alertsService: AlertServiceContract; + private readonly logger: Logger; constructor(clientArgs: CaseClientFactoryArguments) { this._scopedClusterClient = clientArgs.scopedClusterClient; @@ -59,96 +61,190 @@ export class CaseClientHandler implements CaseClient { this._savedObjectsClient = clientArgs.savedObjectsClient; this._userActionService = clientArgs.userActionService; this._alertsService = clientArgs.alertsService; + this.logger = clientArgs.logger; } public async create(caseInfo: CasePostRequest) { - return create({ - savedObjectsClient: this._savedObjectsClient, - caseService: this._caseService, - caseConfigureService: this._caseConfigureService, - userActionService: this._userActionService, - user: this.user, - theCase: caseInfo, - }); + try { + return create({ + savedObjectsClient: this._savedObjectsClient, + caseService: this._caseService, + caseConfigureService: this._caseConfigureService, + userActionService: this._userActionService, + user: this.user, + theCase: caseInfo, + logger: this.logger, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to create a new case using client: ${error}`, + error, + logger: this.logger, + }); + } } public async update(cases: CasesPatchRequest) { - return update({ - savedObjectsClient: this._savedObjectsClient, - caseService: this._caseService, - userActionService: this._userActionService, - user: this.user, - cases, - caseClient: this, - }); + try { + return update({ + savedObjectsClient: this._savedObjectsClient, + caseService: this._caseService, + userActionService: this._userActionService, + user: this.user, + cases, + caseClient: this, + logger: this.logger, + }); + } catch (error) { + const caseIDVersions = cases.cases.map((caseInfo) => ({ + id: caseInfo.id, + version: caseInfo.version, + })); + throw createCaseError({ + message: `Failed to update cases using client: ${JSON.stringify(caseIDVersions)}: ${error}`, + error, + logger: this.logger, + }); + } } public async addComment({ caseId, comment }: CaseClientAddComment) { - return addComment({ - savedObjectsClient: this._savedObjectsClient, - caseService: this._caseService, - userActionService: this._userActionService, - caseClient: this, - caseId, - comment, - user: this.user, - }); + try { + return addComment({ + savedObjectsClient: this._savedObjectsClient, + caseService: this._caseService, + userActionService: this._userActionService, + caseClient: this, + caseId, + comment, + user: this.user, + logger: this.logger, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to add comment using client case id: ${caseId}: ${error}`, + error, + logger: this.logger, + }); + } } public async getFields(fields: ConfigureFields) { - return getFields(fields); + try { + return getFields(fields); + } catch (error) { + throw createCaseError({ + message: `Failed to retrieve fields using client: ${error}`, + error, + logger: this.logger, + }); + } } public async getMappings(args: MappingsClient) { - return getMappings({ - ...args, - savedObjectsClient: this._savedObjectsClient, - connectorMappingsService: this._connectorMappingsService, - caseClient: this, - }); + try { + return getMappings({ + ...args, + savedObjectsClient: this._savedObjectsClient, + connectorMappingsService: this._connectorMappingsService, + caseClient: this, + logger: this.logger, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to get mappings using client: ${error}`, + error, + logger: this.logger, + }); + } } public async updateAlertsStatus(args: CaseClientUpdateAlertsStatus) { - return updateAlertsStatus({ - ...args, - alertsService: this._alertsService, - scopedClusterClient: this._scopedClusterClient, - }); + try { + return updateAlertsStatus({ + ...args, + alertsService: this._alertsService, + scopedClusterClient: this._scopedClusterClient, + logger: this.logger, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to update alerts status using client ids: ${JSON.stringify( + args.ids + )} \nindices: ${JSON.stringify([...args.indices])} \nstatus: ${args.status}: ${error}`, + error, + logger: this.logger, + }); + } } public async get(args: CaseClientGet) { - return get({ - ...args, - caseService: this._caseService, - savedObjectsClient: this._savedObjectsClient, - }); + try { + return get({ + ...args, + caseService: this._caseService, + savedObjectsClient: this._savedObjectsClient, + logger: this.logger, + }); + } catch (error) { + this.logger.error(`Failed to get case using client id: ${args.id}: ${error}`); + throw error; + } } public async getUserActions(args: CaseClientGetUserActions) { - return getUserActions({ - ...args, - savedObjectsClient: this._savedObjectsClient, - userActionService: this._userActionService, - }); + try { + return getUserActions({ + ...args, + savedObjectsClient: this._savedObjectsClient, + userActionService: this._userActionService, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to get user actions using client id: ${args.caseId}: ${error}`, + error, + logger: this.logger, + }); + } } public async getAlerts(args: CaseClientGetAlerts) { - return getAlerts({ - ...args, - alertsService: this._alertsService, - scopedClusterClient: this._scopedClusterClient, - }); + try { + return getAlerts({ + ...args, + alertsService: this._alertsService, + scopedClusterClient: this._scopedClusterClient, + logger: this.logger, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to get alerts using client ids: ${JSON.stringify( + args.ids + )} \nindices: ${JSON.stringify([...args.indices])}: ${error}`, + error, + logger: this.logger, + }); + } } public async push(args: CaseClientPush) { - return push({ - ...args, - savedObjectsClient: this._savedObjectsClient, - caseService: this._caseService, - userActionService: this._userActionService, - user: this.user, - caseClient: this, - caseConfigureService: this._caseConfigureService, - }); + try { + return push({ + ...args, + savedObjectsClient: this._savedObjectsClient, + caseService: this._caseService, + userActionService: this._userActionService, + user: this.user, + caseClient: this, + caseConfigureService: this._caseConfigureService, + logger: this.logger, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to push case using client id: ${args.caseId}: ${error}`, + error, + logger: this.logger, + }); + } } } diff --git a/x-pack/plugins/case/server/client/comments/add.test.ts b/x-pack/plugins/case/server/client/comments/add.test.ts index c9b1e4fd13272..123ecec6abea3 100644 --- a/x-pack/plugins/case/server/client/comments/add.test.ts +++ b/x-pack/plugins/case/server/client/comments/add.test.ts @@ -7,6 +7,7 @@ import { omit } from 'lodash/fp'; import { CommentType } from '../../../common/api'; +import { isCaseError } from '../../common/error'; import { createMockSavedObjectsRepository, mockCaseComments, @@ -297,7 +298,7 @@ describe('addComment', () => { caseCommentSavedObject: mockCaseComments, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client + return caseClient.client .addComment({ caseId: 'mock-id-1', // @ts-expect-error @@ -326,7 +327,7 @@ describe('addComment', () => { ['comment'].forEach((attribute) => { const requestAttributes = omit(attribute, allRequestAttributes); - caseClient.client + return caseClient.client .addComment({ caseId: 'mock-id-1', // @ts-expect-error @@ -353,7 +354,7 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); ['alertId', 'index'].forEach((attribute) => { - caseClient.client + return caseClient.client .addComment({ caseId: 'mock-id-1', comment: { @@ -387,7 +388,7 @@ describe('addComment', () => { ['alertId', 'index'].forEach((attribute) => { const requestAttributes = omit(attribute, allRequestAttributes); - caseClient.client + return caseClient.client .addComment({ caseId: 'mock-id-1', // @ts-expect-error @@ -414,7 +415,7 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); ['comment'].forEach((attribute) => { - caseClient.client + return caseClient.client .addComment({ caseId: 'mock-id-1', comment: { @@ -437,14 +438,14 @@ describe('addComment', () => { }); test('it throws when the case does not exists', async () => { - expect.assertions(3); + expect.assertions(4); const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client + return caseClient.client .addComment({ caseId: 'not-exists', comment: { @@ -454,20 +455,22 @@ describe('addComment', () => { }) .catch((e) => { expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(404); + expect(isCaseError(e)).toBeTruthy(); + const boomErr = e.boomify(); + expect(boomErr.isBoom).toBe(true); + expect(boomErr.output.statusCode).toBe(404); }); }); test('it throws when postNewCase throws', async () => { - expect.assertions(3); + expect.assertions(4); const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client + return caseClient.client .addComment({ caseId: 'mock-id-1', comment: { @@ -477,13 +480,15 @@ describe('addComment', () => { }) .catch((e) => { expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); + expect(isCaseError(e)).toBeTruthy(); + const boomErr = e.boomify(); + expect(boomErr.isBoom).toBe(true); + expect(boomErr.output.statusCode).toBe(400); }); }); test('it throws when the case is closed and the comment is of type alert', async () => { - expect.assertions(3); + expect.assertions(4); const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, @@ -491,7 +496,7 @@ describe('addComment', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client + return caseClient.client .addComment({ caseId: 'mock-id-4', comment: { @@ -506,8 +511,10 @@ describe('addComment', () => { }) .catch((e) => { expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); + expect(isCaseError(e)).toBeTruthy(); + const boomErr = e.boomify(); + expect(boomErr.isBoom).toBe(true); + expect(boomErr.output.statusCode).toBe(400); }); }); }); diff --git a/x-pack/plugins/case/server/client/comments/add.ts b/x-pack/plugins/case/server/client/comments/add.ts index 4c1cc59a95750..d3d7047e71bd3 100644 --- a/x-pack/plugins/case/server/client/comments/add.ts +++ b/x-pack/plugins/case/server/client/comments/add.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; +import { SavedObject, SavedObjectsClientContract, Logger } from 'src/core/server'; import { decodeCommentRequest, getAlertIds, @@ -38,6 +38,7 @@ import { import { CaseServiceSetup, CaseUserActionServiceSetup } from '../../services'; import { CommentableCase } from '../../common'; import { CaseClientHandler } from '..'; +import { createCaseError } from '../../common/error'; import { CASE_COMMENT_SAVED_OBJECT } from '../../saved_object_types'; import { MAX_GENERATED_ALERTS_PER_SUB_CASE } from '../../../common/constants'; @@ -104,6 +105,7 @@ interface AddCommentFromRuleArgs { savedObjectsClient: SavedObjectsClientContract; caseService: CaseServiceSetup; userActionService: CaseUserActionServiceSetup; + logger: Logger; } const addGeneratedAlerts = async ({ @@ -113,6 +115,7 @@ const addGeneratedAlerts = async ({ caseClient, caseId, comment, + logger, }: AddCommentFromRuleArgs): Promise => { const query = pipe( AlertCommentRequestRt.decode(comment), @@ -125,88 +128,104 @@ const addGeneratedAlerts = async ({ if (comment.type !== CommentType.generatedAlert) { throw Boom.internal('Attempting to add a non generated alert in the wrong context'); } - const createdDate = new Date().toISOString(); - const caseInfo = await caseService.getCase({ - client: savedObjectsClient, - id: caseId, - }); + try { + const createdDate = new Date().toISOString(); - if ( - query.type === CommentType.generatedAlert && - caseInfo.attributes.type !== CaseType.collection - ) { - throw Boom.badRequest('Sub case style alert comment cannot be added to an individual case'); - } + const caseInfo = await caseService.getCase({ + client: savedObjectsClient, + id: caseId, + }); - const userDetails: User = { - username: caseInfo.attributes.created_by?.username, - full_name: caseInfo.attributes.created_by?.full_name, - email: caseInfo.attributes.created_by?.email, - }; + if ( + query.type === CommentType.generatedAlert && + caseInfo.attributes.type !== CaseType.collection + ) { + throw Boom.badRequest('Sub case style alert comment cannot be added to an individual case'); + } - const subCase = await getSubCase({ - caseService, - savedObjectsClient, - caseId, - createdAt: createdDate, - userActionService, - user: userDetails, - }); + const userDetails: User = { + username: caseInfo.attributes.created_by?.username, + full_name: caseInfo.attributes.created_by?.full_name, + email: caseInfo.attributes.created_by?.email, + }; - const commentableCase = new CommentableCase({ - collection: caseInfo, - subCase, - soClient: savedObjectsClient, - service: caseService, - }); + const subCase = await getSubCase({ + caseService, + savedObjectsClient, + caseId, + createdAt: createdDate, + userActionService, + user: userDetails, + }); - const { - comment: newComment, - commentableCase: updatedCase, - } = await commentableCase.createComment({ createdDate, user: userDetails, commentReq: query }); - - if ( - (newComment.attributes.type === CommentType.alert || - newComment.attributes.type === CommentType.generatedAlert) && - caseInfo.attributes.settings.syncAlerts - ) { - const ids = getAlertIds(query); - await caseClient.updateAlertsStatus({ - ids, - status: subCase.attributes.status, - indices: new Set([ - ...(Array.isArray(newComment.attributes.index) - ? newComment.attributes.index - : [newComment.attributes.index]), - ]), + const commentableCase = new CommentableCase({ + logger, + collection: caseInfo, + subCase, + soClient: savedObjectsClient, + service: caseService, }); - } - await userActionService.postUserActions({ - client: savedObjectsClient, - actions: [ - buildCommentUserActionItem({ - action: 'create', - actionAt: createdDate, - actionBy: { ...userDetails }, - caseId: updatedCase.caseId, - subCaseId: updatedCase.subCaseId, - commentId: newComment.id, - fields: ['comment'], - newValue: JSON.stringify(query), - }), - ], - }); + const { + comment: newComment, + commentableCase: updatedCase, + } = await commentableCase.createComment({ createdDate, user: userDetails, commentReq: query }); + + if ( + (newComment.attributes.type === CommentType.alert || + newComment.attributes.type === CommentType.generatedAlert) && + caseInfo.attributes.settings.syncAlerts + ) { + const ids = getAlertIds(query); + await caseClient.updateAlertsStatus({ + ids, + status: subCase.attributes.status, + indices: new Set([ + ...(Array.isArray(newComment.attributes.index) + ? newComment.attributes.index + : [newComment.attributes.index]), + ]), + }); + } + + await userActionService.postUserActions({ + client: savedObjectsClient, + actions: [ + buildCommentUserActionItem({ + action: 'create', + actionAt: createdDate, + actionBy: { ...userDetails }, + caseId: updatedCase.caseId, + subCaseId: updatedCase.subCaseId, + commentId: newComment.id, + fields: ['comment'], + newValue: JSON.stringify(query), + }), + ], + }); - return updatedCase.encode(); + return updatedCase.encode(); + } catch (error) { + throw createCaseError({ + message: `Failed while adding a generated alert to case id: ${caseId} error: ${error}`, + error, + logger, + }); + } }; -async function getCombinedCase( - service: CaseServiceSetup, - client: SavedObjectsClientContract, - id: string -): Promise { +async function getCombinedCase({ + service, + client, + id, + logger, +}: { + service: CaseServiceSetup; + client: SavedObjectsClientContract; + id: string; + logger: Logger; +}): Promise { const [casePromise, subCasePromise] = await Promise.allSettled([ service.getCase({ client, @@ -225,6 +244,7 @@ async function getCombinedCase( id: subCasePromise.value.references[0].id, }); return new CommentableCase({ + logger, collection: caseValue, subCase: subCasePromise.value, service, @@ -238,7 +258,12 @@ async function getCombinedCase( if (casePromise.status === 'rejected') { throw casePromise.reason; } else { - return new CommentableCase({ collection: casePromise.value, service, soClient: client }); + return new CommentableCase({ + logger, + collection: casePromise.value, + service, + soClient: client, + }); } } @@ -250,6 +275,7 @@ interface AddCommentArgs { caseService: CaseServiceSetup; userActionService: CaseUserActionServiceSetup; user: User; + logger: Logger; } export const addComment = async ({ @@ -260,6 +286,7 @@ export const addComment = async ({ caseId, comment, user, + logger, }: AddCommentArgs): Promise => { const query = pipe( CommentRequestRt.decode(comment), @@ -274,56 +301,70 @@ export const addComment = async ({ savedObjectsClient, userActionService, caseService, + logger, }); } decodeCommentRequest(comment); - const createdDate = new Date().toISOString(); - - const combinedCase = await getCombinedCase(caseService, savedObjectsClient, caseId); - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = user; - const userInfo: User = { - username, - full_name, - email, - }; - - const { comment: newComment, commentableCase: updatedCase } = await combinedCase.createComment({ - createdDate, - user: userInfo, - commentReq: query, - }); + try { + const createdDate = new Date().toISOString(); - if (newComment.attributes.type === CommentType.alert && updatedCase.settings.syncAlerts) { - const ids = getAlertIds(query); - await caseClient.updateAlertsStatus({ - ids, - status: updatedCase.status, - indices: new Set([ - ...(Array.isArray(newComment.attributes.index) - ? newComment.attributes.index - : [newComment.attributes.index]), - ]), + const combinedCase = await getCombinedCase({ + service: caseService, + client: savedObjectsClient, + id: caseId, + logger, }); - } - await userActionService.postUserActions({ - client: savedObjectsClient, - actions: [ - buildCommentUserActionItem({ - action: 'create', - actionAt: createdDate, - actionBy: { username, full_name, email }, - caseId: updatedCase.caseId, - subCaseId: updatedCase.subCaseId, - commentId: newComment.id, - fields: ['comment'], - newValue: JSON.stringify(query), - }), - ], - }); + // eslint-disable-next-line @typescript-eslint/naming-convention + const { username, full_name, email } = user; + const userInfo: User = { + username, + full_name, + email, + }; + + const { comment: newComment, commentableCase: updatedCase } = await combinedCase.createComment({ + createdDate, + user: userInfo, + commentReq: query, + }); + + if (newComment.attributes.type === CommentType.alert && updatedCase.settings.syncAlerts) { + const ids = getAlertIds(query); + await caseClient.updateAlertsStatus({ + ids, + status: updatedCase.status, + indices: new Set([ + ...(Array.isArray(newComment.attributes.index) + ? newComment.attributes.index + : [newComment.attributes.index]), + ]), + }); + } + + await userActionService.postUserActions({ + client: savedObjectsClient, + actions: [ + buildCommentUserActionItem({ + action: 'create', + actionAt: createdDate, + actionBy: { username, full_name, email }, + caseId: updatedCase.caseId, + subCaseId: updatedCase.subCaseId, + commentId: newComment.id, + fields: ['comment'], + newValue: JSON.stringify(query), + }), + ], + }); - return updatedCase.encode(); + return updatedCase.encode(); + } catch (error) { + throw createCaseError({ + message: `Failed while adding a comment to case id: ${caseId} error: ${error}`, + error, + logger, + }); + } }; diff --git a/x-pack/plugins/case/server/client/configure/get_mappings.ts b/x-pack/plugins/case/server/client/configure/get_mappings.ts index 5dd90efd8a2d7..5553580a41560 100644 --- a/x-pack/plugins/case/server/client/configure/get_mappings.ts +++ b/x-pack/plugins/case/server/client/configure/get_mappings.ts @@ -5,13 +5,14 @@ * 2.0. */ -import { SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsClientContract, Logger } from 'src/core/server'; import { ActionsClient } from '../../../../actions/server'; import { ConnectorMappingsAttributes, ConnectorTypes } from '../../../common/api'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server/saved_objects'; import { ConnectorMappingsServiceSetup } from '../../services'; import { CaseClientHandler } from '..'; +import { createCaseError } from '../../common/error'; interface GetMappingsArgs { savedObjectsClient: SavedObjectsClientContract; @@ -20,6 +21,7 @@ interface GetMappingsArgs { caseClient: CaseClientHandler; connectorType: string; connectorId: string; + logger: Logger; } export const getMappings = async ({ @@ -29,42 +31,51 @@ export const getMappings = async ({ caseClient, connectorType, connectorId, + logger, }: GetMappingsArgs): Promise => { - if (connectorType === ConnectorTypes.none) { - return []; - } - const myConnectorMappings = await connectorMappingsService.find({ - client: savedObjectsClient, - options: { - hasReference: { - type: ACTION_SAVED_OBJECT_TYPE, - id: connectorId, - }, - }, - }); - let theMapping; - // Create connector mappings if there are none - if (myConnectorMappings.total === 0) { - const res = await caseClient.getFields({ - actionsClient, - connectorId, - connectorType, - }); - theMapping = await connectorMappingsService.post({ + try { + if (connectorType === ConnectorTypes.none) { + return []; + } + const myConnectorMappings = await connectorMappingsService.find({ client: savedObjectsClient, - attributes: { - mappings: res.defaultMappings, - }, - references: [ - { + options: { + hasReference: { type: ACTION_SAVED_OBJECT_TYPE, - name: `associated-${ACTION_SAVED_OBJECT_TYPE}`, id: connectorId, }, - ], + }, + }); + let theMapping; + // Create connector mappings if there are none + if (myConnectorMappings.total === 0) { + const res = await caseClient.getFields({ + actionsClient, + connectorId, + connectorType, + }); + theMapping = await connectorMappingsService.post({ + client: savedObjectsClient, + attributes: { + mappings: res.defaultMappings, + }, + references: [ + { + type: ACTION_SAVED_OBJECT_TYPE, + name: `associated-${ACTION_SAVED_OBJECT_TYPE}`, + id: connectorId, + }, + ], + }); + } else { + theMapping = myConnectorMappings.saved_objects[0]; + } + return theMapping ? theMapping.attributes.mappings : []; + } catch (error) { + throw createCaseError({ + message: `Failed to retrieve mapping connector id: ${connectorId} type: ${connectorType}: ${error}`, + error, + logger, }); - } else { - theMapping = myConnectorMappings.saved_objects[0]; } - return theMapping ? theMapping.attributes.mappings : []; }; diff --git a/x-pack/plugins/case/server/client/index.test.ts b/x-pack/plugins/case/server/client/index.test.ts index 8a085bf29f214..6f4b4b136f68f 100644 --- a/x-pack/plugins/case/server/client/index.test.ts +++ b/x-pack/plugins/case/server/client/index.test.ts @@ -7,6 +7,7 @@ import { elasticsearchServiceMock, + loggingSystemMock, savedObjectsClientMock, } from '../../../../../src/core/server/mocks'; import { nullUser } from '../common'; @@ -22,6 +23,7 @@ jest.mock('./client'); import { CaseClientHandler } from './client'; import { createExternalCaseClient } from './index'; +const logger = loggingSystemMock.create().get('case'); const esClient = elasticsearchServiceMock.createElasticsearchClient(); const caseConfigureService = createConfigureServiceMock(); const alertsService = createAlertServiceMock(); @@ -41,6 +43,7 @@ describe('createExternalCaseClient()', () => { user: nullUser, savedObjectsClient, userActionService, + logger, }); expect(CaseClientHandler).toHaveBeenCalledTimes(1); }); diff --git a/x-pack/plugins/case/server/client/mocks.ts b/x-pack/plugins/case/server/client/mocks.ts index 302745913babb..98ffed0eaf8c5 100644 --- a/x-pack/plugins/case/server/client/mocks.ts +++ b/x-pack/plugins/case/server/client/mocks.ts @@ -47,7 +47,6 @@ export const createCaseClientWithMockSavedObjectsClient = async ({ }; }> => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); - // const actionsMock = createActionsClient(); const log = loggingSystemMock.create().get('case'); const auth = badAuth ? authenticationMock.createInvalid() : authenticationMock.create(); @@ -78,6 +77,7 @@ export const createCaseClientWithMockSavedObjectsClient = async ({ userActionService, alertsService, scopedClusterClient: esClient, + logger: log, }); return { client: caseClient, diff --git a/x-pack/plugins/case/server/client/types.ts b/x-pack/plugins/case/server/client/types.ts index d6a8f6b5d706c..adc66d8b1ea77 100644 --- a/x-pack/plugins/case/server/client/types.ts +++ b/x-pack/plugins/case/server/client/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'kibana/server'; import { ActionsClient } from '../../../actions/server'; import { CasePostRequest, @@ -76,6 +76,7 @@ export interface CaseClientFactoryArguments { savedObjectsClient: SavedObjectsClientContract; userActionService: CaseUserActionServiceSetup; alertsService: AlertServiceContract; + logger: Logger; } export interface ConfigureFields { diff --git a/x-pack/plugins/case/server/common/error.ts b/x-pack/plugins/case/server/common/error.ts new file mode 100644 index 0000000000000..95b05fd612e60 --- /dev/null +++ b/x-pack/plugins/case/server/common/error.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Boom, isBoom } from '@hapi/boom'; +import { Logger } from 'src/core/server'; + +/** + * Helper class for wrapping errors while preserving the original thrown error. + */ +class CaseError extends Error { + public readonly wrappedError?: Error; + constructor(message?: string, originalError?: Error) { + super(message); + this.name = this.constructor.name; // for stack traces + if (isCaseError(originalError)) { + this.wrappedError = originalError.wrappedError; + } else { + this.wrappedError = originalError; + } + } + + /** + * This function creates a boom representation of the error. If the wrapped error is a boom we'll grab the statusCode + * and data from that. + */ + public boomify(): Boom { + const message = this.message ?? this.wrappedError?.message; + let statusCode = 500; + let data: unknown | undefined; + + if (isBoom(this.wrappedError)) { + data = this.wrappedError?.data; + statusCode = this.wrappedError?.output?.statusCode ?? 500; + } + + return new Boom(message, { + data, + statusCode, + }); + } +} + +/** + * Type guard for determining if an error is a CaseError + */ +export function isCaseError(error: unknown): error is CaseError { + return error instanceof CaseError; +} + +/** + * Create a CaseError that wraps the original thrown error. This also logs the message that will be placed in the CaseError + * if the logger was defined. + */ +export function createCaseError({ + message, + error, + logger, +}: { + message?: string; + error?: Error; + logger?: Logger; +}) { + const logMessage: string | undefined = message ?? error?.toString(); + if (logMessage !== undefined) { + logger?.error(logMessage); + } + + return new CaseError(message, error); +} diff --git a/x-pack/plugins/case/server/common/models/commentable_case.ts b/x-pack/plugins/case/server/common/models/commentable_case.ts index 3ae225999db4e..1ff5b7beadcaf 100644 --- a/x-pack/plugins/case/server/common/models/commentable_case.ts +++ b/x-pack/plugins/case/server/common/models/commentable_case.ts @@ -11,6 +11,7 @@ import { SavedObjectReference, SavedObjectsClientContract, SavedObjectsUpdateResponse, + Logger, } from 'src/core/server'; import { AssociationType, @@ -35,6 +36,7 @@ import { } from '../../routes/api/utils'; import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../saved_object_types'; import { CaseServiceSetup } from '../../services'; +import { createCaseError } from '../error'; import { countAlertsForID } from '../index'; interface UpdateCommentResp { @@ -52,6 +54,7 @@ interface CommentableCaseParams { subCase?: SavedObject; soClient: SavedObjectsClientContract; service: CaseServiceSetup; + logger: Logger; } /** @@ -63,11 +66,14 @@ export class CommentableCase { private readonly subCase?: SavedObject; private readonly soClient: SavedObjectsClientContract; private readonly service: CaseServiceSetup; - constructor({ collection, subCase, soClient, service }: CommentableCaseParams) { + private readonly logger: Logger; + + constructor({ collection, subCase, soClient, service, logger }: CommentableCaseParams) { this.collection = collection; this.subCase = subCase; this.soClient = soClient; this.service = service; + this.logger = logger; } public get status(): CaseStatuses { @@ -119,55 +125,64 @@ export class CommentableCase { } private async update({ date, user }: { date: string; user: User }): Promise { - let updatedSubCaseAttributes: SavedObject | undefined; + try { + let updatedSubCaseAttributes: SavedObject | undefined; + + if (this.subCase) { + const updatedSubCase = await this.service.patchSubCase({ + client: this.soClient, + subCaseId: this.subCase.id, + updatedAttributes: { + updated_at: date, + updated_by: { + ...user, + }, + }, + version: this.subCase.version, + }); + + updatedSubCaseAttributes = { + ...this.subCase, + attributes: { + ...this.subCase.attributes, + ...updatedSubCase.attributes, + }, + version: updatedSubCase.version ?? this.subCase.version, + }; + } - if (this.subCase) { - const updatedSubCase = await this.service.patchSubCase({ + const updatedCase = await this.service.patchCase({ client: this.soClient, - subCaseId: this.subCase.id, + caseId: this.collection.id, updatedAttributes: { updated_at: date, - updated_by: { - ...user, - }, + updated_by: { ...user }, }, - version: this.subCase.version, + version: this.collection.version, }); - updatedSubCaseAttributes = { - ...this.subCase, - attributes: { - ...this.subCase.attributes, - ...updatedSubCase.attributes, + // this will contain the updated sub case information if the sub case was defined initially + return new CommentableCase({ + collection: { + ...this.collection, + attributes: { + ...this.collection.attributes, + ...updatedCase.attributes, + }, + version: updatedCase.version ?? this.collection.version, }, - version: updatedSubCase.version ?? this.subCase.version, - }; + subCase: updatedSubCaseAttributes, + soClient: this.soClient, + service: this.service, + logger: this.logger, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to update commentable case, sub case id: ${this.subCaseId} case id: ${this.caseId}: ${error}`, + error, + logger: this.logger, + }); } - - const updatedCase = await this.service.patchCase({ - client: this.soClient, - caseId: this.collection.id, - updatedAttributes: { - updated_at: date, - updated_by: { ...user }, - }, - version: this.collection.version, - }); - - // this will contain the updated sub case information if the sub case was defined initially - return new CommentableCase({ - collection: { - ...this.collection, - attributes: { - ...this.collection.attributes, - ...updatedCase.attributes, - }, - version: updatedCase.version ?? this.collection.version, - }, - subCase: updatedSubCaseAttributes, - soClient: this.soClient, - service: this.service, - }); } /** @@ -182,25 +197,33 @@ export class CommentableCase { updatedAt: string; user: User; }): Promise { - const { id, version, ...queryRestAttributes } = updateRequest; + try { + const { id, version, ...queryRestAttributes } = updateRequest; - const [comment, commentableCase] = await Promise.all([ - this.service.patchComment({ - client: this.soClient, - commentId: id, - updatedAttributes: { - ...queryRestAttributes, - updated_at: updatedAt, - updated_by: user, - }, - version, - }), - this.update({ date: updatedAt, user }), - ]); - return { - comment, - commentableCase, - }; + const [comment, commentableCase] = await Promise.all([ + this.service.patchComment({ + client: this.soClient, + commentId: id, + updatedAttributes: { + ...queryRestAttributes, + updated_at: updatedAt, + updated_by: user, + }, + version, + }), + this.update({ date: updatedAt, user }), + ]); + return { + comment, + commentableCase, + }; + } catch (error) { + throw createCaseError({ + message: `Failed to update comment in commentable case, sub case id: ${this.subCaseId} case id: ${this.caseId}: ${error}`, + error, + logger: this.logger, + }); + } } /** @@ -215,33 +238,41 @@ export class CommentableCase { user: User; commentReq: CommentRequest; }): Promise { - if (commentReq.type === CommentType.alert) { - if (this.status === CaseStatuses.closed) { - throw Boom.badRequest('Alert cannot be attached to a closed case'); - } + try { + if (commentReq.type === CommentType.alert) { + if (this.status === CaseStatuses.closed) { + throw Boom.badRequest('Alert cannot be attached to a closed case'); + } - if (!this.subCase && this.collection.attributes.type === CaseType.collection) { - throw Boom.badRequest('Alert cannot be attached to a collection case'); + if (!this.subCase && this.collection.attributes.type === CaseType.collection) { + throw Boom.badRequest('Alert cannot be attached to a collection case'); + } } - } - const [comment, commentableCase] = await Promise.all([ - this.service.postNewComment({ - client: this.soClient, - attributes: transformNewComment({ - associationType: this.subCase ? AssociationType.subCase : AssociationType.case, - createdDate, - ...commentReq, - ...user, + const [comment, commentableCase] = await Promise.all([ + this.service.postNewComment({ + client: this.soClient, + attributes: transformNewComment({ + associationType: this.subCase ? AssociationType.subCase : AssociationType.case, + createdDate, + ...commentReq, + ...user, + }), + references: this.buildRefsToCase(), }), - references: this.buildRefsToCase(), - }), - this.update({ date: createdDate, user }), - ]); - return { - comment, - commentableCase, - }; + this.update({ date: createdDate, user }), + ]); + return { + comment, + commentableCase, + }; + } catch (error) { + throw createCaseError({ + message: `Failed creating a comment on a commentable case, sub case id: ${this.subCaseId} case id: ${this.caseId}: ${error}`, + error, + logger: this.logger, + }); + } } private formatCollectionForEncoding(totalComment: number) { @@ -255,65 +286,74 @@ export class CommentableCase { } public async encode(): Promise { - const collectionCommentStats = await this.service.getAllCaseComments({ - client: this.soClient, - id: this.collection.id, - options: { - fields: [], - page: 1, - perPage: 1, - }, - }); + try { + const collectionCommentStats = await this.service.getAllCaseComments({ + client: this.soClient, + id: this.collection.id, + options: { + fields: [], + page: 1, + perPage: 1, + }, + }); - const collectionComments = await this.service.getAllCaseComments({ - client: this.soClient, - id: this.collection.id, - options: { - fields: [], - page: 1, - perPage: collectionCommentStats.total, - }, - }); + const collectionComments = await this.service.getAllCaseComments({ + client: this.soClient, + id: this.collection.id, + options: { + fields: [], + page: 1, + perPage: collectionCommentStats.total, + }, + }); - const collectionTotalAlerts = - countAlertsForID({ comments: collectionComments, id: this.collection.id }) ?? 0; + const collectionTotalAlerts = + countAlertsForID({ comments: collectionComments, id: this.collection.id }) ?? 0; - const caseResponse = { - comments: flattenCommentSavedObjects(collectionComments.saved_objects), - totalAlerts: collectionTotalAlerts, - ...this.formatCollectionForEncoding(collectionCommentStats.total), - }; + const caseResponse = { + comments: flattenCommentSavedObjects(collectionComments.saved_objects), + totalAlerts: collectionTotalAlerts, + ...this.formatCollectionForEncoding(collectionCommentStats.total), + }; - if (this.subCase) { - const subCaseComments = await this.service.getAllSubCaseComments({ - client: this.soClient, - id: this.subCase.id, - }); - const totalAlerts = countAlertsForID({ comments: subCaseComments, id: this.subCase.id }) ?? 0; + if (this.subCase) { + const subCaseComments = await this.service.getAllSubCaseComments({ + client: this.soClient, + id: this.subCase.id, + }); + const totalAlerts = + countAlertsForID({ comments: subCaseComments, id: this.subCase.id }) ?? 0; - return CaseResponseRt.encode({ - ...caseResponse, - /** - * For now we need the sub case comments and totals to be exposed on the top level of the response so that the UI - * functionality can stay the same. Ideally in the future we can refactor this so that the UI will look for the - * comments either in the top level for a case or a collection or in the subCases field if it is a sub case. - * - * If we ever need to return both the collection's comments and the sub case comments we'll need to refactor it then - * as well. - */ - comments: flattenCommentSavedObjects(subCaseComments.saved_objects), - totalComment: subCaseComments.saved_objects.length, - totalAlerts, - subCases: [ - flattenSubCaseSavedObject({ - savedObject: this.subCase, - totalComment: subCaseComments.saved_objects.length, - totalAlerts, - }), - ], + return CaseResponseRt.encode({ + ...caseResponse, + /** + * For now we need the sub case comments and totals to be exposed on the top level of the response so that the UI + * functionality can stay the same. Ideally in the future we can refactor this so that the UI will look for the + * comments either in the top level for a case or a collection or in the subCases field if it is a sub case. + * + * If we ever need to return both the collection's comments and the sub case comments we'll need to refactor it then + * as well. + */ + comments: flattenCommentSavedObjects(subCaseComments.saved_objects), + totalComment: subCaseComments.saved_objects.length, + totalAlerts, + subCases: [ + flattenSubCaseSavedObject({ + savedObject: this.subCase, + totalComment: subCaseComments.saved_objects.length, + totalAlerts, + }), + ], + }); + } + + return CaseResponseRt.encode(caseResponse); + } catch (error) { + throw createCaseError({ + message: `Failed encoding the commentable case, sub case id: ${this.subCaseId} case id: ${this.caseId}: ${error}`, + error, + logger: this.logger, }); } - - return CaseResponseRt.encode(caseResponse); } } diff --git a/x-pack/plugins/case/server/connectors/case/index.ts b/x-pack/plugins/case/server/connectors/case/index.ts index fd2fc7389f9ca..4a1d0569bde6d 100644 --- a/x-pack/plugins/case/server/connectors/case/index.ts +++ b/x-pack/plugins/case/server/connectors/case/index.ts @@ -6,7 +6,7 @@ */ import { curry } from 'lodash'; - +import { Logger } from 'src/core/server'; import { ActionTypeExecutorResult } from '../../../../actions/common'; import { CasePatchRequest, @@ -26,6 +26,7 @@ import * as i18n from './translations'; import { GetActionTypeParams, isCommentGeneratedAlert, separator } from '..'; import { nullUser } from '../../common'; +import { createCaseError } from '../../common/error'; const supportedSubActions: string[] = ['create', 'update', 'addComment']; @@ -84,6 +85,7 @@ async function executor( connectorMappingsService, userActionService, alertsService, + logger, }); if (!supportedSubActions.includes(subAction)) { @@ -93,9 +95,17 @@ async function executor( } if (subAction === 'create') { - data = await caseClient.create({ - ...(subActionParams as CasePostRequest), - }); + try { + data = await caseClient.create({ + ...(subActionParams as CasePostRequest), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to create a case using connector: ${error}`, + error, + logger, + }); + } } if (subAction === 'update') { @@ -107,13 +117,29 @@ async function executor( {} as CasePatchRequest ); - data = await caseClient.update({ cases: [updateParamsWithoutNullValues] }); + try { + data = await caseClient.update({ cases: [updateParamsWithoutNullValues] }); + } catch (error) { + throw createCaseError({ + message: `Failed to update case using connector id: ${updateParamsWithoutNullValues?.id} version: ${updateParamsWithoutNullValues?.version}: ${error}`, + error, + logger, + }); + } } if (subAction === 'addComment') { const { caseId, comment } = subActionParams as ExecutorSubActionAddCommentParams; - const formattedComment = transformConnectorComment(comment); - data = await caseClient.addComment({ caseId, comment: formattedComment }); + try { + const formattedComment = transformConnectorComment(comment, logger); + data = await caseClient.addComment({ caseId, comment: formattedComment }); + } catch (error) { + throw createCaseError({ + message: `Failed to create comment using connector case id: ${caseId}: ${error}`, + error, + logger, + }); + } } return { status: 'ok', data: data ?? {}, actionId }; @@ -127,7 +153,19 @@ interface AttachmentAlerts { indices: string[]; rule: { id: string | null; name: string | null }; } -export const transformConnectorComment = (comment: CommentSchemaType): CommentRequest => { + +/** + * Convert a connector style comment passed through the action plugin to the expected format for the add comment functionality. + * + * @param comment an object defining the comment to be attached to a case/sub case + * @param logger an optional logger to handle logging an error if parsing failed + * + * Note: This is exported so that the integration tests can use it. + */ +export const transformConnectorComment = ( + comment: CommentSchemaType, + logger?: Logger +): CommentRequest => { if (isCommentGeneratedAlert(comment)) { try { const genAlerts: Array<{ @@ -162,7 +200,11 @@ export const transformConnectorComment = (comment: CommentSchemaType): CommentRe rule, }; } catch (e) { - throw new Error(`Error parsing generated alert in case connector -> ${e.message}`); + throw createCaseError({ + message: `Error parsing generated alert in case connector -> ${e}`, + error: e, + logger, + }); } } else { return comment; diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 1c00c26a7c0b0..43daa51958429 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -97,11 +97,13 @@ export class CasePlugin { connectorMappingsService: this.connectorMappingsService, userActionService: this.userActionService, alertsService: this.alertsService, + logger: this.log, }) ); const router = core.http.createRouter(); initCaseApi({ + logger: this.log, caseService: this.caseService, caseConfigureService: this.caseConfigureService, connectorMappingsService: this.connectorMappingsService, @@ -137,6 +139,7 @@ export class CasePlugin { connectorMappingsService: this.connectorMappingsService!, userActionService: this.userActionService!, alertsService: this.alertsService!, + logger: this.log, }); }; @@ -156,6 +159,7 @@ export class CasePlugin { connectorMappingsService, userActionService, alertsService, + logger, }: { core: CoreSetup; caseService: CaseServiceSetup; @@ -163,6 +167,7 @@ export class CasePlugin { connectorMappingsService: ConnectorMappingsServiceSetup; userActionService: CaseUserActionServiceSetup; alertsService: AlertServiceContract; + logger: Logger; }): IContextProvider => { return async (context, request, response) => { const [{ savedObjects }] = await core.getStartServices(); @@ -178,6 +183,7 @@ export class CasePlugin { userActionService, alertsService, user, + logger, }); }, }; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts index b4230a05749a1..6b0c4adf9a680 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts @@ -35,6 +35,7 @@ export const createRoute = async ( postUserActions: jest.fn(), getUserActions: jest.fn(), }, + logger: log, }); return router[method].mock.calls[0][1]; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts index 492be96fb4aa9..7f66602c61fff 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts @@ -57,6 +57,7 @@ export const createRouteContext = async (client: any, badAuth = false) => { userActionService, alertsService, scopedClusterClient: esClient, + logger: log, }); return { context, services: { userActionService } }; diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts index e0b3a4420f4b5..fd250b74fff1e 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts @@ -13,7 +13,12 @@ import { wrapError } from '../../utils'; import { CASE_COMMENTS_URL } from '../../../../../common/constants'; import { AssociationType } from '../../../../../common/api'; -export function initDeleteAllCommentsApi({ caseService, router, userActionService }: RouteDeps) { +export function initDeleteAllCommentsApi({ + caseService, + router, + userActionService, + logger, +}: RouteDeps) { router.delete( { path: CASE_COMMENTS_URL, @@ -70,6 +75,9 @@ export function initDeleteAllCommentsApi({ caseService, router, userActionServic return response.noContent(); } catch (error) { + logger.error( + `Failed to delete all comments in route case id: ${request.params.case_id} sub case id: ${request.query?.subCaseId}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts index cae0809ea5f0b..f1c5fdc2b7cc8 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts @@ -14,7 +14,12 @@ import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; -export function initDeleteCommentApi({ caseService, router, userActionService }: RouteDeps) { +export function initDeleteCommentApi({ + caseService, + router, + userActionService, + logger, +}: RouteDeps) { router.delete( { path: CASE_COMMENT_DETAILS_URL, @@ -78,6 +83,9 @@ export function initDeleteCommentApi({ caseService, router, userActionService }: return response.noContent(); } catch (error) { + logger.error( + `Failed to delete comment in route case id: ${request.params.case_id} comment id: ${request.params.comment_id} sub case id: ${request.query?.subCaseId}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts b/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts index 0ec0f1871c7ad..57ddd84e8742c 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts @@ -30,7 +30,7 @@ const FindQueryParamsRt = rt.partial({ subCaseId: rt.string, }); -export function initFindCaseCommentsApi({ caseService, router }: RouteDeps) { +export function initFindCaseCommentsApi({ caseService, router, logger }: RouteDeps) { router.get( { path: `${CASE_COMMENTS_URL}/_find`, @@ -82,6 +82,9 @@ export function initFindCaseCommentsApi({ caseService, router }: RouteDeps) { const theComments = await caseService.getCommentsByAssociation(args); return response.ok({ body: CommentsResponseRt.encode(transformComments(theComments)) }); } catch (error) { + logger.error( + `Failed to find comments in route case id: ${request.params.case_id}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts index 8bf49ec3e27a1..770efe0109744 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts @@ -14,7 +14,7 @@ import { flattenCommentSavedObjects, wrapError } from '../../utils'; import { CASE_COMMENTS_URL } from '../../../../../common/constants'; import { defaultSortField } from '../../../../common'; -export function initGetAllCommentsApi({ caseService, router }: RouteDeps) { +export function initGetAllCommentsApi({ caseService, router, logger }: RouteDeps) { router.get( { path: CASE_COMMENTS_URL, @@ -58,6 +58,9 @@ export function initGetAllCommentsApi({ caseService, router }: RouteDeps) { body: AllCommentsResponseRt.encode(flattenCommentSavedObjects(comments.saved_objects)), }); } catch (error) { + logger.error( + `Failed to get all comments in route case id: ${request.params.case_id} include sub case comments: ${request.query?.includeSubCaseComments} sub case id: ${request.query?.subCaseId}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts index 6484472af0c3c..9dedfccd3a250 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts @@ -12,7 +12,7 @@ import { RouteDeps } from '../../types'; import { flattenCommentSavedObject, wrapError } from '../../utils'; import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; -export function initGetCommentApi({ caseService, router }: RouteDeps) { +export function initGetCommentApi({ caseService, router, logger }: RouteDeps) { router.get( { path: CASE_COMMENT_DETAILS_URL, @@ -35,6 +35,9 @@ export function initGetCommentApi({ caseService, router }: RouteDeps) { body: CommentResponseRt.encode(flattenCommentSavedObject(comment)), }); } catch (error) { + logger.error( + `Failed to get comment in route case id: ${request.params.case_id} comment id: ${request.params.comment_id}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts index 01b0e17464053..f5db2dc004a1d 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts @@ -12,7 +12,7 @@ import { identity } from 'fp-ts/lib/function'; import { schema } from '@kbn/config-schema'; import Boom from '@hapi/boom'; -import { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract, Logger } from 'kibana/server'; import { CommentableCase } from '../../../../common'; import { CommentPatchRequestRt, throwErrors, User } from '../../../../../common/api'; import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../../saved_object_types'; @@ -26,10 +26,17 @@ interface CombinedCaseParams { service: CaseServiceSetup; client: SavedObjectsClientContract; caseID: string; + logger: Logger; subCaseId?: string; } -async function getCommentableCase({ service, client, caseID, subCaseId }: CombinedCaseParams) { +async function getCommentableCase({ + service, + client, + caseID, + subCaseId, + logger, +}: CombinedCaseParams) { if (subCaseId) { const [caseInfo, subCase] = await Promise.all([ service.getCase({ @@ -41,22 +48,23 @@ async function getCommentableCase({ service, client, caseID, subCaseId }: Combin id: subCaseId, }), ]); - return new CommentableCase({ collection: caseInfo, service, subCase, soClient: client }); + return new CommentableCase({ + collection: caseInfo, + service, + subCase, + soClient: client, + logger, + }); } else { const caseInfo = await service.getCase({ client, id: caseID, }); - return new CommentableCase({ collection: caseInfo, service, soClient: client }); + return new CommentableCase({ collection: caseInfo, service, soClient: client, logger }); } } -export function initPatchCommentApi({ - caseConfigureService, - caseService, - router, - userActionService, -}: RouteDeps) { +export function initPatchCommentApi({ caseService, router, userActionService, logger }: RouteDeps) { router.patch( { path: CASE_COMMENTS_URL, @@ -88,6 +96,7 @@ export function initPatchCommentApi({ client, caseID: request.params.case_id, subCaseId: request.query?.subCaseId, + logger, }); const myComment = await caseService.getComment({ @@ -161,6 +170,9 @@ export function initPatchCommentApi({ body: await updatedCase.encode(), }); } catch (error) { + logger.error( + `Failed to patch comment in route case id: ${request.params.case_id} sub case id: ${request.query?.subCaseId}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts index 607f3f381f067..b8dc43dbf3fae 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts @@ -11,7 +11,7 @@ import { RouteDeps } from '../../types'; import { CASE_COMMENTS_URL } from '../../../../../common/constants'; import { CommentRequest } from '../../../../../common/api'; -export function initPostCommentApi({ router }: RouteDeps) { +export function initPostCommentApi({ router, logger }: RouteDeps) { router.post( { path: CASE_COMMENTS_URL, @@ -41,6 +41,9 @@ export function initPostCommentApi({ router }: RouteDeps) { body: await caseClient.addComment({ caseId, comment }), }); } catch (error) { + logger.error( + `Failed to post comment in route case id: ${request.params.case_id} sub case id: ${request.query?.subCaseId}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts index 33226d39a2595..2ca34d25482dd 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts @@ -12,7 +12,7 @@ import { wrapError } from '../../utils'; import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; import { transformESConnectorToCaseConnector } from '../helpers'; -export function initGetCaseConfigure({ caseConfigureService, router }: RouteDeps) { +export function initGetCaseConfigure({ caseConfigureService, router, logger }: RouteDeps) { router.get( { path: CASE_CONFIGURE_URL, @@ -63,6 +63,7 @@ export function initGetCaseConfigure({ caseConfigureService, router }: RouteDeps : {}, }); } catch (error) { + logger.error(`Failed to get case configure in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts index 0a368e0276bb5..81ffc06355ff5 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts @@ -28,7 +28,7 @@ const isConnectorSupported = ( * Be aware that this api will only return 20 connectors */ -export function initCaseConfigureGetActionConnector({ router }: RouteDeps) { +export function initCaseConfigureGetActionConnector({ router, logger }: RouteDeps) { router.get( { path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`, @@ -52,6 +52,7 @@ export function initCaseConfigureGetActionConnector({ router }: RouteDeps) { ); return response.ok({ body: results }); } catch (error) { + logger.error(`Failed to get connectors in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts index 02d39465373f9..cd764bb0e8a3e 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts @@ -24,7 +24,12 @@ import { transformESConnectorToCaseConnector, } from '../helpers'; -export function initPatchCaseConfigure({ caseConfigureService, caseService, router }: RouteDeps) { +export function initPatchCaseConfigure({ + caseConfigureService, + caseService, + router, + logger, +}: RouteDeps) { router.patch( { path: CASE_CONFIGURE_URL, @@ -107,6 +112,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout }), }); } catch (error) { + logger.error(`Failed to get patch configure in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts index db3d5cd6a2e56..f619a727e2e7a 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts @@ -24,7 +24,12 @@ import { transformESConnectorToCaseConnector, } from '../helpers'; -export function initPostCaseConfigure({ caseConfigureService, caseService, router }: RouteDeps) { +export function initPostCaseConfigure({ + caseConfigureService, + caseService, + router, + logger, +}: RouteDeps) { router.post( { path: CASE_CONFIGURE_URL, @@ -96,6 +101,7 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route }), }); } catch (error) { + logger.error(`Failed to post case configure in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts index 497e33d7feb30..5f2a6c67220c3 100644 --- a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts @@ -46,7 +46,7 @@ async function deleteSubCases({ ); } -export function initDeleteCasesApi({ caseService, router, userActionService }: RouteDeps) { +export function initDeleteCasesApi({ caseService, router, userActionService, logger }: RouteDeps) { router.delete( { path: CASES_URL, @@ -111,6 +111,9 @@ export function initDeleteCasesApi({ caseService, router, userActionService }: R return response.noContent(); } catch (error) { + logger.error( + `Failed to delete cases in route ids: ${JSON.stringify(request.query.ids)}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/find_cases.ts b/x-pack/plugins/case/server/routes/api/cases/find_cases.ts index 8ba83b42c06d7..d04f01eb73537 100644 --- a/x-pack/plugins/case/server/routes/api/cases/find_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/find_cases.ts @@ -22,7 +22,7 @@ import { RouteDeps } from '../types'; import { CASES_URL } from '../../../../common/constants'; import { constructQueryOptions } from './helpers'; -export function initFindCasesApi({ caseService, caseConfigureService, router }: RouteDeps) { +export function initFindCasesApi({ caseService, router, logger }: RouteDeps) { router.get( { path: `${CASES_URL}/_find`, @@ -77,6 +77,7 @@ export function initFindCasesApi({ caseService, caseConfigureService, router }: ), }); } catch (error) { + logger.error(`Failed to find cases in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/get_case.ts b/x-pack/plugins/case/server/routes/api/cases/get_case.ts index a3311796fa5cd..8a34e3a5b2431 100644 --- a/x-pack/plugins/case/server/routes/api/cases/get_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/get_case.ts @@ -11,7 +11,7 @@ import { RouteDeps } from '../types'; import { wrapError } from '../utils'; import { CASE_DETAILS_URL } from '../../../../common/constants'; -export function initGetCaseApi({ caseConfigureService, caseService, router }: RouteDeps) { +export function initGetCaseApi({ router, logger }: RouteDeps) { router.get( { path: CASE_DETAILS_URL, @@ -26,10 +26,10 @@ export function initGetCaseApi({ caseConfigureService, caseService, router }: Ro }, }, async (context, request, response) => { - const caseClient = context.case.getCaseClient(); - const id = request.params.case_id; - try { + const caseClient = context.case.getCaseClient(); + const id = request.params.case_id; + return response.ok({ body: await caseClient.get({ id, @@ -38,6 +38,9 @@ export function initGetCaseApi({ caseConfigureService, caseService, router }: Ro }), }); } catch (error) { + logger.error( + `Failed to retrieve case in route case id: ${request.params.case_id} \ninclude comments: ${request.query.includeComments} \ninclude sub comments: ${request.query.includeSubCaseComments}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts index 67d4d21a57634..2bff6000d5d6a 100644 --- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts @@ -10,7 +10,7 @@ import { RouteDeps } from '../types'; import { CASES_URL } from '../../../../common/constants'; import { CasesPatchRequest } from '../../../../common/api'; -export function initPatchCasesApi({ router }: RouteDeps) { +export function initPatchCasesApi({ router, logger }: RouteDeps) { router.patch( { path: CASES_URL, @@ -19,18 +19,19 @@ export function initPatchCasesApi({ router }: RouteDeps) { }, }, async (context, request, response) => { - if (!context.case) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } + try { + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } - const caseClient = context.case.getCaseClient(); - const cases = request.body as CasesPatchRequest; + const caseClient = context.case.getCaseClient(); + const cases = request.body as CasesPatchRequest; - try { return response.ok({ body: await caseClient.update(cases), }); } catch (error) { + logger.error(`Failed to patch cases in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/post_case.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.ts index 349ed6c3e5af9..1328d95826130 100644 --- a/x-pack/plugins/case/server/routes/api/cases/post_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/post_case.ts @@ -11,7 +11,7 @@ import { RouteDeps } from '../types'; import { CASES_URL } from '../../../../common/constants'; import { CasePostRequest } from '../../../../common/api'; -export function initPostCaseApi({ router }: RouteDeps) { +export function initPostCaseApi({ router, logger }: RouteDeps) { router.post( { path: CASES_URL, @@ -20,17 +20,18 @@ export function initPostCaseApi({ router }: RouteDeps) { }, }, async (context, request, response) => { - if (!context.case) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } - const caseClient = context.case.getCaseClient(); - const theCase = request.body as CasePostRequest; - try { + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } + const caseClient = context.case.getCaseClient(); + const theCase = request.body as CasePostRequest; + return response.ok({ body: await caseClient.create({ ...theCase }), }); } catch (error) { + logger.error(`Failed to post case in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.ts index c1f0a2cb59cb1..cfd2f6b9a61ad 100644 --- a/x-pack/plugins/case/server/routes/api/cases/push_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/push_case.ts @@ -16,7 +16,7 @@ import { throwErrors, CasePushRequestParamsRt } from '../../../../common/api'; import { RouteDeps } from '../types'; import { CASE_PUSH_URL } from '../../../../common/constants'; -export function initPushCaseApi({ router }: RouteDeps) { +export function initPushCaseApi({ router, logger }: RouteDeps) { router.post( { path: CASE_PUSH_URL, @@ -26,18 +26,18 @@ export function initPushCaseApi({ router }: RouteDeps) { }, }, async (context, request, response) => { - if (!context.case) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } + try { + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } - const caseClient = context.case.getCaseClient(); - const actionsClient = context.actions?.getActionsClient(); + const caseClient = context.case.getCaseClient(); + const actionsClient = context.actions?.getActionsClient(); - if (actionsClient == null) { - return response.badRequest({ body: 'Action client not found' }); - } + if (actionsClient == null) { + return response.badRequest({ body: 'Action client not found' }); + } - try { const params = pipe( CasePushRequestParamsRt.decode(request.params), fold(throwErrors(Boom.badRequest), identity) @@ -51,6 +51,7 @@ export function initPushCaseApi({ router }: RouteDeps) { }), }); } catch (error) { + logger.error(`Failed to push case in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts b/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts index 3d724ca5fc966..e5433f4972239 100644 --- a/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts +++ b/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts @@ -10,7 +10,7 @@ import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; import { CASE_REPORTERS_URL } from '../../../../../common/constants'; -export function initGetReportersApi({ caseService, router }: RouteDeps) { +export function initGetReportersApi({ caseService, router, logger }: RouteDeps) { router.get( { path: CASE_REPORTERS_URL, @@ -24,6 +24,7 @@ export function initGetReportersApi({ caseService, router }: RouteDeps) { }); return response.ok({ body: UsersRt.encode(reporters) }); } catch (error) { + logger.error(`Failed to get reporters in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts b/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts index f3cd0e2bdda5c..d0addfff09124 100644 --- a/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts +++ b/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts @@ -12,7 +12,7 @@ import { CasesStatusResponseRt, caseStatuses } from '../../../../../common/api'; import { CASE_STATUS_URL } from '../../../../../common/constants'; import { constructQueryOptions } from '../helpers'; -export function initGetCasesStatusApi({ caseService, router }: RouteDeps) { +export function initGetCasesStatusApi({ caseService, router, logger }: RouteDeps) { router.get( { path: CASE_STATUS_URL, @@ -41,6 +41,7 @@ export function initGetCasesStatusApi({ caseService, router }: RouteDeps) { }), }); } catch (error) { + logger.error(`Failed to get status stats in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/sub_case/delete_sub_cases.ts b/x-pack/plugins/case/server/routes/api/cases/sub_case/delete_sub_cases.ts index db701dd0fc82b..fd33afbd7df8e 100644 --- a/x-pack/plugins/case/server/routes/api/cases/sub_case/delete_sub_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/sub_case/delete_sub_cases.ts @@ -13,7 +13,12 @@ import { wrapError } from '../../utils'; import { SUB_CASES_PATCH_DEL_URL } from '../../../../../common/constants'; import { CASE_SAVED_OBJECT } from '../../../../saved_object_types'; -export function initDeleteSubCasesApi({ caseService, router, userActionService }: RouteDeps) { +export function initDeleteSubCasesApi({ + caseService, + router, + userActionService, + logger, +}: RouteDeps) { router.delete( { path: SUB_CASES_PATCH_DEL_URL, @@ -80,6 +85,9 @@ export function initDeleteSubCasesApi({ caseService, router, userActionService } return response.noContent(); } catch (error) { + logger.error( + `Failed to delete sub cases in route ids: ${JSON.stringify(request.query.ids)}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/sub_case/find_sub_cases.ts b/x-pack/plugins/case/server/routes/api/cases/sub_case/find_sub_cases.ts index 98052ccaeaba8..c24dde1944f83 100644 --- a/x-pack/plugins/case/server/routes/api/cases/sub_case/find_sub_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/sub_case/find_sub_cases.ts @@ -24,7 +24,7 @@ import { SUB_CASES_URL } from '../../../../../common/constants'; import { constructQueryOptions } from '../helpers'; import { defaultPage, defaultPerPage } from '../..'; -export function initFindSubCasesApi({ caseService, router }: RouteDeps) { +export function initFindSubCasesApi({ caseService, router, logger }: RouteDeps) { router.get( { path: `${SUB_CASES_URL}/_find`, @@ -88,6 +88,9 @@ export function initFindSubCasesApi({ caseService, router }: RouteDeps) { ), }); } catch (error) { + logger.error( + `Failed to find sub cases in route case id: ${request.params.case_id}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/sub_case/get_sub_case.ts b/x-pack/plugins/case/server/routes/api/cases/sub_case/get_sub_case.ts index b6d9a7345dbdd..32dcc924e1a08 100644 --- a/x-pack/plugins/case/server/routes/api/cases/sub_case/get_sub_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/sub_case/get_sub_case.ts @@ -13,7 +13,7 @@ import { flattenSubCaseSavedObject, wrapError } from '../../utils'; import { SUB_CASE_DETAILS_URL } from '../../../../../common/constants'; import { countAlertsForID } from '../../../../common'; -export function initGetSubCaseApi({ caseService, router }: RouteDeps) { +export function initGetSubCaseApi({ caseService, router, logger }: RouteDeps) { router.get( { path: SUB_CASE_DETAILS_URL, @@ -70,6 +70,9 @@ export function initGetSubCaseApi({ caseService, router }: RouteDeps) { ), }); } catch (error) { + logger.error( + `Failed to get sub case in route case id: ${request.params.case_id} sub case id: ${request.params.sub_case_id} include comments: ${request.query?.includeComments}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts b/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts index 4b8e4920852c2..73aacc2c2b0ba 100644 --- a/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts @@ -14,6 +14,7 @@ import { KibanaRequest, SavedObject, SavedObjectsFindResponse, + Logger, } from 'kibana/server'; import { CaseClient } from '../../../../client'; @@ -47,6 +48,7 @@ import { import { getCaseToUpdate } from '../helpers'; import { buildSubCaseUserActions } from '../../../../services/user_actions/helpers'; import { addAlertInfoToStatusMap } from '../../../../common'; +import { createCaseError } from '../../../../common/error'; interface UpdateArgs { client: SavedObjectsClientContract; @@ -55,6 +57,7 @@ interface UpdateArgs { request: KibanaRequest; caseClient: CaseClient; subCases: SubCasesPatchRequest; + logger: Logger; } function checkNonExistingOrConflict( @@ -216,41 +219,53 @@ async function updateAlerts({ caseService, client, caseClient, + logger, }: { subCasesToSync: SubCasePatchRequest[]; caseService: CaseServiceSetup; client: SavedObjectsClientContract; caseClient: CaseClient; + logger: Logger; }) { - const subCasesToSyncMap = subCasesToSync.reduce((acc, subCase) => { - acc.set(subCase.id, subCase); - return acc; - }, new Map()); - // get all the alerts for all sub cases that need to be synced - const totalAlerts = await getAlertComments({ caseService, client, subCasesToSync }); - // create a map of the status (open, closed, etc) to alert info that needs to be updated - const alertsToUpdate = totalAlerts.saved_objects.reduce((acc, alertComment) => { - if (isCommentRequestTypeAlertOrGenAlert(alertComment.attributes)) { - const id = getID(alertComment); - const status = - id !== undefined - ? subCasesToSyncMap.get(id)?.status ?? CaseStatuses.open - : CaseStatuses.open; - - addAlertInfoToStatusMap({ comment: alertComment.attributes, statusMap: acc, status }); - } - return acc; - }, new Map()); - - // This does at most 3 calls to Elasticsearch to update the status of the alerts to either open, closed, or in-progress - for (const [status, alertInfo] of alertsToUpdate.entries()) { - if (alertInfo.ids.length > 0 && alertInfo.indices.size > 0) { - caseClient.updateAlertsStatus({ - ids: alertInfo.ids, - status, - indices: alertInfo.indices, - }); + try { + const subCasesToSyncMap = subCasesToSync.reduce((acc, subCase) => { + acc.set(subCase.id, subCase); + return acc; + }, new Map()); + // get all the alerts for all sub cases that need to be synced + const totalAlerts = await getAlertComments({ caseService, client, subCasesToSync }); + // create a map of the status (open, closed, etc) to alert info that needs to be updated + const alertsToUpdate = totalAlerts.saved_objects.reduce((acc, alertComment) => { + if (isCommentRequestTypeAlertOrGenAlert(alertComment.attributes)) { + const id = getID(alertComment); + const status = + id !== undefined + ? subCasesToSyncMap.get(id)?.status ?? CaseStatuses.open + : CaseStatuses.open; + + addAlertInfoToStatusMap({ comment: alertComment.attributes, statusMap: acc, status }); + } + return acc; + }, new Map()); + + // This does at most 3 calls to Elasticsearch to update the status of the alerts to either open, closed, or in-progress + for (const [status, alertInfo] of alertsToUpdate.entries()) { + if (alertInfo.ids.length > 0 && alertInfo.indices.size > 0) { + caseClient.updateAlertsStatus({ + ids: alertInfo.ids, + status, + indices: alertInfo.indices, + }); + } } + } catch (error) { + throw createCaseError({ + message: `Failed to update alert status while updating sub cases: ${JSON.stringify( + subCasesToSync + )}: ${error}`, + logger, + error, + }); } } @@ -261,133 +276,152 @@ async function update({ request, caseClient, subCases, + logger, }: UpdateArgs): Promise { const query = pipe( excess(SubCasesPatchRequestRt).decode(subCases), fold(throwErrors(Boom.badRequest), identity) ); - const bulkSubCases = await caseService.getSubCases({ - client, - ids: query.subCases.map((q) => q.id), - }); + try { + const bulkSubCases = await caseService.getSubCases({ + client, + ids: query.subCases.map((q) => q.id), + }); - const subCasesMap = bulkSubCases.saved_objects.reduce((acc, so) => { - acc.set(so.id, so); - return acc; - }, new Map>()); + const subCasesMap = bulkSubCases.saved_objects.reduce((acc, so) => { + acc.set(so.id, so); + return acc; + }, new Map>()); - checkNonExistingOrConflict(query.subCases, subCasesMap); + checkNonExistingOrConflict(query.subCases, subCasesMap); - const nonEmptySubCaseRequests = getValidUpdateRequests(query.subCases, subCasesMap); + const nonEmptySubCaseRequests = getValidUpdateRequests(query.subCases, subCasesMap); - if (nonEmptySubCaseRequests.length <= 0) { - throw Boom.notAcceptable('All update fields are identical to current version.'); - } + if (nonEmptySubCaseRequests.length <= 0) { + throw Boom.notAcceptable('All update fields are identical to current version.'); + } - const subIDToParentCase = await getParentCases({ - client, - caseService, - subCaseIDs: nonEmptySubCaseRequests.map((subCase) => subCase.id), - subCasesMap, - }); + const subIDToParentCase = await getParentCases({ + client, + caseService, + subCaseIDs: nonEmptySubCaseRequests.map((subCase) => subCase.id), + subCasesMap, + }); - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = await caseService.getUser({ request }); - const updatedAt = new Date().toISOString(); - const updatedCases = await caseService.patchSubCases({ - client, - subCases: nonEmptySubCaseRequests.map((thisCase) => { - const { id: subCaseId, version, ...updateSubCaseAttributes } = thisCase; - let closedInfo: { closed_at: string | null; closed_by: User | null } = { - closed_at: null, - closed_by: null, - }; - - if ( - updateSubCaseAttributes.status && - updateSubCaseAttributes.status === CaseStatuses.closed - ) { - closedInfo = { - closed_at: updatedAt, - closed_by: { email, full_name, username }, - }; - } else if ( - updateSubCaseAttributes.status && - (updateSubCaseAttributes.status === CaseStatuses.open || - updateSubCaseAttributes.status === CaseStatuses['in-progress']) - ) { - closedInfo = { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { username, full_name, email } = await caseService.getUser({ request }); + const updatedAt = new Date().toISOString(); + const updatedCases = await caseService.patchSubCases({ + client, + subCases: nonEmptySubCaseRequests.map((thisCase) => { + const { id: subCaseId, version, ...updateSubCaseAttributes } = thisCase; + let closedInfo: { closed_at: string | null; closed_by: User | null } = { closed_at: null, closed_by: null, }; - } - return { - subCaseId, - updatedAttributes: { - ...updateSubCaseAttributes, - ...closedInfo, - updated_at: updatedAt, - updated_by: { email, full_name, username }, - }, - version, - }; - }), - }); - const subCasesToSyncAlertsFor = nonEmptySubCaseRequests.filter((subCaseToUpdate) => { - const storedSubCase = subCasesMap.get(subCaseToUpdate.id); - const parentCase = subIDToParentCase.get(subCaseToUpdate.id); - return ( - storedSubCase !== undefined && - subCaseToUpdate.status !== undefined && - storedSubCase.attributes.status !== subCaseToUpdate.status && - parentCase?.attributes.settings.syncAlerts - ); - }); + if ( + updateSubCaseAttributes.status && + updateSubCaseAttributes.status === CaseStatuses.closed + ) { + closedInfo = { + closed_at: updatedAt, + closed_by: { email, full_name, username }, + }; + } else if ( + updateSubCaseAttributes.status && + (updateSubCaseAttributes.status === CaseStatuses.open || + updateSubCaseAttributes.status === CaseStatuses['in-progress']) + ) { + closedInfo = { + closed_at: null, + closed_by: null, + }; + } + return { + subCaseId, + updatedAttributes: { + ...updateSubCaseAttributes, + ...closedInfo, + updated_at: updatedAt, + updated_by: { email, full_name, username }, + }, + version, + }; + }), + }); - await updateAlerts({ - caseService, - client, - caseClient, - subCasesToSync: subCasesToSyncAlertsFor, - }); + const subCasesToSyncAlertsFor = nonEmptySubCaseRequests.filter((subCaseToUpdate) => { + const storedSubCase = subCasesMap.get(subCaseToUpdate.id); + const parentCase = subIDToParentCase.get(subCaseToUpdate.id); + return ( + storedSubCase !== undefined && + subCaseToUpdate.status !== undefined && + storedSubCase.attributes.status !== subCaseToUpdate.status && + parentCase?.attributes.settings.syncAlerts + ); + }); - const returnUpdatedSubCases = updatedCases.saved_objects.reduce( - (acc, updatedSO) => { - const originalSubCase = subCasesMap.get(updatedSO.id); - if (originalSubCase) { - acc.push( - flattenSubCaseSavedObject({ - savedObject: { - ...originalSubCase, - ...updatedSO, - attributes: { ...originalSubCase.attributes, ...updatedSO.attributes }, - references: originalSubCase.references, - version: updatedSO.version ?? originalSubCase.version, - }, - }) - ); - } - return acc; - }, - [] - ); + await updateAlerts({ + caseService, + client, + caseClient, + subCasesToSync: subCasesToSyncAlertsFor, + logger, + }); - await userActionService.postUserActions({ - client, - actions: buildSubCaseUserActions({ - originalSubCases: bulkSubCases.saved_objects, - updatedSubCases: updatedCases.saved_objects, - actionDate: updatedAt, - actionBy: { email, full_name, username }, - }), - }); + const returnUpdatedSubCases = updatedCases.saved_objects.reduce( + (acc, updatedSO) => { + const originalSubCase = subCasesMap.get(updatedSO.id); + if (originalSubCase) { + acc.push( + flattenSubCaseSavedObject({ + savedObject: { + ...originalSubCase, + ...updatedSO, + attributes: { ...originalSubCase.attributes, ...updatedSO.attributes }, + references: originalSubCase.references, + version: updatedSO.version ?? originalSubCase.version, + }, + }) + ); + } + return acc; + }, + [] + ); + + await userActionService.postUserActions({ + client, + actions: buildSubCaseUserActions({ + originalSubCases: bulkSubCases.saved_objects, + updatedSubCases: updatedCases.saved_objects, + actionDate: updatedAt, + actionBy: { email, full_name, username }, + }), + }); - return SubCasesResponseRt.encode(returnUpdatedSubCases); + return SubCasesResponseRt.encode(returnUpdatedSubCases); + } catch (error) { + const idVersions = query.subCases.map((subCase) => ({ + id: subCase.id, + version: subCase.version, + })); + throw createCaseError({ + message: `Failed to update sub cases: ${JSON.stringify(idVersions)}: ${error}`, + error, + logger, + }); + } } -export function initPatchSubCasesApi({ router, caseService, userActionService }: RouteDeps) { +export function initPatchSubCasesApi({ + router, + caseService, + userActionService, + logger, +}: RouteDeps) { router.patch( { path: SUB_CASES_PATCH_DEL_URL, @@ -396,10 +430,10 @@ export function initPatchSubCasesApi({ router, caseService, userActionService }: }, }, async (context, request, response) => { - const caseClient = context.case.getCaseClient(); - const subCases = request.body as SubCasesPatchRequest; - try { + const caseClient = context.case.getCaseClient(); + const subCases = request.body as SubCasesPatchRequest; + return response.ok({ body: await update({ request, @@ -408,9 +442,11 @@ export function initPatchSubCasesApi({ router, caseService, userActionService }: client: context.core.savedObjects.client, caseService, userActionService, + logger, }), }); } catch (error) { + logger.error(`Failed to patch sub cases in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts index 488f32a795811..2efef9ac67f80 100644 --- a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts @@ -11,7 +11,7 @@ import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; import { CASE_USER_ACTIONS_URL, SUB_CASE_USER_ACTIONS_URL } from '../../../../../common/constants'; -export function initGetAllCaseUserActionsApi({ router }: RouteDeps) { +export function initGetAllCaseUserActionsApi({ router, logger }: RouteDeps) { router.get( { path: CASE_USER_ACTIONS_URL, @@ -22,25 +22,28 @@ export function initGetAllCaseUserActionsApi({ router }: RouteDeps) { }, }, async (context, request, response) => { - if (!context.case) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } + try { + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } - const caseClient = context.case.getCaseClient(); - const caseId = request.params.case_id; + const caseClient = context.case.getCaseClient(); + const caseId = request.params.case_id; - try { return response.ok({ body: await caseClient.getUserActions({ caseId }), }); } catch (error) { + logger.error( + `Failed to retrieve case user actions in route case id: ${request.params.case_id}: ${error}` + ); return response.customError(wrapError(error)); } } ); } -export function initGetAllSubCaseUserActionsApi({ router }: RouteDeps) { +export function initGetAllSubCaseUserActionsApi({ router, logger }: RouteDeps) { router.get( { path: SUB_CASE_USER_ACTIONS_URL, @@ -52,19 +55,22 @@ export function initGetAllSubCaseUserActionsApi({ router }: RouteDeps) { }, }, async (context, request, response) => { - if (!context.case) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } + try { + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } - const caseClient = context.case.getCaseClient(); - const caseId = request.params.case_id; - const subCaseId = request.params.sub_case_id; + const caseClient = context.case.getCaseClient(); + const caseId = request.params.case_id; + const subCaseId = request.params.sub_case_id; - try { return response.ok({ body: await caseClient.getUserActions({ caseId, subCaseId }), }); } catch (error) { + logger.error( + `Failed to retrieve sub case user actions in route case id: ${request.params.case_id} sub case id: ${request.params.sub_case_id}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/types.ts b/x-pack/plugins/case/server/routes/api/types.ts index 395880e5c1410..6ce40e01c7752 100644 --- a/x-pack/plugins/case/server/routes/api/types.ts +++ b/x-pack/plugins/case/server/routes/api/types.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; + import type { CaseConfigureServiceSetup, CaseServiceSetup, @@ -20,6 +22,7 @@ export interface RouteDeps { connectorMappingsService: ConnectorMappingsServiceSetup; router: CasesRouter; userActionService: CaseUserActionServiceSetup; + logger: Logger; } export enum SortFieldCase { diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index 084b1a17a1434..298f8bb877cda 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -6,7 +6,7 @@ */ import { isEmpty } from 'lodash'; -import { badRequest, boomify, isBoom } from '@hapi/boom'; +import { badRequest, Boom, boomify, isBoom } from '@hapi/boom'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; @@ -45,6 +45,7 @@ import { import { transformESConnectorToCaseConnector } from './cases/helpers'; import { SortFieldCase } from './types'; +import { isCaseError } from '../../common/error'; export const transformNewSubCase = ({ createdAt, @@ -182,9 +183,19 @@ export const transformNewComment = ({ }; }; +/** + * Transforms an error into the correct format for a kibana response. + */ export function wrapError(error: any): CustomHttpResponseOptions { - const options = { statusCode: error.statusCode ?? 500 }; - const boom = isBoom(error) ? error : boomify(error, options); + let boom: Boom; + + if (isCaseError(error)) { + boom = error.boomify(); + } else { + const options = { statusCode: error.statusCode ?? 500 }; + boom = isBoom(error) ? error : boomify(error, options); + } + return { body: boom, headers: boom.output.headers as { [key: string]: string }, diff --git a/x-pack/plugins/case/server/services/alerts/index.test.ts b/x-pack/plugins/case/server/services/alerts/index.test.ts index 35aa3ff80efc1..3b1020d3ef556 100644 --- a/x-pack/plugins/case/server/services/alerts/index.test.ts +++ b/x-pack/plugins/case/server/services/alerts/index.test.ts @@ -8,10 +8,11 @@ import { KibanaRequest } from 'kibana/server'; import { CaseStatuses } from '../../../common/api'; import { AlertService, AlertServiceContract } from '.'; -import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; describe('updateAlertsStatus', () => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); + const logger = loggingSystemMock.create().get('case'); describe('happy path', () => { let alertService: AlertServiceContract; @@ -21,6 +22,7 @@ describe('updateAlertsStatus', () => { request: {} as KibanaRequest, status: CaseStatuses.closed, scopedClusterClient: esClient, + logger, }; beforeEach(async () => { @@ -50,6 +52,7 @@ describe('updateAlertsStatus', () => { status: CaseStatuses.closed, indices: new Set(['']), scopedClusterClient: esClient, + logger, }) ).toBeUndefined(); }); diff --git a/x-pack/plugins/case/server/services/alerts/index.ts b/x-pack/plugins/case/server/services/alerts/index.ts index a19e533418bc9..45245b86ba2d5 100644 --- a/x-pack/plugins/case/server/services/alerts/index.ts +++ b/x-pack/plugins/case/server/services/alerts/index.ts @@ -9,9 +9,10 @@ import _ from 'lodash'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import { ElasticsearchClient } from 'kibana/server'; +import { ElasticsearchClient, Logger } from 'kibana/server'; import { CaseStatuses } from '../../../common/api'; import { MAX_ALERTS_PER_SUB_CASE } from '../../../common/constants'; +import { createCaseError } from '../../common/error'; export type AlertServiceContract = PublicMethodsOf; @@ -20,12 +21,14 @@ interface UpdateAlertsStatusArgs { status: CaseStatuses; indices: Set; scopedClusterClient: ElasticsearchClient; + logger: Logger; } interface GetAlertsArgs { ids: string[]; indices: Set; scopedClusterClient: ElasticsearchClient; + logger: Logger; } interface Alert { @@ -57,56 +60,75 @@ export class AlertService { status, indices, scopedClusterClient, + logger, }: UpdateAlertsStatusArgs) { const sanitizedIndices = getValidIndices(indices); if (sanitizedIndices.length <= 0) { - // log that we only had invalid indices + logger.warn(`Empty alert indices when updateAlertsStatus ids: ${JSON.stringify(ids)}`); return; } - const result = await scopedClusterClient.updateByQuery({ - index: sanitizedIndices, - conflicts: 'abort', - body: { - script: { - source: `ctx._source.signal.status = '${status}'`, - lang: 'painless', + try { + const result = await scopedClusterClient.updateByQuery({ + index: sanitizedIndices, + conflicts: 'abort', + body: { + script: { + source: `ctx._source.signal.status = '${status}'`, + lang: 'painless', + }, + query: { ids: { values: ids } }, }, - query: { ids: { values: ids } }, - }, - ignore_unavailable: true, - }); - - return result; + ignore_unavailable: true, + }); + + return result; + } catch (error) { + throw createCaseError({ + message: `Failed to update alert status ids: ${JSON.stringify(ids)}: ${error}`, + error, + logger, + }); + } } public async getAlerts({ scopedClusterClient, ids, indices, + logger, }: GetAlertsArgs): Promise { const index = getValidIndices(indices); if (index.length <= 0) { + logger.warn(`Empty alert indices when retrieving alerts ids: ${JSON.stringify(ids)}`); return; } - const result = await scopedClusterClient.search({ - index, - body: { - query: { - bool: { - filter: { - ids: { - values: ids, + try { + const result = await scopedClusterClient.search({ + index, + body: { + query: { + bool: { + filter: { + ids: { + values: ids, + }, }, }, }, }, - }, - size: MAX_ALERTS_PER_SUB_CASE, - ignore_unavailable: true, - }); - - return result.body; + size: MAX_ALERTS_PER_SUB_CASE, + ignore_unavailable: true, + }); + + return result.body; + } catch (error) { + throw createCaseError({ + message: `Failed to retrieve alerts ids: ${JSON.stringify(ids)}: ${error}`, + error, + logger, + }); + } } } diff --git a/x-pack/plugins/case/server/services/connector_mappings/index.ts b/x-pack/plugins/case/server/services/connector_mappings/index.ts index cc387d8e6fe3e..d4fda10276d2b 100644 --- a/x-pack/plugins/case/server/services/connector_mappings/index.ts +++ b/x-pack/plugins/case/server/services/connector_mappings/index.ts @@ -41,7 +41,7 @@ export class ConnectorMappingsService { this.log.debug(`Attempting to find all connector mappings`); return await client.find({ ...options, type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT }); } catch (error) { - this.log.debug(`Attempting to find all connector mappings`); + this.log.error(`Attempting to find all connector mappings: ${error}`); throw error; } }, @@ -52,7 +52,7 @@ export class ConnectorMappingsService { references, }); } catch (error) { - this.log.debug(`Error on POST a new connector mappings: ${error}`); + this.log.error(`Error on POST a new connector mappings: ${error}`); throw error; } }, diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index a9e5c26960830..f74e91ca10224 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -621,7 +621,7 @@ export class CaseService implements CaseServiceSetup { ], }); } catch (error) { - this.log.debug(`Error on POST a new sub case: ${error}`); + this.log.error(`Error on POST a new sub case for id ${caseId}: ${error}`); throw error; } } @@ -642,7 +642,7 @@ export class CaseService implements CaseServiceSetup { return subCases.saved_objects[0]; } catch (error) { - this.log.debug(`Error finding the most recent sub case for case: ${caseId}`); + this.log.error(`Error finding the most recent sub case for case: ${caseId}: ${error}`); throw error; } } @@ -652,7 +652,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to DELETE sub case ${id}`); return await client.delete(SUB_CASE_SAVED_OBJECT, id); } catch (error) { - this.log.debug(`Error on DELETE sub case ${id}: ${error}`); + this.log.error(`Error on DELETE sub case ${id}: ${error}`); throw error; } } @@ -662,7 +662,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to DELETE case ${caseId}`); return await client.delete(CASE_SAVED_OBJECT, caseId); } catch (error) { - this.log.debug(`Error on DELETE case ${caseId}: ${error}`); + this.log.error(`Error on DELETE case ${caseId}: ${error}`); throw error; } } @@ -671,7 +671,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to GET comment ${commentId}`); return await client.delete(CASE_COMMENT_SAVED_OBJECT, commentId); } catch (error) { - this.log.debug(`Error on GET comment ${commentId}: ${error}`); + this.log.error(`Error on GET comment ${commentId}: ${error}`); throw error; } } @@ -683,7 +683,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to GET case ${caseId}`); return await client.get(CASE_SAVED_OBJECT, caseId); } catch (error) { - this.log.debug(`Error on GET case ${caseId}: ${error}`); + this.log.error(`Error on GET case ${caseId}: ${error}`); throw error; } } @@ -692,7 +692,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to GET sub case ${id}`); return await client.get(SUB_CASE_SAVED_OBJECT, id); } catch (error) { - this.log.debug(`Error on GET sub case ${id}: ${error}`); + this.log.error(`Error on GET sub case ${id}: ${error}`); throw error; } } @@ -705,7 +705,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to GET sub cases ${ids.join(', ')}`); return await client.bulkGet(ids.map((id) => ({ type: SUB_CASE_SAVED_OBJECT, id }))); } catch (error) { - this.log.debug(`Error on GET cases ${ids.join(', ')}: ${error}`); + this.log.error(`Error on GET cases ${ids.join(', ')}: ${error}`); throw error; } } @@ -720,7 +720,7 @@ export class CaseService implements CaseServiceSetup { caseIds.map((caseId) => ({ type: CASE_SAVED_OBJECT, id: caseId })) ); } catch (error) { - this.log.debug(`Error on GET cases ${caseIds.join(', ')}: ${error}`); + this.log.error(`Error on GET cases ${caseIds.join(', ')}: ${error}`); throw error; } } @@ -732,7 +732,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to GET comment ${commentId}`); return await client.get(CASE_COMMENT_SAVED_OBJECT, commentId); } catch (error) { - this.log.debug(`Error on GET comment ${commentId}: ${error}`); + this.log.error(`Error on GET comment ${commentId}: ${error}`); throw error; } } @@ -749,7 +749,7 @@ export class CaseService implements CaseServiceSetup { type: CASE_SAVED_OBJECT, }); } catch (error) { - this.log.debug(`Error on find cases: ${error}`); + this.log.error(`Error on find cases: ${error}`); throw error; } } @@ -786,7 +786,7 @@ export class CaseService implements CaseServiceSetup { type: SUB_CASE_SAVED_OBJECT, }); } catch (error) { - this.log.debug(`Error on find sub cases: ${error}`); + this.log.error(`Error on find sub cases: ${error}`); throw error; } } @@ -824,7 +824,7 @@ export class CaseService implements CaseServiceSetup { }, }); } catch (error) { - this.log.debug( + this.log.error( `Error on GET all sub cases for case collection id ${ids.join(', ')}: ${error}` ); throw error; @@ -847,7 +847,7 @@ export class CaseService implements CaseServiceSetup { options, }: FindCommentsArgs): Promise> { try { - this.log.debug(`Attempting to GET all comments for id ${id}`); + this.log.debug(`Attempting to GET all comments for id ${JSON.stringify(id)}`); if (options?.page !== undefined || options?.perPage !== undefined) { return client.find({ type: CASE_COMMENT_SAVED_OBJECT, @@ -874,7 +874,7 @@ export class CaseService implements CaseServiceSetup { ...options, }); } catch (error) { - this.log.debug(`Error on GET all comments for ${id}: ${error}`); + this.log.error(`Error on GET all comments for ${JSON.stringify(id)}: ${error}`); throw error; } } @@ -915,7 +915,7 @@ export class CaseService implements CaseServiceSetup { ); } - this.log.debug(`Attempting to GET all comments for case caseID ${id}`); + this.log.debug(`Attempting to GET all comments for case caseID ${JSON.stringify(id)}`); return this.getAllComments({ client, id, @@ -927,7 +927,7 @@ export class CaseService implements CaseServiceSetup { }, }); } catch (error) { - this.log.debug(`Error on GET all comments for case ${id}: ${error}`); + this.log.error(`Error on GET all comments for case ${JSON.stringify(id)}: ${error}`); throw error; } } @@ -948,7 +948,7 @@ export class CaseService implements CaseServiceSetup { }; } - this.log.debug(`Attempting to GET all comments for sub case caseID ${id}`); + this.log.debug(`Attempting to GET all comments for sub case caseID ${JSON.stringify(id)}`); return this.getAllComments({ client, id, @@ -959,7 +959,7 @@ export class CaseService implements CaseServiceSetup { }, }); } catch (error) { - this.log.debug(`Error on GET all comments for sub case ${id}: ${error}`); + this.log.error(`Error on GET all comments for sub case ${JSON.stringify(id)}: ${error}`); throw error; } } @@ -969,7 +969,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to GET all reporters`); return await readReporters({ client }); } catch (error) { - this.log.debug(`Error on GET all reporters: ${error}`); + this.log.error(`Error on GET all reporters: ${error}`); throw error; } } @@ -978,7 +978,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to GET all cases`); return await readTags({ client }); } catch (error) { - this.log.debug(`Error on GET cases: ${error}`); + this.log.error(`Error on GET cases: ${error}`); throw error; } } @@ -1003,7 +1003,7 @@ export class CaseService implements CaseServiceSetup { email: null, }; } catch (error) { - this.log.debug(`Error on GET cases: ${error}`); + this.log.error(`Error on GET cases: ${error}`); throw error; } } @@ -1012,7 +1012,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to POST a new case`); return await client.create(CASE_SAVED_OBJECT, { ...attributes }); } catch (error) { - this.log.debug(`Error on POST a new case: ${error}`); + this.log.error(`Error on POST a new case: ${error}`); throw error; } } @@ -1021,7 +1021,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to POST a new comment`); return await client.create(CASE_COMMENT_SAVED_OBJECT, attributes, { references }); } catch (error) { - this.log.debug(`Error on POST a new comment: ${error}`); + this.log.error(`Error on POST a new comment: ${error}`); throw error; } } @@ -1030,7 +1030,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to UPDATE case ${caseId}`); return await client.update(CASE_SAVED_OBJECT, caseId, { ...updatedAttributes }, { version }); } catch (error) { - this.log.debug(`Error on UPDATE case ${caseId}: ${error}`); + this.log.error(`Error on UPDATE case ${caseId}: ${error}`); throw error; } } @@ -1046,7 +1046,7 @@ export class CaseService implements CaseServiceSetup { })) ); } catch (error) { - this.log.debug(`Error on UPDATE case ${cases.map((c) => c.caseId).join(', ')}: ${error}`); + this.log.error(`Error on UPDATE case ${cases.map((c) => c.caseId).join(', ')}: ${error}`); throw error; } } @@ -1062,7 +1062,7 @@ export class CaseService implements CaseServiceSetup { { version } ); } catch (error) { - this.log.debug(`Error on UPDATE comment ${commentId}: ${error}`); + this.log.error(`Error on UPDATE comment ${commentId}: ${error}`); throw error; } } @@ -1080,7 +1080,7 @@ export class CaseService implements CaseServiceSetup { })) ); } catch (error) { - this.log.debug( + this.log.error( `Error on UPDATE comments ${comments.map((c) => c.commentId).join(', ')}: ${error}` ); throw error; @@ -1096,7 +1096,7 @@ export class CaseService implements CaseServiceSetup { { version } ); } catch (error) { - this.log.debug(`Error on UPDATE sub case ${subCaseId}: ${error}`); + this.log.error(`Error on UPDATE sub case ${subCaseId}: ${error}`); throw error; } } @@ -1115,7 +1115,7 @@ export class CaseService implements CaseServiceSetup { })) ); } catch (error) { - this.log.debug( + this.log.error( `Error on UPDATE sub case ${subCases.map((c) => c.subCaseId).join(', ')}: ${error}` ); throw error; diff --git a/x-pack/plugins/case/server/services/user_actions/index.ts b/x-pack/plugins/case/server/services/user_actions/index.ts index d05ada0dba30c..785c81021b584 100644 --- a/x-pack/plugins/case/server/services/user_actions/index.ts +++ b/x-pack/plugins/case/server/services/user_actions/index.ts @@ -66,7 +66,7 @@ export class CaseUserActionService { sortOrder: 'asc', }); } catch (error) { - this.log.debug(`Error on GET case user action: ${error}`); + this.log.error(`Error on GET case user action case id: ${caseId}: ${error}`); throw error; } }, @@ -77,7 +77,7 @@ export class CaseUserActionService { actions.map((action) => ({ type: CASE_USER_ACTION_SAVED_OBJECT, ...action })) ); } catch (error) { - this.log.debug(`Error on POST a new case user action: ${error}`); + this.log.error(`Error on POST a new case user action: ${error}`); throw error; } }, From 38bcde1aea0058fb226261f6ee13bd7be345ac17 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Sat, 27 Feb 2021 08:44:47 +0100 Subject: [PATCH 29/32] [APM] Include services with only metric documents (#92378) * [APM] Include services with only metric documents Closes #92075. * Explain as_mutable_array * Use kuery instead of uiFilters for API tests --- .../apm/common/utils/as_mutable_array.ts | 41 +++++++++ .../__snapshots__/queries.test.ts.snap | 51 ++++++++++- .../get_service_transaction_stats.ts | 13 ++- .../get_services_from_metric_documents.ts | 84 +++++++++++++++++++ .../get_services/get_services_items.ts | 43 +++++++--- .../tests/services/top_services.ts | 43 ++++++++++ 6 files changed, 255 insertions(+), 20 deletions(-) create mode 100644 x-pack/plugins/apm/common/utils/as_mutable_array.ts create mode 100644 x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.ts diff --git a/x-pack/plugins/apm/common/utils/as_mutable_array.ts b/x-pack/plugins/apm/common/utils/as_mutable_array.ts new file mode 100644 index 0000000000000..ce1d7e607ec4c --- /dev/null +++ b/x-pack/plugins/apm/common/utils/as_mutable_array.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. + */ + +// Sometimes we use `as const` to have a more specific type, +// because TypeScript by default will widen the value type of an +// array literal. Consider the following example: +// +// const filter = [ +// { term: { 'agent.name': 'nodejs' } }, +// { range: { '@timestamp': { gte: 'now-15m ' }} +// ]; + +// The result value type will be: + +// const filter: ({ +// term: { +// 'agent.name'?: string +// }; +// range?: undefined +// } | { +// term?: undefined; +// range: { +// '@timestamp': { +// gte: string +// } +// } +// })[]; + +// This can sometimes leads to issues. In those cases, we can +// use `as const`. However, the Readonly type is not compatible +// with Array. This function returns a mutable version of a type. + +export function asMutableArray>( + arr: T +): T extends Readonly<[...infer U]> ? U : unknown[] { + return arr as any; +} diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index dec5be8da32f4..7e7e073c0d2f6 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -118,7 +118,6 @@ Array [ "environments": Object { "terms": Object { "field": "service.environment", - "missing": "", }, }, "outcomes": Object { @@ -197,6 +196,56 @@ Array [ "size": 0, }, }, + Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, + "body": Object { + "aggs": Object { + "services": Object { + "aggs": Object { + "environments": Object { + "terms": Object { + "field": "service.environment", + }, + }, + "latest": Object { + "top_metrics": Object { + "metrics": Object { + "field": "agent.name", + }, + "sort": Object { + "@timestamp": "desc", + }, + }, + }, + }, + "terms": Object { + "field": "service.name", + "size": 500, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + ], + }, + }, + "size": 0, + }, + }, ] `; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts index 5f0302035462c..10c7420d0f3b0 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts @@ -40,15 +40,15 @@ interface AggregationParams { kuery?: string; setup: ServicesItemsSetup; searchAggregatedTransactions: boolean; + maxNumServices: number; } -const MAX_NUMBER_OF_SERVICES = 500; - export async function getServiceTransactionStats({ environment, kuery, setup, searchAggregatedTransactions, + maxNumServices, }: AggregationParams) { return withApmSpan('get_service_transaction_stats', async () => { const { apmEventClient, start, end } = setup; @@ -92,7 +92,7 @@ export async function getServiceTransactionStats({ services: { terms: { field: SERVICE_NAME, - size: MAX_NUMBER_OF_SERVICES, + size: maxNumServices, }, aggs: { transactionType: { @@ -104,7 +104,6 @@ export async function getServiceTransactionStats({ environments: { terms: { field: SERVICE_ENVIRONMENT, - missing: '', }, }, sample: { @@ -147,9 +146,9 @@ export async function getServiceTransactionStats({ return { serviceName: bucket.key as string, transactionType: topTransactionTypeBucket.key as string, - environments: topTransactionTypeBucket.environments.buckets - .map((environmentBucket) => environmentBucket.key as string) - .filter(Boolean), + environments: topTransactionTypeBucket.environments.buckets.map( + (environmentBucket) => environmentBucket.key as string + ), agentName: topTransactionTypeBucket.sample.top[0].metrics[ AGENT_NAME ] as AgentName, diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.ts new file mode 100644 index 0000000000000..cabd44c1e6907 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.ts @@ -0,0 +1,84 @@ +/* + * 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 { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; +import { + AGENT_NAME, + SERVICE_ENVIRONMENT, + SERVICE_NAME, +} from '../../../../common/elasticsearch_fieldnames'; +import { environmentQuery, kqlQuery, rangeQuery } from '../../../utils/queries'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { withApmSpan } from '../../../utils/with_apm_span'; + +export function getServicesFromMetricDocuments({ + environment, + setup, + maxNumServices, + kuery, +}: { + setup: Setup & SetupTimeRange; + environment?: string; + maxNumServices: number; + kuery?: string; +}) { + return withApmSpan('get_services_from_metric_documents', async () => { + const { apmEventClient, start, end } = setup; + + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.metric], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ], + }, + }, + aggs: { + services: { + terms: { + field: SERVICE_NAME, + size: maxNumServices, + }, + aggs: { + environments: { + terms: { + field: SERVICE_ENVIRONMENT, + }, + }, + latest: { + top_metrics: { + metrics: { field: AGENT_NAME } as const, + sort: { '@timestamp': 'desc' }, + }, + }, + }, + }, + }, + }, + }); + + return ( + response.aggregations?.services.buckets.map((bucket) => { + return { + serviceName: bucket.key as string, + environments: bucket.environments.buckets.map( + (envBucket) => envBucket.key as string + ), + agentName: bucket.latest.top[0].metrics[AGENT_NAME] as AgentName, + }; + }) ?? [] + ); + }); +} diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 1ddc7a6583c81..3b9792519d261 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -6,15 +6,18 @@ */ import { Logger } from '@kbn/logging'; +import { asMutableArray } from '../../../../common/utils/as_mutable_array'; import { joinByKey } from '../../../../common/utils/join_by_key'; -import { getServicesProjection } from '../../../projections/services'; import { withApmSpan } from '../../../utils/with_apm_span'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getHealthStatuses } from './get_health_statuses'; +import { getServicesFromMetricDocuments } from './get_services_from_metric_documents'; import { getServiceTransactionStats } from './get_service_transaction_stats'; export type ServicesItemsSetup = Setup & SetupTimeRange; +const MAX_NUMBER_OF_SERVICES = 500; + export async function getServicesItems({ environment, kuery, @@ -32,33 +35,49 @@ export async function getServicesItems({ const params = { environment, kuery, - projection: getServicesProjection({ - kuery, - setup, - searchAggregatedTransactions, - }), setup, searchAggregatedTransactions, + maxNumServices: MAX_NUMBER_OF_SERVICES, }; - const [transactionStats, healthStatuses] = await Promise.all([ + const [ + transactionStats, + servicesFromMetricDocuments, + healthStatuses, + ] = await Promise.all([ getServiceTransactionStats(params), + getServicesFromMetricDocuments(params), getHealthStatuses(params).catch((err) => { logger.error(err); return []; }), ]); - const apmServices = transactionStats.map(({ serviceName }) => serviceName); + const foundServiceNames = transactionStats.map( + ({ serviceName }) => serviceName + ); + + const servicesWithOnlyMetricDocuments = servicesFromMetricDocuments.filter( + ({ serviceName }) => !foundServiceNames.includes(serviceName) + ); + + const allServiceNames = foundServiceNames.concat( + servicesWithOnlyMetricDocuments.map(({ serviceName }) => serviceName) + ); // make sure to exclude health statuses from services // that are not found in APM data const matchedHealthStatuses = healthStatuses.filter(({ serviceName }) => - apmServices.includes(serviceName) + allServiceNames.includes(serviceName) ); - const allMetrics = [...transactionStats, ...matchedHealthStatuses]; - - return joinByKey(allMetrics, 'serviceName'); + return joinByKey( + asMutableArray([ + ...transactionStats, + ...servicesWithOnlyMetricDocuments, + ...matchedHealthStatuses, + ] as const), + 'serviceName' + ); }); } diff --git a/x-pack/test/apm_api_integration/tests/services/top_services.ts b/x-pack/test/apm_api_integration/tests/services/top_services.ts index 3896bc1c6fabc..37f7b09e8b7d2 100644 --- a/x-pack/test/apm_api_integration/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/tests/services/top_services.ts @@ -255,6 +255,49 @@ export default function ApiTest({ getService }: FtrProviderContext) { } ); + registry.when( + 'APM Services Overview with a basic license when data is loaded excluding transaction events', + { config: 'basic', archives: [archiveName] }, + () => { + it('includes services that only report metric data', async () => { + interface Response { + status: number; + body: APIReturnType<'GET /api/apm/services'>; + } + + const [unfilteredResponse, filteredResponse] = await Promise.all([ + supertest.get(`/api/apm/services?start=${start}&end=${end}`) as Promise, + supertest.get( + `/api/apm/services?start=${start}&end=${end}&kuery=${encodeURIComponent( + 'not (processor.event:transaction)' + )}` + ) as Promise, + ]); + + expect(unfilteredResponse.body.items.length).to.be.greaterThan(0); + + const unfilteredServiceNames = unfilteredResponse.body.items + .map((item) => item.serviceName) + .sort(); + + const filteredServiceNames = filteredResponse.body.items + .map((item) => item.serviceName) + .sort(); + + expect(unfilteredServiceNames).to.eql(filteredServiceNames); + + expect( + filteredResponse.body.items.every((item) => { + // make sure it did not query transaction data + return isEmpty(item.avgResponseTime); + }) + ).to.be(true); + + expect(filteredResponse.body.items.every((item) => !!item.agentName)).to.be(true); + }); + } + ); + registry.when( 'APM Services overview with a trial license when data is loaded', { config: 'trial', archives: [archiveName] }, From 58a5a7c67d661d7d6b2cdaa75728db5afc7ff897 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Sun, 28 Feb 2021 00:47:40 +0000 Subject: [PATCH 30/32] skip flaky suite (#89072) --- x-pack/test/functional/apps/uptime/overview.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/uptime/overview.ts b/x-pack/test/functional/apps/uptime/overview.ts index 6c9eb24070d8f..b9c1767e4a8cf 100644 --- a/x-pack/test/functional/apps/uptime/overview.ts +++ b/x-pack/test/functional/apps/uptime/overview.ts @@ -15,7 +15,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const testSubjects = getService('testSubjects'); - describe('overview page', function () { + // FLAKY: https://github.com/elastic/kibana/issues/89072 + describe.skip('overview page', function () { const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078'; const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078'; From c6336e50bfe53eaec616c9fd13c9a15ddaba7d75 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Sun, 28 Feb 2021 00:56:02 +0000 Subject: [PATCH 31/32] skip flaky suite (#91939) --- x-pack/test/accessibility/apps/search_profiler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/accessibility/apps/search_profiler.ts b/x-pack/test/accessibility/apps/search_profiler.ts index 7fba45175c831..6559d58be6298 100644 --- a/x-pack/test/accessibility/apps/search_profiler.ts +++ b/x-pack/test/accessibility/apps/search_profiler.ts @@ -15,7 +15,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); const flyout = getService('flyout'); - describe('Accessibility Search Profiler Editor', () => { + // FLAKY: https://github.com/elastic/kibana/issues/91939 + describe.skip('Accessibility Search Profiler Editor', () => { before(async () => { await PageObjects.common.navigateToApp('searchProfiler'); await a11y.testAppSnapshot(); From c116477198a1cd78e2e43bfb38d413cb1f67a2a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Mon, 1 Mar 2021 08:59:03 +0100 Subject: [PATCH 32/32] [Logs UI] Round up the end timestamp in the log stream embeddable (#92833) This fixes the interpretation of the end timestamp in the log stream embeddable such that it rounds it up. Without this a date range of `now/d` to `now/d` would be resolved to a zero-duration time range even though the date picker semantics are "Today". --- .../public/components/log_stream/log_stream_embeddable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx index e1427bc96e7e0..c69c7ebff2b9e 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx @@ -66,7 +66,7 @@ export class LogStreamEmbeddable extends Embeddable { } const startTimestamp = datemathToEpochMillis(this.input.timeRange.from); - const endTimestamp = datemathToEpochMillis(this.input.timeRange.to); + const endTimestamp = datemathToEpochMillis(this.input.timeRange.to, 'up'); if (!startTimestamp || !endTimestamp) { return;