From 3907d53df53ff128a1496b973d15e129abe2419e Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Wed, 22 Sep 2021 13:22:38 +0200 Subject: [PATCH 01/39] persistable state docs (#105202) --- dev_docs/key_concepts/persistable_state.mdx | 83 +++++++++++++++++++ examples/embeddable_examples/kibana.json | 2 +- .../migrations_embeddable_factory.ts | 11 +++ .../server/merge_migration_function_maps.ts | 25 ++++++ .../server/searchable_list_saved_object.ts | 21 +++-- src/plugins/kibana_utils/server/index.ts | 1 + 6 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 dev_docs/key_concepts/persistable_state.mdx create mode 100644 examples/embeddable_examples/server/merge_migration_function_maps.ts diff --git a/dev_docs/key_concepts/persistable_state.mdx b/dev_docs/key_concepts/persistable_state.mdx new file mode 100644 index 0000000000000..77c7e41ff2e53 --- /dev/null +++ b/dev_docs/key_concepts/persistable_state.mdx @@ -0,0 +1,83 @@ +--- +id: kibDevDocsPersistableStateIntro +slug: /kibana-dev-docs/persistable-state-intro +title: Persistable State +summary: Persitable state is a key concept to understand when building a Kibana plugin. +date: 2021-02-02 +tags: ['kibana','dev', 'contributor', 'api docs'] +--- + + “Persistable state” is developer-defined state that supports being persisted by a plugin other than the one defining it. Persistable State needs to be serializable and the owner can/should provide utilities to migrate it, extract and inject any it may contain, as well as telemetry collection utilities. + +## Exposing state that can be persisted + +Any plugin that exposes state that another plugin might persist should implement interface on their `setup` contract. This will allow plugins persisting the state to easily access migrations and other utilities. + +Example: Data plugin allows you to generate filters. Those filters can be persisted by applications in their saved +objects or in the URL. In order to allow apps to migrate the filters in case the structure changes in the future, the Data plugin implements `PersistableStateService` on . + +note: There is currently no obvious way for a plugin to know which state is safe to persist. The developer must manually look for a matching `PersistableStateService`, or ad-hoc provided migration utilities (as is the case with Rule Type Parameters). +In the future, we hope to make it easier for producers of state to understand when they need to write a migration with changes, and also make it easier for consumers of such state, to understand whether it is safe to persist. + +## Exposing state that can be persisted but is not owned by plugin exposing it (registry) + +Any plugin that owns collection of items (registry) whose state/configuration can be persisted should implement `PersistableStateService` +interface on their `setup` contract and each item in the collection should implement interface. + +Example: Embeddable plugin owns the registry of embeddable factories to which other plugins can register new embeddable factories. Dashboard plugin +stores a bunch of embeddable panels input in its saved object and URL. Embeddable plugin setup contract implements `PersistableStateService` +interface and each `EmbeddableFactory` needs to implement `PersistableStateDefinition` interface. + +Embeddable plugin exposes this interfaces: +``` +// EmbeddableInput implements Serializable + +export interface EmbeddableRegistryDefinition extends PersistableStateDefinition { + id: string; + ... +} + +export interface EmbeddableSetup extends PersistableStateService; +``` + +Note: if your plugin doesn't expose the state (it is the only one storing state), the plugin doesn't need to implement the `PersistableStateService` interface. +If the state your plugin is storing can be provided by other plugins (your plugin owns a registry) items in that registry still need to implement `PersistableStateDefinition` interface. + +## Storing persistable state as part of saved object + +Any plugin that stores any persistable state as part of their saved object should make sure that its saved object migration +and reference extraction and injection methods correctly use the matching `PersistableStateService` implementation for the state they are storing. + +Take a look at [example saved object](https://github.com/elastic/kibana/blob/master/examples/embeddable_examples/server/searchable_list_saved_object.ts#L32) which stores an embeddable state. Note how the `migrations`, `extractReferences` and `injectReferences` are defined. + +## Storing persistable state as part of URL + +When storing persistable state as part of URL you must make sure your URL is versioned. When loading the state `migrateToLatest` method +of `PersistableStateService` should be called, which will migrate the state from its original version to latest. + +note: Currently there is no recommended way on how to store version in url and its up to every application to decide on how to implement that. + +## Available state operations + +### Extraction/Injection of References + +In order to support import and export, and space-sharing capabilities, Saved Objects need to explicitly list any references they contain to other Saved Objects. +To support persisting your state in saved objects owned by another plugin, the and methods of Persistable State interface should be implemented. + + + +[See example embeddable providing extract/inject functions](https://github.com/elastic/kibana/blob/master/examples/embeddable_examples/public/migrations/migrations_embeddable_factory.ts) + +### Migrations and Backward compatibility + +As your plugin evolves, you may need to change your state in a breaking way. If that happens, you should write a migration to upgrade the state that existed prior to the change. + +. + +[See an example saved object storing embeddable state implementing saved object migration function](https://github.com/elastic/kibana/blob/master/examples/embeddable_examples/server/searchable_list_saved_object.ts) + +[See example embeddable providing migration functions](https://github.com/elastic/kibana/blob/master/examples/embeddable_examples/public/migrations/migrations_embeddable_factory.ts) + +## Telemetry + +You might want to collect statistics about how your state is used. If that is the case you should implement the telemetry method of Persistable State interface. diff --git a/examples/embeddable_examples/kibana.json b/examples/embeddable_examples/kibana.json index d725a5c94a9c8..103857804b5d4 100644 --- a/examples/embeddable_examples/kibana.json +++ b/examples/embeddable_examples/kibana.json @@ -9,7 +9,7 @@ "githubTeam": "kibana-app-services" }, "description": "Example app that shows how to register custom embeddables", - "requiredPlugins": ["embeddable", "uiActions", "savedObjects", "dashboard"], + "requiredPlugins": ["embeddable", "uiActions", "savedObjects", "dashboard", "kibanaUtils"], "optionalPlugins": [], "extraPublicDirs": ["public/todo", "public/hello_world", "public/todo/todo_ref_embeddable"], "requiredBundles": ["kibanaReact"] diff --git a/examples/embeddable_examples/public/migrations/migrations_embeddable_factory.ts b/examples/embeddable_examples/public/migrations/migrations_embeddable_factory.ts index c1ceaaca3e466..61e6bfa56ec47 100644 --- a/examples/embeddable_examples/public/migrations/migrations_embeddable_factory.ts +++ b/examples/embeddable_examples/public/migrations/migrations_embeddable_factory.ts @@ -7,6 +7,7 @@ */ import { i18n } from '@kbn/i18n'; +import { EmbeddableStateWithType } from '../../../../src/plugins/embeddable/common'; import { IContainer, EmbeddableInput, @@ -35,6 +36,16 @@ export class SimpleEmbeddableFactoryDefinition '7.3.0': migration730, }; + public extract(state: EmbeddableStateWithType) { + // this embeddable does not store references to other saved objects + return { state, references: [] }; + } + + public inject(state: EmbeddableStateWithType) { + // this embeddable does not store references to other saved objects + return state; + } + /** * In our simple example, we let everyone have permissions to edit this. Most * embeddables should check the UI Capabilities service to be sure of diff --git a/examples/embeddable_examples/server/merge_migration_function_maps.ts b/examples/embeddable_examples/server/merge_migration_function_maps.ts new file mode 100644 index 0000000000000..01a46949e6bbf --- /dev/null +++ b/examples/embeddable_examples/server/merge_migration_function_maps.ts @@ -0,0 +1,25 @@ +/* + * 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 { mergeWith } from 'lodash'; +import type { SerializableRecord } from '@kbn/utility-types'; +import { MigrateFunctionsObject, MigrateFunction } from '../../../src/plugins/kibana_utils/common'; + +export const mergeMigrationFunctionMaps = ( + obj1: MigrateFunctionsObject, + obj2: MigrateFunctionsObject +) => { + const customizer = (objValue: MigrateFunction, srcValue: MigrateFunction) => { + if (!srcValue || !objValue) { + return srcValue || objValue; + } + return (state: SerializableRecord) => objValue(srcValue(state)); + }; + + return mergeWith({ ...obj1 }, obj2, customizer); +}; diff --git a/examples/embeddable_examples/server/searchable_list_saved_object.ts b/examples/embeddable_examples/server/searchable_list_saved_object.ts index ac4656c7c2b77..a3b12a05323f0 100644 --- a/examples/embeddable_examples/server/searchable_list_saved_object.ts +++ b/examples/embeddable_examples/server/searchable_list_saved_object.ts @@ -9,9 +9,12 @@ import { mapValues } from 'lodash'; import { SavedObjectsType, SavedObjectUnsanitizedDoc } from 'kibana/server'; import { EmbeddableSetup } from '../../../src/plugins/embeddable/server'; +// NOTE: this should rather be imported from 'plugins/kibana_utils/server' but examples at the moment don't +// allow static imports from plugins so this code was duplicated +import { mergeMigrationFunctionMaps } from './merge_migration_function_maps'; export const searchableListSavedObject = (embeddable: EmbeddableSetup) => { - return { + const searchableListSO: SavedObjectsType = { name: 'searchableList', hidden: false, namespaceType: 'single', @@ -30,14 +33,22 @@ export const searchableListSavedObject = (embeddable: EmbeddableSetup) => { }, }, migrations: () => { - // we assume all the migration will be done by embeddables service and that saved object holds no extra state besides that of searchable list embeddable input\ - // if saved object would hold additional information we would need to merge the response from embeddables.getAllMigrations with our custom migrations. - return mapValues(embeddable.getAllMigrations(), (migrate) => { + // there are no migrations defined for the saved object at the moment, possibly they would be added in the future + const searchableListSavedObjectMigrations = {}; + + // we don't know if embeddables have any migrations defined so we need to fetch them and map the received functions so we pass + // them the correct input and that we correctly map the response + const embeddableMigrations = mapValues(embeddable.getAllMigrations(), (migrate) => { return (state: SavedObjectUnsanitizedDoc) => ({ ...state, attributes: migrate(state.attributes), }); }); + + // we merge our and embeddable migrations and return + return mergeMigrationFunctionMaps(searchableListSavedObjectMigrations, embeddableMigrations); }, - } as SavedObjectsType; + }; + + return searchableListSO; }; diff --git a/src/plugins/kibana_utils/server/index.ts b/src/plugins/kibana_utils/server/index.ts index 483c5aa92b45e..42847042be151 100644 --- a/src/plugins/kibana_utils/server/index.ts +++ b/src/plugins/kibana_utils/server/index.ts @@ -15,6 +15,7 @@ export { Get, Set, url, + mergeMigrationFunctionMaps, } from '../common'; export { KbnServerError, reportServerError, getKbnServerError } from './report_server_error'; From 11700a7d7ad29dd249a86f0d52cab556caa9b67b Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Wed, 22 Sep 2021 12:35:31 +0100 Subject: [PATCH 02/39] [Security Solution] Update data field for JA3 fingerprint (#112571) * update data field for JA3 fingerprint * update unit test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../security_solution/factory/network/tls/__mocks__/index.ts | 4 ++-- .../factory/network/tls/query.tls_network.dsl.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/__mocks__/index.ts index 8616a2ef14856..41cf691e00f99 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/__mocks__/index.ts @@ -81,7 +81,7 @@ export const formattedSearchStrategyResponse = { issuers: { terms: { field: 'tls.server.issuer' } }, subjects: { terms: { field: 'tls.server.subject' } }, not_after: { terms: { field: 'tls.server.not_after' } }, - ja3: { terms: { field: 'tls.server.ja3s' } }, + ja3: { terms: { field: 'tls.client.ja3s' } }, }, }, }, @@ -136,7 +136,7 @@ export const expectedDsl = { issuers: { terms: { field: 'tls.server.issuer' } }, subjects: { terms: { field: 'tls.server.subject' } }, not_after: { terms: { field: 'tls.server.not_after' } }, - ja3: { terms: { field: 'tls.server.ja3s' } }, + ja3: { terms: { field: 'tls.client.ja3s' } }, }, }, }, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/query.tls_network.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/query.tls_network.dsl.ts index be60b33ae2d22..7f3f649ed965a 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/query.tls_network.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/query.tls_network.dsl.ts @@ -47,7 +47,7 @@ const getAggs = (querySize: number, sort: SortField) => ({ }, ja3: { terms: { - field: 'tls.server.ja3s', + field: 'tls.client.ja3s', }, }, }, From 525d3a59206c4dabe1b6bc822e848c8f534350c4 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Wed, 22 Sep 2021 07:04:15 -0500 Subject: [PATCH 03/39] [data views] remove some any instances from service (#112632) * remove some any instances from data view * lint fix * simplify scripted field type --- .../data_views/data_views/_pattern_cache.ts | 4 +-- .../common/data_views/data_views/data_view.ts | 32 +++++++++---------- .../data_views/data_views/data_views.ts | 6 ++-- .../data/common/data_views/lib/get_title.ts | 9 +++--- .../data_views/lib/validate_index_pattern.ts | 2 +- .../application/services/use_es_doc_search.ts | 2 +- .../field_validators/index_pattern_field.ts | 6 ++-- 7 files changed, 30 insertions(+), 31 deletions(-) diff --git a/src/plugins/data/common/data_views/data_views/_pattern_cache.ts b/src/plugins/data/common/data_views/data_views/_pattern_cache.ts index f304d0e93d79c..19db5b21e5934 100644 --- a/src/plugins/data/common/data_views/data_views/_pattern_cache.ts +++ b/src/plugins/data/common/data_views/data_views/_pattern_cache.ts @@ -16,12 +16,12 @@ export interface DataViewCache { } export function createDataViewCache(): DataViewCache { - const vals: Record = {}; + const vals: Record> = {}; const cache: DataViewCache = { get: (id: string) => { return vals[id]; }, - set: (id: string, prom: any) => { + set: (id: string, prom: Promise) => { vals[id] = prom; return prom; }, diff --git a/src/plugins/data/common/data_views/data_views/data_view.ts b/src/plugins/data/common/data_views/data_views/data_view.ts index e08d1e62bae06..596887e83ec05 100644 --- a/src/plugins/data/common/data_views/data_views/data_view.ts +++ b/src/plugins/data/common/data_views/data_views/data_view.ts @@ -158,7 +158,7 @@ export class DataView implements IIndexPattern { }; getComputedFields() { - const scriptFields: any = {}; + const scriptFields: Record = {}; if (!this.fields) { return { storedFields: ['*'], @@ -171,23 +171,21 @@ export class DataView implements IIndexPattern { // Date value returned in "_source" could be in any number of formats // Use a docvalue for each date field to ensure standardized formats when working with date fields // indexPattern.flattenHit will override "_source" values when the same field is also defined in "fields" - const docvalueFields = reject(this.fields.getByType('date'), 'scripted').map( - (dateField: any) => { - return { - field: dateField.name, - format: - dateField.esTypes && dateField.esTypes.indexOf('date_nanos') !== -1 - ? 'strict_date_time' - : 'date_time', - }; - } - ); + const docvalueFields = reject(this.fields.getByType('date'), 'scripted').map((dateField) => { + return { + field: dateField.name, + format: + dateField.esTypes && dateField.esTypes.indexOf('date_nanos') !== -1 + ? 'strict_date_time' + : 'date_time', + }; + }); each(this.getScriptedFields(), function (field) { scriptFields[field.name] = { script: { - source: field.script, - lang: field.lang, + source: field.script as string, + lang: field.lang as string, }, }; }); @@ -227,7 +225,7 @@ export class DataView implements IIndexPattern { */ getSourceFiltering() { return { - excludes: (this.sourceFilters && this.sourceFilters.map((filter: any) => filter.value)) || [], + excludes: (this.sourceFilters && this.sourceFilters.map((filter) => filter.value)) || [], }; } @@ -299,8 +297,8 @@ export class DataView implements IIndexPattern { } isTimeNanosBased(): boolean { - const timeField: any = this.getTimeField(); - return timeField && timeField.esTypes && timeField.esTypes.indexOf('date_nanos') !== -1; + const timeField = this.getTimeField(); + return !!(timeField && timeField.esTypes && timeField.esTypes.indexOf('date_nanos') !== -1); } getTimeField() { diff --git a/src/plugins/data/common/data_views/data_views/data_views.ts b/src/plugins/data/common/data_views/data_views/data_views.ts index 1284f00436324..f903c65d95d2a 100644 --- a/src/plugins/data/common/data_views/data_views/data_views.ts +++ b/src/plugins/data/common/data_views/data_views/data_views.ts @@ -289,7 +289,7 @@ export class DataViewsService { indexPattern.fields.replaceAll(fieldsWithSavedAttrs); } catch (err) { if (err instanceof DataViewMissingIndices) { - this.onNotification({ title: (err as any).message, color: 'danger', iconType: 'alert' }); + this.onNotification({ title: err.message, color: 'danger', iconType: 'alert' }); } this.onError(err, { @@ -334,7 +334,7 @@ export class DataViewsService { return this.fieldArrayToMap(updatedFieldList, fieldAttrs); } catch (err) { if (err instanceof DataViewMissingIndices) { - this.onNotification({ title: (err as any).message, color: 'danger', iconType: 'alert' }); + this.onNotification({ title: err.message, color: 'danger', iconType: 'alert' }); return {}; } @@ -475,7 +475,7 @@ export class DataViewsService { } catch (err) { if (err instanceof DataViewMissingIndices) { this.onNotification({ - title: (err as any).message, + title: err.message, color: 'danger', iconType: 'alert', }); diff --git a/src/plugins/data/common/data_views/lib/get_title.ts b/src/plugins/data/common/data_views/lib/get_title.ts index efebbc302f22c..94185eae46893 100644 --- a/src/plugins/data/common/data_views/lib/get_title.ts +++ b/src/plugins/data/common/data_views/lib/get_title.ts @@ -6,17 +6,18 @@ * Side Public License, v 1. */ -import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../core/public'; +import { SavedObjectsClientContract } from '../../../../../core/public'; import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../constants'; +import { DataViewAttributes } from '../types'; export async function getTitle( client: SavedObjectsClientContract, indexPatternId: string -): Promise> { - const savedObject = (await client.get( +): Promise { + const savedObject = await client.get( DATA_VIEW_SAVED_OBJECT_TYPE, indexPatternId - )) as SimpleSavedObject; + ); if (savedObject.error) { throw new Error(`Unable to get index-pattern title: ${savedObject.error.message}`); diff --git a/src/plugins/data/common/data_views/lib/validate_index_pattern.ts b/src/plugins/data/common/data_views/lib/validate_index_pattern.ts index 454d0bc1a0c6e..f86ba28e7cde4 100644 --- a/src/plugins/data/common/data_views/lib/validate_index_pattern.ts +++ b/src/plugins/data/common/data_views/lib/validate_index_pattern.ts @@ -24,7 +24,7 @@ function findIllegalCharacters(indexPattern: string): string[] { } export function validateDataView(indexPattern: string) { - const errors: Record = {}; + const errors: { [ILLEGAL_CHARACTERS_KEY]?: string[]; [CONTAINS_SPACES_KEY]?: boolean } = {}; const illegalCharacters = findIllegalCharacters(indexPattern); diff --git a/src/plugins/discover/public/application/services/use_es_doc_search.ts b/src/plugins/discover/public/application/services/use_es_doc_search.ts index a2f0cd6f8442b..c5216c483fd10 100644 --- a/src/plugins/discover/public/application/services/use_es_doc_search.ts +++ b/src/plugins/discover/public/application/services/use_es_doc_search.ts @@ -37,7 +37,7 @@ export function buildSearchBody( }, }, stored_fields: computedFields.storedFields, - script_fields: computedFields.scriptFields, + script_fields: computedFields.scriptFields as Record, version: true, }, }; diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index_pattern_field.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index_pattern_field.ts index 5aadefa6005fa..1b6667fce41ab 100644 --- a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index_pattern_field.ts +++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index_pattern_field.ts @@ -37,7 +37,7 @@ export const indexPatternField = // Validate illegal characters const errors = indexPatterns.validate(value); - if (errors[indexPatterns.ILLEGAL_CHARACTERS_KEY]) { + if (errors.ILLEGAL_CHARACTERS) { return { code: 'ERR_FIELD_FORMAT', formatType: 'INDEX_PATTERN', @@ -45,8 +45,8 @@ export const indexPatternField = defaultMessage: 'The index pattern contains the invalid {characterListLength, plural, one {character} other {characters}} { characterList }.', values: { - characterList: errors[indexPatterns.ILLEGAL_CHARACTERS_KEY].join(' '), - characterListLength: errors[indexPatterns.ILLEGAL_CHARACTERS_KEY].length, + characterList: errors.ILLEGAL_CHARACTERS.join(' '), + characterListLength: errors.ILLEGAL_CHARACTERS.length, }, }), }; From 70f635b14d8d62d62aec2aacc6a9cf79e60f81fd Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Wed, 22 Sep 2021 08:59:49 -0400 Subject: [PATCH 04/39] [Cases] Migrate user actions connector ID V2 (#112710) * Making progress * Fleshing out the extraction logic * Finishing migration logic and starting more tests * Finishing migration unit tests * Making progress on services * Finishing transform to es schema * Finishing transform functionality and unit tests * reverting migration data updates * Cleaning up type errors * fixing test error * Working migration tests * Refactoring retrieval of connector fields * Refactoring connector id in and tests in frontend * Fixing tests and finished refactoring parse string * Fixing integration test * Fixing integration tests * Removing some duplicate code and updating test name * Fixing create connector user action bug * Addressing feedback and logging error * Moving parsing function to common * Fixing type errors * Fixing type errors * Addressing feedback * Fixing lint errors * Adjusting import for user action changes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/cases/common/api/cases/case.ts | 16 +- .../cases/common/api/cases/user_actions.ts | 3 +- .../cases/common/api/connectors/index.ts | 12 +- x-pack/plugins/cases/common/index.ts | 1 + x-pack/plugins/cases/common/ui/types.ts | 2 + .../cases/common/utils/user_actions.ts | 18 + .../cases/public/common/user_actions/index.ts | 8 + .../common/user_actions/parsers.test.ts | 86 + .../public/common/user_actions/parsers.ts | 77 + .../components/edit_connector/helpers.test.ts | 187 ++ .../components/edit_connector/helpers.ts | 30 +- .../user_action_tree/helpers.test.tsx | 108 +- .../components/user_action_tree/helpers.tsx | 53 +- .../components/user_action_tree/index.tsx | 10 +- .../plugins/cases/public/containers/mock.ts | 123 +- .../use_get_case_user_actions.test.tsx | 237 +- .../containers/use_get_case_user_actions.tsx | 51 +- .../plugins/cases/public/containers/utils.ts | 8 - .../cases/server/client/attachments/add.ts | 6 +- .../cases/server/client/attachments/update.ts | 9 +- .../cases/server/client/cases/create.ts | 2 +- .../cases/server/client/cases/delete.ts | 2 +- .../plugins/cases/server/client/cases/mock.ts | 18 +- .../plugins/cases/server/client/cases/push.ts | 2 +- .../cases/server/client/cases/utils.test.ts | 6 +- .../cases/server/client/cases/utils.ts | 30 +- .../server/client/user_actions/get.test.ts | 106 + .../cases/server/client/user_actions/get.ts | 47 +- .../plugins/cases/server/common/constants.ts | 29 + .../migrations/cases.test.ts | 570 ++-- .../saved_object_types/migrations/cases.ts | 14 +- .../migrations/configuration.test.ts | 142 +- .../migrations/configuration.ts | 9 +- .../saved_object_types/migrations/index.ts | 57 +- .../migrations/user_actions.test.ts | 562 ++++ .../migrations/user_actions.ts | 159 + .../migrations/utils.test.ts | 229 -- .../saved_object_types/migrations/utils.ts | 73 - .../cases/server/services/cases/index.test.ts | 8 +- .../cases/server/services/test_utils.ts | 17 +- .../services/user_actions/helpers.test.ts | 332 ++ .../server/services/user_actions/helpers.ts | 192 +- .../services/user_actions/index.test.ts | 557 ++++ .../server/services/user_actions/index.ts | 101 +- .../services/user_actions/transform.test.ts | 1246 +++++++ .../server/services/user_actions/transform.ts | 320 ++ .../server/services/user_actions/types.ts | 14 + .../tests/common/cases/delete_cases.ts | 4 +- .../tests/common/cases/import_export.ts | 85 +- .../tests/common/cases/patch_cases.ts | 4 + .../tests/common/cases/post_case.ts | 7 +- .../tests/common/comments/post_comment.ts | 2 + .../user_actions/get_all_user_actions.ts | 19 +- .../tests/common/user_actions/migrations.ts | 149 +- .../tests/trial/cases/push_case.ts | 3 +- .../user_actions/get_all_user_actions.ts | 4 +- .../migrations/7.13_user_actions/data.json.gz | Bin 0 -> 2078 bytes .../7.13_user_actions/mappings.json | 2954 +++++++++++++++++ 58 files changed, 7949 insertions(+), 1171 deletions(-) create mode 100644 x-pack/plugins/cases/common/utils/user_actions.ts create mode 100644 x-pack/plugins/cases/public/common/user_actions/index.ts create mode 100644 x-pack/plugins/cases/public/common/user_actions/parsers.test.ts create mode 100644 x-pack/plugins/cases/public/common/user_actions/parsers.ts create mode 100644 x-pack/plugins/cases/public/components/edit_connector/helpers.test.ts create mode 100644 x-pack/plugins/cases/server/client/user_actions/get.test.ts create mode 100644 x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.test.ts create mode 100644 x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.ts delete mode 100644 x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts delete mode 100644 x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts create mode 100644 x-pack/plugins/cases/server/services/user_actions/helpers.test.ts create mode 100644 x-pack/plugins/cases/server/services/user_actions/index.test.ts create mode 100644 x-pack/plugins/cases/server/services/user_actions/transform.test.ts create mode 100644 x-pack/plugins/cases/server/services/user_actions/transform.ts create mode 100644 x-pack/plugins/cases/server/services/user_actions/types.ts create mode 100644 x-pack/test/functional/es_archives/cases/migrations/7.13_user_actions/data.json.gz create mode 100644 x-pack/test/functional/es_archives/cases/migrations/7.13_user_actions/mappings.json diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index 37a491cdad4c0..05a053307b29c 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -87,8 +87,11 @@ const CaseBasicRt = rt.type({ owner: rt.string, }); -export const CaseExternalServiceBasicRt = rt.type({ - connector_id: rt.union([rt.string, rt.null]), +/** + * This represents the push to service UserAction. It lacks the connector_id because that is stored in a different field + * within the user action object in the API response. + */ +export const CaseUserActionExternalServiceRt = rt.type({ connector_name: rt.string, external_id: rt.string, external_title: rt.string, @@ -97,7 +100,14 @@ export const CaseExternalServiceBasicRt = rt.type({ pushed_by: UserRT, }); -const CaseFullExternalServiceRt = rt.union([CaseExternalServiceBasicRt, rt.null]); +export const CaseExternalServiceBasicRt = rt.intersection([ + rt.type({ + connector_id: rt.union([rt.string, rt.null]), + }), + CaseUserActionExternalServiceRt, +]); + +export const CaseFullExternalServiceRt = rt.union([CaseExternalServiceBasicRt, rt.null]); export const CaseAttributesRt = rt.intersection([ CaseBasicRt, diff --git a/x-pack/plugins/cases/common/api/cases/user_actions.ts b/x-pack/plugins/cases/common/api/cases/user_actions.ts index 03912c550d77a..e86ce5248a6f9 100644 --- a/x-pack/plugins/cases/common/api/cases/user_actions.ts +++ b/x-pack/plugins/cases/common/api/cases/user_actions.ts @@ -34,7 +34,6 @@ const UserActionRt = rt.union([ rt.literal('push-to-service'), ]); -// TO DO change state to status const CaseUserActionBasicRT = rt.type({ action_field: UserActionFieldRt, action: UserActionRt, @@ -51,6 +50,8 @@ const CaseUserActionResponseRT = rt.intersection([ action_id: rt.string, case_id: rt.string, comment_id: rt.union([rt.string, rt.null]), + new_val_connector_id: rt.union([rt.string, rt.null]), + old_val_connector_id: rt.union([rt.string, rt.null]), }), rt.partial({ sub_case_id: rt.string }), ]); diff --git a/x-pack/plugins/cases/common/api/connectors/index.ts b/x-pack/plugins/cases/common/api/connectors/index.ts index 77af90b5d08cb..2b3483b4f6184 100644 --- a/x-pack/plugins/cases/common/api/connectors/index.ts +++ b/x-pack/plugins/cases/common/api/connectors/index.ts @@ -84,14 +84,22 @@ export const ConnectorTypeFieldsRt = rt.union([ ConnectorSwimlaneTypeFieldsRt, ]); +/** + * This type represents the connector's format when it is encoded within a user action. + */ +export const CaseUserActionConnectorRt = rt.intersection([ + rt.type({ name: rt.string }), + ConnectorTypeFieldsRt, +]); + export const CaseConnectorRt = rt.intersection([ rt.type({ id: rt.string, - name: rt.string, }), - ConnectorTypeFieldsRt, + CaseUserActionConnectorRt, ]); +export type CaseUserActionConnector = rt.TypeOf; export type CaseConnector = rt.TypeOf; export type ConnectorTypeFields = rt.TypeOf; export type ConnectorJiraTypeFields = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/index.ts b/x-pack/plugins/cases/common/index.ts index 5305318cc9aa6..d38b1a779981c 100644 --- a/x-pack/plugins/cases/common/index.ts +++ b/x-pack/plugins/cases/common/index.ts @@ -12,3 +12,4 @@ export * from './constants'; export * from './api'; export * from './ui/types'; export * from './utils/connectors_api'; +export * from './utils/user_actions'; diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index bf4ec0da6ee56..c89c3eb08263b 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -66,7 +66,9 @@ export interface CaseUserActions { caseId: string; commentId: string | null; newValue: string | null; + newValConnectorId: string | null; oldValue: string | null; + oldValConnectorId: string | null; } export interface CaseExternalService { diff --git a/x-pack/plugins/cases/common/utils/user_actions.ts b/x-pack/plugins/cases/common/utils/user_actions.ts new file mode 100644 index 0000000000000..7de0d7066eaed --- /dev/null +++ b/x-pack/plugins/cases/common/utils/user_actions.ts @@ -0,0 +1,18 @@ +/* + * 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 function isCreateConnector(action?: string, actionFields?: string[]): boolean { + return action === 'create' && actionFields != null && actionFields.includes('connector'); +} + +export function isUpdateConnector(action?: string, actionFields?: string[]): boolean { + return action === 'update' && actionFields != null && actionFields.includes('connector'); +} + +export function isPush(action?: string, actionFields?: string[]): boolean { + return action === 'push-to-service' && actionFields != null && actionFields.includes('pushed'); +} diff --git a/x-pack/plugins/cases/public/common/user_actions/index.ts b/x-pack/plugins/cases/public/common/user_actions/index.ts new file mode 100644 index 0000000000000..507455f7102a7 --- /dev/null +++ b/x-pack/plugins/cases/public/common/user_actions/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './parsers'; diff --git a/x-pack/plugins/cases/public/common/user_actions/parsers.test.ts b/x-pack/plugins/cases/public/common/user_actions/parsers.test.ts new file mode 100644 index 0000000000000..c6d13cc41686c --- /dev/null +++ b/x-pack/plugins/cases/public/common/user_actions/parsers.test.ts @@ -0,0 +1,86 @@ +/* + * 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 { ConnectorTypes, noneConnectorId } from '../../../common'; +import { parseStringAsConnector, parseStringAsExternalService } from './parsers'; + +describe('user actions utility functions', () => { + describe('parseStringAsConnector', () => { + it('return null if the data is null', () => { + expect(parseStringAsConnector('', null)).toBeNull(); + }); + + it('return null if the data is not a json object', () => { + expect(parseStringAsConnector('', 'blah')).toBeNull(); + }); + + it('return null if the data is not a valid connector', () => { + expect(parseStringAsConnector('', JSON.stringify({ a: '1' }))).toBeNull(); + }); + + it('return null if id is null but the data is a connector other than none', () => { + expect( + parseStringAsConnector( + null, + JSON.stringify({ type: ConnectorTypes.jira, name: '', fields: null }) + ) + ).toBeNull(); + }); + + it('return the id as the none connector if the data is the none connector', () => { + expect( + parseStringAsConnector( + null, + JSON.stringify({ type: ConnectorTypes.none, name: '', fields: null }) + ) + ).toEqual({ id: noneConnectorId, type: ConnectorTypes.none, name: '', fields: null }); + }); + + it('returns a decoded connector with the specified id', () => { + expect( + parseStringAsConnector( + 'a', + JSON.stringify({ type: ConnectorTypes.jira, name: 'hi', fields: null }) + ) + ).toEqual({ id: 'a', type: ConnectorTypes.jira, name: 'hi', fields: null }); + }); + }); + + describe('parseStringAsExternalService', () => { + it('returns null when the data is null', () => { + expect(parseStringAsExternalService('', null)).toBeNull(); + }); + + it('returns null when the data is not valid json', () => { + expect(parseStringAsExternalService('', 'blah')).toBeNull(); + }); + + it('returns null when the data is not a valid external service object', () => { + expect(parseStringAsExternalService('', JSON.stringify({ a: '1' }))).toBeNull(); + }); + + it('returns the decoded external service with the connector_id field added', () => { + const externalServiceInfo = { + connector_name: 'name', + external_id: '1', + external_title: 'title', + external_url: 'abc', + pushed_at: '1', + pushed_by: { + username: 'a', + email: 'a@a.com', + full_name: 'a', + }, + }; + + expect(parseStringAsExternalService('500', JSON.stringify(externalServiceInfo))).toEqual({ + ...externalServiceInfo, + connector_id: '500', + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/common/user_actions/parsers.ts b/x-pack/plugins/cases/public/common/user_actions/parsers.ts new file mode 100644 index 0000000000000..dfea22443aa51 --- /dev/null +++ b/x-pack/plugins/cases/public/common/user_actions/parsers.ts @@ -0,0 +1,77 @@ +/* + * 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 { + CaseUserActionConnectorRt, + CaseConnector, + ConnectorTypes, + noneConnectorId, + CaseFullExternalService, + CaseUserActionExternalServiceRt, +} from '../../../common'; + +export const parseStringAsConnector = ( + id: string | null, + encodedData: string | null +): CaseConnector | null => { + if (encodedData == null) { + return null; + } + + const decodedConnector = parseString(encodedData); + + if (!CaseUserActionConnectorRt.is(decodedConnector)) { + return null; + } + + if (id == null && decodedConnector.type === ConnectorTypes.none) { + return { + ...decodedConnector, + id: noneConnectorId, + }; + } else if (id == null) { + return null; + } else { + // id does not equal null or undefined and the connector type does not equal none + // so return the connector with its id + return { + ...decodedConnector, + id, + }; + } +}; + +const parseString = (params: string | null): unknown | null => { + if (params == null) { + return null; + } + + try { + return JSON.parse(params); + } catch { + return null; + } +}; + +export const parseStringAsExternalService = ( + id: string | null, + encodedData: string | null +): CaseFullExternalService => { + if (encodedData == null) { + return null; + } + + const decodedExternalService = parseString(encodedData); + if (!CaseUserActionExternalServiceRt.is(decodedExternalService)) { + return null; + } + + return { + ...decodedExternalService, + connector_id: id, + }; +}; diff --git a/x-pack/plugins/cases/public/components/edit_connector/helpers.test.ts b/x-pack/plugins/cases/public/components/edit_connector/helpers.test.ts new file mode 100644 index 0000000000000..e20d6b37258bc --- /dev/null +++ b/x-pack/plugins/cases/public/components/edit_connector/helpers.test.ts @@ -0,0 +1,187 @@ +/* + * 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 { CaseUserActionConnector, ConnectorTypes } from '../../../common'; +import { CaseUserActions } from '../../containers/types'; +import { getConnectorFieldsFromUserActions } from './helpers'; + +describe('helpers', () => { + describe('getConnectorFieldsFromUserActions', () => { + it('returns null when it cannot find the connector id', () => { + expect(getConnectorFieldsFromUserActions('a', [])).toBeNull(); + }); + + it('returns null when the value fields are not valid encoded fields', () => { + expect( + getConnectorFieldsFromUserActions('a', [createUserAction({ newValue: 'a', oldValue: 'a' })]) + ).toBeNull(); + }); + + it('returns null when it cannot find the connector id in a non empty array', () => { + expect( + getConnectorFieldsFromUserActions('a', [ + createUserAction({ + newValue: JSON.stringify({ a: '1' }), + oldValue: JSON.stringify({ a: '1' }), + }), + ]) + ).toBeNull(); + }); + + it('returns the fields when it finds the connector id in the new value', () => { + expect( + getConnectorFieldsFromUserActions('a', [ + createUserAction({ + newValue: createEncodedJiraConnector(), + oldValue: JSON.stringify({ a: '1' }), + newValConnectorId: 'a', + }), + ]) + ).toEqual(defaultJiraFields); + }); + + it('returns the fields when it finds the connector id in the new value and the old value is null', () => { + expect( + getConnectorFieldsFromUserActions('a', [ + createUserAction({ + newValue: createEncodedJiraConnector(), + newValConnectorId: 'a', + }), + ]) + ).toEqual(defaultJiraFields); + }); + + it('returns the fields when it finds the connector id in the old value', () => { + const expectedFields = { ...defaultJiraFields, issueType: '5' }; + + expect( + getConnectorFieldsFromUserActions('id-to-find', [ + createUserAction({ + newValue: createEncodedJiraConnector(), + oldValue: createEncodedJiraConnector({ + fields: expectedFields, + }), + newValConnectorId: 'b', + oldValConnectorId: 'id-to-find', + }), + ]) + ).toEqual(expectedFields); + }); + + it('returns the fields when it finds the connector id in the second user action', () => { + const expectedFields = { ...defaultJiraFields, issueType: '5' }; + + expect( + getConnectorFieldsFromUserActions('id-to-find', [ + createUserAction({ + newValue: createEncodedJiraConnector(), + oldValue: createEncodedJiraConnector(), + newValConnectorId: 'b', + oldValConnectorId: 'a', + }), + createUserAction({ + newValue: createEncodedJiraConnector(), + oldValue: createEncodedJiraConnector({ fields: expectedFields }), + newValConnectorId: 'b', + oldValConnectorId: 'id-to-find', + }), + ]) + ).toEqual(expectedFields); + }); + + it('ignores a parse failure and finds the right user action', () => { + expect( + getConnectorFieldsFromUserActions('none', [ + createUserAction({ + newValue: 'b', + newValConnectorId: null, + }), + createUserAction({ + newValue: createEncodedJiraConnector({ + type: ConnectorTypes.none, + name: '', + fields: null, + }), + newValConnectorId: null, + }), + ]) + ).toBeNull(); + }); + + it('returns null when the id matches but the encoded value is null', () => { + expect( + getConnectorFieldsFromUserActions('b', [ + createUserAction({ + newValue: null, + newValConnectorId: 'b', + }), + ]) + ).toBeNull(); + }); + + it('returns null when the action fields is not of length 1', () => { + expect( + getConnectorFieldsFromUserActions('id-to-find', [ + createUserAction({ + newValue: JSON.stringify({ a: '1', fields: { hello: '1' } }), + oldValue: JSON.stringify({ a: '1', fields: { hi: '2' } }), + newValConnectorId: 'b', + oldValConnectorId: 'id-to-find', + actionField: ['connector', 'connector'], + }), + ]) + ).toBeNull(); + }); + + it('matches the none connector the searched for id is none', () => { + expect( + getConnectorFieldsFromUserActions('none', [ + createUserAction({ + newValue: createEncodedJiraConnector({ + type: ConnectorTypes.none, + name: '', + fields: null, + }), + newValConnectorId: null, + }), + ]) + ).toBeNull(); + }); + }); +}); + +function createUserAction(fields: Partial): CaseUserActions { + return { + action: 'update', + actionAt: '', + actionBy: {}, + actionField: ['connector'], + actionId: '', + caseId: '', + commentId: '', + newValConnectorId: null, + oldValConnectorId: null, + newValue: null, + oldValue: null, + ...fields, + }; +} + +function createEncodedJiraConnector(fields?: Partial): string { + return JSON.stringify({ + type: ConnectorTypes.jira, + name: 'name', + fields: defaultJiraFields, + ...fields, + }); +} + +const defaultJiraFields = { + issueType: '1', + parent: null, + priority: null, +}; diff --git a/x-pack/plugins/cases/public/components/edit_connector/helpers.ts b/x-pack/plugins/cases/public/components/edit_connector/helpers.ts index 36eb3f58c8aaf..b97035c458aca 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/helpers.ts +++ b/x-pack/plugins/cases/public/components/edit_connector/helpers.ts @@ -5,23 +5,33 @@ * 2.0. */ +import { ConnectorTypeFields } from '../../../common'; import { CaseUserActions } from '../../containers/types'; +import { parseStringAsConnector } from '../../common/user_actions'; -export const getConnectorFieldsFromUserActions = (id: string, userActions: CaseUserActions[]) => { +export const getConnectorFieldsFromUserActions = ( + id: string, + userActions: CaseUserActions[] +): ConnectorTypeFields['fields'] => { try { for (const action of [...userActions].reverse()) { if (action.actionField.length === 1 && action.actionField[0] === 'connector') { - if (action.oldValue && action.newValue) { - const oldValue = JSON.parse(action.oldValue); - const newValue = JSON.parse(action.newValue); + const parsedNewConnector = parseStringAsConnector( + action.newValConnectorId, + action.newValue + ); - if (newValue.id === id) { - return newValue.fields; - } + if (parsedNewConnector && id === parsedNewConnector.id) { + return parsedNewConnector.fields; + } + + const parsedOldConnector = parseStringAsConnector( + action.oldValConnectorId, + action.oldValue + ); - if (oldValue.id === id) { - return oldValue.fields; - } + if (parsedOldConnector && id === parsedOldConnector.id) { + return parsedOldConnector.fields; } } } diff --git a/x-pack/plugins/cases/public/components/user_action_tree/helpers.test.tsx b/x-pack/plugins/cases/public/components/user_action_tree/helpers.test.tsx index b49a010cff38f..841f0d36bbf17 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/helpers.test.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/helpers.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { CaseStatuses } from '../../../common'; +import { CaseStatuses, ConnectorTypes } from '../../../common'; import { basicPush, getUserAction } from '../../containers/mock'; import { getLabelTitle, @@ -129,7 +129,7 @@ describe('User action tree helpers', () => { `${i18n.PUSHED_NEW_INCIDENT} ${basicPush.connectorName}` ); expect(wrapper.find(`[data-test-subj="pushed-value"]`).first().prop('href')).toEqual( - JSON.parse(action.newValue).external_url + JSON.parse(action.newValue!).external_url ); }); @@ -142,50 +142,74 @@ describe('User action tree helpers', () => { `${i18n.UPDATE_INCIDENT} ${basicPush.connectorName}` ); expect(wrapper.find(`[data-test-subj="pushed-value"]`).first().prop('href')).toEqual( - JSON.parse(action.newValue).external_url + JSON.parse(action.newValue!).external_url ); }); - it('label title generated for update connector - change connector', () => { - const action = { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: 'servicenow-1' }), - newValue: JSON.stringify({ id: 'resilient-2' }), - }; - const result: string | JSX.Element = getConnectorLabelTitle({ - action, - connectors, - }); - - expect(result).toEqual('selected My Connector 2 as incident management system'); - }); - - it('label title generated for update connector - change connector to none', () => { - const action = { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: 'servicenow-1' }), - newValue: JSON.stringify({ id: 'none' }), - }; - const result: string | JSX.Element = getConnectorLabelTitle({ - action, - connectors, + describe('getConnectorLabelTitle', () => { + it('returns an empty string when the encoded old value is null', () => { + const result = getConnectorLabelTitle({ + action: getUserAction(['connector'], 'update', { oldValue: null }), + connectors, + }); + + expect(result).toEqual(''); + }); + + it('returns an empty string when the encoded new value is null', () => { + const result = getConnectorLabelTitle({ + action: getUserAction(['connector'], 'update', { newValue: null }), + connectors, + }); + + expect(result).toEqual(''); + }); + + it('returns the change connector label', () => { + const result: string | JSX.Element = getConnectorLabelTitle({ + action: getUserAction(['connector'], 'update', { + oldValue: JSON.stringify({ + type: ConnectorTypes.serviceNowITSM, + name: 'a', + fields: null, + }), + oldValConnectorId: 'servicenow-1', + newValue: JSON.stringify({ type: ConnectorTypes.resilient, name: 'a', fields: null }), + newValConnectorId: 'resilient-2', + }), + connectors, + }); + + expect(result).toEqual('selected My Connector 2 as incident management system'); + }); + + it('returns the removed connector label', () => { + const result: string | JSX.Element = getConnectorLabelTitle({ + action: getUserAction(['connector'], 'update', { + oldValue: JSON.stringify({ type: ConnectorTypes.serviceNowITSM, name: '', fields: null }), + oldValConnectorId: 'servicenow-1', + newValue: JSON.stringify({ type: ConnectorTypes.none, name: '', fields: null }), + newValConnectorId: 'none', + }), + connectors, + }); + + expect(result).toEqual('removed external incident management system'); + }); + + it('returns the connector fields changed label', () => { + const result: string | JSX.Element = getConnectorLabelTitle({ + action: getUserAction(['connector'], 'update', { + oldValue: JSON.stringify({ type: ConnectorTypes.serviceNowITSM, name: '', fields: null }), + oldValConnectorId: 'servicenow-1', + newValue: JSON.stringify({ type: ConnectorTypes.serviceNowITSM, name: '', fields: null }), + newValConnectorId: 'servicenow-1', + }), + connectors, + }); + + expect(result).toEqual('changed connector field'); }); - - expect(result).toEqual('removed external incident management system'); - }); - - it('label title generated for update connector - field change', () => { - const action = { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: 'servicenow-1' }), - newValue: JSON.stringify({ id: 'servicenow-1' }), - }; - const result: string | JSX.Element = getConnectorLabelTitle({ - action, - connectors, - }); - - expect(result).toEqual('changed connector field'); }); describe('toStringArray', () => { diff --git a/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx b/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx index 744b14926b358..2eb44f91190c6 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx @@ -23,10 +23,11 @@ import { CommentType, Comment, CommentRequestActionsType, + noneConnectorId, } from '../../../common'; import { CaseUserActions } from '../../containers/types'; import { CaseServices } from '../../containers/use_get_case_user_actions'; -import { parseString } from '../../containers/utils'; +import { parseStringAsConnector, parseStringAsExternalService } from '../../common/user_actions'; import { Tags } from '../tag_list/tags'; import { UserActionUsernameWithAvatar } from './user_action_username_with_avatar'; import { UserActionTimestamp } from './user_action_timestamp'; @@ -97,23 +98,27 @@ export const getConnectorLabelTitle = ({ action: CaseUserActions; connectors: ActionConnector[]; }) => { - const oldValue = parseString(`${action.oldValue}`); - const newValue = parseString(`${action.newValue}`); + const oldConnector = parseStringAsConnector(action.oldValConnectorId, action.oldValue); + const newConnector = parseStringAsConnector(action.newValConnectorId, action.newValue); - if (oldValue === null || newValue === null) { + if (!oldConnector || !newConnector) { return ''; } - // Connector changed - if (oldValue.id !== newValue.id) { - const newConnector = connectors.find((c) => c.id === newValue.id); - return newValue.id != null && newValue.id !== 'none' && newConnector != null - ? i18n.SELECTED_THIRD_PARTY(newConnector.name) - : i18n.REMOVED_THIRD_PARTY; - } else { - // Field changed + // if the ids are the same, assume we just changed the fields + if (oldConnector.id === newConnector.id) { return i18n.CHANGED_CONNECTOR_FIELD; } + + // ids are not the same so check and see if the id is a valid connector and then return its name + // if the connector id is the none connector value then it must have been removed + const newConnectorActionInfo = connectors.find((c) => c.id === newConnector.id); + if (newConnector.id !== noneConnectorId && newConnectorActionInfo != null) { + return i18n.SELECTED_THIRD_PARTY(newConnectorActionInfo.name); + } + + // it wasn't a valid connector or it was the none connector, so it must have been removed + return i18n.REMOVED_THIRD_PARTY; }; const getTagsLabelTitle = (action: CaseUserActions) => { @@ -133,7 +138,8 @@ const getTagsLabelTitle = (action: CaseUserActions) => { }; export const getPushedServiceLabelTitle = (action: CaseUserActions, firstPush: boolean) => { - const pushedVal = JSON.parse(action.newValue ?? '') as CaseFullExternalService; + const externalService = parseStringAsExternalService(action.newValConnectorId, action.newValue); + return ( {`${firstPush ? i18n.PUSHED_NEW_INCIDENT : i18n.UPDATE_INCIDENT} ${ - pushedVal?.connector_name + externalService?.connector_name }`} - - {pushedVal?.external_title} + + {externalService?.external_title} @@ -157,20 +163,19 @@ export const getPushedServiceLabelTitle = (action: CaseUserActions, firstPush: b export const getPushInfo = ( caseServices: CaseServices, - // a JSON parse failure will result in null for parsedValue - parsedValue: { connector_id: string | null; connector_name: string } | null, + externalService: CaseFullExternalService | undefined, index: number ) => - parsedValue != null && parsedValue.connector_id != null + externalService != null && externalService.connector_id != null ? { - firstPush: caseServices[parsedValue.connector_id]?.firstPushIndex === index, - parsedConnectorId: parsedValue.connector_id, - parsedConnectorName: parsedValue.connector_name, + firstPush: caseServices[externalService.connector_id]?.firstPushIndex === index, + parsedConnectorId: externalService.connector_id, + parsedConnectorName: externalService.connector_name, } : { firstPush: false, - parsedConnectorId: 'none', - parsedConnectorName: 'none', + parsedConnectorId: noneConnectorId, + parsedConnectorName: noneConnectorId, }; const getUpdateActionIcon = (actionField: string): string => { diff --git a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx index 784817229caf9..7ea415324194c 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx @@ -35,7 +35,7 @@ import { Ecs, } from '../../../common'; import { CaseServices } from '../../containers/use_get_case_user_actions'; -import { parseString } from '../../containers/utils'; +import { parseStringAsExternalService } from '../../common/user_actions'; import { OnUpdateFields } from '../case_view'; import { getConnectorLabelTitle, @@ -512,10 +512,14 @@ export const UserActionTree = React.memo( // Pushed information if (action.actionField.length === 1 && action.actionField[0] === 'pushed') { - const parsedValue = parseString(`${action.newValue}`); + const parsedExternalService = parseStringAsExternalService( + action.newValConnectorId, + action.newValue + ); + const { firstPush, parsedConnectorId, parsedConnectorName } = getPushInfo( caseServices, - parsedValue, + parsedExternalService, index ); diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index c955bb34240e2..fcd564969d486 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -9,6 +9,7 @@ import { ActionLicense, AllCases, Case, CasesStatus, CaseUserActions, Comment } import { AssociationType, + CaseUserActionConnector, CaseResponse, CasesFindResponse, CasesResponse, @@ -19,6 +20,9 @@ import { CommentResponse, CommentType, ConnectorTypes, + isCreateConnector, + isPush, + isUpdateConnector, SECURITY_SOLUTION_OWNER, UserAction, UserActionField, @@ -240,7 +244,9 @@ export const pushedCase: Case = { const basicAction = { actionAt: basicCreatedAt, actionBy: elasticUser, + oldValConnectorId: null, oldValue: null, + newValConnectorId: null, newValue: 'what a cool value', caseId: basicCaseId, commentId: null, @@ -308,12 +314,7 @@ export const basicCaseSnake: CaseResponse = { closed_at: null, closed_by: null, comments: [basicCommentSnake], - connector: { - id: 'none', - name: 'My Connector', - type: ConnectorTypes.none, - fields: null, - }, + connector: { id: 'none', name: 'My Connector', type: ConnectorTypes.none, fields: null }, created_at: basicCreatedAt, created_by: elasticUserSnake, external_service: null, @@ -328,8 +329,8 @@ export const casesStatusSnake: CasesStatusResponse = { count_open_cases: 20, }; +export const pushConnectorId = '123'; export const pushSnake = { - connector_id: '123', connector_name: 'connector name', external_id: 'external_id', external_title: 'external title', @@ -350,7 +351,7 @@ export const pushedCaseSnake = { type: ConnectorTypes.jira, fields: null, }, - external_service: basicPushSnake, + external_service: { ...basicPushSnake, connector_id: pushConnectorId }, }; export const reporters: string[] = ['alexis', 'kim', 'maria', 'steph']; @@ -385,17 +386,20 @@ const basicActionSnake = { comment_id: null, owner: SECURITY_SOLUTION_OWNER, }; -export const getUserActionSnake = (af: UserActionField, a: UserAction) => ({ - ...basicActionSnake, - action_id: `${af[0]}-${a}`, - action_field: af, - action: a, - comment_id: af[0] === 'comment' ? basicCommentId : null, - new_value: - a === 'push-to-service' && af[0] === 'pushed' - ? JSON.stringify(basicPushSnake) - : basicAction.newValue, -}); +export const getUserActionSnake = (af: UserActionField, a: UserAction) => { + const isPushToService = a === 'push-to-service' && af[0] === 'pushed'; + + return { + ...basicActionSnake, + action_id: `${af[0]}-${a}`, + action_field: af, + action: a, + comment_id: af[0] === 'comment' ? basicCommentId : null, + new_value: isPushToService ? JSON.stringify(basicPushSnake) : basicAction.newValue, + new_val_connector_id: isPushToService ? pushConnectorId : null, + old_val_connector_id: null, + }; +}; export const caseUserActionsSnake: CaseUserActionsResponse = [ getUserActionSnake(['description'], 'create'), @@ -405,17 +409,76 @@ export const caseUserActionsSnake: CaseUserActionsResponse = [ // user actions -export const getUserAction = (af: UserActionField, a: UserAction) => ({ - ...basicAction, - actionId: `${af[0]}-${a}`, - actionField: af, - action: a, - commentId: af[0] === 'comment' ? basicCommentId : null, - newValue: - a === 'push-to-service' && af[0] === 'pushed' - ? JSON.stringify(basicPushSnake) - : basicAction.newValue, -}); +export const getUserAction = ( + af: UserActionField, + a: UserAction, + overrides?: Partial +): CaseUserActions => { + return { + ...basicAction, + actionId: `${af[0]}-${a}`, + actionField: af, + action: a, + commentId: af[0] === 'comment' ? basicCommentId : null, + ...getValues(a, af, overrides), + }; +}; + +const getValues = ( + userAction: UserAction, + actionFields: UserActionField, + overrides?: Partial +): Partial => { + if (isCreateConnector(userAction, actionFields)) { + return { + newValue: + overrides?.newValue === undefined ? JSON.stringify(basicCaseSnake) : overrides.newValue, + newValConnectorId: overrides?.newValConnectorId ?? null, + oldValue: null, + oldValConnectorId: null, + }; + } else if (isUpdateConnector(userAction, actionFields)) { + return { + newValue: + overrides?.newValue === undefined + ? JSON.stringify({ name: 'My Connector', type: ConnectorTypes.none, fields: null }) + : overrides.newValue, + newValConnectorId: overrides?.newValConnectorId ?? null, + oldValue: + overrides?.oldValue === undefined + ? JSON.stringify({ name: 'My Connector2', type: ConnectorTypes.none, fields: null }) + : overrides.oldValue, + oldValConnectorId: overrides?.oldValConnectorId ?? null, + }; + } else if (isPush(userAction, actionFields)) { + return { + newValue: + overrides?.newValue === undefined ? JSON.stringify(basicPushSnake) : overrides?.newValue, + newValConnectorId: + overrides?.newValConnectorId === undefined ? pushConnectorId : overrides.newValConnectorId, + oldValue: overrides?.oldValue ?? null, + oldValConnectorId: overrides?.oldValConnectorId ?? null, + }; + } else { + return { + newValue: overrides?.newValue === undefined ? basicAction.newValue : overrides.newValue, + newValConnectorId: overrides?.newValConnectorId ?? null, + oldValue: overrides?.oldValue ?? null, + oldValConnectorId: overrides?.oldValConnectorId ?? null, + }; + } +}; + +export const getJiraConnectorWithoutId = (overrides?: Partial) => { + return JSON.stringify({ + name: 'jira1', + type: ConnectorTypes.jira, + ...jiraFields, + ...overrides, + }); +}; + +export const jiraFields = { fields: { issueType: '10006', priority: null, parent: null } }; export const getAlertUserAction = () => ({ ...basicAction, diff --git a/x-pack/plugins/cases/public/containers/use_get_case_user_actions.test.tsx b/x-pack/plugins/cases/public/containers/use_get_case_user_actions.test.tsx index 62b4cf92434cd..e7e46fa46c7cc 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_user_actions.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_user_actions.test.tsx @@ -18,7 +18,9 @@ import { basicPushSnake, caseUserActions, elasticUser, + getJiraConnectorWithoutId, getUserAction, + jiraFields, } from './mock'; import * as api from './api'; @@ -299,15 +301,14 @@ describe('useGetCaseUserActions', () => { const pushAction123 = getUserAction(['pushed'], 'push-to-service'); const push456 = { ...basicPushSnake, - connector_id: '456', connector_name: 'other connector name', external_id: 'other_external_id', }; - const pushAction456 = { - ...getUserAction(['pushed'], 'push-to-service'), + const pushAction456 = getUserAction(['pushed'], 'push-to-service', { newValue: JSON.stringify(push456), - }; + newValConnectorId: '456', + }); const userActions = [ ...caseUserActions, @@ -346,15 +347,14 @@ describe('useGetCaseUserActions', () => { const pushAction123 = getUserAction(['pushed'], 'push-to-service'); const push456 = { ...basicPushSnake, - connector_id: '456', connector_name: 'other connector name', external_id: 'other_external_id', }; - const pushAction456 = { - ...getUserAction(['pushed'], 'push-to-service'), + const pushAction456 = getUserAction(['pushed'], 'push-to-service', { newValue: JSON.stringify(push456), - }; + newValConnectorId: '456', + }); const userActions = [ ...caseUserActions, @@ -392,11 +392,7 @@ describe('useGetCaseUserActions', () => { const userActions = [ ...caseUserActions, getUserAction(['pushed'], 'push-to-service'), - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - }, + createUpdateConnectorFields123HighPriorityUserAction(), ]; const result = getPushedInfo(userActions, '123'); @@ -418,11 +414,7 @@ describe('useGetCaseUserActions', () => { const userActions = [ ...caseUserActions, getUserAction(['pushed'], 'push-to-service'), - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), - newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - }, + createChangeConnector123To456UserAction(), ]; const result = getPushedInfo(userActions, '123'); @@ -444,16 +436,8 @@ describe('useGetCaseUserActions', () => { const userActions = [ ...caseUserActions, getUserAction(['pushed'], 'push-to-service'), - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), - newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - }, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), - }, + createChangeConnector123To456UserAction(), + createChangeConnector456To123UserAction(), ]; const result = getPushedInfo(userActions, '123'); @@ -474,22 +458,10 @@ describe('useGetCaseUserActions', () => { it('Change fields and connector after push - hasDataToPush: true', () => { const userActions = [ ...caseUserActions, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - }, + createUpdateConnectorFields123HighPriorityUserAction(), getUserAction(['pushed'], 'push-to-service'), - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - }, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'Low' } }), - }, + createChangeConnector123HighPriorityTo456UserAction(), + createChangeConnector456To123PriorityLowUserAction(), ]; const result = getPushedInfo(userActions, '123'); @@ -510,22 +482,10 @@ describe('useGetCaseUserActions', () => { it('Change only connector after push - hasDataToPush: false', () => { const userActions = [ ...caseUserActions, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - }, + createUpdateConnectorFields123HighPriorityUserAction(), getUserAction(['pushed'], 'push-to-service'), - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - }, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - }, + createChangeConnector123HighPriorityTo456UserAction(), + createChangeConnector456To123HighPriorityUserAction(), ]; const result = getPushedInfo(userActions, '123'); @@ -547,45 +507,24 @@ describe('useGetCaseUserActions', () => { const pushAction123 = getUserAction(['pushed'], 'push-to-service'); const push456 = { ...basicPushSnake, - connector_id: '456', connector_name: 'other connector name', external_id: 'other_external_id', }; - const pushAction456 = { - ...getUserAction(['pushed'], 'push-to-service'), + const pushAction456 = getUserAction(['pushed'], 'push-to-service', { newValue: JSON.stringify(push456), - }; + newValConnectorId: '456', + }); const userActions = [ ...caseUserActions, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - }, + createUpdateConnectorFields123HighPriorityUserAction(), pushAction123, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - }, + createChangeConnector123HighPriorityTo456UserAction(), pushAction456, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'Low' } }), - }, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'Low' } }), - newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - }, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'Low' } }), - }, + createChangeConnector456To123PriorityLowUserAction(), + createChangeConnector123LowPriorityTo456UserAction(), + createChangeConnector456To123PriorityLowUserAction(), ]; const result = getPushedInfo(userActions, '123'); @@ -617,34 +556,22 @@ describe('useGetCaseUserActions', () => { const pushAction123 = getUserAction(['pushed'], 'push-to-service'); const push456 = { ...basicPushSnake, - connector_id: '456', connector_name: 'other connector name', external_id: 'other_external_id', }; - const pushAction456 = { - ...getUserAction(['pushed'], 'push-to-service'), + const pushAction456 = getUserAction(['pushed'], 'push-to-service', { + newValConnectorId: '456', newValue: JSON.stringify(push456), - }; + }); + const userActions = [ ...caseUserActions, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - }, + createUpdateConnectorFields123HighPriorityUserAction(), pushAction123, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - }, + createChangeConnector123HighPriorityTo456UserAction(), pushAction456, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - }, + createChangeConnector456To123HighPriorityUserAction(), ]; const result = getPushedInfo(userActions, '123'); @@ -675,22 +602,10 @@ describe('useGetCaseUserActions', () => { it('Changing other connectors fields does not count as an update', () => { const userActions = [ ...caseUserActions, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: null } }), - newValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - }, + createUpdateConnectorFields123HighPriorityUserAction(), getUserAction(['pushed'], 'push-to-service'), - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '123', fields: { issueType: '10006', priority: 'High' } }), - newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - }, - { - ...getUserAction(['connector'], 'update'), - oldValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '6' } }), - newValue: JSON.stringify({ id: '456', fields: { issueTypes: ['10'], severity: '3' } }), - }, + createChangeConnector123HighPriorityTo456UserAction(), + createUpdateConnectorFields456HighPriorityUserAction(), ]; const result = getPushedInfo(userActions, '123'); @@ -709,3 +624,83 @@ describe('useGetCaseUserActions', () => { }); }); }); + +const jira123HighPriorityFields = { + fields: { ...jiraFields.fields, priority: 'High' }, +}; + +const jira123LowPriorityFields = { + fields: { ...jiraFields.fields, priority: 'Low' }, +}; + +const jira456Fields = { + fields: { issueType: '10', parent: null, priority: null }, +}; + +const jira456HighPriorityFields = { + fields: { ...jira456Fields.fields, priority: 'High' }, +}; + +const createUpdateConnectorFields123HighPriorityUserAction = () => + getUserAction(['connector'], 'update', { + oldValue: getJiraConnectorWithoutId(), + newValue: getJiraConnectorWithoutId(jira123HighPriorityFields), + oldValConnectorId: '123', + newValConnectorId: '123', + }); + +const createUpdateConnectorFields456HighPriorityUserAction = () => + getUserAction(['connector'], 'update', { + oldValue: getJiraConnectorWithoutId(jira456Fields), + newValue: getJiraConnectorWithoutId(jira456HighPriorityFields), + oldValConnectorId: '456', + newValConnectorId: '456', + }); + +const createChangeConnector123HighPriorityTo456UserAction = () => + getUserAction(['connector'], 'update', { + oldValue: getJiraConnectorWithoutId(jira123HighPriorityFields), + oldValConnectorId: '123', + newValue: getJiraConnectorWithoutId(jira456Fields), + newValConnectorId: '456', + }); + +const createChangeConnector123To456UserAction = () => + getUserAction(['connector'], 'update', { + oldValue: getJiraConnectorWithoutId(), + oldValConnectorId: '123', + newValue: getJiraConnectorWithoutId(jira456Fields), + newValConnectorId: '456', + }); + +const createChangeConnector123LowPriorityTo456UserAction = () => + getUserAction(['connector'], 'update', { + oldValue: getJiraConnectorWithoutId(jira123LowPriorityFields), + oldValConnectorId: '123', + newValue: getJiraConnectorWithoutId(jira456Fields), + newValConnectorId: '456', + }); + +const createChangeConnector456To123UserAction = () => + getUserAction(['connector'], 'update', { + oldValue: getJiraConnectorWithoutId(jira456Fields), + oldValConnectorId: '456', + newValue: getJiraConnectorWithoutId(), + newValConnectorId: '123', + }); + +const createChangeConnector456To123HighPriorityUserAction = () => + getUserAction(['connector'], 'update', { + oldValue: getJiraConnectorWithoutId(jira456Fields), + oldValConnectorId: '456', + newValue: getJiraConnectorWithoutId(jira123HighPriorityFields), + newValConnectorId: '123', + }); + +const createChangeConnector456To123PriorityLowUserAction = () => + getUserAction(['connector'], 'update', { + oldValue: getJiraConnectorWithoutId(jira456Fields), + oldValConnectorId: '456', + newValue: getJiraConnectorWithoutId(jira123LowPriorityFields), + newValConnectorId: '123', + }); diff --git a/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx b/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx index e481519ba19a3..36d600c3f1c9d 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx @@ -18,7 +18,8 @@ import { } from '../../common'; import { getCaseUserActions, getSubCaseUserActions } from './api'; import * as i18n from './translations'; -import { convertToCamelCase, parseString } from './utils'; +import { convertToCamelCase } from './utils'; +import { parseStringAsConnector, parseStringAsExternalService } from '../common/user_actions'; import { useToasts } from '../common/lib/kibana'; export interface CaseService extends CaseExternalService { @@ -58,8 +59,24 @@ export interface UseGetCaseUserActions extends CaseUserActionsState { ) => Promise; } -const getExternalService = (value: string): CaseExternalService | null => - convertToCamelCase(parseString(`${value}`)); +const unknownExternalServiceConnectorId = 'unknown'; + +const getExternalService = ( + connectorId: string | null, + encodedValue: string | null +): CaseExternalService | null => { + const decodedValue = parseStringAsExternalService(connectorId, encodedValue); + + if (decodedValue == null) { + return null; + } + return { + ...convertToCamelCase(decodedValue), + // if in the rare case that the connector id is null we'll set it to unknown if we need to reference it in the UI + // anywhere. The id would only ever be null if a migration failed or some logic error within the backend occurred + connectorId: connectorId ?? unknownExternalServiceConnectorId, + }; +}; const groupConnectorFields = ( userActions: CaseUserActions[] @@ -69,22 +86,26 @@ const groupConnectorFields = ( return acc; } - const oldValue = parseString(`${mua.oldValue}`); - const newValue = parseString(`${mua.newValue}`); + const oldConnector = parseStringAsConnector(mua.oldValConnectorId, mua.oldValue); + const newConnector = parseStringAsConnector(mua.newValConnectorId, mua.newValue); - if (oldValue == null || newValue == null) { + if (!oldConnector || !newConnector) { return acc; } return { ...acc, - [oldValue.id]: [ - ...(acc[oldValue.id] || []), - ...(oldValue.id === newValue.id ? [oldValue.fields, newValue.fields] : [oldValue.fields]), + [oldConnector.id]: [ + ...(acc[oldConnector.id] || []), + ...(oldConnector.id === newConnector.id + ? [oldConnector.fields, newConnector.fields] + : [oldConnector.fields]), ], - [newValue.id]: [ - ...(acc[newValue.id] || []), - ...(oldValue.id === newValue.id ? [oldValue.fields, newValue.fields] : [newValue.fields]), + [newConnector.id]: [ + ...(acc[newConnector.id] || []), + ...(oldConnector.id === newConnector.id + ? [oldConnector.fields, newConnector.fields] + : [newConnector.fields]), ], }; }, {} as Record>); @@ -137,9 +158,7 @@ export const getPushedInfo = ( const hasDataToPushForConnector = (connectorId: string): boolean => { const caseUserActionsReversed = [...caseUserActions].reverse(); const lastPushOfConnectorReversedIndex = caseUserActionsReversed.findIndex( - (mua) => - mua.action === 'push-to-service' && - getExternalService(`${mua.newValue}`)?.connectorId === connectorId + (mua) => mua.action === 'push-to-service' && mua.newValConnectorId === connectorId ); if (lastPushOfConnectorReversedIndex === -1) { @@ -190,7 +209,7 @@ export const getPushedInfo = ( return acc; } - const externalService = getExternalService(`${cua.newValue}`); + const externalService = getExternalService(cua.newValConnectorId, cua.newValue); if (externalService === null) { return acc; } diff --git a/x-pack/plugins/cases/public/containers/utils.ts b/x-pack/plugins/cases/public/containers/utils.ts index de67b1cfbd6fa..b0cc0c72fee78 100644 --- a/x-pack/plugins/cases/public/containers/utils.ts +++ b/x-pack/plugins/cases/public/containers/utils.ts @@ -36,14 +36,6 @@ import * as i18n from './translations'; export const getTypedPayload = (a: unknown): T => a as T; -export const parseString = (params: string) => { - try { - return JSON.parse(params); - } catch { - return null; - } -}; - export const convertArrayToCamelCase = (arrayOfSnakes: unknown[]): unknown[] => arrayOfSnakes.reduce((acc: unknown[], value) => { if (isArray(value)) { diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts index 507405d58cef1..b84a6bd84c43b 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -106,7 +106,7 @@ async function getSubCase({ caseId, subCaseId: newSubCase.id, fields: ['status', 'sub_case'], - newValue: JSON.stringify({ status: newSubCase.attributes.status }), + newValue: { status: newSubCase.attributes.status }, owner: newSubCase.attributes.owner, }), ], @@ -220,7 +220,7 @@ const addGeneratedAlerts = async ( subCaseId: updatedCase.subCaseId, commentId: newComment.id, fields: ['comment'], - newValue: JSON.stringify(query), + newValue: query, owner: newComment.attributes.owner, }), ], @@ -408,7 +408,7 @@ export const addComment = async ( subCaseId: updatedCase.subCaseId, commentId: newComment.id, fields: ['comment'], - newValue: JSON.stringify(query), + newValue: query, owner: newComment.attributes.owner, }), ], diff --git a/x-pack/plugins/cases/server/client/attachments/update.ts b/x-pack/plugins/cases/server/client/attachments/update.ts index 9816efd9a8452..b5e9e6c372355 100644 --- a/x-pack/plugins/cases/server/client/attachments/update.ts +++ b/x-pack/plugins/cases/server/client/attachments/update.ts @@ -17,6 +17,7 @@ import { SUB_CASE_SAVED_OBJECT, CaseResponse, CommentPatchRequest, + CommentRequest, } from '../../../common'; import { AttachmentService, CasesService } from '../../services'; import { CasesClientArgs } from '..'; @@ -193,12 +194,12 @@ export async function update( subCaseId: subCaseID, commentId: updatedComment.id, fields: ['comment'], - newValue: JSON.stringify(queryRestAttributes), - oldValue: JSON.stringify( + // casting because typescript is complaining that it's not a Record even though it is + newValue: queryRestAttributes as CommentRequest, + oldValue: // We are interested only in ContextBasicRt attributes // myComment.attribute contains also CommentAttributesBasicRt attributes - pick(Object.keys(queryRestAttributes), myComment.attributes) - ), + pick(Object.keys(queryRestAttributes), myComment.attributes), owner: myComment.attributes.owner, }), ], diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 887990fef8938..488bc523f7796 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -106,7 +106,7 @@ export const create = async ( actionBy: { username, full_name, email }, caseId: newCase.id, fields: ['description', 'status', 'tags', 'title', 'connector', 'settings', OWNER_FIELD], - newValue: JSON.stringify(query), + newValue: query, owner: newCase.attributes.owner, }), ], diff --git a/x-pack/plugins/cases/server/client/cases/delete.ts b/x-pack/plugins/cases/server/client/cases/delete.ts index 80a687a0e72f8..4333535f17a24 100644 --- a/x-pack/plugins/cases/server/client/cases/delete.ts +++ b/x-pack/plugins/cases/server/client/cases/delete.ts @@ -168,7 +168,7 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P 'settings', OWNER_FIELD, 'comment', - ...(ENABLE_CASE_CONNECTOR ? ['sub_case'] : []), + ...(ENABLE_CASE_CONNECTOR ? ['sub_case' as const] : []), ], owner: caseInfo.attributes.owner, }) diff --git a/x-pack/plugins/cases/server/client/cases/mock.ts b/x-pack/plugins/cases/server/client/cases/mock.ts index 313d6cd12a6db..22520cea11014 100644 --- a/x-pack/plugins/cases/server/client/cases/mock.ts +++ b/x-pack/plugins/cases/server/client/cases/mock.ts @@ -231,8 +231,10 @@ export const userActions: CaseUserActionsResponse = [ username: 'elastic', }, new_value: - '{"title":"Case SIR","tags":["sir"],"description":"testing sir","connector":{"id":"456","name":"ServiceNow SN","type":".servicenow-sir","fields":{"category":"Denial of Service","destIp":true,"malwareHash":true,"malwareUrl":true,"priority":"2","sourceIp":true,"subcategory":"45"}},"settings":{"syncAlerts":true}}', + '{"title":"Case SIR","tags":["sir"],"description":"testing sir","connector":{"name":"ServiceNow SN","type":".servicenow-sir","fields":{"category":"Denial of Service","destIp":true,"malwareHash":true,"malwareUrl":true,"priority":"2","sourceIp":true,"subcategory":"45"}},"settings":{"syncAlerts":true}}', + new_val_connector_id: '456', old_value: null, + old_val_connector_id: null, action_id: 'fd830c60-6646-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: null, @@ -248,7 +250,9 @@ export const userActions: CaseUserActionsResponse = [ username: 'elastic', }, new_value: - '{"pushed_at":"2021-02-03T17:41:26.108Z","pushed_by":{"username":"elastic","full_name":"Elastic","email":"elastic@elastic.co"},"connector_id":"456","connector_name":"ServiceNow SN","external_id":"external-id","external_title":"SIR0010037","external_url":"https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id"}', + '{"pushed_at":"2021-02-03T17:41:26.108Z","pushed_by":{"username":"elastic","full_name":"Elastic","email":"elastic@elastic.co"},"connector_name":"ServiceNow SN","external_id":"external-id","external_title":"SIR0010037","external_url":"https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id"}', + new_val_connector_id: '456', + old_val_connector_id: null, old_value: null, action_id: '0a801750-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', @@ -265,6 +269,8 @@ export const userActions: CaseUserActionsResponse = [ username: 'elastic', }, new_value: '{"type":"alert","alertId":"alert-id-1","index":".siem-signals-default-000008"}', + new_val_connector_id: null, + old_val_connector_id: null, old_value: null, action_id: '7373eb60-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', @@ -282,6 +288,8 @@ export const userActions: CaseUserActionsResponse = [ }, new_value: '{"type":"alert","alertId":"alert-id-2","index":".siem-signals-default-000008"}', old_value: null, + new_val_connector_id: null, + old_val_connector_id: null, action_id: '7abc6410-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: 'comment-alert-2', @@ -297,8 +305,10 @@ export const userActions: CaseUserActionsResponse = [ username: 'elastic', }, new_value: - '{"pushed_at":"2021-02-03T17:45:29.400Z","pushed_by":{"username":"elastic","full_name":"Elastic","email":"elastic@elastic.co"},"connector_id":"456","connector_name":"ServiceNow SN","external_id":"external-id","external_title":"SIR0010037","external_url":"https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id"}', + '{"pushed_at":"2021-02-03T17:45:29.400Z","pushed_by":{"username":"elastic","full_name":"Elastic","email":"elastic@elastic.co"},"connector_name":"ServiceNow SN","external_id":"external-id","external_title":"SIR0010037","external_url":"https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id"}', + new_val_connector_id: '456', old_value: null, + old_val_connector_id: null, action_id: '9b91d8f0-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: null, @@ -315,6 +325,8 @@ export const userActions: CaseUserActionsResponse = [ }, new_value: '{"comment":"a comment!","type":"user"}', old_value: null, + new_val_connector_id: null, + old_val_connector_id: null, action_id: '0818e5e0-6648-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: 'comment-user-1', diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 3048cf01bb3ba..1b090a653546d 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -241,7 +241,7 @@ export const push = async ( actionBy: { username, full_name, email }, caseId, fields: ['pushed'], - newValue: JSON.stringify(externalService), + newValue: externalService, owner: myCase.attributes.owner, }), ], diff --git a/x-pack/plugins/cases/server/client/cases/utils.test.ts b/x-pack/plugins/cases/server/client/cases/utils.test.ts index d7c45d3e1e9ae..315e9966d347b 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.test.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.test.ts @@ -799,8 +799,10 @@ describe('utils', () => { username: 'elastic', }, new_value: - // The connector id is 123 - '{"pushed_at":"2021-02-03T17:45:29.400Z","pushed_by":{"username":"elastic","full_name":"Elastic","email":"elastic@elastic.co"},"connector_id":"123","connector_name":"ServiceNow SN","external_id":"external-id","external_title":"SIR0010037","external_url":"https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id"}', + // The connector id is 123 + '{"pushed_at":"2021-02-03T17:45:29.400Z","pushed_by":{"username":"elastic","full_name":"Elastic","email":"elastic@elastic.co"},"connector_name":"ServiceNow SN","external_id":"external-id","external_title":"SIR0010037","external_url":"https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id"}', + new_val_connector_id: '123', + old_val_connector_id: null, old_value: null, action_id: '9b91d8f0-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', diff --git a/x-pack/plugins/cases/server/client/cases/utils.ts b/x-pack/plugins/cases/server/client/cases/utils.ts index 359ad4b41ead0..f5cf2fe4b3f51 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.ts @@ -20,6 +20,8 @@ import { CommentRequestUserType, CommentRequestAlertType, CommentRequestActionsType, + CaseUserActionResponse, + isPush, } from '../../../common'; import { ActionsClient } from '../../../../actions/server'; import { CasesClientGetAlertsResponse } from '../../client/alerts/types'; @@ -55,22 +57,36 @@ export const getLatestPushInfo = ( userActions: CaseUserActionsResponse ): { index: number; pushedInfo: CaseFullExternalService } | null => { for (const [index, action] of [...userActions].reverse().entries()) { - if (action.action === 'push-to-service' && action.new_value) + if ( + isPush(action.action, action.action_field) && + isValidNewValue(action) && + connectorId === action.new_val_connector_id + ) { try { const pushedInfo = JSON.parse(action.new_value); - if (pushedInfo.connector_id === connectorId) { - // We returned the index of the element in the userActions array. - // As we traverse the userActions in reverse we need to calculate the index of a normal traversal - return { index: userActions.length - index - 1, pushedInfo }; - } + // We returned the index of the element in the userActions array. + // As we traverse the userActions in reverse we need to calculate the index of a normal traversal + return { + index: userActions.length - index - 1, + pushedInfo: { ...pushedInfo, connector_id: connectorId }, + }; } catch (e) { - // Silence JSON parse errors + // ignore parse failures and check the next user action } + } } return null; }; +type NonNullNewValueAction = Omit & { + new_value: string; + new_val_connector_id: string; +}; + +const isValidNewValue = (userAction: CaseUserActionResponse): userAction is NonNullNewValueAction => + userAction.new_val_connector_id != null && userAction.new_value != null; + const getCommentContent = (comment: CommentResponse): string => { if (comment.type === CommentType.user) { return comment.comment; diff --git a/x-pack/plugins/cases/server/client/user_actions/get.test.ts b/x-pack/plugins/cases/server/client/user_actions/get.test.ts new file mode 100644 index 0000000000000..302e069cde4d1 --- /dev/null +++ b/x-pack/plugins/cases/server/client/user_actions/get.test.ts @@ -0,0 +1,106 @@ +/* + * 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 { CaseUserActionResponse, SUB_CASE_SAVED_OBJECT } from '../../../common'; +import { SUB_CASE_REF_NAME } from '../../common'; +import { extractAttributesWithoutSubCases } from './get'; + +describe('get', () => { + describe('extractAttributesWithoutSubCases', () => { + it('returns an empty array when given an empty array', () => { + expect( + extractAttributesWithoutSubCases({ ...getFindResponseFields(), saved_objects: [] }) + ).toEqual([]); + }); + + it('filters out saved objects with a sub case reference', () => { + expect( + extractAttributesWithoutSubCases({ + ...getFindResponseFields(), + saved_objects: [ + { + type: 'a', + references: [{ name: SUB_CASE_REF_NAME, type: SUB_CASE_SAVED_OBJECT, id: '1' }], + id: 'b', + score: 0, + attributes: {} as CaseUserActionResponse, + }, + ], + }) + ).toEqual([]); + }); + + it('filters out saved objects with a sub case reference with other references', () => { + expect( + extractAttributesWithoutSubCases({ + ...getFindResponseFields(), + saved_objects: [ + { + type: 'a', + references: [ + { name: SUB_CASE_REF_NAME, type: SUB_CASE_SAVED_OBJECT, id: '1' }, + { name: 'a', type: 'b', id: '5' }, + ], + id: 'b', + score: 0, + attributes: {} as CaseUserActionResponse, + }, + ], + }) + ).toEqual([]); + }); + + it('keeps saved objects that do not have a sub case reference', () => { + expect( + extractAttributesWithoutSubCases({ + ...getFindResponseFields(), + saved_objects: [ + { + type: 'a', + references: [ + { name: SUB_CASE_REF_NAME, type: 'awesome', id: '1' }, + { name: 'a', type: 'b', id: '5' }, + ], + id: 'b', + score: 0, + attributes: { field: '1' } as unknown as CaseUserActionResponse, + }, + ], + }) + ).toEqual([{ field: '1' }]); + }); + + it('filters multiple saved objects correctly', () => { + expect( + extractAttributesWithoutSubCases({ + ...getFindResponseFields(), + saved_objects: [ + { + type: 'a', + references: [ + { name: SUB_CASE_REF_NAME, type: 'awesome', id: '1' }, + { name: 'a', type: 'b', id: '5' }, + ], + id: 'b', + score: 0, + attributes: { field: '2' } as unknown as CaseUserActionResponse, + }, + { + type: 'a', + references: [{ name: SUB_CASE_REF_NAME, type: SUB_CASE_SAVED_OBJECT, id: '1' }], + id: 'b', + score: 0, + attributes: { field: '1' } as unknown as CaseUserActionResponse, + }, + ], + }) + ).toEqual([{ field: '2' }]); + }); + }); +}); + +const getFindResponseFields = () => ({ page: 1, per_page: 1, total: 0 }); diff --git a/x-pack/plugins/cases/server/client/user_actions/get.ts b/x-pack/plugins/cases/server/client/user_actions/get.ts index 2a6608014c800..660cf1b6a336e 100644 --- a/x-pack/plugins/cases/server/client/user_actions/get.ts +++ b/x-pack/plugins/cases/server/client/user_actions/get.ts @@ -5,14 +5,14 @@ * 2.0. */ +import { SavedObjectReference, SavedObjectsFindResponse } from 'kibana/server'; import { - CASE_COMMENT_SAVED_OBJECT, - CASE_SAVED_OBJECT, CaseUserActionsResponse, CaseUserActionsResponseRt, SUB_CASE_SAVED_OBJECT, + CaseUserActionResponse, } from '../../../common'; -import { createCaseError, checkEnabledCaseConnectorOrThrow } from '../../common'; +import { createCaseError, checkEnabledCaseConnectorOrThrow, SUB_CASE_REF_NAME } from '../../common'; import { CasesClientArgs } from '..'; import { Operations } from '../../authorization'; import { UserActionGet } from './client'; @@ -40,23 +40,12 @@ export const get = async ( operation: Operations.getUserActions, }); - return CaseUserActionsResponseRt.encode( - userActions.saved_objects.reduce((acc, ua) => { - if (subCaseId == null && ua.references.some((uar) => uar.type === SUB_CASE_SAVED_OBJECT)) { - return acc; - } - return [ - ...acc, - { - ...ua.attributes, - action_id: ua.id, - case_id: ua.references.find((r) => r.type === CASE_SAVED_OBJECT)?.id ?? '', - comment_id: ua.references.find((r) => r.type === CASE_COMMENT_SAVED_OBJECT)?.id ?? null, - sub_case_id: ua.references.find((r) => r.type === SUB_CASE_SAVED_OBJECT)?.id ?? '', - }, - ]; - }, []) - ); + const resultsToEncode = + subCaseId == null + ? extractAttributesWithoutSubCases(userActions) + : extractAttributes(userActions); + + return CaseUserActionsResponseRt.encode(resultsToEncode); } catch (error) { throw createCaseError({ message: `Failed to retrieve user actions case id: ${caseId} sub case id: ${subCaseId}: ${error}`, @@ -65,3 +54,21 @@ export const get = async ( }); } }; + +export function extractAttributesWithoutSubCases( + userActions: SavedObjectsFindResponse +): CaseUserActionsResponse { + // exclude user actions relating to sub cases from the results + const hasSubCaseReference = (references: SavedObjectReference[]) => + references.find((ref) => ref.type === SUB_CASE_SAVED_OBJECT && ref.name === SUB_CASE_REF_NAME); + + return userActions.saved_objects + .filter((so) => !hasSubCaseReference(so.references)) + .map((so) => so.attributes); +} + +function extractAttributes( + userActions: SavedObjectsFindResponse +): CaseUserActionsResponse { + return userActions.saved_objects.map((so) => so.attributes); +} diff --git a/x-pack/plugins/cases/server/common/constants.ts b/x-pack/plugins/cases/server/common/constants.ts index 1f6af310d6ece..eba0a64a5c0be 100644 --- a/x-pack/plugins/cases/server/common/constants.ts +++ b/x-pack/plugins/cases/server/common/constants.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../common'; + /** * The name of the saved object reference indicating the action connector ID. This is stored in the Saved Object reference * field's name property. @@ -15,3 +17,30 @@ export const CONNECTOR_ID_REFERENCE_NAME = 'connectorId'; * The name of the saved object reference indicating the action connector ID that was used to push a case. */ export const PUSH_CONNECTOR_ID_REFERENCE_NAME = 'pushConnectorId'; + +/** + * The name of the saved object reference indicating the action connector ID that was used for + * adding a connector, or updating the existing connector for a user action's old_value field. + */ +export const USER_ACTION_OLD_ID_REF_NAME = 'oldConnectorId'; + +/** + * The name of the saved object reference indicating the action connector ID that was used for pushing a case, + * for a user action's old_value field. + */ +export const USER_ACTION_OLD_PUSH_ID_REF_NAME = 'oldPushConnectorId'; + +/** + * The name of the saved object reference indicating the caseId reference + */ +export const CASE_REF_NAME = `associated-${CASE_SAVED_OBJECT}`; + +/** + * The name of the saved object reference indicating the commentId reference + */ +export const COMMENT_REF_NAME = `associated-${CASE_COMMENT_SAVED_OBJECT}`; + +/** + * The name of the saved object reference indicating the subCaseId reference + */ +export const SUB_CASE_REF_NAME = `associated-${SUB_CASE_SAVED_OBJECT}`; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts index bca12a86a544e..9020f65ae352c 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts @@ -30,322 +30,324 @@ const create_7_14_0_case = ({ }, }); -describe('7.15.0 connector ID migration', () => { - it('does not create a reference when the connector.id is none', () => { - const caseSavedObject = create_7_14_0_case({ connector: getNoneCaseConnector() }); - - const migratedConnector = caseConnectorIdMigration( - caseSavedObject - ) as SavedObjectSanitizedDoc; - - expect(migratedConnector.references.length).toBe(0); - expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); - expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": null, - "name": "none", - "type": ".none", - } - `); - }); +describe('case migrations', () => { + describe('7.15.0 connector ID migration', () => { + it('does not create a reference when the connector.id is none', () => { + const caseSavedObject = create_7_14_0_case({ connector: getNoneCaseConnector() }); + + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "name": "none", + "type": ".none", + } + `); + }); - it('does not create a reference when the connector is undefined', () => { - const caseSavedObject = create_7_14_0_case(); - - const migratedConnector = caseConnectorIdMigration( - caseSavedObject - ) as SavedObjectSanitizedDoc; - - expect(migratedConnector.references.length).toBe(0); - expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); - expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": null, - "name": "none", - "type": ".none", - } - `); - }); + it('does not create a reference when the connector is undefined', () => { + const caseSavedObject = create_7_14_0_case(); - it('sets the connector to the default none connector if the connector.id is undefined', () => { - const caseSavedObject = create_7_14_0_case({ - connector: { - fields: null, - name: ConnectorTypes.jira, - type: ConnectorTypes.jira, - } as ESCaseConnectorWithId, - }); + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; - const migratedConnector = caseConnectorIdMigration( - caseSavedObject - ) as SavedObjectSanitizedDoc; - - expect(migratedConnector.references.length).toBe(0); - expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); - expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": null, - "name": "none", - "type": ".none", - } - `); - }); + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "name": "none", + "type": ".none", + } + `); + }); - it('does not create a reference when the external_service is null', () => { - const caseSavedObject = create_7_14_0_case({ externalService: null }); + it('sets the connector to the default none connector if the connector.id is undefined', () => { + const caseSavedObject = create_7_14_0_case({ + connector: { + fields: null, + name: ConnectorTypes.jira, + type: ConnectorTypes.jira, + } as ESCaseConnectorWithId, + }); + + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "name": "none", + "type": ".none", + } + `); + }); - const migratedConnector = caseConnectorIdMigration( - caseSavedObject - ) as SavedObjectSanitizedDoc; + it('does not create a reference when the external_service is null', () => { + const caseSavedObject = create_7_14_0_case({ externalService: null }); - expect(migratedConnector.references.length).toBe(0); - expect(migratedConnector.attributes.external_service).toBeNull(); - }); + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; - it('does not create a reference when the external_service is undefined and sets external_service to null', () => { - const caseSavedObject = create_7_14_0_case(); + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.external_service).toBeNull(); + }); - const migratedConnector = caseConnectorIdMigration( - caseSavedObject - ) as SavedObjectSanitizedDoc; + it('does not create a reference when the external_service is undefined and sets external_service to null', () => { + const caseSavedObject = create_7_14_0_case(); - expect(migratedConnector.references.length).toBe(0); - expect(migratedConnector.attributes.external_service).toBeNull(); - }); + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; - it('does not create a reference when the external_service.connector_id is none', () => { - const caseSavedObject = create_7_14_0_case({ - externalService: createExternalService({ connector_id: noneConnectorId }), + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.external_service).toBeNull(); }); - const migratedConnector = caseConnectorIdMigration( - caseSavedObject - ) as SavedObjectSanitizedDoc; - - expect(migratedConnector.references.length).toBe(0); - expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(` - Object { - "connector_name": ".jira", - "external_id": "100", - "external_title": "awesome", - "external_url": "http://www.google.com", - "pushed_at": "2019-11-25T21:54:48.952Z", - "pushed_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - } - `); - }); - - it('preserves the existing references when migrating', () => { - const caseSavedObject = { - ...create_7_14_0_case(), - references: [{ id: '1', name: 'awesome', type: 'hello' }], - }; + it('does not create a reference when the external_service.connector_id is none', () => { + const caseSavedObject = create_7_14_0_case({ + externalService: createExternalService({ connector_id: noneConnectorId }), + }); - const migratedConnector = caseConnectorIdMigration( - caseSavedObject - ) as SavedObjectSanitizedDoc; + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; - expect(migratedConnector.references.length).toBe(1); - expect(migratedConnector.references).toMatchInlineSnapshot(` - Array [ + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(` Object { - "id": "1", - "name": "awesome", - "type": "hello", - }, - ] - `); - }); + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + }); - it('creates a connector reference and removes the connector.id field', () => { - const caseSavedObject = create_7_14_0_case({ - connector: { - id: '123', - fields: null, - name: 'connector', - type: ConnectorTypes.jira, - }, + it('preserves the existing references when migrating', () => { + const caseSavedObject = { + ...create_7_14_0_case(), + references: [{ id: '1', name: 'awesome', type: 'hello' }], + }; + + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(1); + expect(migratedConnector.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "awesome", + "type": "hello", + }, + ] + `); }); - const migratedConnector = caseConnectorIdMigration( - caseSavedObject - ) as SavedObjectSanitizedDoc; - - expect(migratedConnector.references.length).toBe(1); - expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); - expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": null, - "name": "connector", - "type": ".jira", - } - `); - expect(migratedConnector.references).toMatchInlineSnapshot(` - Array [ - Object { - "id": "123", - "name": "connectorId", - "type": "action", + it('creates a connector reference and removes the connector.id field', () => { + const caseSavedObject = create_7_14_0_case({ + connector: { + id: '123', + fields: null, + name: 'connector', + type: ConnectorTypes.jira, }, - ] - `); - }); + }); - it('creates a push connector reference and removes the connector_id field', () => { - const caseSavedObject = create_7_14_0_case({ - externalService: { - connector_id: '100', - connector_name: '.jira', - external_id: '100', - external_title: 'awesome', - external_url: 'http://www.google.com', - pushed_at: '2019-11-25T21:54:48.952Z', - pushed_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, - }, - }); + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; - const migratedConnector = caseConnectorIdMigration( - caseSavedObject - ) as SavedObjectSanitizedDoc; - - expect(migratedConnector.references.length).toBe(1); - expect(migratedConnector.attributes.external_service).not.toHaveProperty('connector_id'); - expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(` - Object { - "connector_name": ".jira", - "external_id": "100", - "external_title": "awesome", - "external_url": "http://www.google.com", - "pushed_at": "2019-11-25T21:54:48.952Z", - "pushed_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - } - `); - expect(migratedConnector.references).toMatchInlineSnapshot(` - Array [ + expect(migratedConnector.references.length).toBe(1); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` Object { - "id": "100", - "name": "pushConnectorId", - "type": "action", - }, - ] - `); - }); + "fields": null, + "name": "connector", + "type": ".jira", + } + `); + expect(migratedConnector.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "123", + "name": "connectorId", + "type": "action", + }, + ] + `); + }); - it('does not create a reference and preserves the existing external_service fields when connector_id is null', () => { - const caseSavedObject = create_7_14_0_case({ - externalService: { - connector_id: null, - connector_name: '.jira', - external_id: '100', - external_title: 'awesome', - external_url: 'http://www.google.com', - pushed_at: '2019-11-25T21:54:48.952Z', - pushed_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', + it('creates a push connector reference and removes the connector_id field', () => { + const caseSavedObject = create_7_14_0_case({ + externalService: { + connector_id: '100', + connector_name: '.jira', + external_id: '100', + external_title: 'awesome', + external_url: 'http://www.google.com', + pushed_at: '2019-11-25T21:54:48.952Z', + pushed_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, }, - }, + }); + + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(1); + expect(migratedConnector.attributes.external_service).not.toHaveProperty('connector_id'); + expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(` + Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + expect(migratedConnector.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); }); - const migratedConnector = caseConnectorIdMigration( - caseSavedObject - ) as SavedObjectSanitizedDoc; - - expect(migratedConnector.references.length).toBe(0); - expect(migratedConnector.attributes.external_service).not.toHaveProperty('connector_id'); - expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(` - Object { - "connector_name": ".jira", - "external_id": "100", - "external_title": "awesome", - "external_url": "http://www.google.com", - "pushed_at": "2019-11-25T21:54:48.952Z", - "pushed_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", + it('does not create a reference and preserves the existing external_service fields when connector_id is null', () => { + const caseSavedObject = create_7_14_0_case({ + externalService: { + connector_id: null, + connector_name: '.jira', + external_id: '100', + external_title: 'awesome', + external_url: 'http://www.google.com', + pushed_at: '2019-11-25T21:54:48.952Z', + pushed_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, }, - } - `); - }); + }); - it('migrates both connector and external_service when provided', () => { - const caseSavedObject = create_7_14_0_case({ - externalService: { - connector_id: '100', - connector_name: '.jira', - external_id: '100', - external_title: 'awesome', - external_url: 'http://www.google.com', - pushed_at: '2019-11-25T21:54:48.952Z', - pushed_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, - }, - connector: { - id: '123', - fields: null, - name: 'connector', - type: ConnectorTypes.jira, - }, + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.external_service).not.toHaveProperty('connector_id'); + expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(` + Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); }); - const migratedConnector = caseConnectorIdMigration( - caseSavedObject - ) as SavedObjectSanitizedDoc; - - expect(migratedConnector.references.length).toBe(2); - expect(migratedConnector.attributes.external_service).not.toHaveProperty('connector_id'); - expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(` - Object { - "connector_name": ".jira", - "external_id": "100", - "external_title": "awesome", - "external_url": "http://www.google.com", - "pushed_at": "2019-11-25T21:54:48.952Z", - "pushed_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", + it('migrates both connector and external_service when provided', () => { + const caseSavedObject = create_7_14_0_case({ + externalService: { + connector_id: '100', + connector_name: '.jira', + external_id: '100', + external_title: 'awesome', + external_url: 'http://www.google.com', + pushed_at: '2019-11-25T21:54:48.952Z', + pushed_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, }, - } - `); - expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); - expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": null, - "name": "connector", - "type": ".jira", - } - `); - expect(migratedConnector.references).toMatchInlineSnapshot(` - Array [ - Object { - "id": "123", - "name": "connectorId", - "type": "action", + connector: { + id: '123', + fields: null, + name: 'connector', + type: ConnectorTypes.jira, }, + }); + + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(2); + expect(migratedConnector.attributes.external_service).not.toHaveProperty('connector_id'); + expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(` Object { - "id": "100", - "name": "pushConnectorId", - "type": "action", - }, - ] - `); + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "name": "connector", + "type": ".jira", + } + `); + expect(migratedConnector.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "123", + "name": "connectorId", + "type": "action", + }, + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); + }); }); }); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts index bffd4171270ef..80f02fa3bf6a6 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts @@ -14,7 +14,11 @@ import { } from '../../../../../../src/core/server'; import { ESConnectorFields } from '../../services'; import { ConnectorTypes, CaseType } from '../../../common'; -import { transformConnectorIdToReference, transformPushConnectorIdToReference } from './utils'; +import { + transformConnectorIdToReference, + transformPushConnectorIdToReference, +} from '../../services/user_actions/transform'; +import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME } from '../../common'; interface UnsanitizedCaseConnector { connector_id: string; @@ -50,11 +54,13 @@ export const caseConnectorIdMigration = ( // removing the id field since it will be stored in the references instead const { connector, external_service, ...restAttributes } = doc.attributes; - const { transformedConnector, references: connectorReferences } = - transformConnectorIdToReference(connector); + const { transformedConnector, references: connectorReferences } = transformConnectorIdToReference( + CONNECTOR_ID_REFERENCE_NAME, + connector + ); const { transformedPushConnector, references: pushConnectorReferences } = - transformPushConnectorIdToReference(external_service); + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, external_service); const { references = [] } = doc; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts index 4467b499817a5..9ae0285598dbf 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts @@ -40,87 +40,89 @@ const create_7_14_0_configSchema = (connector?: ESCaseConnectorWithId) => ({ }, }); -describe('7.15.0 connector ID migration', () => { - it('does not create a reference when the connector ID is none', () => { - const configureSavedObject = create_7_14_0_configSchema(getNoneCaseConnector()); +describe('configuration migrations', () => { + describe('7.15.0 connector ID migration', () => { + it('does not create a reference when the connector ID is none', () => { + const configureSavedObject = create_7_14_0_configSchema(getNoneCaseConnector()); - const migratedConnector = configureConnectorIdMigration( - configureSavedObject - ) as SavedObjectSanitizedDoc; + const migratedConnector = configureConnectorIdMigration( + configureSavedObject + ) as SavedObjectSanitizedDoc; - expect(migratedConnector.references.length).toBe(0); - expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); - }); + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + }); - it('does not create a reference when the connector is undefined and defaults it to the none connector', () => { - const configureSavedObject = create_7_14_0_configSchema(); + it('does not create a reference when the connector is undefined and defaults it to the none connector', () => { + const configureSavedObject = create_7_14_0_configSchema(); - const migratedConnector = configureConnectorIdMigration( - configureSavedObject - ) as SavedObjectSanitizedDoc; + const migratedConnector = configureConnectorIdMigration( + configureSavedObject + ) as SavedObjectSanitizedDoc; - expect(migratedConnector.references.length).toBe(0); - expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": null, - "name": "none", - "type": ".none", - } - `); - }); - - it('creates a reference using the connector id', () => { - const configureSavedObject = create_7_14_0_configSchema({ - id: '123', - fields: null, - name: 'connector', - type: ConnectorTypes.jira, + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "name": "none", + "type": ".none", + } + `); }); - const migratedConnector = configureConnectorIdMigration( - configureSavedObject - ) as SavedObjectSanitizedDoc; + it('creates a reference using the connector id', () => { + const configureSavedObject = create_7_14_0_configSchema({ + id: '123', + fields: null, + name: 'connector', + type: ConnectorTypes.jira, + }); - expect(migratedConnector.references).toEqual([ - { id: '123', type: ACTION_SAVED_OBJECT_TYPE, name: CONNECTOR_ID_REFERENCE_NAME }, - ]); - expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); - }); + const migratedConnector = configureConnectorIdMigration( + configureSavedObject + ) as SavedObjectSanitizedDoc; - it('returns the other attributes and default connector when the connector is undefined', () => { - const configureSavedObject = create_7_14_0_configSchema(); + expect(migratedConnector.references).toEqual([ + { id: '123', type: ACTION_SAVED_OBJECT_TYPE, name: CONNECTOR_ID_REFERENCE_NAME }, + ]); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + }); - const migratedConnector = configureConnectorIdMigration( - configureSavedObject - ) as SavedObjectSanitizedDoc; + it('returns the other attributes and default connector when the connector is undefined', () => { + const configureSavedObject = create_7_14_0_configSchema(); - expect(migratedConnector).toMatchInlineSnapshot(` - Object { - "attributes": Object { - "closure_type": "close-by-pushing", - "connector": Object { - "fields": null, - "name": "none", - "type": ".none", - }, - "created_at": "2020-04-09T09:43:51.778Z", - "created_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "owner": "securitySolution", - "updated_at": "2020-04-09T09:43:51.778Z", - "updated_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", + const migratedConnector = configureConnectorIdMigration( + configureSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "closure_type": "close-by-pushing", + "connector": Object { + "fields": null, + "name": "none", + "type": ".none", + }, + "created_at": "2020-04-09T09:43:51.778Z", + "created_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "owner": "securitySolution", + "updated_at": "2020-04-09T09:43:51.778Z", + "updated_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, }, - }, - "id": "1", - "references": Array [], - "type": "cases-configure", - } - `); + "id": "1", + "references": Array [], + "type": "cases-configure", + } + `); + }); }); }); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.ts index 527d40fca2e35..f9937253e0d2f 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.ts @@ -13,7 +13,8 @@ import { } from '../../../../../../src/core/server'; import { ConnectorTypes } from '../../../common'; import { addOwnerToSO, SanitizedCaseOwner } from '.'; -import { transformConnectorIdToReference } from './utils'; +import { transformConnectorIdToReference } from '../../services/user_actions/transform'; +import { CONNECTOR_ID_REFERENCE_NAME } from '../../common'; interface UnsanitizedConfigureConnector { connector_id: string; @@ -34,8 +35,10 @@ export const configureConnectorIdMigration = ( ): SavedObjectSanitizedDoc => { // removing the id field since it will be stored in the references instead const { connector, ...restAttributes } = doc.attributes; - const { transformedConnector, references: connectorReferences } = - transformConnectorIdToReference(connector); + const { transformedConnector, references: connectorReferences } = transformConnectorIdToReference( + CONNECTOR_ID_REFERENCE_NAME, + connector + ); const { references = [] } = doc; return { diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts index a445131073d19..a4f50fbfcde5b 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts @@ -5,24 +5,17 @@ * 2.0. */ -/* eslint-disable @typescript-eslint/naming-convention */ - import { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc, } from '../../../../../../src/core/server'; -import { ConnectorTypes, SECURITY_SOLUTION_OWNER } from '../../../common'; +import { SECURITY_SOLUTION_OWNER } from '../../../common'; export { caseMigrations } from './cases'; export { configureMigrations } from './configuration'; +export { userActionsMigrations } from './user_actions'; export { createCommentsMigrations, CreateCommentsMigrationsDeps } from './comments'; -interface UserActions { - action_field: string[]; - new_value: string; - old_value: string; -} - export interface SanitizedCaseOwner { owner: string; } @@ -38,52 +31,6 @@ export const addOwnerToSO = >( references: doc.references || [], }); -export const userActionsMigrations = { - '7.10.0': (doc: SavedObjectUnsanitizedDoc): SavedObjectSanitizedDoc => { - const { action_field, new_value, old_value, ...restAttributes } = doc.attributes; - - if ( - action_field == null || - !Array.isArray(action_field) || - action_field[0] !== 'connector_id' - ) { - return { ...doc, references: doc.references || [] }; - } - - return { - ...doc, - attributes: { - ...restAttributes, - action_field: ['connector'], - new_value: - new_value != null - ? JSON.stringify({ - id: new_value, - name: 'none', - type: ConnectorTypes.none, - fields: null, - }) - : new_value, - old_value: - old_value != null - ? JSON.stringify({ - id: old_value, - name: 'none', - type: ConnectorTypes.none, - fields: null, - }) - : old_value, - }, - references: doc.references || [], - }; - }, - '7.14.0': ( - doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addOwnerToSO(doc); - }, -}; - export const connectorMappingsMigrations = { '7.14.0': ( doc: SavedObjectUnsanitizedDoc> diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.test.ts new file mode 100644 index 0000000000000..e71c8db0db694 --- /dev/null +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.test.ts @@ -0,0 +1,562 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import { SavedObjectMigrationContext, SavedObjectSanitizedDoc } from 'kibana/server'; +import { migrationMocks } from 'src/core/server/mocks'; +import { CaseUserActionAttributes, CASE_USER_ACTION_SAVED_OBJECT } from '../../../common'; +import { + createConnectorObject, + createExternalService, + createJiraConnector, +} from '../../services/test_utils'; +import { userActionsConnectorIdMigration } from './user_actions'; + +const create_7_14_0_userAction = ( + params: { + action?: string; + action_field?: string[]; + new_value?: string | null | object; + old_value?: string | null | object; + } = {} +) => { + const { new_value, old_value, ...restParams } = params; + + return { + type: CASE_USER_ACTION_SAVED_OBJECT, + id: '1', + attributes: { + ...restParams, + new_value: new_value && typeof new_value === 'object' ? JSON.stringify(new_value) : new_value, + old_value: old_value && typeof old_value === 'object' ? JSON.stringify(old_value) : old_value, + }, + }; +}; + +describe('user action migrations', () => { + describe('7.15.0 connector ID migration', () => { + describe('userActionsConnectorIdMigration', () => { + let context: jest.Mocked; + + beforeEach(() => { + context = migrationMocks.createContext(); + }); + + describe('push user action', () => { + it('extracts the external_service connector_id to references for a new pushed user action', () => { + const userAction = create_7_14_0_userAction({ + action: 'push-to-service', + action_field: ['pushed'], + new_value: createExternalService(), + old_value: null, + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + const parsedExternalService = JSON.parse(migratedUserAction.attributes.new_value!); + expect(parsedExternalService).not.toHaveProperty('connector_id'); + expect(parsedExternalService).toMatchInlineSnapshot(` + Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + + expect(migratedUserAction.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); + + expect(migratedUserAction.attributes.old_value).toBeNull(); + }); + + it('extract the external_service connector_id to references for new and old pushed user action', () => { + const userAction = create_7_14_0_userAction({ + action: 'push-to-service', + action_field: ['pushed'], + new_value: createExternalService(), + old_value: createExternalService({ connector_id: '5' }), + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + const parsedNewExternalService = JSON.parse(migratedUserAction.attributes.new_value!); + const parsedOldExternalService = JSON.parse(migratedUserAction.attributes.old_value!); + + expect(parsedNewExternalService).not.toHaveProperty('connector_id'); + expect(parsedOldExternalService).not.toHaveProperty('connector_id'); + expect(migratedUserAction.references).toEqual([ + { id: '100', name: 'pushConnectorId', type: 'action' }, + { id: '5', name: 'oldPushConnectorId', type: 'action' }, + ]); + }); + + it('preserves the existing references after extracting the external_service connector_id field', () => { + const userAction = { + ...create_7_14_0_userAction({ + action: 'push-to-service', + action_field: ['pushed'], + new_value: createExternalService(), + old_value: createExternalService({ connector_id: '5' }), + }), + references: [{ id: '500', name: 'someReference', type: 'ref' }], + }; + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + const parsedNewExternalService = JSON.parse(migratedUserAction.attributes.new_value!); + const parsedOldExternalService = JSON.parse(migratedUserAction.attributes.old_value!); + + expect(parsedNewExternalService).not.toHaveProperty('connector_id'); + expect(parsedOldExternalService).not.toHaveProperty('connector_id'); + expect(migratedUserAction.references).toEqual([ + { id: '500', name: 'someReference', type: 'ref' }, + { id: '100', name: 'pushConnectorId', type: 'action' }, + { id: '5', name: 'oldPushConnectorId', type: 'action' }, + ]); + }); + + it('leaves the object unmodified when it is not a valid push user action', () => { + const userAction = create_7_14_0_userAction({ + action: 'push-to-service', + action_field: ['invalid field'], + new_value: 'hello', + old_value: null, + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + expect(migratedUserAction.attributes.old_value).toBeNull(); + expect(migratedUserAction).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "action": "push-to-service", + "action_field": Array [ + "invalid field", + ], + "new_value": "hello", + "old_value": null, + }, + "id": "1", + "references": Array [], + "type": "cases-user-actions", + } + `); + }); + + it('leaves the object unmodified when it new value is invalid json', () => { + const userAction = create_7_14_0_userAction({ + action: 'push-to-service', + action_field: ['pushed'], + new_value: '{a', + old_value: null, + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + expect(migratedUserAction.attributes.old_value).toBeNull(); + expect(migratedUserAction.attributes.new_value).toEqual('{a'); + expect(migratedUserAction).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "action": "push-to-service", + "action_field": Array [ + "pushed", + ], + "new_value": "{a", + "old_value": null, + }, + "id": "1", + "references": Array [], + "type": "cases-user-actions", + } + `); + }); + + it('logs an error new value is invalid json', () => { + const userAction = create_7_14_0_userAction({ + action: 'push-to-service', + action_field: ['pushed'], + new_value: '{a', + old_value: null, + }); + + userActionsConnectorIdMigration(userAction, context); + + expect(context.log.error).toHaveBeenCalled(); + }); + }); + + describe('update connector user action', () => { + it('extracts the connector id to references for a new create connector user action', () => { + const userAction = create_7_14_0_userAction({ + action: 'update', + action_field: ['connector'], + new_value: createJiraConnector(), + old_value: null, + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + const parsedConnector = JSON.parse(migratedUserAction.attributes.new_value!); + expect(parsedConnector).not.toHaveProperty('id'); + expect(parsedConnector).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + } + `); + + expect(migratedUserAction.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + + expect(migratedUserAction.attributes.old_value).toBeNull(); + }); + + it('extracts the connector id to references for a new and old create connector user action', () => { + const userAction = create_7_14_0_userAction({ + action: 'update', + action_field: ['connector'], + new_value: createJiraConnector(), + old_value: { ...createJiraConnector(), id: '5' }, + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + const parsedNewConnector = JSON.parse(migratedUserAction.attributes.new_value!); + const parsedOldConnector = JSON.parse(migratedUserAction.attributes.new_value!); + + expect(parsedNewConnector).not.toHaveProperty('id'); + expect(parsedOldConnector).not.toHaveProperty('id'); + + expect(migratedUserAction.references).toEqual([ + { id: '1', name: 'connectorId', type: 'action' }, + { id: '5', name: 'oldConnectorId', type: 'action' }, + ]); + }); + + it('preserves the existing references after extracting the connector.id field', () => { + const userAction = { + ...create_7_14_0_userAction({ + action: 'update', + action_field: ['connector'], + new_value: createJiraConnector(), + old_value: { ...createJiraConnector(), id: '5' }, + }), + references: [{ id: '500', name: 'someReference', type: 'ref' }], + }; + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + const parsedNewConnectorId = JSON.parse(migratedUserAction.attributes.new_value!); + const parsedOldConnectorId = JSON.parse(migratedUserAction.attributes.old_value!); + + expect(parsedNewConnectorId).not.toHaveProperty('id'); + expect(parsedOldConnectorId).not.toHaveProperty('id'); + expect(migratedUserAction.references).toEqual([ + { id: '500', name: 'someReference', type: 'ref' }, + { id: '1', name: 'connectorId', type: 'action' }, + { id: '5', name: 'oldConnectorId', type: 'action' }, + ]); + }); + + it('leaves the object unmodified when it is not a valid create connector user action', () => { + const userAction = create_7_14_0_userAction({ + action: 'update', + action_field: ['invalid action'], + new_value: 'new json value', + old_value: 'old value', + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + expect(migratedUserAction).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "action": "update", + "action_field": Array [ + "invalid action", + ], + "new_value": "new json value", + "old_value": "old value", + }, + "id": "1", + "references": Array [], + "type": "cases-user-actions", + } + `); + }); + + it('leaves the object unmodified when old_value is invalid json', () => { + const userAction = create_7_14_0_userAction({ + action: 'update', + action_field: ['connector'], + new_value: '{}', + old_value: '{b', + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + expect(migratedUserAction).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "action": "update", + "action_field": Array [ + "connector", + ], + "new_value": "{}", + "old_value": "{b", + }, + "id": "1", + "references": Array [], + "type": "cases-user-actions", + } + `); + }); + + it('logs an error message when old_value is invalid json', () => { + const userAction = create_7_14_0_userAction({ + action: 'update', + action_field: ['connector'], + new_value: createJiraConnector(), + old_value: '{b', + }); + + userActionsConnectorIdMigration(userAction, context); + + expect(context.log.error).toHaveBeenCalled(); + }); + }); + + describe('create connector user action', () => { + it('extracts the connector id to references for a new create connector user action', () => { + const userAction = create_7_14_0_userAction({ + action: 'create', + action_field: ['connector'], + new_value: createConnectorObject(), + old_value: null, + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + const parsedConnector = JSON.parse(migratedUserAction.attributes.new_value!); + expect(parsedConnector.connector).not.toHaveProperty('id'); + expect(parsedConnector).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + }, + } + `); + + expect(migratedUserAction.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + + expect(migratedUserAction.attributes.old_value).toBeNull(); + }); + + it('extracts the connector id to references for a new and old create connector user action', () => { + const userAction = create_7_14_0_userAction({ + action: 'create', + action_field: ['connector'], + new_value: createConnectorObject(), + old_value: createConnectorObject({ id: '5' }), + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + const parsedNewConnector = JSON.parse(migratedUserAction.attributes.new_value!); + const parsedOldConnector = JSON.parse(migratedUserAction.attributes.new_value!); + + expect(parsedNewConnector.connector).not.toHaveProperty('id'); + expect(parsedOldConnector.connector).not.toHaveProperty('id'); + + expect(migratedUserAction.references).toEqual([ + { id: '1', name: 'connectorId', type: 'action' }, + { id: '5', name: 'oldConnectorId', type: 'action' }, + ]); + }); + + it('preserves the existing references after extracting the connector.id field', () => { + const userAction = { + ...create_7_14_0_userAction({ + action: 'create', + action_field: ['connector'], + new_value: createConnectorObject(), + old_value: createConnectorObject({ id: '5' }), + }), + references: [{ id: '500', name: 'someReference', type: 'ref' }], + }; + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + const parsedNewConnectorId = JSON.parse(migratedUserAction.attributes.new_value!); + const parsedOldConnectorId = JSON.parse(migratedUserAction.attributes.old_value!); + + expect(parsedNewConnectorId.connector).not.toHaveProperty('id'); + expect(parsedOldConnectorId.connector).not.toHaveProperty('id'); + expect(migratedUserAction.references).toEqual([ + { id: '500', name: 'someReference', type: 'ref' }, + { id: '1', name: 'connectorId', type: 'action' }, + { id: '5', name: 'oldConnectorId', type: 'action' }, + ]); + }); + + it('leaves the object unmodified when it is not a valid create connector user action', () => { + const userAction = create_7_14_0_userAction({ + action: 'create', + action_field: ['invalid action'], + new_value: 'new json value', + old_value: 'old value', + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + expect(migratedUserAction).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "action": "create", + "action_field": Array [ + "invalid action", + ], + "new_value": "new json value", + "old_value": "old value", + }, + "id": "1", + "references": Array [], + "type": "cases-user-actions", + } + `); + }); + + it('leaves the object unmodified when new_value is invalid json', () => { + const userAction = create_7_14_0_userAction({ + action: 'create', + action_field: ['connector'], + new_value: 'new json value', + old_value: 'old value', + }); + + const migratedUserAction = userActionsConnectorIdMigration( + userAction, + context + ) as SavedObjectSanitizedDoc; + + expect(migratedUserAction).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "action": "create", + "action_field": Array [ + "connector", + ], + "new_value": "new json value", + "old_value": "old value", + }, + "id": "1", + "references": Array [], + "type": "cases-user-actions", + } + `); + }); + + it('logs an error message when new_value is invalid json', () => { + const userAction = create_7_14_0_userAction({ + action: 'create', + action_field: ['connector'], + new_value: 'new json value', + old_value: 'old value', + }); + + userActionsConnectorIdMigration(userAction, context); + + expect(context.log.error).toHaveBeenCalled(); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.ts new file mode 100644 index 0000000000000..ed6b57ef647f9 --- /dev/null +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.ts @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import { addOwnerToSO, SanitizedCaseOwner } from '.'; +import { + SavedObjectUnsanitizedDoc, + SavedObjectSanitizedDoc, + SavedObjectMigrationContext, + LogMeta, +} from '../../../../../../src/core/server'; +import { ConnectorTypes, isCreateConnector, isPush, isUpdateConnector } from '../../../common'; + +import { extractConnectorIdFromJson } from '../../services/user_actions/transform'; +import { UserActionFieldType } from '../../services/user_actions/types'; + +interface UserActions { + action_field: string[]; + new_value: string; + old_value: string; +} + +interface UserActionUnmigratedConnectorDocument { + action?: string; + action_field?: string[]; + new_value?: string | null; + old_value?: string | null; +} + +interface UserActionLogMeta extends LogMeta { + migrations: { userAction: { id: string } }; +} + +export function userActionsConnectorIdMigration( + doc: SavedObjectUnsanitizedDoc, + context: SavedObjectMigrationContext +): SavedObjectSanitizedDoc { + const originalDocWithReferences = { ...doc, references: doc.references ?? [] }; + + if (!isConnectorUserAction(doc.attributes.action, doc.attributes.action_field)) { + return originalDocWithReferences; + } + + try { + return formatDocumentWithConnectorReferences(doc); + } catch (error) { + logError(doc.id, context, error); + + return originalDocWithReferences; + } +} + +function isConnectorUserAction(action?: string, actionFields?: string[]): boolean { + return ( + isCreateConnector(action, actionFields) || + isUpdateConnector(action, actionFields) || + isPush(action, actionFields) + ); +} + +function formatDocumentWithConnectorReferences( + doc: SavedObjectUnsanitizedDoc +): SavedObjectSanitizedDoc { + const { new_value, old_value, action, action_field, ...restAttributes } = doc.attributes; + const { references = [] } = doc; + + const { transformedActionDetails: transformedNewValue, references: newValueConnectorRefs } = + extractConnectorIdFromJson({ + action, + actionFields: action_field, + actionDetails: new_value, + fieldType: UserActionFieldType.New, + }); + + const { transformedActionDetails: transformedOldValue, references: oldValueConnectorRefs } = + extractConnectorIdFromJson({ + action, + actionFields: action_field, + actionDetails: old_value, + fieldType: UserActionFieldType.Old, + }); + + return { + ...doc, + attributes: { + ...restAttributes, + action, + action_field, + new_value: transformedNewValue, + old_value: transformedOldValue, + }, + references: [...references, ...newValueConnectorRefs, ...oldValueConnectorRefs], + }; +} + +function logError(id: string, context: SavedObjectMigrationContext, error: Error) { + context.log.error( + `Failed to migrate user action connector doc id: ${id} version: ${context.migrationVersion} error: ${error.message}`, + { + migrations: { + userAction: { + id, + }, + }, + } + ); +} + +export const userActionsMigrations = { + '7.10.0': (doc: SavedObjectUnsanitizedDoc): SavedObjectSanitizedDoc => { + const { action_field, new_value, old_value, ...restAttributes } = doc.attributes; + + if ( + action_field == null || + !Array.isArray(action_field) || + action_field[0] !== 'connector_id' + ) { + return { ...doc, references: doc.references || [] }; + } + + return { + ...doc, + attributes: { + ...restAttributes, + action_field: ['connector'], + new_value: + new_value != null + ? JSON.stringify({ + id: new_value, + name: 'none', + type: ConnectorTypes.none, + fields: null, + }) + : new_value, + old_value: + old_value != null + ? JSON.stringify({ + id: old_value, + name: 'none', + type: ConnectorTypes.none, + fields: null, + }) + : old_value, + }, + references: doc.references || [], + }; + }, + '7.14.0': ( + doc: SavedObjectUnsanitizedDoc> + ): SavedObjectSanitizedDoc => { + return addOwnerToSO(doc); + }, + '7.16.0': userActionsConnectorIdMigration, +}; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts deleted file mode 100644 index f591bef6b3236..0000000000000 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts +++ /dev/null @@ -1,229 +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 { noneConnectorId } from '../../../common'; -import { createExternalService, createJiraConnector } from '../../services/test_utils'; -import { transformConnectorIdToReference, transformPushConnectorIdToReference } from './utils'; - -describe('migration utils', () => { - describe('transformConnectorIdToReference', () => { - it('returns the default none connector when the connector is undefined', () => { - expect(transformConnectorIdToReference().transformedConnector).toMatchInlineSnapshot(` - Object { - "connector": Object { - "fields": null, - "name": "none", - "type": ".none", - }, - } - `); - }); - - it('returns the default none connector when the id is undefined', () => { - expect(transformConnectorIdToReference({ id: undefined }).transformedConnector) - .toMatchInlineSnapshot(` - Object { - "connector": Object { - "fields": null, - "name": "none", - "type": ".none", - }, - } - `); - }); - - it('returns the default none connector when the id is none', () => { - expect(transformConnectorIdToReference({ id: noneConnectorId }).transformedConnector) - .toMatchInlineSnapshot(` - Object { - "connector": Object { - "fields": null, - "name": "none", - "type": ".none", - }, - } - `); - }); - - it('returns the default none connector when the id is none and other fields are defined', () => { - expect( - transformConnectorIdToReference({ ...createJiraConnector(), id: noneConnectorId }) - .transformedConnector - ).toMatchInlineSnapshot(` - Object { - "connector": Object { - "fields": null, - "name": "none", - "type": ".none", - }, - } - `); - }); - - it('returns an empty array of references when the connector is undefined', () => { - expect(transformConnectorIdToReference().references.length).toBe(0); - }); - - it('returns an empty array of references when the id is undefined', () => { - expect(transformConnectorIdToReference({ id: undefined }).references.length).toBe(0); - }); - - it('returns an empty array of references when the id is the none connector', () => { - expect(transformConnectorIdToReference({ id: noneConnectorId }).references.length).toBe(0); - }); - - it('returns an empty array of references when the id is the none connector and other fields are defined', () => { - expect( - transformConnectorIdToReference({ ...createJiraConnector(), id: noneConnectorId }) - .references.length - ).toBe(0); - }); - - it('returns a jira connector', () => { - const transformedFields = transformConnectorIdToReference(createJiraConnector()); - expect(transformedFields.transformedConnector).toMatchInlineSnapshot(` - Object { - "connector": Object { - "fields": Object { - "issueType": "bug", - "parent": "2", - "priority": "high", - }, - "name": ".jira", - "type": ".jira", - }, - } - `); - expect(transformedFields.references).toMatchInlineSnapshot(` - Array [ - Object { - "id": "1", - "name": "connectorId", - "type": "action", - }, - ] - `); - }); - }); - - describe('transformPushConnectorIdToReference', () => { - it('sets external_service to null when it is undefined', () => { - expect(transformPushConnectorIdToReference().transformedPushConnector).toMatchInlineSnapshot(` - Object { - "external_service": null, - } - `); - }); - - it('sets external_service to null when it is null', () => { - expect(transformPushConnectorIdToReference(null).transformedPushConnector) - .toMatchInlineSnapshot(` - Object { - "external_service": null, - } - `); - }); - - it('returns an object when external_service is defined but connector_id is undefined', () => { - expect( - transformPushConnectorIdToReference({ connector_id: undefined }).transformedPushConnector - ).toMatchInlineSnapshot(` - Object { - "external_service": Object {}, - } - `); - }); - - it('returns an object when external_service is defined but connector_id is null', () => { - expect(transformPushConnectorIdToReference({ connector_id: null }).transformedPushConnector) - .toMatchInlineSnapshot(` - Object { - "external_service": Object {}, - } - `); - }); - - it('returns an object when external_service is defined but connector_id is none', () => { - const otherFields = { otherField: 'hi' }; - - expect( - transformPushConnectorIdToReference({ ...otherFields, connector_id: noneConnectorId }) - .transformedPushConnector - ).toMatchInlineSnapshot(` - Object { - "external_service": Object { - "otherField": "hi", - }, - } - `); - }); - - it('returns an empty array of references when the external_service is undefined', () => { - expect(transformPushConnectorIdToReference().references.length).toBe(0); - }); - - it('returns an empty array of references when the external_service is null', () => { - expect(transformPushConnectorIdToReference(null).references.length).toBe(0); - }); - - it('returns an empty array of references when the connector_id is undefined', () => { - expect( - transformPushConnectorIdToReference({ connector_id: undefined }).references.length - ).toBe(0); - }); - - it('returns an empty array of references when the connector_id is null', () => { - expect( - transformPushConnectorIdToReference({ connector_id: undefined }).references.length - ).toBe(0); - }); - - it('returns an empty array of references when the connector_id is the none connector', () => { - expect( - transformPushConnectorIdToReference({ connector_id: noneConnectorId }).references.length - ).toBe(0); - }); - - it('returns an empty array of references when the connector_id is the none connector and other fields are defined', () => { - expect( - transformPushConnectorIdToReference({ - ...createExternalService(), - connector_id: noneConnectorId, - }).references.length - ).toBe(0); - }); - - it('returns the external_service connector', () => { - const transformedFields = transformPushConnectorIdToReference(createExternalService()); - expect(transformedFields.transformedPushConnector).toMatchInlineSnapshot(` - Object { - "external_service": Object { - "connector_name": ".jira", - "external_id": "100", - "external_title": "awesome", - "external_url": "http://www.google.com", - "pushed_at": "2019-11-25T21:54:48.952Z", - "pushed_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - }, - } - `); - expect(transformedFields.references).toMatchInlineSnapshot(` - Array [ - Object { - "id": "100", - "name": "pushConnectorId", - "type": "action", - }, - ] - `); - }); - }); -}); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts deleted file mode 100644 index 0100a04cde679..0000000000000 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts +++ /dev/null @@ -1,73 +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. - */ - -/* eslint-disable @typescript-eslint/naming-convention */ - -import { noneConnectorId } from '../../../common'; -import { SavedObjectReference } from '../../../../../../src/core/server'; -import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { - getNoneCaseConnector, - CONNECTOR_ID_REFERENCE_NAME, - PUSH_CONNECTOR_ID_REFERENCE_NAME, -} from '../../common'; - -export const transformConnectorIdToReference = (connector?: { - id?: string; -}): { transformedConnector: Record; references: SavedObjectReference[] } => { - const { id: connectorId, ...restConnector } = connector ?? {}; - - const references = createConnectorReference( - connectorId, - ACTION_SAVED_OBJECT_TYPE, - CONNECTOR_ID_REFERENCE_NAME - ); - - const { id: ignoreNoneId, ...restNoneConnector } = getNoneCaseConnector(); - const connectorFieldsToReturn = - connector && references.length > 0 ? restConnector : restNoneConnector; - - return { - transformedConnector: { - connector: connectorFieldsToReturn, - }, - references, - }; -}; - -const createConnectorReference = ( - id: string | null | undefined, - type: string, - name: string -): SavedObjectReference[] => { - return id && id !== noneConnectorId - ? [ - { - id, - type, - name, - }, - ] - : []; -}; - -export const transformPushConnectorIdToReference = ( - external_service?: { connector_id?: string | null } | null -): { transformedPushConnector: Record; references: SavedObjectReference[] } => { - const { connector_id: pushConnectorId, ...restExternalService } = external_service ?? {}; - - const references = createConnectorReference( - pushConnectorId, - ACTION_SAVED_OBJECT_TYPE, - PUSH_CONNECTOR_ID_REFERENCE_NAME - ); - - return { - transformedPushConnector: { external_service: external_service ? restExternalService : null }, - references, - }; -}; diff --git a/x-pack/plugins/cases/server/services/cases/index.test.ts b/x-pack/plugins/cases/server/services/cases/index.test.ts index 18f4ff867cfa9..8c71abe5bff4f 100644 --- a/x-pack/plugins/cases/server/services/cases/index.test.ts +++ b/x-pack/plugins/cases/server/services/cases/index.test.ts @@ -40,6 +40,7 @@ import { createSavedObjectReferences, createCaseSavedObjectResponse, basicCaseFields, + createSOFindResponse, } from '../test_utils'; import { ESCaseAttributes } from './types'; @@ -87,13 +88,6 @@ const createFindSO = ( score: 0, }); -const createSOFindResponse = (savedObjects: Array>) => ({ - saved_objects: savedObjects, - total: savedObjects.length, - per_page: savedObjects.length, - page: 1, -}); - const createCaseUpdateParams = ( connector?: CaseConnector, externalService?: CaseFullExternalService diff --git a/x-pack/plugins/cases/server/services/test_utils.ts b/x-pack/plugins/cases/server/services/test_utils.ts index b712ea07f9c71..07743eda61212 100644 --- a/x-pack/plugins/cases/server/services/test_utils.ts +++ b/x-pack/plugins/cases/server/services/test_utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SavedObject, SavedObjectReference } from 'kibana/server'; +import { SavedObject, SavedObjectReference, SavedObjectsFindResult } from 'kibana/server'; import { ESConnectorFields } from '.'; import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME } from '../common'; import { @@ -54,7 +54,7 @@ export const createESJiraConnector = ( { key: 'parent', value: '2' }, ], type: ConnectorTypes.jira, - ...(overrides && { ...overrides }), + ...overrides, }; }; @@ -94,7 +94,7 @@ export const createExternalService = ( email: 'testemail@elastic.co', username: 'elastic', }, - ...(overrides && { ...overrides }), + ...overrides, }); export const basicCaseFields = { @@ -198,3 +198,14 @@ export const createSavedObjectReferences = ({ ] : []), ]; + +export const createConnectorObject = (overrides?: Partial) => ({ + connector: { ...createJiraConnector(), ...overrides }, +}); + +export const createSOFindResponse = (savedObjects: Array>) => ({ + saved_objects: savedObjects, + total: savedObjects.length, + per_page: savedObjects.length, + page: 1, +}); diff --git a/x-pack/plugins/cases/server/services/user_actions/helpers.test.ts b/x-pack/plugins/cases/server/services/user_actions/helpers.test.ts new file mode 100644 index 0000000000000..7bcbaf58d0f6e --- /dev/null +++ b/x-pack/plugins/cases/server/services/user_actions/helpers.test.ts @@ -0,0 +1,332 @@ +/* + * 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 { UserActionField } from '../../../common'; +import { createConnectorObject, createExternalService, createJiraConnector } from '../test_utils'; +import { buildCaseUserActionItem } from './helpers'; + +const defaultFields = () => ({ + actionAt: 'now', + actionBy: { + email: 'a', + full_name: 'j', + username: '1', + }, + caseId: '300', + owner: 'securitySolution', +}); + +describe('user action helpers', () => { + describe('buildCaseUserActionItem', () => { + describe('push user action', () => { + it('extracts the external_service connector_id to references for a new pushed user action', () => { + const userAction = buildCaseUserActionItem({ + ...defaultFields(), + action: 'push-to-service', + fields: ['pushed'], + newValue: createExternalService(), + }); + + const parsedExternalService = JSON.parse(userAction.attributes.new_value!); + expect(parsedExternalService).not.toHaveProperty('connector_id'); + expect(parsedExternalService).toMatchInlineSnapshot(` + Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + + expect(userAction.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "300", + "name": "associated-cases", + "type": "cases", + }, + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); + + expect(userAction.attributes.old_value).toBeNull(); + }); + + it('extract the external_service connector_id to references for new and old pushed user action', () => { + const userAction = buildCaseUserActionItem({ + ...defaultFields(), + action: 'push-to-service', + fields: ['pushed'], + newValue: createExternalService(), + oldValue: createExternalService({ connector_id: '5' }), + }); + + const parsedNewExternalService = JSON.parse(userAction.attributes.new_value!); + const parsedOldExternalService = JSON.parse(userAction.attributes.old_value!); + + expect(parsedNewExternalService).not.toHaveProperty('connector_id'); + expect(parsedOldExternalService).not.toHaveProperty('connector_id'); + expect(userAction.references).toEqual([ + { id: '300', name: 'associated-cases', type: 'cases' }, + { id: '100', name: 'pushConnectorId', type: 'action' }, + { id: '5', name: 'oldPushConnectorId', type: 'action' }, + ]); + }); + + it('leaves the object unmodified when it is not a valid push user action', () => { + const userAction = buildCaseUserActionItem({ + ...defaultFields(), + action: 'push-to-service', + fields: ['invalid field'] as unknown as UserActionField, + newValue: 'hello' as unknown as Record, + }); + + expect(userAction.attributes.old_value).toBeNull(); + expect(userAction).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "action": "push-to-service", + "action_at": "now", + "action_by": Object { + "email": "a", + "full_name": "j", + "username": "1", + }, + "action_field": Array [ + "invalid field", + ], + "new_value": "hello", + "old_value": null, + "owner": "securitySolution", + }, + "references": Array [ + Object { + "id": "300", + "name": "associated-cases", + "type": "cases", + }, + ], + } + `); + }); + }); + + describe('update connector user action', () => { + it('extracts the connector id to references for a new create connector user action', () => { + const userAction = buildCaseUserActionItem({ + ...defaultFields(), + action: 'update', + fields: ['connector'], + newValue: createJiraConnector(), + }); + + const parsedConnector = JSON.parse(userAction.attributes.new_value!); + expect(parsedConnector).not.toHaveProperty('id'); + expect(parsedConnector).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + } + `); + + expect(userAction.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "300", + "name": "associated-cases", + "type": "cases", + }, + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + + expect(userAction.attributes.old_value).toBeNull(); + }); + + it('extracts the connector id to references for a new and old create connector user action', () => { + const userAction = buildCaseUserActionItem({ + ...defaultFields(), + action: 'update', + fields: ['connector'], + newValue: createJiraConnector(), + oldValue: { ...createJiraConnector(), id: '5' }, + }); + + const parsedNewConnector = JSON.parse(userAction.attributes.new_value!); + const parsedOldConnector = JSON.parse(userAction.attributes.new_value!); + + expect(parsedNewConnector).not.toHaveProperty('id'); + expect(parsedOldConnector).not.toHaveProperty('id'); + + expect(userAction.references).toEqual([ + { id: '300', name: 'associated-cases', type: 'cases' }, + { id: '1', name: 'connectorId', type: 'action' }, + { id: '5', name: 'oldConnectorId', type: 'action' }, + ]); + }); + + it('leaves the object unmodified when it is not a valid create connector user action', () => { + const userAction = buildCaseUserActionItem({ + ...defaultFields(), + action: 'update', + fields: ['invalid field'] as unknown as UserActionField, + newValue: 'hello' as unknown as Record, + oldValue: 'old value' as unknown as Record, + }); + + expect(userAction).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "action": "update", + "action_at": "now", + "action_by": Object { + "email": "a", + "full_name": "j", + "username": "1", + }, + "action_field": Array [ + "invalid field", + ], + "new_value": "hello", + "old_value": "old value", + "owner": "securitySolution", + }, + "references": Array [ + Object { + "id": "300", + "name": "associated-cases", + "type": "cases", + }, + ], + } + `); + }); + }); + + describe('create connector user action', () => { + it('extracts the connector id to references for a new create connector user action', () => { + const userAction = buildCaseUserActionItem({ + ...defaultFields(), + action: 'create', + fields: ['connector'], + newValue: createConnectorObject(), + }); + + const parsedConnector = JSON.parse(userAction.attributes.new_value!); + expect(parsedConnector.connector).not.toHaveProperty('id'); + expect(parsedConnector).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + }, + } + `); + + expect(userAction.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "300", + "name": "associated-cases", + "type": "cases", + }, + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + + expect(userAction.attributes.old_value).toBeNull(); + }); + + it('extracts the connector id to references for a new and old create connector user action', () => { + const userAction = buildCaseUserActionItem({ + ...defaultFields(), + action: 'create', + fields: ['connector'], + newValue: createConnectorObject(), + oldValue: createConnectorObject({ id: '5' }), + }); + + const parsedNewConnector = JSON.parse(userAction.attributes.new_value!); + const parsedOldConnector = JSON.parse(userAction.attributes.new_value!); + + expect(parsedNewConnector.connector).not.toHaveProperty('id'); + expect(parsedOldConnector.connector).not.toHaveProperty('id'); + + expect(userAction.references).toEqual([ + { id: '300', name: 'associated-cases', type: 'cases' }, + { id: '1', name: 'connectorId', type: 'action' }, + { id: '5', name: 'oldConnectorId', type: 'action' }, + ]); + }); + + it('leaves the object unmodified when it is not a valid create connector user action', () => { + const userAction = buildCaseUserActionItem({ + ...defaultFields(), + action: 'create', + fields: ['invalid action'] as unknown as UserActionField, + newValue: 'new json value' as unknown as Record, + oldValue: 'old value' as unknown as Record, + }); + + expect(userAction).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "action": "create", + "action_at": "now", + "action_by": Object { + "email": "a", + "full_name": "j", + "username": "1", + }, + "action_field": Array [ + "invalid action", + ], + "new_value": "new json value", + "old_value": "old value", + "owner": "securitySolution", + }, + "references": Array [ + Object { + "id": "300", + "name": "associated-cases", + "type": "cases", + }, + ], + } + `); + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/services/user_actions/helpers.ts b/x-pack/plugins/cases/server/services/user_actions/helpers.ts index 223e731aa8d9b..e91b69f0995bd 100644 --- a/x-pack/plugins/cases/server/services/user_actions/helpers.ts +++ b/x-pack/plugins/cases/server/services/user_actions/helpers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SavedObject, SavedObjectsUpdateResponse } from 'kibana/server'; +import { SavedObject, SavedObjectReference, SavedObjectsUpdateResponse } from 'kibana/server'; import { get, isPlainObject, isString } from 'lodash'; import deepEqual from 'fast-deep-equal'; @@ -23,8 +23,68 @@ import { } from '../../../common'; import { isTwoArraysDifference } from '../../client/utils'; import { UserActionItem } from '.'; +import { extractConnectorId } from './transform'; +import { UserActionFieldType } from './types'; +import { CASE_REF_NAME, COMMENT_REF_NAME, SUB_CASE_REF_NAME } from '../../common'; -export const transformNewUserAction = ({ +interface BuildCaseUserActionParams { + action: UserAction; + actionAt: string; + actionBy: User; + caseId: string; + owner: string; + fields: UserActionField; + newValue?: Record | string | null; + oldValue?: Record | string | null; + subCaseId?: string; +} + +export const buildCaseUserActionItem = ({ + action, + actionAt, + actionBy, + caseId, + fields, + newValue, + oldValue, + subCaseId, + owner, +}: BuildCaseUserActionParams): UserActionItem => { + const { transformedActionDetails: transformedNewValue, references: newValueReferences } = + extractConnectorId({ + action, + actionFields: fields, + actionDetails: newValue, + fieldType: UserActionFieldType.New, + }); + + const { transformedActionDetails: transformedOldValue, references: oldValueReferences } = + extractConnectorId({ + action, + actionFields: fields, + actionDetails: oldValue, + fieldType: UserActionFieldType.Old, + }); + + return { + attributes: transformNewUserAction({ + actionField: fields, + action, + actionAt, + owner, + ...actionBy, + newValue: transformedNewValue, + oldValue: transformedOldValue, + }), + references: [ + ...createCaseReferences(caseId, subCaseId), + ...newValueReferences, + ...oldValueReferences, + ], + }; +}; + +const transformNewUserAction = ({ actionField, action, actionAt, @@ -55,103 +115,43 @@ export const transformNewUserAction = ({ owner, }); -interface BuildCaseUserAction { - action: UserAction; - actionAt: string; - actionBy: User; - caseId: string; - owner: string; - fields: UserActionField | unknown[]; - newValue?: string | unknown; - oldValue?: string | unknown; - subCaseId?: string; -} +const createCaseReferences = (caseId: string, subCaseId?: string): SavedObjectReference[] => [ + { + type: CASE_SAVED_OBJECT, + name: CASE_REF_NAME, + id: caseId, + }, + ...(subCaseId + ? [ + { + type: SUB_CASE_SAVED_OBJECT, + name: SUB_CASE_REF_NAME, + id: subCaseId, + }, + ] + : []), +]; -interface BuildCommentUserActionItem extends BuildCaseUserAction { +interface BuildCommentUserActionItem extends BuildCaseUserActionParams { commentId: string; } -export const buildCommentUserActionItem = ({ - action, - actionAt, - actionBy, - caseId, - commentId, - fields, - newValue, - oldValue, - subCaseId, - owner, -}: BuildCommentUserActionItem): UserActionItem => ({ - attributes: transformNewUserAction({ - actionField: fields as UserActionField, - action, - actionAt, - owner, - ...actionBy, - newValue: newValue as string, - oldValue: oldValue as string, - }), - references: [ - { - type: CASE_SAVED_OBJECT, - name: `associated-${CASE_SAVED_OBJECT}`, - id: caseId, - }, - { - type: CASE_COMMENT_SAVED_OBJECT, - name: `associated-${CASE_COMMENT_SAVED_OBJECT}`, - id: commentId, - }, - ...(subCaseId - ? [ - { - type: SUB_CASE_SAVED_OBJECT, - id: subCaseId, - name: `associated-${SUB_CASE_SAVED_OBJECT}`, - }, - ] - : []), - ], -}); +export const buildCommentUserActionItem = (params: BuildCommentUserActionItem): UserActionItem => { + const { commentId } = params; + const { attributes, references } = buildCaseUserActionItem(params); -export const buildCaseUserActionItem = ({ - action, - actionAt, - actionBy, - caseId, - fields, - newValue, - oldValue, - subCaseId, - owner, -}: BuildCaseUserAction): UserActionItem => ({ - attributes: transformNewUserAction({ - actionField: fields as UserActionField, - action, - actionAt, - owner, - ...actionBy, - newValue: newValue as string, - oldValue: oldValue as string, - }), - references: [ - { - type: CASE_SAVED_OBJECT, - name: `associated-${CASE_SAVED_OBJECT}`, - id: caseId, - }, - ...(subCaseId - ? [ - { - type: SUB_CASE_SAVED_OBJECT, - name: `associated-${SUB_CASE_SAVED_OBJECT}`, - id: subCaseId, - }, - ] - : []), - ], -}); + return { + attributes, + references: [ + ...references, + { + type: CASE_COMMENT_SAVED_OBJECT, + name: COMMENT_REF_NAME, + id: commentId, + }, + ], + }; +}; const userActionFieldsAllowed: UserActionField = [ 'comment', @@ -278,8 +278,8 @@ const buildGenericCaseUserActions = ({ caseId, subCaseId, fields: [field], - newValue: JSON.stringify(updatedValue), - oldValue: JSON.stringify(origValue), + newValue: updatedValue, + oldValue: origValue, owner: originalItem.attributes.owner, }), ]; diff --git a/x-pack/plugins/cases/server/services/user_actions/index.test.ts b/x-pack/plugins/cases/server/services/user_actions/index.test.ts new file mode 100644 index 0000000000000..c4a350f4ac015 --- /dev/null +++ b/x-pack/plugins/cases/server/services/user_actions/index.test.ts @@ -0,0 +1,557 @@ +/* + * 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 { SavedObject, SavedObjectsFindResult } from 'kibana/server'; +import { transformFindResponseToExternalModel, UserActionItem } from '.'; +import { + CaseUserActionAttributes, + CASE_USER_ACTION_SAVED_OBJECT, + UserAction, + UserActionField, +} from '../../../common'; + +import { + createConnectorObject, + createExternalService, + createJiraConnector, + createSOFindResponse, +} from '../test_utils'; +import { buildCaseUserActionItem, buildCommentUserActionItem } from './helpers'; + +const createConnectorUserAction = ( + subCaseId?: string, + overrides?: Partial +): SavedObject => { + return { + ...createUserActionSO({ + action: 'create', + fields: ['connector'], + newValue: createConnectorObject(), + subCaseId, + }), + ...(overrides && { ...overrides }), + }; +}; + +const updateConnectorUserAction = ({ + subCaseId, + overrides, + oldValue, +}: { + subCaseId?: string; + overrides?: Partial; + oldValue?: string | null | Record; +} = {}): SavedObject => { + return { + ...createUserActionSO({ + action: 'update', + fields: ['connector'], + newValue: createJiraConnector(), + oldValue, + subCaseId, + }), + ...(overrides && { ...overrides }), + }; +}; + +const pushConnectorUserAction = ({ + subCaseId, + overrides, + oldValue, +}: { + subCaseId?: string; + overrides?: Partial; + oldValue?: string | null | Record; +} = {}): SavedObject => { + return { + ...createUserActionSO({ + action: 'push-to-service', + fields: ['pushed'], + newValue: createExternalService(), + oldValue, + subCaseId, + }), + ...(overrides && { ...overrides }), + }; +}; + +const createUserActionFindSO = ( + userAction: SavedObject +): SavedObjectsFindResult => ({ + ...userAction, + score: 0, +}); + +const createUserActionSO = ({ + action, + fields, + subCaseId, + newValue, + oldValue, + attributesOverrides, + commentId, +}: { + action: UserAction; + fields: UserActionField; + subCaseId?: string; + newValue?: string | null | Record; + oldValue?: string | null | Record; + attributesOverrides?: Partial; + commentId?: string; +}): SavedObject => { + const defaultParams = { + action, + actionAt: 'abc', + actionBy: { + email: 'a', + username: 'b', + full_name: 'abc', + }, + caseId: '1', + subCaseId, + fields, + newValue, + oldValue, + owner: 'securitySolution', + }; + + let userAction: UserActionItem; + + if (commentId) { + userAction = buildCommentUserActionItem({ + commentId, + ...defaultParams, + }); + } else { + userAction = buildCaseUserActionItem(defaultParams); + } + + return { + type: CASE_USER_ACTION_SAVED_OBJECT, + id: '100', + attributes: { + ...userAction.attributes, + ...(attributesOverrides && { ...attributesOverrides }), + }, + references: userAction.references, + }; +}; + +describe('CaseUserActionService', () => { + describe('transformFindResponseToExternalModel', () => { + it('does not populate the ids when the response is an empty array', () => { + expect(transformFindResponseToExternalModel(createSOFindResponse([]))).toMatchInlineSnapshot(` + Object { + "page": 1, + "per_page": 0, + "saved_objects": Array [], + "total": 0, + } + `); + }); + + it('preserves the saved object fields and attributes when inject the ids', () => { + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(createConnectorUserAction())]) + ); + + expect(transformed).toMatchInlineSnapshot(` + Object { + "page": 1, + "per_page": 1, + "saved_objects": Array [ + Object { + "attributes": Object { + "action": "create", + "action_at": "abc", + "action_by": Object { + "email": "a", + "full_name": "abc", + "username": "b", + }, + "action_field": Array [ + "connector", + ], + "action_id": "100", + "case_id": "1", + "comment_id": null, + "new_val_connector_id": "1", + "new_value": "{\\"connector\\":{\\"name\\":\\".jira\\",\\"type\\":\\".jira\\",\\"fields\\":{\\"issueType\\":\\"bug\\",\\"priority\\":\\"high\\",\\"parent\\":\\"2\\"}}}", + "old_val_connector_id": null, + "old_value": null, + "owner": "securitySolution", + "sub_case_id": "", + }, + "id": "100", + "references": Array [ + Object { + "id": "1", + "name": "associated-cases", + "type": "cases", + }, + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ], + "score": 0, + "type": "cases-user-actions", + }, + ], + "total": 1, + } + `); + }); + + it('populates the new_val_connector_id for multiple user actions', () => { + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([ + createUserActionFindSO(createConnectorUserAction()), + createUserActionFindSO(createConnectorUserAction()), + ]) + ); + + expect(transformed.saved_objects[0].attributes.new_val_connector_id).toEqual('1'); + expect(transformed.saved_objects[1].attributes.new_val_connector_id).toEqual('1'); + }); + + it('populates the old_val_connector_id for multiple user actions', () => { + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([ + createUserActionFindSO( + createUserActionSO({ + action: 'create', + fields: ['connector'], + oldValue: createConnectorObject(), + }) + ), + createUserActionFindSO( + createUserActionSO({ + action: 'create', + fields: ['connector'], + oldValue: createConnectorObject({ id: '10' }), + }) + ), + ]) + ); + + expect(transformed.saved_objects[0].attributes.old_val_connector_id).toEqual('1'); + expect(transformed.saved_objects[1].attributes.old_val_connector_id).toEqual('10'); + }); + + describe('reference ids', () => { + it('sets case_id to an empty string when it cannot find the reference', () => { + const userAction = { + ...createConnectorUserAction(), + references: [], + }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.case_id).toEqual(''); + }); + + it('sets comment_id to null when it cannot find the reference', () => { + const userAction = { + ...createUserActionSO({ action: 'create', fields: ['connector'], commentId: '5' }), + references: [], + }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.comment_id).toBeNull(); + }); + + it('sets sub_case_id to an empty string when it cannot find the reference', () => { + const userAction = { + ...createUserActionSO({ action: 'create', fields: ['connector'], subCaseId: '5' }), + references: [], + }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.comment_id).toBeNull(); + }); + + it('sets case_id correctly when it finds the reference', () => { + const userAction = createConnectorUserAction(); + + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.case_id).toEqual('1'); + }); + + it('sets comment_id correctly when it finds the reference', () => { + const userAction = createUserActionSO({ + action: 'create', + fields: ['connector'], + commentId: '5', + }); + + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.comment_id).toEqual('5'); + }); + + it('sets sub_case_id correctly when it finds the reference', () => { + const userAction = { + ...createUserActionSO({ action: 'create', fields: ['connector'], subCaseId: '5' }), + }; + + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.sub_case_id).toEqual('5'); + }); + + it('sets action_id correctly to the saved object id', () => { + const userAction = { + ...createUserActionSO({ action: 'create', fields: ['connector'], subCaseId: '5' }), + }; + + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.action_id).toEqual('100'); + }); + }); + + describe('create connector', () => { + it('does not populate the new_val_connector_id when it cannot find the reference', () => { + const userAction = { ...createConnectorUserAction(), references: [] }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.new_val_connector_id).toBeNull(); + }); + + it('does not populate the old_val_connector_id when it cannot find the reference', () => { + const userAction = { ...createConnectorUserAction(), references: [] }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.old_val_connector_id).toBeNull(); + }); + + it('does not populate the new_val_connector_id when the reference exists but the action and fields are invalid', () => { + const validUserAction = createConnectorUserAction(); + const invalidUserAction = { + ...validUserAction, + attributes: { ...validUserAction.attributes, action: 'invalid' }, + }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([ + createUserActionFindSO(invalidUserAction as SavedObject), + ]) + ); + + expect(transformed.saved_objects[0].attributes.new_val_connector_id).toBeNull(); + }); + + it('does not populate the old_val_connector_id when the reference exists but the action and fields are invalid', () => { + const validUserAction = createUserActionSO({ + action: 'create', + fields: ['connector'], + oldValue: createConnectorObject(), + }); + + const invalidUserAction = { + ...validUserAction, + attributes: { ...validUserAction.attributes, action: 'invalid' }, + }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([ + createUserActionFindSO(invalidUserAction as SavedObject), + ]) + ); + + expect(transformed.saved_objects[0].attributes.old_val_connector_id).toBeNull(); + }); + + it('populates the new_val_connector_id', () => { + const userAction = createConnectorUserAction(); + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.new_val_connector_id).toEqual('1'); + }); + + it('populates the old_val_connector_id', () => { + const userAction = createUserActionSO({ + action: 'create', + fields: ['connector'], + oldValue: createConnectorObject(), + }); + + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.old_val_connector_id).toEqual('1'); + }); + }); + + describe('update connector', () => { + it('does not populate the new_val_connector_id when it cannot find the reference', () => { + const userAction = { ...updateConnectorUserAction(), references: [] }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.new_val_connector_id).toBeNull(); + }); + + it('does not populate the old_val_connector_id when it cannot find the reference', () => { + const userAction = { + ...updateConnectorUserAction({ oldValue: createJiraConnector() }), + references: [], + }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.old_val_connector_id).toBeNull(); + }); + + it('does not populate the new_val_connector_id when the reference exists but the action and fields are invalid', () => { + const validUserAction = updateConnectorUserAction(); + const invalidUserAction = { + ...validUserAction, + attributes: { ...validUserAction.attributes, action: 'invalid' }, + }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([ + createUserActionFindSO(invalidUserAction as SavedObject), + ]) + ); + + expect(transformed.saved_objects[0].attributes.new_val_connector_id).toBeNull(); + }); + + it('does not populate the old_val_connector_id when the reference exists but the action and fields are invalid', () => { + const validUserAction = updateConnectorUserAction({ oldValue: createJiraConnector() }); + + const invalidUserAction = { + ...validUserAction, + attributes: { ...validUserAction.attributes, action: 'invalid' }, + }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([ + createUserActionFindSO(invalidUserAction as SavedObject), + ]) + ); + + expect(transformed.saved_objects[0].attributes.old_val_connector_id).toBeNull(); + }); + + it('populates the new_val_connector_id', () => { + const userAction = updateConnectorUserAction(); + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.new_val_connector_id).toEqual('1'); + }); + + it('populates the old_val_connector_id', () => { + const userAction = updateConnectorUserAction({ oldValue: createJiraConnector() }); + + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.old_val_connector_id).toEqual('1'); + }); + }); + + describe('push connector', () => { + it('does not populate the new_val_connector_id when it cannot find the reference', () => { + const userAction = { ...pushConnectorUserAction(), references: [] }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.new_val_connector_id).toBeNull(); + }); + + it('does not populate the old_val_connector_id when it cannot find the reference', () => { + const userAction = { + ...pushConnectorUserAction({ oldValue: createExternalService() }), + references: [], + }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.old_val_connector_id).toBeNull(); + }); + + it('does not populate the new_val_connector_id when the reference exists but the action and fields are invalid', () => { + const validUserAction = pushConnectorUserAction(); + const invalidUserAction = { + ...validUserAction, + attributes: { ...validUserAction.attributes, action: 'invalid' }, + }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([ + createUserActionFindSO(invalidUserAction as SavedObject), + ]) + ); + + expect(transformed.saved_objects[0].attributes.new_val_connector_id).toBeNull(); + }); + + it('does not populate the old_val_connector_id when the reference exists but the action and fields are invalid', () => { + const validUserAction = pushConnectorUserAction({ oldValue: createExternalService() }); + + const invalidUserAction = { + ...validUserAction, + attributes: { ...validUserAction.attributes, action: 'invalid' }, + }; + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([ + createUserActionFindSO(invalidUserAction as SavedObject), + ]) + ); + + expect(transformed.saved_objects[0].attributes.old_val_connector_id).toBeNull(); + }); + + it('populates the new_val_connector_id', () => { + const userAction = pushConnectorUserAction(); + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.new_val_connector_id).toEqual('100'); + }); + + it('populates the old_val_connector_id', () => { + const userAction = pushConnectorUserAction({ oldValue: createExternalService() }); + + const transformed = transformFindResponseToExternalModel( + createSOFindResponse([createUserActionFindSO(userAction)]) + ); + + expect(transformed.saved_objects[0].attributes.old_val_connector_id).toEqual('100'); + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/services/user_actions/index.ts b/x-pack/plugins/cases/server/services/user_actions/index.ts index b702448165554..4f158862e3d63 100644 --- a/x-pack/plugins/cases/server/services/user_actions/index.ts +++ b/x-pack/plugins/cases/server/services/user_actions/index.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { Logger, SavedObjectReference } from 'kibana/server'; +import { + Logger, + SavedObjectReference, + SavedObjectsFindResponse, + SavedObjectsFindResult, +} from 'kibana/server'; import { CASE_SAVED_OBJECT, @@ -13,8 +18,17 @@ import { CaseUserActionAttributes, MAX_DOCS_PER_PAGE, SUB_CASE_SAVED_OBJECT, + CaseUserActionResponse, + CASE_COMMENT_SAVED_OBJECT, + isCreateConnector, + isPush, + isUpdateConnector, } from '../../../common'; import { ClientArgs } from '..'; +import { UserActionFieldType } from './types'; +import { CASE_REF_NAME, COMMENT_REF_NAME, SUB_CASE_REF_NAME } from '../../common'; +import { ConnectorIdReferenceName, PushConnectorIdReferenceName } from './transform'; +import { findConnectorIdReference } from '../transform'; interface GetCaseUserActionArgs extends ClientArgs { caseId: string; @@ -33,12 +47,16 @@ interface PostCaseUserActionArgs extends ClientArgs { export class CaseUserActionService { constructor(private readonly log: Logger) {} - public async getAll({ unsecuredSavedObjectsClient, caseId, subCaseId }: GetCaseUserActionArgs) { + public async getAll({ + unsecuredSavedObjectsClient, + caseId, + subCaseId, + }: GetCaseUserActionArgs): Promise> { try { const id = subCaseId ?? caseId; const type = subCaseId ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; - return await unsecuredSavedObjectsClient.find({ + const userActions = await unsecuredSavedObjectsClient.find({ type: CASE_USER_ACTION_SAVED_OBJECT, hasReference: { type, id }, page: 1, @@ -46,17 +64,22 @@ export class CaseUserActionService { sortField: 'action_at', sortOrder: 'asc', }); + + return transformFindResponseToExternalModel(userActions); } catch (error) { this.log.error(`Error on GET case user action case id: ${caseId}: ${error}`); throw error; } } - public async bulkCreate({ unsecuredSavedObjectsClient, actions }: PostCaseUserActionArgs) { + public async bulkCreate({ + unsecuredSavedObjectsClient, + actions, + }: PostCaseUserActionArgs): Promise { try { this.log.debug(`Attempting to POST a new case user action`); - return await unsecuredSavedObjectsClient.bulkCreate( + await unsecuredSavedObjectsClient.bulkCreate( actions.map((action) => ({ type: CASE_USER_ACTION_SAVED_OBJECT, ...action })) ); } catch (error) { @@ -65,3 +88,71 @@ export class CaseUserActionService { } } } + +export function transformFindResponseToExternalModel( + userActions: SavedObjectsFindResponse +): SavedObjectsFindResponse { + return { + ...userActions, + saved_objects: userActions.saved_objects.map((so) => ({ + ...so, + ...transformToExternalModel(so), + })), + }; +} + +function transformToExternalModel( + userAction: SavedObjectsFindResult +): SavedObjectsFindResult { + const { references } = userAction; + + const newValueConnectorId = getConnectorIdFromReferences(UserActionFieldType.New, userAction); + const oldValueConnectorId = getConnectorIdFromReferences(UserActionFieldType.Old, userAction); + + const caseId = findReferenceId(CASE_REF_NAME, CASE_SAVED_OBJECT, references) ?? ''; + const commentId = + findReferenceId(COMMENT_REF_NAME, CASE_COMMENT_SAVED_OBJECT, references) ?? null; + const subCaseId = findReferenceId(SUB_CASE_REF_NAME, SUB_CASE_SAVED_OBJECT, references) ?? ''; + + return { + ...userAction, + attributes: { + ...userAction.attributes, + action_id: userAction.id, + case_id: caseId, + comment_id: commentId, + sub_case_id: subCaseId, + new_val_connector_id: newValueConnectorId, + old_val_connector_id: oldValueConnectorId, + }, + }; +} + +function getConnectorIdFromReferences( + fieldType: UserActionFieldType, + userAction: SavedObjectsFindResult +): string | null { + const { + // eslint-disable-next-line @typescript-eslint/naming-convention + attributes: { action, action_field }, + references, + } = userAction; + + if (isCreateConnector(action, action_field) || isUpdateConnector(action, action_field)) { + return findConnectorIdReference(ConnectorIdReferenceName[fieldType], references)?.id ?? null; + } else if (isPush(action, action_field)) { + return ( + findConnectorIdReference(PushConnectorIdReferenceName[fieldType], references)?.id ?? null + ); + } + + return null; +} + +function findReferenceId( + name: string, + type: string, + references: SavedObjectReference[] +): string | undefined { + return references.find((ref) => ref.name === name && ref.type === type)?.id; +} diff --git a/x-pack/plugins/cases/server/services/user_actions/transform.test.ts b/x-pack/plugins/cases/server/services/user_actions/transform.test.ts new file mode 100644 index 0000000000000..2d28770617094 --- /dev/null +++ b/x-pack/plugins/cases/server/services/user_actions/transform.test.ts @@ -0,0 +1,1246 @@ +/* + * 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 { noneConnectorId } from '../../../common'; +import { + CONNECTOR_ID_REFERENCE_NAME, + getNoneCaseConnector, + PUSH_CONNECTOR_ID_REFERENCE_NAME, + USER_ACTION_OLD_ID_REF_NAME, + USER_ACTION_OLD_PUSH_ID_REF_NAME, +} from '../../common'; +import { createConnectorObject, createExternalService, createJiraConnector } from '../test_utils'; +import { + extractConnectorIdHelper, + extractConnectorIdFromJson, + extractConnectorId, + transformConnectorIdToReference, + transformPushConnectorIdToReference, +} from './transform'; +import { UserActionFieldType } from './types'; + +describe('user action transform utils', () => { + describe('transformConnectorIdToReference', () => { + it('returns the default none connector when the connector is undefined', () => { + expect(transformConnectorIdToReference(CONNECTOR_ID_REFERENCE_NAME).transformedConnector) + .toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": null, + "name": "none", + "type": ".none", + }, + } + `); + }); + + it('returns the default none connector when the id is undefined', () => { + expect( + transformConnectorIdToReference(CONNECTOR_ID_REFERENCE_NAME, { id: undefined }) + .transformedConnector + ).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": null, + "name": "none", + "type": ".none", + }, + } + `); + }); + + it('returns the default none connector when the id is none', () => { + expect( + transformConnectorIdToReference(CONNECTOR_ID_REFERENCE_NAME, { id: noneConnectorId }) + .transformedConnector + ).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": null, + "name": "none", + "type": ".none", + }, + } + `); + }); + + it('returns the default none connector when the id is none and other fields are defined', () => { + expect( + transformConnectorIdToReference(CONNECTOR_ID_REFERENCE_NAME, { + ...createJiraConnector(), + id: noneConnectorId, + }).transformedConnector + ).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": null, + "name": "none", + "type": ".none", + }, + } + `); + }); + + it('returns an empty array of references when the connector is undefined', () => { + expect(transformConnectorIdToReference(CONNECTOR_ID_REFERENCE_NAME).references.length).toBe( + 0 + ); + }); + + it('returns an empty array of references when the id is undefined', () => { + expect( + transformConnectorIdToReference(CONNECTOR_ID_REFERENCE_NAME, { id: undefined }).references + .length + ).toBe(0); + }); + + it('returns an empty array of references when the id is the none connector', () => { + expect( + transformConnectorIdToReference(CONNECTOR_ID_REFERENCE_NAME, { id: noneConnectorId }) + .references.length + ).toBe(0); + }); + + it('returns an empty array of references when the id is the none connector and other fields are defined', () => { + expect( + transformConnectorIdToReference(CONNECTOR_ID_REFERENCE_NAME, { + ...createJiraConnector(), + id: noneConnectorId, + }).references.length + ).toBe(0); + }); + + it('returns a jira connector', () => { + const transformedFields = transformConnectorIdToReference( + CONNECTOR_ID_REFERENCE_NAME, + createJiraConnector() + ); + expect(transformedFields.transformedConnector).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + }, + } + `); + expect(transformedFields.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + }); + + it('returns a jira connector with the user action reference name', () => { + const transformedFields = transformConnectorIdToReference( + USER_ACTION_OLD_ID_REF_NAME, + createJiraConnector() + ); + expect(transformedFields.transformedConnector).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + }, + } + `); + expect(transformedFields.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "oldConnectorId", + "type": "action", + }, + ] + `); + }); + }); + + describe('transformPushConnectorIdToReference', () => { + it('sets external_service to null when it is undefined', () => { + expect( + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME) + .transformedPushConnector + ).toMatchInlineSnapshot(` + Object { + "external_service": null, + } + `); + }); + + it('sets external_service to null when it is null', () => { + expect( + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, null) + .transformedPushConnector + ).toMatchInlineSnapshot(` + Object { + "external_service": null, + } + `); + }); + + it('returns an object when external_service is defined but connector_id is undefined', () => { + expect( + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, { + connector_id: undefined, + }).transformedPushConnector + ).toMatchInlineSnapshot(` + Object { + "external_service": Object {}, + } + `); + }); + + it('returns an object when external_service is defined but connector_id is null', () => { + expect( + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, { + connector_id: null, + }).transformedPushConnector + ).toMatchInlineSnapshot(` + Object { + "external_service": Object {}, + } + `); + }); + + it('returns an object when external_service is defined but connector_id is none', () => { + const otherFields = { otherField: 'hi' }; + + expect( + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, { + ...otherFields, + connector_id: noneConnectorId, + }).transformedPushConnector + ).toMatchInlineSnapshot(` + Object { + "external_service": Object { + "otherField": "hi", + }, + } + `); + }); + + it('returns an empty array of references when the external_service is undefined', () => { + expect( + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME).references.length + ).toBe(0); + }); + + it('returns an empty array of references when the external_service is null', () => { + expect( + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, null).references + .length + ).toBe(0); + }); + + it('returns an empty array of references when the connector_id is undefined', () => { + expect( + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, { + connector_id: undefined, + }).references.length + ).toBe(0); + }); + + it('returns an empty array of references when the connector_id is null', () => { + expect( + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, { + connector_id: undefined, + }).references.length + ).toBe(0); + }); + + it('returns an empty array of references when the connector_id is the none connector', () => { + expect( + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, { + connector_id: noneConnectorId, + }).references.length + ).toBe(0); + }); + + it('returns an empty array of references when the connector_id is the none connector and other fields are defined', () => { + expect( + transformPushConnectorIdToReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, { + ...createExternalService(), + connector_id: noneConnectorId, + }).references.length + ).toBe(0); + }); + + it('returns the external_service connector', () => { + const transformedFields = transformPushConnectorIdToReference( + PUSH_CONNECTOR_ID_REFERENCE_NAME, + createExternalService() + ); + expect(transformedFields.transformedPushConnector).toMatchInlineSnapshot(` + Object { + "external_service": Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + }, + } + `); + expect(transformedFields.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); + }); + + it('returns the external_service connector with a user actions reference name', () => { + const transformedFields = transformPushConnectorIdToReference( + USER_ACTION_OLD_PUSH_ID_REF_NAME, + createExternalService() + ); + + expect(transformedFields.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "100", + "name": "oldPushConnectorId", + "type": "action", + }, + ] + `); + }); + }); + + describe('extractConnectorIdHelper', () => { + it('throws an error when action details has a circular reference', () => { + const circularRef = { prop: {} }; + circularRef.prop = circularRef; + + expect(() => { + extractConnectorIdHelper({ + action: 'a', + actionFields: [], + actionDetails: circularRef, + fieldType: UserActionFieldType.New, + }); + }).toThrow(); + }); + + describe('create action', () => { + it('returns no references and untransformed json when actionDetails is not a valid connector', () => { + expect( + extractConnectorIdHelper({ + action: 'create', + actionFields: ['connector'], + actionDetails: { a: 'hello' }, + fieldType: UserActionFieldType.New, + }) + ).toMatchInlineSnapshot(` + Object { + "references": Array [], + "transformedActionDetails": "{\\"a\\":\\"hello\\"}", + } + `); + }); + + it('returns no references and untransformed json when the action is create and action fields does not contain connector', () => { + expect( + extractConnectorIdHelper({ + action: 'create', + actionFields: ['', 'something', 'onnector'], + actionDetails: { a: 'hello' }, + fieldType: UserActionFieldType.New, + }) + ).toMatchInlineSnapshot(` + Object { + "references": Array [], + "transformedActionDetails": "{\\"a\\":\\"hello\\"}", + } + `); + }); + + it('returns the stringified json without the id', () => { + const jiraConnector = createConnectorObject(); + + const { transformedActionDetails } = extractConnectorIdHelper({ + action: 'create', + actionFields: ['connector'], + actionDetails: jiraConnector, + fieldType: UserActionFieldType.New, + }); + + expect(JSON.parse(transformedActionDetails)).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + }, + } + `); + }); + + it('removes the connector.id when the connector is none', () => { + const connector = { connector: getNoneCaseConnector() }; + + const { transformedActionDetails } = extractConnectorIdHelper({ + action: 'create', + actionFields: ['connector'], + actionDetails: connector, + fieldType: UserActionFieldType.New, + })!; + + const parsedJson = JSON.parse(transformedActionDetails); + + expect(parsedJson.connector).not.toHaveProperty('id'); + expect(parsedJson).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": null, + "name": "none", + "type": ".none", + }, + } + `); + }); + + it('does not return a reference when the connector is none', () => { + const connector = { connector: getNoneCaseConnector() }; + + const { references } = extractConnectorIdHelper({ + action: 'create', + actionFields: ['connector'], + actionDetails: connector, + fieldType: UserActionFieldType.New, + })!; + + expect(references).toEqual([]); + }); + + it('returns a reference to the connector.id', () => { + const connector = createConnectorObject(); + + const { references } = extractConnectorIdHelper({ + action: 'create', + actionFields: ['connector'], + actionDetails: connector, + fieldType: UserActionFieldType.New, + })!; + + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + }); + + it('returns an old reference name to the connector.id', () => { + const connector = createConnectorObject(); + + const { references } = extractConnectorIdHelper({ + action: 'create', + actionFields: ['connector'], + actionDetails: connector, + fieldType: UserActionFieldType.Old, + })!; + + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "oldConnectorId", + "type": "action", + }, + ] + `); + }); + + it('returns the transformed connector and the description', () => { + const details = { ...createConnectorObject(), description: 'a description' }; + + const { transformedActionDetails } = extractConnectorIdHelper({ + action: 'create', + actionFields: ['connector'], + actionDetails: details, + fieldType: UserActionFieldType.Old, + })!; + + const parsedJson = JSON.parse(transformedActionDetails); + + expect(parsedJson).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + }, + "description": "a description", + } + `); + }); + }); + + describe('update action', () => { + it('returns no references and untransformed json when actionDetails is not a valid connector', () => { + expect( + extractConnectorIdHelper({ + action: 'update', + actionFields: ['connector'], + actionDetails: { a: 'hello' }, + fieldType: UserActionFieldType.New, + }) + ).toMatchInlineSnapshot(` + Object { + "references": Array [], + "transformedActionDetails": "{\\"a\\":\\"hello\\"}", + } + `); + }); + + it('returns no references and untransformed json when the action is update and action fields does not contain connector', () => { + expect( + extractConnectorIdHelper({ + action: 'update', + actionFields: ['', 'something', 'onnector'], + actionDetails: 5, + fieldType: UserActionFieldType.New, + }) + ).toMatchInlineSnapshot(` + Object { + "references": Array [], + "transformedActionDetails": "5", + } + `); + }); + + it('returns the stringified json without the id', () => { + const jiraConnector = createJiraConnector(); + + const { transformedActionDetails } = extractConnectorIdHelper({ + action: 'update', + actionFields: ['connector'], + actionDetails: jiraConnector, + fieldType: UserActionFieldType.New, + }); + + const transformedConnetor = JSON.parse(transformedActionDetails!); + expect(transformedConnetor).not.toHaveProperty('id'); + expect(transformedConnetor).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + } + `); + }); + + it('returns the stringified json without the id when the connector is none', () => { + const connector = getNoneCaseConnector(); + + const { transformedActionDetails } = extractConnectorIdHelper({ + action: 'update', + actionFields: ['connector'], + actionDetails: connector, + fieldType: UserActionFieldType.New, + }); + + const transformedConnetor = JSON.parse(transformedActionDetails); + expect(transformedConnetor).not.toHaveProperty('id'); + expect(transformedConnetor).toMatchInlineSnapshot(` + Object { + "fields": null, + "name": "none", + "type": ".none", + } + `); + }); + + it('returns a reference to the connector.id', () => { + const jiraConnector = createJiraConnector(); + + const { references } = extractConnectorIdHelper({ + action: 'update', + actionFields: ['connector'], + actionDetails: jiraConnector, + fieldType: UserActionFieldType.New, + })!; + + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + }); + + it('does not return a reference when the connector is none', () => { + const connector = getNoneCaseConnector(); + + const { references } = extractConnectorIdHelper({ + action: 'update', + actionFields: ['connector'], + actionDetails: connector, + fieldType: UserActionFieldType.New, + })!; + + expect(references).toEqual([]); + }); + + it('returns an old reference name to the connector.id', () => { + const jiraConnector = createJiraConnector(); + + const { references } = extractConnectorIdHelper({ + action: 'update', + actionFields: ['connector'], + actionDetails: jiraConnector, + fieldType: UserActionFieldType.Old, + })!; + + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "oldConnectorId", + "type": "action", + }, + ] + `); + }); + }); + + describe('push action', () => { + it('returns no references and untransformed json when actionDetails is not a valid external_service', () => { + expect( + extractConnectorIdHelper({ + action: 'push-to-service', + actionFields: ['pushed'], + actionDetails: { a: 'hello' }, + fieldType: UserActionFieldType.New, + }) + ).toMatchInlineSnapshot(` + Object { + "references": Array [], + "transformedActionDetails": "{\\"a\\":\\"hello\\"}", + } + `); + }); + + it('returns no references and untransformed json when the action is push-to-service and action fields does not contain pushed', () => { + expect( + extractConnectorIdHelper({ + action: 'push-to-service', + actionFields: ['', 'something', 'ushed'], + actionDetails: { a: 'hello' }, + fieldType: UserActionFieldType.New, + }) + ).toMatchInlineSnapshot(` + Object { + "references": Array [], + "transformedActionDetails": "{\\"a\\":\\"hello\\"}", + } + `); + }); + + it('returns the stringified json without the connector_id', () => { + const externalService = createExternalService(); + + const { transformedActionDetails } = extractConnectorIdHelper({ + action: 'push-to-service', + actionFields: ['pushed'], + actionDetails: externalService, + fieldType: UserActionFieldType.New, + }); + + const transformedExternalService = JSON.parse(transformedActionDetails); + expect(transformedExternalService).not.toHaveProperty('connector_id'); + expect(transformedExternalService).toMatchInlineSnapshot(` + Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + }); + + it('returns a reference to the connector_id', () => { + const externalService = createExternalService(); + + const { references } = extractConnectorIdHelper({ + action: 'push-to-service', + actionFields: ['pushed'], + actionDetails: externalService, + fieldType: UserActionFieldType.New, + })!; + + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); + }); + + it('returns an old reference name to the connector_id', () => { + const externalService = createExternalService(); + + const { references } = extractConnectorIdHelper({ + action: 'push-to-service', + actionFields: ['pushed'], + actionDetails: externalService, + fieldType: UserActionFieldType.Old, + })!; + + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "100", + "name": "oldPushConnectorId", + "type": "action", + }, + ] + `); + }); + }); + }); + + describe('extractConnectorId', () => { + it('returns null when the action details has a circular reference', () => { + const circularRef = { prop: {} }; + circularRef.prop = circularRef; + + const { transformedActionDetails, references } = extractConnectorId({ + action: 'a', + actionFields: ['a'], + actionDetails: circularRef, + fieldType: UserActionFieldType.New, + }); + + expect(transformedActionDetails).toBeNull(); + expect(references).toEqual([]); + }); + + describe('fails to extract the id', () => { + it('returns a null transformed action details when it is initially null', () => { + const { transformedActionDetails, references } = extractConnectorId({ + action: 'a', + actionFields: ['a'], + actionDetails: null, + fieldType: UserActionFieldType.New, + }); + + expect(transformedActionDetails).toBeNull(); + expect(references).toEqual([]); + }); + + it('returns an undefined transformed action details when it is initially undefined', () => { + const { transformedActionDetails, references } = extractConnectorId({ + action: 'a', + actionFields: ['a'], + actionDetails: undefined, + fieldType: UserActionFieldType.New, + }); + + expect(transformedActionDetails).toBeUndefined(); + expect(references).toEqual([]); + }); + + it('returns a json encoded string and empty references when the action is not a valid connector', () => { + const { transformedActionDetails, references } = extractConnectorId({ + action: 'a', + actionFields: ['a'], + actionDetails: { a: 'hello' }, + fieldType: UserActionFieldType.New, + }); + + expect(JSON.parse(transformedActionDetails!)).toEqual({ a: 'hello' }); + expect(references).toEqual([]); + }); + + it('returns a json encoded string and empty references when the action details is an invalid object', () => { + const { transformedActionDetails, references } = extractConnectorId({ + action: 'a', + actionFields: ['a'], + actionDetails: 5 as unknown as Record, + fieldType: UserActionFieldType.New, + }); + + expect(transformedActionDetails!).toEqual('5'); + expect(references).toEqual([]); + }); + }); + + describe('create', () => { + it('extracts the connector.id from a new create jira connector to the references', () => { + const { transformedActionDetails, references } = extractConnectorId({ + action: 'create', + actionFields: ['connector'], + actionDetails: createConnectorObject(), + fieldType: UserActionFieldType.New, + }); + + const parsedJson = JSON.parse(transformedActionDetails!); + + expect(parsedJson).not.toHaveProperty('id'); + expect(parsedJson).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + }, + } + `); + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + }); + + it('extracts the connector.id from an old create jira connector to the references', () => { + const { transformedActionDetails, references } = extractConnectorId({ + action: 'create', + actionFields: ['connector'], + actionDetails: createConnectorObject(), + fieldType: UserActionFieldType.Old, + }); + + const parsedJson = JSON.parse(transformedActionDetails!); + + expect(parsedJson).not.toHaveProperty('id'); + expect(parsedJson).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + }, + } + `); + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "oldConnectorId", + "type": "action", + }, + ] + `); + }); + }); + + describe('update', () => { + it('extracts the connector.id from a new create jira connector to the references', () => { + const { transformedActionDetails, references } = extractConnectorId({ + action: 'update', + actionFields: ['connector'], + actionDetails: createJiraConnector(), + fieldType: UserActionFieldType.New, + }); + + const parsedJson = JSON.parse(transformedActionDetails!); + + expect(parsedJson).not.toHaveProperty('id'); + expect(parsedJson).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + } + `); + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + }); + + it('extracts the connector.id from an old create jira connector to the references', () => { + const { transformedActionDetails, references } = extractConnectorId({ + action: 'update', + actionFields: ['connector'], + actionDetails: createJiraConnector(), + fieldType: UserActionFieldType.Old, + }); + + const parsedJson = JSON.parse(transformedActionDetails!); + + expect(parsedJson).not.toHaveProperty('id'); + expect(parsedJson).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + } + `); + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "oldConnectorId", + "type": "action", + }, + ] + `); + }); + }); + + describe('push action', () => { + it('returns the stringified json without the connector_id', () => { + const externalService = createExternalService(); + + const { transformedActionDetails } = extractConnectorId({ + action: 'push-to-service', + actionFields: ['pushed'], + actionDetails: externalService, + fieldType: UserActionFieldType.New, + }); + + const transformedExternalService = JSON.parse(transformedActionDetails!); + expect(transformedExternalService).not.toHaveProperty('connector_id'); + expect(transformedExternalService).toMatchInlineSnapshot(` + Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + }); + + it('returns a reference to the connector_id', () => { + const externalService = createExternalService(); + + const { references } = extractConnectorId({ + action: 'push-to-service', + actionFields: ['pushed'], + actionDetails: externalService, + fieldType: UserActionFieldType.New, + })!; + + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); + }); + + it('returns a reference to the old action details connector_id', () => { + const externalService = createExternalService(); + + const { references } = extractConnectorId({ + action: 'push-to-service', + actionFields: ['pushed'], + actionDetails: externalService, + fieldType: UserActionFieldType.Old, + })!; + + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "100", + "name": "oldPushConnectorId", + "type": "action", + }, + ] + `); + }); + }); + }); + + describe('extractConnectorIdFromJson', () => { + describe('fails to extract the id', () => { + it('returns no references and null transformed json when action is undefined', () => { + expect( + extractConnectorIdFromJson({ + actionFields: [], + actionDetails: undefined, + fieldType: UserActionFieldType.New, + }) + ).toEqual({ + transformedActionDetails: undefined, + references: [], + }); + }); + + it('returns no references and undefined transformed json when actionFields is undefined', () => { + expect( + extractConnectorIdFromJson({ action: 'a', fieldType: UserActionFieldType.New }) + ).toEqual({ + transformedActionDetails: undefined, + references: [], + }); + }); + + it('returns no references and undefined transformed json when actionDetails is undefined', () => { + expect( + extractConnectorIdFromJson({ + action: 'a', + actionFields: [], + fieldType: UserActionFieldType.New, + }) + ).toEqual({ + transformedActionDetails: undefined, + references: [], + }); + }); + + it('returns no references and undefined transformed json when actionDetails is null', () => { + expect( + extractConnectorIdFromJson({ + action: 'a', + actionFields: [], + actionDetails: null, + fieldType: UserActionFieldType.New, + }) + ).toEqual({ + transformedActionDetails: null, + references: [], + }); + }); + + it('throws an error when actionDetails is invalid json', () => { + expect(() => + extractConnectorIdFromJson({ + action: 'a', + actionFields: [], + actionDetails: '{a', + fieldType: UserActionFieldType.New, + }) + ).toThrow(); + }); + }); + + describe('create action', () => { + it('returns the stringified json without the id', () => { + const jiraConnector = createConnectorObject(); + + const { transformedActionDetails } = extractConnectorIdFromJson({ + action: 'create', + actionFields: ['connector'], + actionDetails: JSON.stringify(jiraConnector), + fieldType: UserActionFieldType.New, + }); + + expect(JSON.parse(transformedActionDetails!)).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + }, + } + `); + }); + + it('returns a reference to the connector.id', () => { + const jiraConnector = createConnectorObject(); + + const { references } = extractConnectorIdFromJson({ + action: 'create', + actionFields: ['connector'], + actionDetails: JSON.stringify(jiraConnector), + fieldType: UserActionFieldType.New, + })!; + + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + }); + }); + + describe('update action', () => { + it('returns the stringified json without the id', () => { + const jiraConnector = createJiraConnector(); + + const { transformedActionDetails } = extractConnectorIdFromJson({ + action: 'update', + actionFields: ['connector'], + actionDetails: JSON.stringify(jiraConnector), + fieldType: UserActionFieldType.New, + }); + + const transformedConnetor = JSON.parse(transformedActionDetails!); + expect(transformedConnetor).not.toHaveProperty('id'); + expect(transformedConnetor).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + } + `); + }); + + it('returns a reference to the connector.id', () => { + const jiraConnector = createJiraConnector(); + + const { references } = extractConnectorIdFromJson({ + action: 'update', + actionFields: ['connector'], + actionDetails: JSON.stringify(jiraConnector), + fieldType: UserActionFieldType.New, + })!; + + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + }); + }); + + describe('push action', () => { + it('returns the stringified json without the connector_id', () => { + const externalService = createExternalService(); + + const { transformedActionDetails } = extractConnectorIdFromJson({ + action: 'push-to-service', + actionFields: ['pushed'], + actionDetails: JSON.stringify(externalService), + fieldType: UserActionFieldType.New, + }); + + const transformedExternalService = JSON.parse(transformedActionDetails!); + expect(transformedExternalService).not.toHaveProperty('connector_id'); + expect(transformedExternalService).toMatchInlineSnapshot(` + Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + }); + + it('returns a reference to the connector_id', () => { + const externalService = createExternalService(); + + const { references } = extractConnectorIdFromJson({ + action: 'push-to-service', + actionFields: ['pushed'], + actionDetails: JSON.stringify(externalService), + fieldType: UserActionFieldType.New, + })!; + + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/services/user_actions/transform.ts b/x-pack/plugins/cases/server/services/user_actions/transform.ts new file mode 100644 index 0000000000000..93595374208a3 --- /dev/null +++ b/x-pack/plugins/cases/server/services/user_actions/transform.ts @@ -0,0 +1,320 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as rt from 'io-ts'; +import { isString } from 'lodash'; + +import { SavedObjectReference } from '../../../../../../src/core/server'; +import { + CaseAttributes, + CaseConnector, + CaseConnectorRt, + CaseExternalServiceBasicRt, + isCreateConnector, + isPush, + isUpdateConnector, + noneConnectorId, +} from '../../../common'; +import { + CONNECTOR_ID_REFERENCE_NAME, + getNoneCaseConnector, + PUSH_CONNECTOR_ID_REFERENCE_NAME, + USER_ACTION_OLD_ID_REF_NAME, + USER_ACTION_OLD_PUSH_ID_REF_NAME, +} from '../../common'; +import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; +import { UserActionFieldType } from './types'; + +/** + * Extracts the connector id from a json encoded string and formats it as a saved object reference. This will remove + * the field it extracted the connector id from. + */ +export function extractConnectorIdFromJson({ + action, + actionFields, + actionDetails, + fieldType, +}: { + action?: string; + actionFields?: string[]; + actionDetails?: string | null; + fieldType: UserActionFieldType; +}): { transformedActionDetails?: string | null; references: SavedObjectReference[] } { + if (!action || !actionFields || !actionDetails) { + return { transformedActionDetails: actionDetails, references: [] }; + } + + const decodedJson = JSON.parse(actionDetails); + + return extractConnectorIdHelper({ + action, + actionFields, + actionDetails: decodedJson, + fieldType, + }); +} + +/** + * Extracts the connector id from an unencoded object and formats it as a saved object reference. + * This will remove the field it extracted the connector id from. + */ +export function extractConnectorId({ + action, + actionFields, + actionDetails, + fieldType, +}: { + action: string; + actionFields: string[]; + actionDetails?: Record | string | null; + fieldType: UserActionFieldType; +}): { + transformedActionDetails?: string | null; + references: SavedObjectReference[]; +} { + if (!actionDetails || isString(actionDetails)) { + // the action was null, undefined, or a regular string so just return it unmodified and not encoded + return { transformedActionDetails: actionDetails, references: [] }; + } + + try { + return extractConnectorIdHelper({ + action, + actionFields, + actionDetails, + fieldType, + }); + } catch (error) { + return { transformedActionDetails: encodeActionDetails(actionDetails), references: [] }; + } +} + +function encodeActionDetails(actionDetails: Record): string | null { + try { + return JSON.stringify(actionDetails); + } catch (error) { + return null; + } +} + +/** + * Internal helper function for extracting the connector id. This is only exported for usage in unit tests. + * This function handles encoding the transformed fields as a json string + */ +export function extractConnectorIdHelper({ + action, + actionFields, + actionDetails, + fieldType, +}: { + action: string; + actionFields: string[]; + actionDetails: unknown; + fieldType: UserActionFieldType; +}): { transformedActionDetails: string; references: SavedObjectReference[] } { + let transformedActionDetails: unknown = actionDetails; + let referencesToReturn: SavedObjectReference[] = []; + + try { + if (isCreateCaseConnector(action, actionFields, actionDetails)) { + const { transformedActionDetails: transformedConnectorPortion, references } = + transformConnectorFromCreateAndUpdateAction(actionDetails.connector, fieldType); + + // the above call only transforms the connector portion of the action details so let's add back + // the rest of the details and we'll overwrite the connector portion when the transformed one + transformedActionDetails = { + ...actionDetails, + ...transformedConnectorPortion, + }; + referencesToReturn = references; + } else if (isUpdateCaseConnector(action, actionFields, actionDetails)) { + const { + transformedActionDetails: { connector: transformedConnector }, + references, + } = transformConnectorFromCreateAndUpdateAction(actionDetails, fieldType); + + transformedActionDetails = transformedConnector; + referencesToReturn = references; + } else if (isPushConnector(action, actionFields, actionDetails)) { + ({ transformedActionDetails, references: referencesToReturn } = + transformConnectorFromPushAction(actionDetails, fieldType)); + } + } catch (error) { + // ignore any errors, we'll just return whatever was passed in for action details in that case + } + + return { + transformedActionDetails: JSON.stringify(transformedActionDetails), + references: referencesToReturn, + }; +} + +function isCreateCaseConnector( + action: string, + actionFields: string[], + actionDetails: unknown +): actionDetails is { connector: CaseConnector } { + try { + const unsafeCase = actionDetails as CaseAttributes; + + return ( + isCreateConnector(action, actionFields) && + unsafeCase.connector !== undefined && + CaseConnectorRt.is(unsafeCase.connector) + ); + } catch { + return false; + } +} + +export const ConnectorIdReferenceName: Record = { + [UserActionFieldType.New]: CONNECTOR_ID_REFERENCE_NAME, + [UserActionFieldType.Old]: USER_ACTION_OLD_ID_REF_NAME, +}; + +function transformConnectorFromCreateAndUpdateAction( + connector: CaseConnector, + fieldType: UserActionFieldType +): { + transformedActionDetails: { connector: unknown }; + references: SavedObjectReference[]; +} { + const { transformedConnector, references } = transformConnectorIdToReference( + ConnectorIdReferenceName[fieldType], + connector + ); + + return { + transformedActionDetails: transformedConnector, + references, + }; +} + +type ConnectorIdRefNameType = + | typeof CONNECTOR_ID_REFERENCE_NAME + | typeof USER_ACTION_OLD_ID_REF_NAME; + +export const transformConnectorIdToReference = ( + referenceName: ConnectorIdRefNameType, + connector?: { + id?: string; + } +): { + transformedConnector: { connector: unknown }; + references: SavedObjectReference[]; +} => { + const { id: connectorId, ...restConnector } = connector ?? {}; + + const references = createConnectorReference(connectorId, ACTION_SAVED_OBJECT_TYPE, referenceName); + + const { id: ignoreNoneId, ...restNoneConnector } = getNoneCaseConnector(); + const connectorFieldsToReturn = + connector && isConnectorIdValid(connectorId) ? restConnector : restNoneConnector; + + return { + transformedConnector: { + connector: connectorFieldsToReturn, + }, + references, + }; +}; + +const createConnectorReference = ( + id: string | null | undefined, + type: string, + name: string +): SavedObjectReference[] => { + return isConnectorIdValid(id) + ? [ + { + id, + type, + name, + }, + ] + : []; +}; + +const isConnectorIdValid = (id: string | null | undefined): id is string => + id != null && id !== noneConnectorId; + +function isUpdateCaseConnector( + action: string, + actionFields: string[], + actionDetails: unknown +): actionDetails is CaseConnector { + try { + return isUpdateConnector(action, actionFields) && CaseConnectorRt.is(actionDetails); + } catch { + return false; + } +} + +type CaseExternalService = rt.TypeOf; + +function isPushConnector( + action: string, + actionFields: string[], + actionDetails: unknown +): actionDetails is CaseExternalService { + try { + return isPush(action, actionFields) && CaseExternalServiceBasicRt.is(actionDetails); + } catch { + return false; + } +} + +export const PushConnectorIdReferenceName: Record = + { + [UserActionFieldType.New]: PUSH_CONNECTOR_ID_REFERENCE_NAME, + [UserActionFieldType.Old]: USER_ACTION_OLD_PUSH_ID_REF_NAME, + }; + +function transformConnectorFromPushAction( + externalService: CaseExternalService, + fieldType: UserActionFieldType +): { + transformedActionDetails: {} | null; + references: SavedObjectReference[]; +} { + const { transformedPushConnector, references } = transformPushConnectorIdToReference( + PushConnectorIdReferenceName[fieldType], + externalService + ); + + return { + transformedActionDetails: transformedPushConnector.external_service, + references, + }; +} + +type PushConnectorIdRefNameType = + | typeof PUSH_CONNECTOR_ID_REFERENCE_NAME + | typeof USER_ACTION_OLD_PUSH_ID_REF_NAME; + +export const transformPushConnectorIdToReference = ( + referenceName: PushConnectorIdRefNameType, + external_service?: { connector_id?: string | null } | null +): { + transformedPushConnector: { external_service: {} | null }; + references: SavedObjectReference[]; +} => { + const { connector_id: pushConnectorId, ...restExternalService } = external_service ?? {}; + + const references = createConnectorReference( + pushConnectorId, + ACTION_SAVED_OBJECT_TYPE, + referenceName + ); + + return { + transformedPushConnector: { external_service: external_service ? restExternalService : null }, + references, + }; +}; diff --git a/x-pack/plugins/cases/server/services/user_actions/types.ts b/x-pack/plugins/cases/server/services/user_actions/types.ts new file mode 100644 index 0000000000000..3c67535255ecc --- /dev/null +++ b/x-pack/plugins/cases/server/services/user_actions/types.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Indicates whether which user action field is being parsed, the new_value or the old_value. + */ +export enum UserActionFieldType { + New = 'New', + Old = 'Old', +} diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts index 964e9135aba7b..68f0ba43d889b 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts @@ -85,7 +85,7 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - it('should create a user action when creating a case', async () => { + it('should create a user action when deleting a case', async () => { const postedCase = await createCase(supertest, getPostCaseRequest()); await deleteCases({ supertest, caseIDs: [postedCase.id] }); const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); @@ -106,6 +106,8 @@ export default ({ getService }: FtrProviderContext): void => { action_by: defaultUser, old_value: null, new_value: null, + new_val_connector_id: null, + old_val_connector_id: null, case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/import_export.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/import_export.ts index f149f4b5d13a8..dd1c2e810f150 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/import_export.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/import_export.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import { join } from 'path'; import { SavedObject } from 'kibana/server'; +import supertest from 'supertest'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; import { deleteAllCaseItems, @@ -29,15 +30,16 @@ import { CaseUserActionAttributes, CASE_COMMENT_SAVED_OBJECT, CasePostRequest, + CaseUserActionResponse, } from '../../../../../../plugins/cases/common'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); + const supertestService = getService('supertest'); const es = getService('es'); describe('import and export cases', () => { - const actionsRemover = new ActionsRemover(supertest); + const actionsRemover = new ActionsRemover(supertestService); afterEach(async () => { await deleteAllCaseItems(es); @@ -46,14 +48,14 @@ export default ({ getService }: FtrProviderContext): void => { it('exports a case with its associated user actions and comments', async () => { const caseRequest = getPostCaseRequest(); - const postedCase = await createCase(supertest, caseRequest); + const postedCase = await createCase(supertestService, caseRequest); await createComment({ - supertest, + supertest: supertestService, caseId: postedCase.id, params: postCommentUserReq, }); - const { text } = await supertest + const { text } = await supertestService .post(`/api/saved_objects/_export`) .send({ type: ['cases'], @@ -72,7 +74,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('imports a case with a comment and user actions', async () => { - await supertest + await supertestService .post('/api/saved_objects/_import') .query({ overwrite: true }) .attach( @@ -85,12 +87,12 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .expect(200); - const findResponse = await findCases({ supertest, query: {} }); + const findResponse = await findCases({ supertest: supertestService, query: {} }); expect(findResponse.total).to.eql(1); expect(findResponse.cases[0].title).to.eql('A case to export'); expect(findResponse.cases[0].description).to.eql('a description'); - const { body: commentsResponse }: { body: CommentsResponse } = await supertest + const { body: commentsResponse }: { body: CommentsResponse } = await supertestService .get(`${CASES_URL}/${findResponse.cases[0].id}/comments/_find`) .send() .expect(200); @@ -99,7 +101,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(comment.comment).to.eql('A comment for my case'); const userActions = await getCaseUserActions({ - supertest, + supertest: supertestService, caseID: findResponse.cases[0].id, }); @@ -118,7 +120,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('imports a case with a connector', async () => { - await supertest + await supertestService .post('/api/saved_objects/_import') .query({ overwrite: true }) .attach( @@ -133,35 +135,56 @@ export default ({ getService }: FtrProviderContext): void => { actionsRemover.add('default', '1cd34740-06ad-11ec-babc-0b08808e8e01', 'action', 'actions'); - const findResponse = await findCases({ supertest, query: {} }); - expect(findResponse.total).to.eql(1); - expect(findResponse.cases[0].title).to.eql('A case with a connector'); - expect(findResponse.cases[0].description).to.eql('super description'); + await expectImportToHaveOneCase(supertestService); const userActions = await getCaseUserActions({ - supertest, - caseID: findResponse.cases[0].id, + supertest: supertestService, + caseID: '2e85c3f0-06ad-11ec-babc-0b08808e8e01', }); - expect(userActions).to.have.length(3); - expect(userActions[0].action).to.eql('create'); - expect(includesAllCreateCaseActionFields(userActions[0].action_field)).to.eql(true); - expect(userActions[1].action).to.eql('push-to-service'); - expect(userActions[1].action_field).to.eql(['pushed']); - expect(userActions[1].old_value).to.eql(null); + expectImportToHaveCreateCaseUserAction(userActions[0]); + expectImportToHavePushUserAction(userActions[1]); + expectImportToHaveUpdateConnector(userActions[2]); + }); + }); +}; - const parsedPushNewValue = JSON.parse(userActions[1].new_value!); - expect(parsedPushNewValue.connector_name).to.eql('A jira connector'); - expect(parsedPushNewValue.connector_id).to.eql('1cd34740-06ad-11ec-babc-0b08808e8e01'); +const expectImportToHaveOneCase = async (supertestService: supertest.SuperTest) => { + const findResponse = await findCases({ supertest: supertestService, query: {} }); + expect(findResponse.total).to.eql(1); + expect(findResponse.cases[0].title).to.eql('A case with a connector'); + expect(findResponse.cases[0].description).to.eql('super description'); +}; - expect(userActions[2].action).to.eql('update'); - expect(userActions[2].action_field).to.eql(['connector']); +const expectImportToHaveCreateCaseUserAction = (userAction: CaseUserActionResponse) => { + expect(userAction.action).to.eql('create'); + expect(includesAllCreateCaseActionFields(userAction.action_field)).to.eql(true); +}; - const parsedUpdateNewValue = JSON.parse(userActions[2].new_value!); - expect(parsedUpdateNewValue.id).to.eql('none'); - }); - }); +const expectImportToHavePushUserAction = (userAction: CaseUserActionResponse) => { + expect(userAction.action).to.eql('push-to-service'); + expect(userAction.action_field).to.eql(['pushed']); + expect(userAction.old_value).to.eql(null); + + const parsedPushNewValue = JSON.parse(userAction.new_value!); + expect(parsedPushNewValue.connector_name).to.eql('A jira connector'); + expect(parsedPushNewValue).to.not.have.property('connector_id'); + expect(userAction.new_val_connector_id).to.eql('1cd34740-06ad-11ec-babc-0b08808e8e01'); +}; + +const expectImportToHaveUpdateConnector = (userAction: CaseUserActionResponse) => { + expect(userAction.action).to.eql('update'); + expect(userAction.action_field).to.eql(['connector']); + + const parsedUpdateNewValue = JSON.parse(userAction.new_value!); + expect(parsedUpdateNewValue).to.not.have.property('id'); + // the new val connector id is null because it is the none connector + expect(userAction.new_val_connector_id).to.eql(null); + + const parsedUpdateOldValue = JSON.parse(userAction.old_value!); + expect(parsedUpdateOldValue).to.not.have.property('id'); + expect(userAction.old_val_connector_id).to.eql('1cd34740-06ad-11ec-babc-0b08808e8e01'); }; const ndjsonToObject = (input: string) => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts index 63b2f2e9b90ed..d7c506a6b69d2 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts @@ -126,6 +126,8 @@ export default ({ getService }: FtrProviderContext): void => { action: 'update', action_by: defaultUser, new_value: CaseStatuses.closed, + new_val_connector_id: null, + old_val_connector_id: null, old_value: CaseStatuses.open, case_id: `${postedCase.id}`, comment_id: null, @@ -165,6 +167,8 @@ export default ({ getService }: FtrProviderContext): void => { action_by: defaultUser, new_value: CaseStatuses['in-progress'], old_value: CaseStatuses.open, + old_val_connector_id: null, + new_val_connector_id: null, case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts index 96709ee7c309d..13408c5d309d9 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts @@ -114,6 +114,8 @@ export default ({ getService }: FtrProviderContext): void => { const { new_value, ...rest } = creationUserAction as CaseUserActionResponse; const parsedNewValue = JSON.parse(new_value!); + const { id: connectorId, ...restCaseConnector } = postedCase.connector; + expect(rest).to.eql({ action_field: [ 'description', @@ -127,6 +129,9 @@ export default ({ getService }: FtrProviderContext): void => { action: 'create', action_by: defaultUser, old_value: null, + old_val_connector_id: null, + // the connector id will be null here because it the connector is none + new_val_connector_id: null, case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', @@ -138,7 +143,7 @@ export default ({ getService }: FtrProviderContext): void => { description: postedCase.description, title: postedCase.title, tags: postedCase.tags, - connector: postedCase.connector, + connector: restCaseConnector, settings: postedCase.settings, owner: postedCase.owner, }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts index f4c31c052cddd..942293437b03f 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts @@ -148,7 +148,9 @@ export default ({ getService }: FtrProviderContext): void => { action: 'create', action_by: defaultUser, new_value: `{"comment":"${postCommentUserReq.comment}","type":"${postCommentUserReq.type}","owner":"securitySolutionFixture"}`, + new_val_connector_id: null, old_value: null, + old_val_connector_id: null, case_id: `${postedCase.id}`, comment_id: `${patchedCase.comments![0].id}`, sub_case_id: '', diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts index 35ebb1a4bf7b1..4cae10510d28e 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts @@ -48,6 +48,15 @@ export default ({ getService }: FtrProviderContext): void => { }); it(`on new case, user action: 'create' should be called with actionFields: ['description', 'status', 'tags', 'title', 'connector', 'settings, owner]`, async () => { + const { id: connectorId, ...restConnector } = userActionPostResp.connector; + + const userActionNewValueNoId = { + ...userActionPostResp, + connector: { + ...restConnector, + }, + }; + const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') @@ -73,7 +82,10 @@ export default ({ getService }: FtrProviderContext): void => { ]); expect(body[0].action).to.eql('create'); expect(body[0].old_value).to.eql(null); - expect(JSON.parse(body[0].new_value)).to.eql(userActionPostResp); + expect(body[0].old_val_connector_id).to.eql(null); + // this will be null because it is for the none connector + expect(body[0].new_val_connector_id).to.eql(null); + expect(JSON.parse(body[0].new_value)).to.eql(userActionNewValueNoId); }); it(`on close case, user action: 'update' should be called with actionFields: ['status']`, async () => { @@ -147,18 +159,19 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.length).to.eql(2); expect(body[1].action_field).to.eql(['connector']); expect(body[1].action).to.eql('update'); + // this is null because it is the none connector + expect(body[1].old_val_connector_id).to.eql(null); expect(JSON.parse(body[1].old_value)).to.eql({ - id: 'none', name: 'none', type: '.none', fields: null, }); expect(JSON.parse(body[1].new_value)).to.eql({ - id: '123', name: 'Connector', type: '.jira', fields: { issueType: 'Task', priority: 'High', parent: null }, }); + expect(body[1].new_val_connector_id).to.eql('123'); }); it(`on update tags, user action: 'add' and 'delete' should be called with actionFields: ['tags']`, async () => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/migrations.ts index b4c2dca47bf5f..f9e66880c5230 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/migrations.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/migrations.ts @@ -12,6 +12,10 @@ import { SECURITY_SOLUTION_OWNER, } from '../../../../../../plugins/cases/common/constants'; import { getCaseUserActions } from '../../../../common/lib/utils'; +import { + CaseUserActionResponse, + CaseUserActionsResponse, +} from '../../../../../../plugins/cases/common'; // eslint-disable-next-line import/no-default-export export default function createGetTests({ getService }: FtrProviderContext) { @@ -41,14 +45,18 @@ export default function createGetTests({ getService }: FtrProviderContext) { expect(connectorUserAction.action_field.length).eql(1); expect(connectorUserAction.action_field[0]).eql('connector'); + expect(connectorUserAction.old_val_connector_id).to.eql( + 'c1900ac0-017f-11eb-93f8-d161651bf509' + ); expect(oldValue).to.eql({ - id: 'c1900ac0-017f-11eb-93f8-d161651bf509', name: 'none', type: '.none', fields: null, }); + expect(connectorUserAction.new_val_connector_id).to.eql( + 'b1900ac0-017f-11eb-93f8-d161651bf509' + ); expect(newValue).to.eql({ - id: 'b1900ac0-017f-11eb-93f8-d161651bf509', name: 'none', type: '.none', fields: null, @@ -77,5 +85,142 @@ export default function createGetTests({ getService }: FtrProviderContext) { } }); }); + + describe('7.13 connector id extraction', () => { + let userActions: CaseUserActionsResponse; + + before(async () => { + await esArchiver.load( + 'x-pack/test/functional/es_archives/cases/migrations/7.13_user_actions' + ); + }); + + after(async () => { + await esArchiver.unload( + 'x-pack/test/functional/es_archives/cases/migrations/7.13_user_actions' + ); + }); + + describe('none connector case', () => { + it('removes the connector id from the case create user action and sets the ids to null', async () => { + userActions = await getCaseUserActions({ + supertest, + caseID: 'aa8ac630-005e-11ec-91f1-6daf2ab59fb5', + }); + + const userAction = getUserActionById( + userActions, + 'ab43b5f0-005e-11ec-91f1-6daf2ab59fb5' + )!; + + const newValDecoded = JSON.parse(userAction.new_value!); + expect(newValDecoded.description).to.be('a description'); + expect(newValDecoded.title).to.be('a case'); + expect(newValDecoded.connector).not.have.property('id'); + // the connector id should be none so it should be removed + expect(userAction.new_val_connector_id).to.be(null); + expect(userAction.old_val_connector_id).to.be(null); + }); + + it('sets the connector ids to null for a create user action with null new and old values', async () => { + const userAction = getUserActionById( + userActions, + 'b3094de0-005e-11ec-91f1-6daf2ab59fb5' + )!; + + expect(userAction.new_val_connector_id).to.be(null); + expect(userAction.old_val_connector_id).to.be(null); + }); + }); + + describe('case with many user actions', () => { + before(async () => { + userActions = await getCaseUserActions({ + supertest, + caseID: 'e6fa9370-005e-11ec-91f1-6daf2ab59fb5', + }); + }); + + it('removes the connector id field for a created case user action', async () => { + const userAction = getUserActionById( + userActions, + 'e7882d70-005e-11ec-91f1-6daf2ab59fb5' + )!; + + const newValDecoded = JSON.parse(userAction.new_value!); + expect(newValDecoded.description).to.be('a description'); + expect(newValDecoded.title).to.be('a case'); + + expect(newValDecoded.connector).to.not.have.property('id'); + expect(userAction.new_val_connector_id).to.be('d92243b0-005e-11ec-91f1-6daf2ab59fb5'); + expect(userAction.old_val_connector_id).to.be(null); + }); + + it('removes the connector id from the external service new value', async () => { + const userAction = getUserActionById( + userActions, + 'e9471b80-005e-11ec-91f1-6daf2ab59fb5' + )!; + + const newValDecoded = JSON.parse(userAction.new_value!); + expect(newValDecoded.connector_name).to.be('a jira connector'); + expect(newValDecoded).to.not.have.property('connector_id'); + expect(userAction.new_val_connector_id).to.be('d92243b0-005e-11ec-91f1-6daf2ab59fb5'); + expect(userAction.old_val_connector_id).to.be(null); + }); + + it('sets the connector ids to null for a comment user action', async () => { + const userAction = getUserActionById( + userActions, + 'efe9de50-005e-11ec-91f1-6daf2ab59fb5' + )!; + + const newValDecoded = JSON.parse(userAction.new_value!); + expect(newValDecoded.comment).to.be('a comment'); + expect(userAction.new_val_connector_id).to.be(null); + expect(userAction.old_val_connector_id).to.be(null); + }); + + it('removes the connector id for an update connector action', async () => { + const userAction = getUserActionById( + userActions, + '16cd9e30-005f-11ec-91f1-6daf2ab59fb5' + )!; + + const newValDecoded = JSON.parse(userAction.new_value!); + const oldValDecoded = JSON.parse(userAction.old_value!); + + expect(newValDecoded.name).to.be('a different jira connector'); + expect(oldValDecoded.name).to.be('a jira connector'); + + expect(newValDecoded).to.not.have.property('id'); + expect(oldValDecoded).to.not.have.property('id'); + expect(userAction.new_val_connector_id).to.be('0a572860-005f-11ec-91f1-6daf2ab59fb5'); + expect(userAction.old_val_connector_id).to.be('d92243b0-005e-11ec-91f1-6daf2ab59fb5'); + }); + + it('removes the connector id from the external service new value for second push', async () => { + const userAction = getUserActionById( + userActions, + '1ea33bb0-005f-11ec-91f1-6daf2ab59fb5' + )!; + + const newValDecoded = JSON.parse(userAction.new_value!); + + expect(newValDecoded.connector_name).to.be('a different jira connector'); + + expect(newValDecoded).to.not.have.property('connector_id'); + expect(userAction.new_val_connector_id).to.be('0a572860-005f-11ec-91f1-6daf2ab59fb5'); + expect(userAction.old_val_connector_id).to.be(null); + }); + }); + }); }); } + +function getUserActionById( + userActions: CaseUserActionsResponse, + id: string +): CaseUserActionResponse | undefined { + return userActions.find((userAction) => userAction.action_id === id); +} diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts index 94fe494fc7cc4..0ea66d35b63b8 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts @@ -275,6 +275,8 @@ export default ({ getService }: FtrProviderContext): void => { action: 'push-to-service', action_by: defaultUser, old_value: null, + old_val_connector_id: null, + new_val_connector_id: connector.id, case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', @@ -284,7 +286,6 @@ export default ({ getService }: FtrProviderContext): void => { expect(parsedNewValue).to.eql({ pushed_at: pushedCase.external_service!.pushed_at, pushed_by: defaultUser, - connector_id: connector.id, connector_name: connector.name, external_id: '123', external_title: 'INC01', diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts index 79af6bb279a3d..255a2a4ce28b5 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts @@ -108,8 +108,10 @@ export default ({ getService }: FtrProviderContext): void => { expect(body[1].action_field).to.eql(['pushed']); expect(body[1].action).to.eql('push-to-service'); expect(body[1].old_value).to.eql(null); + expect(body[1].old_val_connector_id).to.eql(null); + expect(body[1].new_val_connector_id).to.eql(configure.connector.id); const newValue = JSON.parse(body[1].new_value); - expect(newValue.connector_id).to.eql(configure.connector.id); + expect(newValue).to.not.have.property('connector_id'); expect(newValue.pushed_by).to.eql(defaultUser); }); }); diff --git a/x-pack/test/functional/es_archives/cases/migrations/7.13_user_actions/data.json.gz b/x-pack/test/functional/es_archives/cases/migrations/7.13_user_actions/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..5f73dfd89d1660ef207e6ec26ce7198f2a722616 GIT binary patch literal 2078 zcmV+(2;ui1iwFpikRD+G17u-zVJ>QOZ*BnXTuXDKI1s+)S5TY-LE>SvxgZy+IT5F>ahZaIhWE-j?DT+?5t5lioJNZyKPi$UR$bs&%<`)2h3okV(a3a zL#Oy&DHWJDNxNT|jw-rg7~^}oGPm^>`SOTBQ4}aL)$g~xMn9kcdl7$V`cR|B4O~BP zqt_0h|8$>z;4rF*UHSD`?VD}BV?wB@n?Bo~_4Cj^nGOj*-2e&YwlJ-7)stzXQl$6l zh6Wf#xgLm6fk0B05?J0t#ZnXtXtJuQ4`NG?L_HWCza&<@-~_JN!GM!|m2&)MuBmhx9q}HCvC}gmQ#_ z+fhiAP6!kL7MvE58|{)2@BjUW$f9tX#Q|M!@kbOLLhJ4>F>@&*`saF2GK$DLhc5DD z4@Y;Kz<&wxUF&#wWg|CggBy49oDxweD&-< zb~uP=O%%kqy?2D7n6?o817_4HLQi#A6T|p`gHoi&YV&;Kh!&9DU`EQUkpQ>*>^d>p zRcqse!#BjXO(=RyYKS+rwIfCl;J@9c08#t<4+mjpmkSxFvQ~^M?wf6@-Y9cWv>TKa zu`x}6x0rysjjNkcEPS!H={QAkk41(0LpobFS25I@I_w+z$nQ5dZ){!e%NyRD_2&{@ zND0~Wv59lv5O~qI{Q!pV-rF3!&^BRRE7IabxCFx4Jx^+cnue!l zJ|JK(w>}RR!qB%}icb;?Te-?HnAYk5A6wi@%Om%3Gnt)hGE~fmCjQn@F_nTKW4MH^O>YEbb(vmNHhKZMDpBhSK34k%}} z16_HGrVj!F1sK!E{n*qNV6(h{xStZ5QshfHTiojakdfp|8LD-HbS4LYl*jGQQxsHj z!VB=0BB2Tjia4bPa*30UROFOT$R9W+8NjqyjU)w({mj!hArW} zQJfE0UYX=hCb>5jcO!7gI@3y)b*7W$sRJ22D+)~AP4dz|^A+@W_Od={mc5oI0DI-E zzZ?qdvI>F<+l@p02 zaX5v>QMt(~d?G+WQ&tye2or%}J0?!wI&9AhQ0W5o2Pj~v#CY_Xo~g@Vt~W$w5tM@T z1zZ}PJ64aN9Cw?bKr~e>^_Txb<0w8>igODIIjNCXWIszp2WS?80jGJr$O0xts=C=+ z0AP-0BLRIkyO?XL)=AmOVkxF$BYWd_D;wE~z}Nxr4lAb1^Z{+a&N!<1|<$U5Iw zRxBaS99|>5;$A8+ zg$Yj)35^VZxS__-2xA(;gQaD+1~znb0krJUJOG&Z8TzH+JZagnc>ptcZO)~I=bF+m z$x-a9QQ19?V Date: Wed, 22 Sep 2021 09:00:52 -0400 Subject: [PATCH 05/39] Moving title to text and hiding user actions and comments (#112745) --- x-pack/plugins/cases/server/saved_object_types/cases.ts | 2 +- x-pack/plugins/cases/server/saved_object_types/comments.ts | 1 + x-pack/plugins/cases/server/saved_object_types/user_actions.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/server/saved_object_types/cases.ts b/x-pack/plugins/cases/server/saved_object_types/cases.ts index a362d77c06626..74c6a053e95c0 100644 --- a/x-pack/plugins/cases/server/saved_object_types/cases.ts +++ b/x-pack/plugins/cases/server/saved_object_types/cases.ts @@ -117,7 +117,7 @@ export const createCaseSavedObjectType = ( type: 'keyword', }, title: { - type: 'keyword', + type: 'text', }, status: { type: 'keyword', diff --git a/x-pack/plugins/cases/server/saved_object_types/comments.ts b/x-pack/plugins/cases/server/saved_object_types/comments.ts index af14123eca580..64e75ad26ae28 100644 --- a/x-pack/plugins/cases/server/saved_object_types/comments.ts +++ b/x-pack/plugins/cases/server/saved_object_types/comments.ts @@ -112,5 +112,6 @@ export const createCaseCommentSavedObjectType = ({ migrations: createCommentsMigrations(migrationDeps), management: { importableAndExportable: true, + visibleInManagement: false, }, }); diff --git a/x-pack/plugins/cases/server/saved_object_types/user_actions.ts b/x-pack/plugins/cases/server/saved_object_types/user_actions.ts index 883105982bcb3..7ef7c639ed9db 100644 --- a/x-pack/plugins/cases/server/saved_object_types/user_actions.ts +++ b/x-pack/plugins/cases/server/saved_object_types/user_actions.ts @@ -51,5 +51,6 @@ export const caseUserActionSavedObjectType: SavedObjectsType = { migrations: userActionsMigrations, management: { importableAndExportable: true, + visibleInManagement: false, }, }; From e39a9d495bda9b2e8fbc9e8fba8d33a9d0f6de55 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Wed, 22 Sep 2021 15:04:58 +0200 Subject: [PATCH 06/39] Fix unhandled promise rejection in socket tests (#112806) --- src/core/server/http/router/socket.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/server/http/router/socket.test.ts b/src/core/server/http/router/socket.test.ts index 60c91786767a6..389c08825d51b 100644 --- a/src/core/server/http/router/socket.test.ts +++ b/src/core/server/http/router/socket.test.ts @@ -92,7 +92,7 @@ describe('KibanaSocket', () => { }); const socket = new KibanaSocket(tlsSocket); - expect(socket.renegotiate({})).resolves.toBe(result); + await expect(socket.renegotiate({})).rejects.toBe(result); expect(spy).toBeCalledTimes(1); }); From 93cc4fcd9b9a2e7e7611c0b082b68c494308820a Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Wed, 22 Sep 2021 09:09:34 -0400 Subject: [PATCH 07/39] [Security Solution][Timeline] Pinned events migrations (#112360) * Starting migration class * Fleshing out migrator * Adding migration tests * Refactoring * Adding migrator to each client * gzipping file * Fixing cypress tests * Cleaning up types and adding additional test * Starting notes migrations * Finishing notes references migration * gzipping data.json * Fixing unit tests * Updating the archive and fixing spelling * Starting pinned events * Fixing more conflicts * Finishing pinned events * fixing pinned events not showing bug * Fixing lint errors Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../types/timeline/pinned_event/index.ts | 6 + .../pinned_events/field_migrator.ts | 18 ++ .../saved_object/pinned_events/index.ts | 208 +++++++++++------- .../migrations/notes.test.ts | 40 ---- .../saved_object_mappings/migrations/notes.ts | 36 +-- .../migrations/pinned_events.ts | 13 ++ .../migrations/{index.ts => types.ts} | 5 +- .../migrations/utils.test.ts | 32 ++- .../saved_object_mappings/migrations/utils.ts | 23 ++ .../timeline/saved_object_mappings/notes.ts | 2 +- .../saved_object_mappings/pinned_events.ts | 5 +- .../saved_object_mappings/timelines.ts | 2 +- .../security_solution/timeline_migrations.ts | 126 +++++++---- .../timelines/7.15.0/data.json.gz | Bin 3118 -> 3118 bytes 14 files changed, 312 insertions(+), 204 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/field_migrator.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/pinned_events.ts rename x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/{index.ts => types.ts} (72%) diff --git a/x-pack/plugins/security_solution/common/types/timeline/pinned_event/index.ts b/x-pack/plugins/security_solution/common/types/timeline/pinned_event/index.ts index dbb19df7a6b05..df230615818ac 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/pinned_event/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/pinned_event/index.ts @@ -30,6 +30,12 @@ export const SavedPinnedEventRuntimeType = runtimeTypes.intersection([ export interface SavedPinnedEvent extends runtimeTypes.TypeOf {} +/** + * This type represents a pinned event type stored in a saved object that does not include any fields that reference + * other saved objects. + */ +export type PinnedEventWithoutExternalRefs = Omit; + /** * Note Saved object type with metadata */ diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/field_migrator.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/field_migrator.ts new file mode 100644 index 0000000000000..5939676c2a924 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/field_migrator.ts @@ -0,0 +1,18 @@ +/* + * 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 { TIMELINE_ID_REF_NAME } from '../../constants'; +import { timelineSavedObjectType } from '../../saved_object_mappings'; +import { FieldMigrator } from '../../utils/migrator'; + +/** + * A migrator to handle moving specific fields that reference the timeline saved object to the references field within a note saved + * object. + */ +export const pinnedEventFieldsMigrator = new FieldMigrator([ + { path: 'timelineId', type: timelineSavedObjectType, name: TIMELINE_ID_REF_NAME }, +]); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/index.ts index b3d262b13cbf3..260531e1106bf 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/index.ts @@ -19,13 +19,13 @@ import { PinnedEventSavedObjectRuntimeType, SavedPinnedEvent, PinnedEvent as PinnedEventResponse, + PinnedEventWithoutExternalRefs, } from '../../../../../common/types/timeline/pinned_event'; -import { PageInfoNote, SortNote } from '../../../../../common/types/timeline/note'; import { FrameworkRequest } from '../../../framework'; -import { pickSavedTimeline } from '../../saved_object/timelines'; -import { convertSavedObjectToSavedTimeline } from '../timelines'; +import { createTimeline } from '../../saved_object/timelines'; import { pinnedEventSavedObjectType } from '../../saved_object_mappings/pinned_events'; +import { pinnedEventFieldsMigrator } from './field_migrator'; import { timelineSavedObjectType } from '../../saved_object_mappings'; export interface PinnedEvent { @@ -46,13 +46,6 @@ export interface PinnedEvent { timelineId: string ) => Promise; - getAllPinnedEvents: ( - request: FrameworkRequest, - pageInfo: PageInfoNote | null, - search: string | null, - sort: SortNote | null - ) => Promise; - persistPinnedEventOnTimeline: ( request: FrameworkRequest, pinnedEventId: string | null, // pinned event saved object id @@ -117,26 +110,7 @@ export const getAllPinnedEventsByTimelineId = async ( ): Promise => { const options: SavedObjectsFindOptions = { type: pinnedEventSavedObjectType, - search: timelineId, - searchFields: ['timelineId'], - }; - return getAllSavedPinnedEvents(request, options); -}; - -export const getAllPinnedEvents = async ( - request: FrameworkRequest, - pageInfo: PageInfoNote | null, - search: string | null, - sort: SortNote | null -): Promise => { - const options: SavedObjectsFindOptions = { - type: pinnedEventSavedObjectType, - perPage: pageInfo != null ? pageInfo.pageSize : undefined, - page: pageInfo != null ? pageInfo.pageIndex : undefined, - search: search != null ? search : undefined, - searchFields: ['timelineId', 'eventId'], - sortField: sort != null ? sort.sortField : undefined, - sortOrder: sort != null ? sort.sortOrder : undefined, + hasReference: { type: timelineSavedObjectType, id: timelineId }, }; return getAllSavedPinnedEvents(request, options); }; @@ -147,51 +121,35 @@ export const persistPinnedEventOnTimeline = async ( eventId: string, timelineId: string | null ): Promise => { - const savedObjectsClient = request.context.core.savedObjects.client; - try { - if (pinnedEventId == null) { - const timelineVersionSavedObject = - timelineId == null - ? await (async () => { - const timelineResult = convertSavedObjectToSavedTimeline( - await savedObjectsClient.create( - timelineSavedObjectType, - pickSavedTimeline(null, {}, request.user || null) - ) - ); - timelineId = timelineResult.savedObjectId; // eslint-disable-line no-param-reassign - return timelineResult.version; - })() - : null; - - if (timelineId != null) { - const allPinnedEventId = await getAllPinnedEventsByTimelineId(request, timelineId); - const isPinnedAlreadyExisting = allPinnedEventId.filter( - (pinnedEvent) => pinnedEvent.eventId === eventId - ); - - if (isPinnedAlreadyExisting.length === 0) { - const savedPinnedEvent: SavedPinnedEvent = { - eventId, - timelineId, - }; - // create Pinned Event on Timeline - return convertSavedObjectToSavedPinnedEvent( - await savedObjectsClient.create( - pinnedEventSavedObjectType, - pickSavedPinnedEvent(pinnedEventId, savedPinnedEvent, request.user || null) - ), - timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined - ); - } - return isPinnedAlreadyExisting[0]; - } - throw new Error('You can NOT pinned event without a timelineID'); + if (pinnedEventId != null) { + // Delete Pinned Event on Timeline + await deletePinnedEventOnTimeline(request, [pinnedEventId]); + return null; } - // Delete Pinned Event on Timeline - await deletePinnedEventOnTimeline(request, [pinnedEventId]); - return null; + + const { timelineId: validatedTimelineId, timelineVersion } = await getValidTimelineIdAndVersion( + request, + timelineId + ); + + const pinnedEvents = await getPinnedEventsInTimelineWithEventId( + request, + validatedTimelineId, + eventId + ); + + // we already had this event pinned so let's just return the one we already had + if (pinnedEvents.length > 0) { + return pinnedEvents[0]; + } + + return await createPinnedEvent({ + request, + eventId, + timelineId: validatedTimelineId, + timelineVersion, + }); } catch (err) { if (getOr(null, 'output.statusCode', err) === 404) { /* @@ -215,11 +173,91 @@ export const persistPinnedEventOnTimeline = async ( } }; +const getValidTimelineIdAndVersion = async ( + request: FrameworkRequest, + timelineId: string | null +): Promise<{ timelineId: string; timelineVersion?: string }> => { + if (timelineId != null) { + return { + timelineId, + }; + } + + const savedObjectsClient = request.context.core.savedObjects.client; + + // create timeline because it didn't exist + const { timeline: timelineResult } = await createTimeline({ + timelineId: null, + timeline: {}, + savedObjectsClient, + userInfo: request.user, + }); + + return { + timelineId: timelineResult.savedObjectId, + timelineVersion: timelineResult.version, + }; +}; + +const getPinnedEventsInTimelineWithEventId = async ( + request: FrameworkRequest, + timelineId: string, + eventId: string +): Promise => { + const allPinnedEventId = await getAllPinnedEventsByTimelineId(request, timelineId); + const pinnedEvents = allPinnedEventId.filter((pinnedEvent) => pinnedEvent.eventId === eventId); + + return pinnedEvents; +}; + +const createPinnedEvent = async ({ + request, + eventId, + timelineId, + timelineVersion, +}: { + request: FrameworkRequest; + eventId: string; + timelineId: string; + timelineVersion?: string; +}) => { + const savedObjectsClient = request.context.core.savedObjects.client; + + const savedPinnedEvent: SavedPinnedEvent = { + eventId, + timelineId, + }; + + const pinnedEventWithCreator = pickSavedPinnedEvent(null, savedPinnedEvent, request.user); + + const { transformedFields: migratedAttributes, references } = + pinnedEventFieldsMigrator.extractFieldsToReferences({ + data: pinnedEventWithCreator, + }); + + const createdPinnedEvent = await savedObjectsClient.create( + pinnedEventSavedObjectType, + migratedAttributes, + { references } + ); + + const repopulatedSavedObject = + pinnedEventFieldsMigrator.populateFieldsFromReferences(createdPinnedEvent); + + // create Pinned Event on Timeline + return convertSavedObjectToSavedPinnedEvent(repopulatedSavedObject, timelineVersion); +}; + const getSavedPinnedEvent = async (request: FrameworkRequest, pinnedEventId: string) => { const savedObjectsClient = request.context.core.savedObjects.client; - const savedObject = await savedObjectsClient.get(pinnedEventSavedObjectType, pinnedEventId); + const savedObject = await savedObjectsClient.get( + pinnedEventSavedObjectType, + pinnedEventId + ); + + const populatedPinnedEvent = pinnedEventFieldsMigrator.populateFieldsFromReferences(savedObject); - return convertSavedObjectToSavedPinnedEvent(savedObject); + return convertSavedObjectToSavedPinnedEvent(populatedPinnedEvent); }; const getAllSavedPinnedEvents = async ( @@ -227,11 +265,14 @@ const getAllSavedPinnedEvents = async ( options: SavedObjectsFindOptions ) => { const savedObjectsClient = request.context.core.savedObjects.client; - const savedObjects = await savedObjectsClient.find(options); + const savedObjects = await savedObjectsClient.find(options); - return savedObjects.saved_objects.map((savedObject) => - convertSavedObjectToSavedPinnedEvent(savedObject) - ); + return savedObjects.saved_objects.map((savedObject) => { + const populatedPinnedEvent = + pinnedEventFieldsMigrator.populateFieldsFromReferences(savedObject); + + return convertSavedObjectToSavedPinnedEvent(populatedPinnedEvent); + }); }; export const savePinnedEvents = ( @@ -284,11 +325,10 @@ export const pickSavedPinnedEvent = ( if (pinnedEventId == null) { savedPinnedEvent.created = dateNow; savedPinnedEvent.createdBy = userInfo?.username ?? UNAUTHENTICATED_USER; - savedPinnedEvent.updated = dateNow; - savedPinnedEvent.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; - } else if (pinnedEventId != null) { - savedPinnedEvent.updated = dateNow; - savedPinnedEvent.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; } + + savedPinnedEvent.updated = dateNow; + savedPinnedEvent.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; + return savedPinnedEvent; }; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.test.ts deleted file mode 100644 index b9649896c25a6..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.test.ts +++ /dev/null @@ -1,40 +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 { TIMELINE_ID_REF_NAME } from '../../constants'; -import { migrateNoteTimelineIdToReferences, TimelineId } from './notes'; - -describe('notes migrations', () => { - describe('7.16.0 timelineId', () => { - it('removes the timelineId from the migrated document', () => { - const migratedDoc = migrateNoteTimelineIdToReferences({ - id: '1', - type: 'awesome', - attributes: { timelineId: '123' }, - }); - - expect(migratedDoc.attributes).toEqual({}); - expect(migratedDoc.references).toEqual([ - // importing the timeline saved object type from the timeline saved object causes a circular import and causes the jest tests to fail - { id: '123', name: TIMELINE_ID_REF_NAME, type: 'siem-ui-timeline' }, - ]); - }); - - it('preserves additional fields when migrating timeline id', () => { - const migratedDoc = migrateNoteTimelineIdToReferences({ - id: '1', - type: 'awesome', - attributes: { awesome: 'yes', timelineId: '123' } as unknown as TimelineId, - }); - - expect(migratedDoc.attributes).toEqual({ awesome: 'yes' }); - expect(migratedDoc.references).toEqual([ - { id: '123', name: TIMELINE_ID_REF_NAME, type: 'siem-ui-timeline' }, - ]); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.ts index a8d753e916afb..76773b7fcd518 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.ts @@ -5,39 +5,9 @@ * 2.0. */ -import { - SavedObjectMigrationMap, - SavedObjectSanitizedDoc, - SavedObjectUnsanitizedDoc, -} from 'kibana/server'; -import { timelineSavedObjectType } from '..'; -import { TIMELINE_ID_REF_NAME } from '../../constants'; -import { createMigratedDoc, createReference } from './utils'; - -export interface TimelineId { - timelineId?: string | null; -} - -export const migrateNoteTimelineIdToReferences = ( - doc: SavedObjectUnsanitizedDoc -): SavedObjectSanitizedDoc => { - const { timelineId, ...restAttributes } = doc.attributes; - - const { references: docReferences = [] } = doc; - const timelineIdReferences = createReference( - timelineId, - TIMELINE_ID_REF_NAME, - timelineSavedObjectType - ); - - return createMigratedDoc({ - doc, - attributes: restAttributes, - docReferences, - migratedReferences: timelineIdReferences, - }); -}; +import { SavedObjectMigrationMap } from 'kibana/server'; +import { migrateTimelineIdToReferences } from './utils'; export const notesMigrations: SavedObjectMigrationMap = { - '7.16.0': migrateNoteTimelineIdToReferences, + '7.16.0': migrateTimelineIdToReferences, }; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/pinned_events.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/pinned_events.ts new file mode 100644 index 0000000000000..4d21190d9381c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/pinned_events.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectMigrationMap } from 'kibana/server'; +import { migrateTimelineIdToReferences } from './utils'; + +export const pinnedEventsMigrations: SavedObjectMigrationMap = { + '7.16.0': migrateTimelineIdToReferences, +}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/types.ts similarity index 72% rename from x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/index.ts rename to x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/types.ts index e4c8858321e14..7c62310a99aa6 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/types.ts @@ -5,5 +5,6 @@ * 2.0. */ -export { timelinesMigrations } from './timelines'; -export { notesMigrations } from './notes'; +export interface TimelineId { + timelineId?: string | null; +} diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.test.ts index 02e3fca996d5d..329f09e85f3a7 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.test.ts @@ -5,9 +5,39 @@ * 2.0. */ -import { createMigratedDoc, createReference } from './utils'; +import { timelineSavedObjectType } from '../timelines'; +import { TIMELINE_ID_REF_NAME } from '../../constants'; +import { TimelineId } from './types'; +import { createMigratedDoc, createReference, migrateTimelineIdToReferences } from './utils'; describe('migration utils', () => { + describe('migrateTimelineIdToReferences', () => { + it('removes the timelineId from the migrated document', () => { + const migratedDoc = migrateTimelineIdToReferences({ + id: '1', + type: 'awesome', + attributes: { timelineId: '123' }, + }); + + expect(migratedDoc.attributes).toEqual({}); + expect(migratedDoc.references).toEqual([ + { id: '123', name: TIMELINE_ID_REF_NAME, type: timelineSavedObjectType }, + ]); + }); + + it('preserves additional fields when migrating timeline id', () => { + const migratedDoc = migrateTimelineIdToReferences({ + id: '1', + type: 'awesome', + attributes: { awesome: 'yes', timelineId: '123' } as unknown as TimelineId, + }); + + expect(migratedDoc.attributes).toEqual({ awesome: 'yes' }); + expect(migratedDoc.references).toEqual([ + { id: '123', name: TIMELINE_ID_REF_NAME, type: timelineSavedObjectType }, + ]); + }); + }); describe('createReference', () => { it('returns an array with a reference when the id is defined', () => { expect(createReference('awesome', 'name', 'type')).toEqual([ diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.ts index ff9b56e6ae2c9..7bd7bc148c263 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.ts @@ -10,6 +10,9 @@ import { SavedObjectSanitizedDoc, SavedObjectUnsanitizedDoc, } from 'kibana/server'; +import { timelineSavedObjectType } from '../timelines'; +import { TIMELINE_ID_REF_NAME } from '../../constants'; +import { TimelineId } from './types'; export function createReference( id: string | null | undefined, @@ -19,6 +22,26 @@ export function createReference( return id != null ? [{ id, name, type }] : []; } +export const migrateTimelineIdToReferences = ( + doc: SavedObjectUnsanitizedDoc +): SavedObjectSanitizedDoc => { + const { timelineId, ...restAttributes } = doc.attributes; + + const { references: docReferences = [] } = doc; + const timelineIdReferences = createReference( + timelineId, + TIMELINE_ID_REF_NAME, + timelineSavedObjectType + ); + + return createMigratedDoc({ + doc, + attributes: restAttributes, + docReferences, + migratedReferences: timelineIdReferences, + }); +}; + export const createMigratedDoc = ({ doc, attributes, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/notes.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/notes.ts index 387f78e5059f4..eda2478e7809d 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/notes.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/notes.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsType } from '../../../../../../../src/core/server'; -import { notesMigrations } from './migrations'; +import { notesMigrations } from './migrations/notes'; export const noteSavedObjectType = 'siem-ui-timeline-note'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/pinned_events.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/pinned_events.ts index fbbffe35a58c0..2f8e72ad763f9 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/pinned_events.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/pinned_events.ts @@ -6,14 +6,12 @@ */ import { SavedObjectsType } from '../../../../../../../src/core/server'; +import { pinnedEventsMigrations } from './migrations/pinned_events'; export const pinnedEventSavedObjectType = 'siem-ui-timeline-pinned-event'; export const pinnedEventSavedObjectMappings: SavedObjectsType['mappings'] = { properties: { - timelineId: { - type: 'keyword', - }, eventId: { type: 'keyword', }, @@ -37,4 +35,5 @@ export const pinnedEventType: SavedObjectsType = { hidden: false, namespaceType: 'single', mappings: pinnedEventSavedObjectMappings, + migrations: pinnedEventsMigrations, }; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/timelines.ts index 8300f72a162ed..e1e3a454087f9 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/timelines.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsType } from '../../../../../../../src/core/server'; -import { timelinesMigrations } from './migrations'; +import { timelinesMigrations } from './migrations/timelines'; export const timelineSavedObjectType = 'siem-ui-timeline'; diff --git a/x-pack/test/api_integration/apis/security_solution/timeline_migrations.ts b/x-pack/test/api_integration/apis/security_solution/timeline_migrations.ts index 9863ebb7ba646..1bfefe04239e2 100644 --- a/x-pack/test/api_integration/apis/security_solution/timeline_migrations.ts +++ b/x-pack/test/api_integration/apis/security_solution/timeline_migrations.ts @@ -6,18 +6,28 @@ */ import expect from '@kbn/expect'; -import { SavedTimeline } from '../../../../plugins/security_solution/common/types/timeline'; -import { SavedNote } from '../../../../plugins/security_solution/common/types/timeline/note'; +import { + noteSavedObjectType, + pinnedEventSavedObjectType, + timelineSavedObjectType, +} from '../../../../plugins/security_solution/server/lib/timeline/saved_object_mappings'; +import { TimelineWithoutExternalRefs } from '../../../../plugins/security_solution/common/types/timeline'; +import { NoteWithoutExternalRefs } from '../../../../plugins/security_solution/common/types/timeline/note'; import { FtrProviderContext } from '../../ftr_provider_context'; import { getSavedObjectFromES } from './utils'; +import { PinnedEventWithoutExternalRefs } from '../../../../plugins/security_solution/common/types/timeline/pinned_event'; interface TimelineWithoutSavedQueryId { - 'siem-ui-timeline': Omit; + [timelineSavedObjectType]: TimelineWithoutExternalRefs; } interface NoteWithoutTimelineId { - 'siem-ui-timeline-note': Omit; + [noteSavedObjectType]: NoteWithoutExternalRefs; +} + +interface PinnedEventWithoutTimelineId { + [pinnedEventSavedObjectType]: PinnedEventWithoutExternalRefs; } export default function ({ getService }: FtrProviderContext) { @@ -28,23 +38,22 @@ export default function ({ getService }: FtrProviderContext) { const es = getService('es'); describe('7.16.0', () => { - describe('notes timelineId', () => { - before(async () => { - await esArchiver.load( - 'x-pack/test/functional/es_archives/security_solution/timelines/7.15.0' - ); - }); - - after(async () => { - await esArchiver.unload( - 'x-pack/test/functional/es_archives/security_solution/timelines/7.15.0' - ); - }); + before(async () => { + await esArchiver.load( + 'x-pack/test/functional/es_archives/security_solution/timelines/7.15.0' + ); + }); + after(async () => { + await esArchiver.unload( + 'x-pack/test/functional/es_archives/security_solution/timelines/7.15.0' + ); + }); + describe('notes timelineId', () => { it('removes the timelineId in the saved object', async () => { const timelines = await getSavedObjectFromES( es, - 'siem-ui-timeline-note', + noteSavedObjectType, { ids: { values: [ @@ -55,13 +64,13 @@ export default function ({ getService }: FtrProviderContext) { } ); - expect( - timelines.body.hits.hits[0]._source?.['siem-ui-timeline-note'] - ).to.not.have.property('timelineId'); + expect(timelines.body.hits.hits[0]._source?.[noteSavedObjectType]).to.not.have.property( + 'timelineId' + ); - expect( - timelines.body.hits.hits[1]._source?.['siem-ui-timeline-note'] - ).to.not.have.property('timelineId'); + expect(timelines.body.hits.hits[1]._source?.[noteSavedObjectType]).to.not.have.property( + 'timelineId' + ); }); it('preserves the eventId in the saved object after migration', async () => { @@ -87,30 +96,18 @@ export default function ({ getService }: FtrProviderContext) { }); describe('savedQueryId', () => { - before(async () => { - await esArchiver.load( - 'x-pack/test/functional/es_archives/security_solution/timelines/7.15.0' - ); - }); - - after(async () => { - await esArchiver.unload( - 'x-pack/test/functional/es_archives/security_solution/timelines/7.15.0' - ); - }); - it('removes the savedQueryId', async () => { const timelines = await getSavedObjectFromES( es, - 'siem-ui-timeline', + timelineSavedObjectType, { ids: { values: ['siem-ui-timeline:8dc70950-1012-11ec-9ad3-2d7c6600c0f7'] }, } ); - expect(timelines.body.hits.hits[0]._source?.['siem-ui-timeline']).to.not.have.property( - 'savedQueryId' - ); + expect( + timelines.body.hits.hits[0]._source?.[timelineSavedObjectType] + ).to.not.have.property('savedQueryId'); }); it('preserves the title in the saved object after migration', async () => { @@ -129,6 +126,57 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body.data.getOneTimeline.savedQueryId).to.be("It's me"); }); }); + + describe('pinned events timelineId', () => { + it('removes the timelineId in the saved object', async () => { + const timelines = await getSavedObjectFromES( + es, + pinnedEventSavedObjectType, + { + ids: { + values: [ + 'siem-ui-timeline-pinned-event:7a9a5540-126e-11ec-83d2-db1096c73738', + 'siem-ui-timeline-pinned-event:98d919b0-126e-11ec-83d2-db1096c73738', + ], + }, + } + ); + + expect( + timelines.body.hits.hits[0]._source?.[pinnedEventSavedObjectType] + ).to.not.have.property('timelineId'); + + expect( + timelines.body.hits.hits[1]._source?.[pinnedEventSavedObjectType] + ).to.not.have.property('timelineId'); + }); + + it('preserves the eventId in the saved object after migration', async () => { + const resp = await supertest + .get('/api/timeline') + .query({ id: '6484cc90-126e-11ec-83d2-db1096c73738' }); + + expect(resp.body.data.getOneTimeline.pinnedEventsSaveObject[0].eventId).to.be( + 'DNo00XsBEVtyvU-8LGNe' + ); + expect(resp.body.data.getOneTimeline.pinnedEventsSaveObject[1].eventId).to.be( + 'Edo00XsBEVtyvU-8LGNe' + ); + }); + + it('returns the timelineId in the response', async () => { + const resp = await supertest + .get('/api/timeline') + .query({ id: '6484cc90-126e-11ec-83d2-db1096c73738' }); + + expect(resp.body.data.getOneTimeline.pinnedEventsSaveObject[0].timelineId).to.be( + '6484cc90-126e-11ec-83d2-db1096c73738' + ); + expect(resp.body.data.getOneTimeline.pinnedEventsSaveObject[1].timelineId).to.be( + '6484cc90-126e-11ec-83d2-db1096c73738' + ); + }); + }); }); }); } diff --git a/x-pack/test/functional/es_archives/security_solution/timelines/7.15.0/data.json.gz b/x-pack/test/functional/es_archives/security_solution/timelines/7.15.0/data.json.gz index e942ef732b22a21382848acce3ba2e8c8e5456fa..91e3e459f826b0e44324906667de1fe6bcf422db 100644 GIT binary patch delta 16 XcmZ1{u}*?rzMF&NzL3*Kc6A;ADYXP_ delta 16 XcmZ1{u}*?rzMF$1`Jux`c6A;AD Date: Wed, 22 Sep 2021 15:12:18 +0200 Subject: [PATCH 08/39] Rename REINDEX_SOURCE_TO_TEMP_INDEX to REINDEX_SOURCE_TO_TEMP_TRANSFORM (#112727) --- .../saved_objects/migrationsv2/README.md | 8 ++--- .../migrations_state_action_machine.ts | 6 ++-- .../migrationsv2/model/model.test.ts | 32 +++++++++---------- .../saved_objects/migrationsv2/model/model.ts | 6 ++-- .../server/saved_objects/migrationsv2/next.ts | 4 +-- .../saved_objects/migrationsv2/types.ts | 6 ++-- 6 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/core/server/saved_objects/migrationsv2/README.md b/src/core/server/saved_objects/migrationsv2/README.md index 5121e66052f40..a6b8e01a3dc6c 100644 --- a/src/core/server/saved_objects/migrationsv2/README.md +++ b/src/core/server/saved_objects/migrationsv2/README.md @@ -36,7 +36,7 @@ - [REINDEX_SOURCE_TO_TEMP_READ](#reindex_source_to_temp_read) - [Next action](#next-action-11) - [New control state](#new-control-state-11) - - [REINDEX_SOURCE_TO_TEMP_INDEX](#reindex_source_to_temp_index) + - [REINDEX_SOURCE_TO_TEMP_TRANSFORM](#REINDEX_SOURCE_TO_TEMP_TRANSFORM) - [Next action](#next-action-12) - [New control state](#new-control-state-12) - [REINDEX_SOURCE_TO_TEMP_INDEX_BULK](#reindex_source_to_temp_index_bulk) @@ -284,11 +284,11 @@ Read the next batch of outdated documents from the source index by using search ### New control state 1. If the batch contained > 0 documents - → `REINDEX_SOURCE_TO_TEMP_INDEX` + → `REINDEX_SOURCE_TO_TEMP_TRANSFORM` 2. If there are no more documents returned → `REINDEX_SOURCE_TO_TEMP_CLOSE_PIT` -## REINDEX_SOURCE_TO_TEMP_INDEX +## REINDEX_SOURCE_TO_TEMP_TRANSFORM ### Next action `transformRawDocs` @@ -357,7 +357,7 @@ documents. If another instance has a disabled plugin it will reindex that plugin's documents without transforming them. Because this instance doesn't know which plugins were disabled by the instance that performed the -`REINDEX_SOURCE_TO_TEMP_INDEX` step, we need to search for outdated documents +`REINDEX_SOURCE_TO_TEMP_TRANSFORM` step, we need to search for outdated documents and transform them to ensure that everything is up to date. ### New control state diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts index d4ad724911277..3a5e592a8b9bf 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts @@ -13,7 +13,7 @@ import type { ElasticsearchClient } from '../../elasticsearch'; import { getErrorMessage, getRequestDebugMeta } from '../../elasticsearch'; import { Model, Next, stateActionMachine } from './state_action_machine'; import { cleanup } from './migrations_state_machine_cleanup'; -import { ReindexSourceToTempIndex, ReindexSourceToTempIndexBulk, State } from './types'; +import { ReindexSourceToTempTransform, ReindexSourceToTempIndexBulk, State } from './types'; import { SavedObjectsRawDoc } from '../serialization'; interface StateTransitionLogMeta extends LogMeta { @@ -115,7 +115,9 @@ export async function migrationStateActionMachine({ const redactedNewState = { ...newState, ...{ - outdatedDocuments: ((newState as ReindexSourceToTempIndex).outdatedDocuments ?? []).map( + outdatedDocuments: ( + (newState as ReindexSourceToTempTransform).outdatedDocuments ?? [] + ).map( (doc) => ({ _id: doc._id, diff --git a/src/core/server/saved_objects/migrationsv2/model/model.test.ts b/src/core/server/saved_objects/migrationsv2/model/model.test.ts index 033a18b488841..3e48a7147bffd 100644 --- a/src/core/server/saved_objects/migrationsv2/model/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model/model.test.ts @@ -20,7 +20,7 @@ import type { ReindexSourceToTempOpenPit, ReindexSourceToTempRead, ReindexSourceToTempClosePit, - ReindexSourceToTempIndex, + ReindexSourceToTempTransform, RefreshTarget, UpdateTargetMappingsState, UpdateTargetMappingsWaitForTaskState, @@ -962,7 +962,7 @@ describe('migrations v2 model', () => { progress: createInitialProgress(), }; - it('REINDEX_SOURCE_TO_TEMP_READ -> REINDEX_SOURCE_TO_TEMP_INDEX if the index has outdated documents to reindex', () => { + it('REINDEX_SOURCE_TO_TEMP_READ -> REINDEX_SOURCE_TO_TEMP_TRANSFORM if the index has outdated documents to reindex', () => { const outdatedDocuments = [{ _id: '1', _source: { type: 'vis' } }]; const lastHitSortValue = [123456]; const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_READ'> = Either.right({ @@ -970,8 +970,8 @@ describe('migrations v2 model', () => { lastHitSortValue, totalHits: 1, }); - const newState = model(state, res) as ReindexSourceToTempIndex; - expect(newState.controlState).toBe('REINDEX_SOURCE_TO_TEMP_INDEX'); + const newState = model(state, res) as ReindexSourceToTempTransform; + expect(newState.controlState).toBe('REINDEX_SOURCE_TO_TEMP_TRANSFORM'); expect(newState.outdatedDocuments).toBe(outdatedDocuments); expect(newState.lastHitSortValue).toBe(lastHitSortValue); expect(newState.progress.processed).toBe(undefined); @@ -1032,16 +1032,16 @@ describe('migrations v2 model', () => { it('REINDEX_SOURCE_TO_TEMP_CLOSE_PIT -> SET_TEMP_WRITE_BLOCK if action succeeded', () => { const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT'> = Either.right({}); - const newState = model(state, res) as ReindexSourceToTempIndex; + const newState = model(state, res) as ReindexSourceToTempTransform; expect(newState.controlState).toBe('SET_TEMP_WRITE_BLOCK'); expect(newState.sourceIndex).toEqual(state.sourceIndex); }); }); - describe('REINDEX_SOURCE_TO_TEMP_INDEX', () => { - const state: ReindexSourceToTempIndex = { + describe('REINDEX_SOURCE_TO_TEMP_TRANSFORM', () => { + const state: ReindexSourceToTempTransform = { ...baseState, - controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX', + controlState: 'REINDEX_SOURCE_TO_TEMP_TRANSFORM', outdatedDocuments: [], versionIndexReadyActions: Option.none, sourceIndex: Option.some('.kibana') as Option.Some, @@ -1059,8 +1059,8 @@ describe('migrations v2 model', () => { }, ] as SavedObjectsRawDoc[]; - it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_INDEX_BULK if action succeeded', () => { - const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.right({ + it('REINDEX_SOURCE_TO_TEMP_TRANSFORM -> REINDEX_SOURCE_TO_TEMP_INDEX_BULK if action succeeded', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_TRANSFORM'> = Either.right({ processedDocs, }); const newState = model(state, res) as ReindexSourceToTempIndexBulk; @@ -1071,7 +1071,7 @@ describe('migrations v2 model', () => { }); it('increments the progress.processed counter', () => { - const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.right({ + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_TRANSFORM'> = Either.right({ processedDocs, }); @@ -1089,8 +1089,8 @@ describe('migrations v2 model', () => { expect(newState.progress.processed).toBe(2); }); - it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_READ if action succeeded but we have carried through previous failures', () => { - const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.right({ + it('REINDEX_SOURCE_TO_TEMP_TRANSFORM -> REINDEX_SOURCE_TO_TEMP_READ if action succeeded but we have carried through previous failures', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_TRANSFORM'> = Either.right({ processedDocs, }); const testState = { @@ -1098,15 +1098,15 @@ describe('migrations v2 model', () => { corruptDocumentIds: ['a:b'], transformErrors: [], }; - const newState = model(testState, res) as ReindexSourceToTempIndex; + const newState = model(testState, res) as ReindexSourceToTempTransform; expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_READ'); expect(newState.corruptDocumentIds.length).toEqual(1); expect(newState.transformErrors.length).toEqual(0); expect(newState.progress.processed).toBe(0); }); - it('REINDEX_SOURCE_TO_TEMP_INDEX -> REINDEX_SOURCE_TO_TEMP_READ when response is left documents_transform_failed', () => { - const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX'> = Either.left({ + it('REINDEX_SOURCE_TO_TEMP_TRANSFORM -> REINDEX_SOURCE_TO_TEMP_READ when response is left documents_transform_failed', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_TRANSFORM'> = Either.left({ type: 'documents_transform_failed', corruptDocumentIds: ['a:b'], transformErrors: [], diff --git a/src/core/server/saved_objects/migrationsv2/model/model.ts b/src/core/server/saved_objects/migrationsv2/model/model.ts index 8aa3d7b83b295..5d8862e48df1a 100644 --- a/src/core/server/saved_objects/migrationsv2/model/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model/model.ts @@ -446,7 +446,7 @@ export const model = (currentState: State, resW: ResponseType): if (res.right.outdatedDocuments.length > 0) { return { ...stateP, - controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX', + controlState: 'REINDEX_SOURCE_TO_TEMP_TRANSFORM', outdatedDocuments: res.right.outdatedDocuments, lastHitSortValue: res.right.lastHitSortValue, progress, @@ -489,11 +489,11 @@ export const model = (currentState: State, resW: ResponseType): } else { throwBadResponse(stateP, res); } - } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP_INDEX') { + } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP_TRANSFORM') { // We follow a similar control flow as for // outdated document search -> outdated document transform -> transform documents bulk index // collecting issues along the way rather than failing - // REINDEX_SOURCE_TO_TEMP_INDEX handles the document transforms + // REINDEX_SOURCE_TO_TEMP_TRANSFORM handles the document transforms const res = resW as ExcludeRetryableEsError>; // Increment the processed documents, no matter what the results are. diff --git a/src/core/server/saved_objects/migrationsv2/next.ts b/src/core/server/saved_objects/migrationsv2/next.ts index 3f3714552725b..433c0998f7567 100644 --- a/src/core/server/saved_objects/migrationsv2/next.ts +++ b/src/core/server/saved_objects/migrationsv2/next.ts @@ -12,7 +12,7 @@ import type { ReindexSourceToTempOpenPit, ReindexSourceToTempRead, ReindexSourceToTempClosePit, - ReindexSourceToTempIndex, + ReindexSourceToTempTransform, MarkVersionIndexReady, InitState, LegacyCreateReindexTargetState, @@ -105,7 +105,7 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra }), REINDEX_SOURCE_TO_TEMP_CLOSE_PIT: (state: ReindexSourceToTempClosePit) => Actions.closePit({ client, pitId: state.sourceIndexPitId }), - REINDEX_SOURCE_TO_TEMP_INDEX: (state: ReindexSourceToTempIndex) => + REINDEX_SOURCE_TO_TEMP_TRANSFORM: (state: ReindexSourceToTempTransform) => Actions.transformDocs({ transformRawDocs, outdatedDocuments: state.outdatedDocuments }), REINDEX_SOURCE_TO_TEMP_INDEX_BULK: (state: ReindexSourceToTempIndexBulk) => Actions.bulkOverwriteTransformedDocuments({ diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts index 49ce12c53aa1a..4f6419930c6cc 100644 --- a/src/core/server/saved_objects/migrationsv2/types.ts +++ b/src/core/server/saved_objects/migrationsv2/types.ts @@ -233,8 +233,8 @@ export interface ReindexSourceToTempClosePit extends PostInitState { readonly sourceIndexPitId: string; } -export interface ReindexSourceToTempIndex extends PostInitState { - readonly controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX'; +export interface ReindexSourceToTempTransform extends PostInitState { + readonly controlState: 'REINDEX_SOURCE_TO_TEMP_TRANSFORM'; readonly outdatedDocuments: SavedObjectsRawDoc[]; readonly sourceIndexPitId: string; readonly lastHitSortValue: number[] | undefined; @@ -434,7 +434,7 @@ export type State = Readonly< | ReindexSourceToTempOpenPit | ReindexSourceToTempRead | ReindexSourceToTempClosePit - | ReindexSourceToTempIndex + | ReindexSourceToTempTransform | ReindexSourceToTempIndexBulk | SetTempWriteBlock | CloneTempToSource From 84e42e42bc6b7c5063ad828e2f0d02be07157e5d Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 22 Sep 2021 07:17:53 -0600 Subject: [PATCH 09/39] [vega] fix vega map validation errors crashing vega (#112700) * [vega] fix vega map validation errors crashing vega * eslint Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../vis_types/vega/public/vega_view/vega_map_view/view.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.ts index cf5bf15d15051..777806d90d9a6 100644 --- a/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.ts +++ b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/view.ts @@ -72,7 +72,7 @@ export class VegaMapView extends VegaBaseView { const { zoom, maxZoom, minZoom } = validateZoomSettings( this._parser.mapConfig, defaults, - this.onWarn + this.onWarn.bind(this) ); const { signals } = this._vegaStateRestorer.restore() || {}; From 1456257a73fbe3a651944deec58f25118142aee2 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 22 Sep 2021 14:31:35 +0100 Subject: [PATCH 10/39] [ML] Fixing jest tests with unhandled promise rejections (#112804) --- .../components/validate_job/validate_job_view.test.js | 2 +- .../explorer/explorer_charts/explorer_charts_container.js | 8 ++++++-- .../bucket_span_estimator/bucket_span_estimator.test.ts | 4 ++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.test.js b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.test.js index 067b03d938ee8..8ec83d8679e87 100644 --- a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.test.js +++ b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.test.js @@ -41,7 +41,7 @@ function prepareTest(messages) { }; const kibana = { services: { - notifications: { toasts: { addDanger: jest.fn() } }, + notifications: { toasts: { addDanger: jest.fn(), addError: jest.fn() } }, }, }; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index c714b388c826f..ddb46edc7b921 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -79,8 +79,12 @@ function ExplorerChartContainer({ let isCancelled = false; const generateLink = async () => { if (!isCancelled && series.functionDescription !== ML_JOB_AGGREGATION.LAT_LONG) { - const singleMetricViewerLink = await getExploreSeriesLink(mlLocator, series, timefilter); - setExplorerSeriesLink(singleMetricViewerLink); + try { + const singleMetricViewerLink = await getExploreSeriesLink(mlLocator, series, timefilter); + setExplorerSeriesLink(singleMetricViewerLink); + } catch (error) { + setExplorerSeriesLink(''); + } } }; generateLink(); diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts index fbe0ff650cc2d..6ffb74131bf6e 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts @@ -12,6 +12,10 @@ import { BucketSpanEstimatorData } from '../../../common/types/job_service'; import { estimateBucketSpanFactory } from './bucket_span_estimator'; +jest.mock('../../lib/log', () => ({ + mlLog: { warn: jest.fn() }, +})); + const callAs = { search: () => Promise.resolve({ body: {} }), cluster: { getSettings: () => Promise.resolve({ body: {} }) }, From 6bf766abb91a4faf9b9379e223510230bf6d5e4e Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Wed, 22 Sep 2021 17:28:26 +0300 Subject: [PATCH 11/39] Fixes flakiness in timelion viz functional test (#112805) * Fixes flakiness in timelion viz functional test * Add sleep --- test/functional/apps/visualize/_timelion.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/functional/apps/visualize/_timelion.ts b/test/functional/apps/visualize/_timelion.ts index ea8cb8b13ba49..85dbf7cc5ca96 100644 --- a/test/functional/apps/visualize/_timelion.ts +++ b/test/functional/apps/visualize/_timelion.ts @@ -277,17 +277,20 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('should show field suggestions for split argument when index pattern set', async () => { await monacoEditor.setCodeEditorValue(''); await monacoEditor.typeCodeEditorValue( - '.es(index=logstash-*, timefield=@timestamp ,split=', + '.es(index=logstash-*, timefield=@timestamp, split=', 'timelionCodeEditor' ); + // wait for split fields to load + await common.sleep(300); const suggestions = await timelion.getSuggestionItemsText(); + expect(suggestions.length).not.to.eql(0); expect(suggestions[0].includes('@message.raw')).to.eql(true); }); it('should show field suggestions for metric argument when index pattern set', async () => { await monacoEditor.typeCodeEditorValue( - '.es(index=logstash-*, timefield=@timestamp ,metric=avg:', + '.es(index=logstash-*, timefield=@timestamp, metric=avg:', 'timelionCodeEditor' ); const suggestions = await timelion.getSuggestionItemsText(); From 1adb492e1569f4de110a6ef428044b1d0a90d3d7 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Wed, 22 Sep 2021 09:37:25 -0500 Subject: [PATCH 12/39] skip flaky suite. #111234 --- test/accessibility/apps/dashboard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/accessibility/apps/dashboard.ts b/test/accessibility/apps/dashboard.ts index 5a3ec9d8fc869..c8a7ac566b55c 100644 --- a/test/accessibility/apps/dashboard.ts +++ b/test/accessibility/apps/dashboard.ts @@ -15,7 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const listingTable = getService('listingTable'); - describe('Dashboard', () => { + describe.skip('Dashboard', () => { const dashboardName = 'Dashboard Listing A11y'; const clonedDashboardName = 'Dashboard Listing A11y Copy'; From 4b71c435a9fed42fbcd1c8f4a3d8d5186ac117fb Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Wed, 22 Sep 2021 09:50:05 -0500 Subject: [PATCH 13/39] [data views] Rename a bunch of index pattern code references to data views (#112770) * rename a bunch of index pattern references to data views --- .../hooks/use_dashboard_app_state.test.tsx | 4 +-- .../lib/load_saved_dashboard_state.ts | 2 +- ...ndex_pattern.stub.ts => data_view.stub.ts} | 27 +++++++++++++------ ...rn.test.ts.snap => data_view.test.ts.snap} | 0 ...s.test.ts.snap => data_views.test.ts.snap} | 0 ...ndex_pattern.stub.ts => data_view.stub.ts} | 14 +++++----- ...ndex_pattern.test.ts => data_view.test.ts} | 2 +- .../common/data_views/data_views/data_view.ts | 2 +- ...ex_patterns.test.ts => data_views.test.ts} | 2 +- .../data_views/data_views/data_views.ts | 26 ++++++++---------- .../data_views/data_views/flatten_hit.test.ts | 8 +++--- .../data_views/data_views/flatten_hit.ts | 4 +-- .../data_views/data_views/format_hit.ts | 14 +++++----- .../errors/duplicate_index_pattern.ts | 2 +- .../data/common/data_views/field.stub.ts | 10 +++---- ...t.ts.snap => data_view_field.test.ts.snap} | 0 ..._field.test.ts => data_view_field.test.ts} | 2 +- ...ex_pattern_field.ts => data_view_field.ts} | 0 .../common/data_views/fields/field_list.ts | 2 +- .../data/common/data_views/fields/index.ts | 2 +- .../data/common/data_views/lib/index.ts | 3 +-- .../data/common/data_views/lib/is_default.ts | 14 ---------- ...ern.test.ts => validate_data_view.test.ts} | 2 +- ...index_pattern.ts => validate_data_view.ts} | 0 src/plugins/data/common/data_views/mocks.ts | 2 +- src/plugins/data/common/stubs.ts | 2 +- ...ndex_pattern.stub.ts => data_view.stub.ts} | 14 +++++----- ....ts => data_views_api_client.test.mock.ts} | 0 ....test.ts => data_views_api_client.test.ts} | 8 +++--- ...api_client.ts => data_views_api_client.ts} | 4 +-- .../public/data_views/data_views/index.ts | 2 +- src/plugins/data/public/data_views/index.ts | 3 +-- src/plugins/data/public/index.ts | 2 -- src/plugins/data/public/plugin.ts | 8 +++--- src/plugins/data/public/stubs.ts | 2 +- .../components/layout/discover_layout.tsx | 8 ++---- ...ver_index_pattern_management.test.tsx.snap | 2 +- .../apps/main/discover_main_route.tsx | 2 +- .../apps/main/utils/update_search_source.ts | 4 +-- .../public/saved_object/saved_object.test.ts | 8 +++--- src/plugins/visualize/public/plugin.ts | 2 +- x-pack/plugins/lens/public/plugin.ts | 6 ++--- 42 files changed, 100 insertions(+), 121 deletions(-) rename src/plugins/data/common/data_views/{index_pattern.stub.ts => data_view.stub.ts} (59%) rename src/plugins/data/common/data_views/data_views/__snapshots__/{index_pattern.test.ts.snap => data_view.test.ts.snap} (100%) rename src/plugins/data/common/data_views/data_views/__snapshots__/{index_patterns.test.ts.snap => data_views.test.ts.snap} (100%) rename src/plugins/data/common/data_views/data_views/{index_pattern.stub.ts => data_view.stub.ts} (85%) rename src/plugins/data/common/data_views/data_views/{index_pattern.test.ts => data_view.test.ts} (99%) rename src/plugins/data/common/data_views/data_views/{index_patterns.test.ts => data_views.test.ts} (99%) rename src/plugins/data/common/data_views/fields/__snapshots__/{index_pattern_field.test.ts.snap => data_view_field.test.ts.snap} (100%) rename src/plugins/data/common/data_views/fields/{index_pattern_field.test.ts => data_view_field.test.ts} (98%) rename src/plugins/data/common/data_views/fields/{index_pattern_field.ts => data_view_field.ts} (100%) delete mode 100644 src/plugins/data/common/data_views/lib/is_default.ts rename src/plugins/data/common/data_views/lib/{validate_index_pattern.test.ts => validate_data_view.test.ts} (94%) rename src/plugins/data/common/data_views/lib/{validate_index_pattern.ts => validate_data_view.ts} (100%) rename src/plugins/data/public/data_views/data_views/{index_pattern.stub.ts => data_view.stub.ts} (87%) rename src/plugins/data/public/data_views/data_views/{index_patterns_api_client.test.mock.ts => data_views_api_client.test.mock.ts} (100%) rename src/plugins/data/public/data_views/data_views/{index_patterns_api_client.test.ts => data_views_api_client.test.ts} (83%) rename src/plugins/data/public/data_views/data_views/{index_patterns_api_client.ts => data_views_api_client.ts} (95%) diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx index 1ac9d680915c6..c3b4075690261 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx @@ -59,9 +59,7 @@ const createDashboardAppStateServices = () => { const defaults = makeDefaultServices(); const indexPatterns = {} as IndexPatternsContract; const defaultIndexPattern = { id: 'foo', fields: [{ name: 'bar' }] } as IIndexPattern; - indexPatterns.ensureDefaultIndexPattern = jest - .fn() - .mockImplementation(() => Promise.resolve(true)); + indexPatterns.ensureDefaultDataView = jest.fn().mockImplementation(() => Promise.resolve(true)); indexPatterns.getDefault = jest .fn() .mockImplementation(() => Promise.resolve(defaultIndexPattern)); diff --git a/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts index 04461a46ad0da..3913608c6beff 100644 --- a/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts +++ b/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts @@ -51,7 +51,7 @@ export const loadSavedDashboardState = async ({ notifications.toasts.addWarning(getDashboard60Warning()); return; } - await indexPatterns.ensureDefaultIndexPattern(); + await indexPatterns.ensureDefaultDataView(); let savedDashboard: DashboardSavedObject | undefined; try { savedDashboard = (await savedDashboards.get(savedDashboardId)) as DashboardSavedObject; diff --git a/src/plugins/data/common/data_views/index_pattern.stub.ts b/src/plugins/data/common/data_views/data_view.stub.ts similarity index 59% rename from src/plugins/data/common/data_views/index_pattern.stub.ts rename to src/plugins/data/common/data_views/data_view.stub.ts index 16624087f83b3..a3279434c7a0b 100644 --- a/src/plugins/data/common/data_views/index_pattern.stub.ts +++ b/src/plugins/data/common/data_views/data_view.stub.ts @@ -7,12 +7,15 @@ */ import { stubFieldSpecMap, stubLogstashFieldSpecMap } from './field.stub'; -import { createStubIndexPattern } from './data_views/index_pattern.stub'; -export { createStubIndexPattern } from './data_views/index_pattern.stub'; +import { createStubDataView } from './data_views/data_view.stub'; +export { + createStubDataView, + createStubDataView as createStubIndexPattern, +} from './data_views/data_view.stub'; import { SavedObject } from '../../../../core/types'; -import { IndexPatternAttributes } from '../types'; +import { DataViewAttributes } from '../types'; -export const stubIndexPattern = createStubIndexPattern({ +export const stubDataView = createStubDataView({ spec: { id: 'logstash-*', fields: stubFieldSpecMap, @@ -21,7 +24,9 @@ export const stubIndexPattern = createStubIndexPattern({ }, }); -export const stubIndexPatternWithoutTimeField = createStubIndexPattern({ +export const stubIndexPattern = stubDataView; + +export const stubDataViewWithoutTimeField = createStubDataView({ spec: { id: 'logstash-*', fields: stubFieldSpecMap, @@ -29,7 +34,9 @@ export const stubIndexPatternWithoutTimeField = createStubIndexPattern({ }, }); -export const stubLogstashIndexPattern = createStubIndexPattern({ +export const stubIndexPatternWithoutTimeField = stubDataViewWithoutTimeField; + +export const stubLogstashDataView = createStubDataView({ spec: { id: 'logstash-*', title: 'logstash-*', @@ -38,9 +45,11 @@ export const stubLogstashIndexPattern = createStubIndexPattern({ }, }); -export function stubbedSavedObjectIndexPattern( +export const stubLogstashIndexPattern = stubLogstashDataView; + +export function stubbedSavedObjectDataView( id: string | null = null -): SavedObject { +): SavedObject { return { id: id ?? '', type: 'index-pattern', @@ -53,3 +62,5 @@ export function stubbedSavedObjectIndexPattern( references: [], }; } + +export const stubbedSavedObjectIndexPattern = stubbedSavedObjectDataView; diff --git a/src/plugins/data/common/data_views/data_views/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/data_views/data_views/__snapshots__/data_view.test.ts.snap similarity index 100% rename from src/plugins/data/common/data_views/data_views/__snapshots__/index_pattern.test.ts.snap rename to src/plugins/data/common/data_views/data_views/__snapshots__/data_view.test.ts.snap diff --git a/src/plugins/data/common/data_views/data_views/__snapshots__/index_patterns.test.ts.snap b/src/plugins/data/common/data_views/data_views/__snapshots__/data_views.test.ts.snap similarity index 100% rename from src/plugins/data/common/data_views/data_views/__snapshots__/index_patterns.test.ts.snap rename to src/plugins/data/common/data_views/data_views/__snapshots__/data_views.test.ts.snap diff --git a/src/plugins/data/common/data_views/data_views/index_pattern.stub.ts b/src/plugins/data/common/data_views/data_views/data_view.stub.ts similarity index 85% rename from src/plugins/data/common/data_views/data_views/index_pattern.stub.ts rename to src/plugins/data/common/data_views/data_views/data_view.stub.ts index 3b6660c6d93dc..5ff2d077812a8 100644 --- a/src/plugins/data/common/data_views/data_views/index_pattern.stub.ts +++ b/src/plugins/data/common/data_views/data_views/data_view.stub.ts @@ -6,18 +6,18 @@ * Side Public License, v 1. */ -import { IndexPattern } from './data_view'; +import { DataView } from './data_view'; import { DataViewSpec } from '../types'; import { FieldFormatsStartCommon } from '../../../../field_formats/common'; import { fieldFormatsMock } from '../../../../field_formats/common/mocks'; /** - * Create a custom stub index pattern. Use it in your unit tests where an {@link IndexPattern} expected. + * Create a custom stub index pattern. Use it in your unit tests where an {@link DataView} expected. * @param spec - Serialized index pattern object * @param opts - Specify index pattern options * @param deps - Optionally provide dependencies, you can provide a custom field formats implementation, by default a dummy mock is used * - * @returns - an {@link IndexPattern} instance + * @returns - an {@link DataView} instance * * * @example @@ -32,7 +32,7 @@ import { fieldFormatsMock } from '../../../../field_formats/common/mocks'; * * ``` */ -export const createStubIndexPattern = ({ +export const createStubDataView = ({ spec, opts, deps, @@ -45,12 +45,10 @@ export const createStubIndexPattern = ({ deps?: { fieldFormats?: FieldFormatsStartCommon; }; -}): IndexPattern => { - const indexPattern = new IndexPattern({ +}): DataView => + new DataView({ spec, metaFields: opts?.metaFields ?? ['_id', '_type', '_source'], shortDotsEnable: opts?.shortDotsEnable, fieldFormats: deps?.fieldFormats ?? fieldFormatsMock, }); - return indexPattern; -}; diff --git a/src/plugins/data/common/data_views/data_views/index_pattern.test.ts b/src/plugins/data/common/data_views/data_views/data_view.test.ts similarity index 99% rename from src/plugins/data/common/data_views/data_views/index_pattern.test.ts rename to src/plugins/data/common/data_views/data_views/data_view.test.ts index 5fd1d0d051acb..6aea86a7adae7 100644 --- a/src/plugins/data/common/data_views/data_views/index_pattern.test.ts +++ b/src/plugins/data/common/data_views/data_views/data_view.test.ts @@ -18,7 +18,7 @@ import { fieldFormatsMock } from '../../../../field_formats/common/mocks'; import { FieldFormat } from '../../../../field_formats/common'; import { RuntimeField } from '../types'; import { stubLogstashFields } from '../field.stub'; -import { stubbedSavedObjectIndexPattern } from '../index_pattern.stub'; +import { stubbedSavedObjectIndexPattern } from '../data_view.stub'; class MockFieldFormatter {} diff --git a/src/plugins/data/common/data_views/data_views/data_view.ts b/src/plugins/data/common/data_views/data_views/data_view.ts index 596887e83ec05..c61f5f7f31e3a 100644 --- a/src/plugins/data/common/data_views/data_views/data_view.ts +++ b/src/plugins/data/common/data_views/data_views/data_view.ts @@ -170,7 +170,7 @@ export class DataView implements IIndexPattern { // Date value returned in "_source" could be in any number of formats // Use a docvalue for each date field to ensure standardized formats when working with date fields - // indexPattern.flattenHit will override "_source" values when the same field is also defined in "fields" + // dataView.flattenHit will override "_source" values when the same field is also defined in "fields" const docvalueFields = reject(this.fields.getByType('date'), 'scripted').map((dateField) => { return { field: dateField.name, diff --git a/src/plugins/data/common/data_views/data_views/index_patterns.test.ts b/src/plugins/data/common/data_views/data_views/data_views.test.ts similarity index 99% rename from src/plugins/data/common/data_views/data_views/index_patterns.test.ts rename to src/plugins/data/common/data_views/data_views/data_views.test.ts index 996700b3c9118..ef9381f16d934 100644 --- a/src/plugins/data/common/data_views/data_views/index_patterns.test.ts +++ b/src/plugins/data/common/data_views/data_views/data_views.test.ts @@ -11,7 +11,7 @@ import { DataViewsService, DataView } from '.'; import { fieldFormatsMock } from '../../../../field_formats/common/mocks'; import { UiSettingsCommon, SavedObjectsClientCommon, SavedObject } from '../types'; -import { stubbedSavedObjectIndexPattern } from '../index_pattern.stub'; +import { stubbedSavedObjectIndexPattern } from '../data_view.stub'; const createFieldsFetcher = jest.fn().mockImplementation(() => ({ getFieldsForWildcard: jest.fn().mockImplementation(() => { diff --git a/src/plugins/data/common/data_views/data_views/data_views.ts b/src/plugins/data/common/data_views/data_views/data_views.ts index f903c65d95d2a..f9b193d154770 100644 --- a/src/plugins/data/common/data_views/data_views/data_views.ts +++ b/src/plugins/data/common/data_views/data_views/data_views.ts @@ -77,9 +77,9 @@ export class DataViewsService { private fieldFormats: FieldFormatsStartCommon; private onNotification: OnNotification; private onError: OnError; - private indexPatternCache: ReturnType; + private dataViewCache: ReturnType; - ensureDefaultIndexPattern: EnsureDefaultDataView; + ensureDefaultDataView: EnsureDefaultDataView; constructor({ uiSettings, @@ -96,12 +96,9 @@ export class DataViewsService { this.fieldFormats = fieldFormats; this.onNotification = onNotification; this.onError = onError; - this.ensureDefaultIndexPattern = createEnsureDefaultDataView( - uiSettings, - onRedirectNoIndexPattern - ); + this.ensureDefaultDataView = createEnsureDefaultDataView(uiSettings, onRedirectNoIndexPattern); - this.indexPatternCache = createDataViewCache(); + this.dataViewCache = createDataViewCache(); } /** @@ -190,9 +187,9 @@ export class DataViewsService { clearCache = (id?: string) => { this.savedObjectsCache = null; if (id) { - this.indexPatternCache.clear(id); + this.dataViewCache.clear(id); } else { - this.indexPatternCache.clearAll(); + this.dataViewCache.clearAll(); } }; @@ -505,12 +502,11 @@ export class DataViewsService { get = async (id: string): Promise => { const indexPatternPromise = - this.indexPatternCache.get(id) || - this.indexPatternCache.set(id, this.getSavedObjectAndInit(id)); + this.dataViewCache.get(id) || this.dataViewCache.set(id, this.getSavedObjectAndInit(id)); // don't cache failed requests indexPatternPromise.catch(() => { - this.indexPatternCache.clear(id); + this.dataViewCache.clear(id); }); return indexPatternPromise; @@ -580,7 +576,7 @@ export class DataViewsService { )) as SavedObject; const createdIndexPattern = await this.initFromSavedObject(response); - this.indexPatternCache.set(createdIndexPattern.id!, Promise.resolve(createdIndexPattern)); + this.dataViewCache.set(createdIndexPattern.id!, Promise.resolve(createdIndexPattern)); if (this.savedObjectsCache) { this.savedObjectsCache.push(response as SavedObject); } @@ -668,7 +664,7 @@ export class DataViewsService { indexPattern.version = samePattern.version; // Clear cache - this.indexPatternCache.clear(indexPattern.id!); + this.dataViewCache.clear(indexPattern.id!); // Try the save again return this.updateSavedObject(indexPattern, saveAttempts, ignoreErrors); @@ -682,7 +678,7 @@ export class DataViewsService { * @param indexPatternId: Id of kibana Index Pattern to delete */ async delete(indexPatternId: string) { - this.indexPatternCache.clear(indexPatternId); + this.dataViewCache.clear(indexPatternId); return this.savedObjectsClient.delete(DATA_VIEW_SAVED_OBJECT_TYPE, indexPatternId); } } diff --git a/src/plugins/data/common/data_views/data_views/flatten_hit.test.ts b/src/plugins/data/common/data_views/data_views/flatten_hit.test.ts index f8e1309a38ffe..73232a65b6b72 100644 --- a/src/plugins/data/common/data_views/data_views/flatten_hit.test.ts +++ b/src/plugins/data/common/data_views/data_views/flatten_hit.test.ts @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import { IndexPattern } from './data_view'; +import { DataView } from './data_view'; import { fieldFormatsMock } from '../../../../field_formats/common/mocks'; import { flattenHitWrapper } from './flatten_hit'; -import { stubbedSavedObjectIndexPattern } from '../index_pattern.stub'; +import { stubbedSavedObjectIndexPattern } from '../data_view.stub'; class MockFieldFormatter {} @@ -24,7 +24,7 @@ function create(id: string) { attributes: { timeFieldName, fields, title }, } = stubbedSavedObjectIndexPattern(id); - return new IndexPattern({ + return new DataView({ spec: { id, type, @@ -41,7 +41,7 @@ function create(id: string) { } describe('flattenHit', () => { - let indexPattern: IndexPattern; + let indexPattern: DataView; // create an indexPattern instance for each test beforeEach(() => { diff --git a/src/plugins/data/common/data_views/data_views/flatten_hit.ts b/src/plugins/data/common/data_views/data_views/flatten_hit.ts index 58a5dff66acc8..ddf484affa298 100644 --- a/src/plugins/data/common/data_views/data_views/flatten_hit.ts +++ b/src/plugins/data/common/data_views/data_views/flatten_hit.ts @@ -103,11 +103,11 @@ function decorateFlattenedWrapper(hit: Record, metaFields: Record, deep = false) { const decorateFlattened = decorateFlattenedWrapper(hit, metaFields); const cached = cache.get(hit); - const flattened = cached || flattenHit(indexPattern, hit, deep); + const flattened = cached || flattenHit(dataView, hit, deep); if (!cached) { cache.set(hit, { ...flattened }); } diff --git a/src/plugins/data/common/data_views/data_views/format_hit.ts b/src/plugins/data/common/data_views/data_views/format_hit.ts index b226013752628..39f7fef564eb0 100644 --- a/src/plugins/data/common/data_views/data_views/format_hit.ts +++ b/src/plugins/data/common/data_views/data_views/format_hit.ts @@ -15,24 +15,24 @@ const partialFormattedCache = new WeakMap(); // Takes a hit, merges it with any stored/scripted fields, and with the metaFields // returns a formatted version -export function formatHitProvider(indexPattern: DataView, defaultFormat: any) { +export function formatHitProvider(dataView: DataView, defaultFormat: any) { function convert( hit: Record, val: any, fieldName: string, type: FieldFormatsContentType = 'html' ) { - const field = indexPattern.fields.getByName(fieldName); - const format = field ? indexPattern.getFormatterForField(field) : defaultFormat; + const field = dataView.fields.getByName(fieldName); + const format = field ? dataView.getFormatterForField(field) : defaultFormat; - return format.convert(val, type, { field, hit, indexPattern }); + return format.convert(val, type, { field, hit, indexPattern: dataView }); } function formatHit(hit: Record, type: string = 'html') { if (type === 'text') { // formatHit of type text is for react components to get rid of // since it's currently just used at the discover's doc view table, caching is not necessary - const flattened = indexPattern.flattenHit(hit); + const flattened = dataView.flattenHit(hit); const result: Record = {}; for (const [key, value] of Object.entries(flattened)) { result[key] = convert(hit, value, key, type); @@ -53,7 +53,7 @@ export function formatHitProvider(indexPattern: DataView, defaultFormat: any) { const cache: Record = {}; formattedCache.set(hit, cache); - _.forOwn(indexPattern.flattenHit(hit), function (val: any, fieldName?: string) { + _.forOwn(dataView.flattenHit(hit), function (val: any, fieldName?: string) { // sync the formatted and partial cache if (!fieldName) { return; @@ -77,7 +77,7 @@ export function formatHitProvider(indexPattern: DataView, defaultFormat: any) { partialFormattedCache.set(hit, partials); } - const val = fieldName === '_source' ? hit._source : indexPattern.flattenHit(hit)[fieldName]; + const val = fieldName === '_source' ? hit._source : dataView.flattenHit(hit)[fieldName]; return convert(hit, val, fieldName); }; diff --git a/src/plugins/data/common/data_views/errors/duplicate_index_pattern.ts b/src/plugins/data/common/data_views/errors/duplicate_index_pattern.ts index d35b09e39aa76..942c104eee4e5 100644 --- a/src/plugins/data/common/data_views/errors/duplicate_index_pattern.ts +++ b/src/plugins/data/common/data_views/errors/duplicate_index_pattern.ts @@ -9,6 +9,6 @@ export class DuplicateDataViewError extends Error { constructor(message: string) { super(message); - this.name = 'DuplicateIndexPatternError'; + this.name = 'DuplicateDataViewError'; } } diff --git a/src/plugins/data/common/data_views/field.stub.ts b/src/plugins/data/common/data_views/field.stub.ts index 03bb0dee33db3..7ff51007bcefa 100644 --- a/src/plugins/data/common/data_views/field.stub.ts +++ b/src/plugins/data/common/data_views/field.stub.ts @@ -6,10 +6,10 @@ * Side Public License, v 1. */ -import { FieldSpec, IndexPatternField } from '.'; +import { FieldSpec, DataViewField } from '.'; -export const createIndexPatternFieldStub = ({ spec }: { spec: FieldSpec }): IndexPatternField => { - return new IndexPatternField(spec); +export const createIndexPatternFieldStub = ({ spec }: { spec: FieldSpec }): DataViewField => { + return new DataViewField(spec); }; export const stubFieldSpecMap: Record = { @@ -71,7 +71,7 @@ export const stubFieldSpecMap: Record = { }, }; -export const stubFields: IndexPatternField[] = Object.values(stubFieldSpecMap).map((spec) => +export const stubFields: DataViewField[] = Object.values(stubFieldSpecMap).map((spec) => createIndexPatternFieldStub({ spec }) ); @@ -404,6 +404,6 @@ export const stubLogstashFieldSpecMap: Record = { }, }; -export const stubLogstashFields: IndexPatternField[] = Object.values(stubLogstashFieldSpecMap).map( +export const stubLogstashFields: DataViewField[] = Object.values(stubLogstashFieldSpecMap).map( (spec) => createIndexPatternFieldStub({ spec }) ); diff --git a/src/plugins/data/common/data_views/fields/__snapshots__/index_pattern_field.test.ts.snap b/src/plugins/data/common/data_views/fields/__snapshots__/data_view_field.test.ts.snap similarity index 100% rename from src/plugins/data/common/data_views/fields/__snapshots__/index_pattern_field.test.ts.snap rename to src/plugins/data/common/data_views/fields/__snapshots__/data_view_field.test.ts.snap diff --git a/src/plugins/data/common/data_views/fields/index_pattern_field.test.ts b/src/plugins/data/common/data_views/fields/data_view_field.test.ts similarity index 98% rename from src/plugins/data/common/data_views/fields/index_pattern_field.test.ts rename to src/plugins/data/common/data_views/fields/data_view_field.test.ts index 906cb0ad1badd..9107036c15c1a 100644 --- a/src/plugins/data/common/data_views/fields/index_pattern_field.test.ts +++ b/src/plugins/data/common/data_views/fields/data_view_field.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IndexPatternField } from './index_pattern_field'; +import { IndexPatternField } from './data_view_field'; import { IndexPattern } from '..'; import { KBN_FIELD_TYPES } from '../../../common'; import { FieldSpec, RuntimeField } from '../types'; diff --git a/src/plugins/data/common/data_views/fields/index_pattern_field.ts b/src/plugins/data/common/data_views/fields/data_view_field.ts similarity index 100% rename from src/plugins/data/common/data_views/fields/index_pattern_field.ts rename to src/plugins/data/common/data_views/fields/data_view_field.ts diff --git a/src/plugins/data/common/data_views/fields/field_list.ts b/src/plugins/data/common/data_views/fields/field_list.ts index 8dd407e16e4c0..e2c850c0c4dd0 100644 --- a/src/plugins/data/common/data_views/fields/field_list.ts +++ b/src/plugins/data/common/data_views/fields/field_list.ts @@ -8,7 +8,7 @@ import { findIndex } from 'lodash'; import { IFieldType } from './types'; -import { DataViewField } from './index_pattern_field'; +import { DataViewField } from './data_view_field'; import { FieldSpec, DataViewFieldMap } from '../types'; import { DataView } from '../data_views'; diff --git a/src/plugins/data/common/data_views/fields/index.ts b/src/plugins/data/common/data_views/fields/index.ts index 53c8ed213cda7..0ff7397c4f7b5 100644 --- a/src/plugins/data/common/data_views/fields/index.ts +++ b/src/plugins/data/common/data_views/fields/index.ts @@ -9,4 +9,4 @@ export * from './types'; export { isFilterable, isNestedField } from './utils'; export * from './field_list'; -export * from './index_pattern_field'; +export * from './data_view_field'; diff --git a/src/plugins/data/common/data_views/lib/index.ts b/src/plugins/data/common/data_views/lib/index.ts index ae59c7d417818..0554232e64cae 100644 --- a/src/plugins/data/common/data_views/lib/index.ts +++ b/src/plugins/data/common/data_views/lib/index.ts @@ -8,7 +8,6 @@ export { DataViewMissingIndices } from './errors'; export { getTitle } from './get_title'; -export { isDefault } from './is_default'; export * from './types'; -export { validateDataView } from './validate_index_pattern'; +export { validateDataView } from './validate_data_view'; diff --git a/src/plugins/data/common/data_views/lib/is_default.ts b/src/plugins/data/common/data_views/lib/is_default.ts deleted file mode 100644 index 5a50d2862c58b..0000000000000 --- a/src/plugins/data/common/data_views/lib/is_default.ts +++ /dev/null @@ -1,14 +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 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 { IIndexPattern } from '../..'; - -export const isDefault = (indexPattern: IIndexPattern) => { - // Default index patterns don't have `type` defined. - return !indexPattern.type; -}; diff --git a/src/plugins/data/common/data_views/lib/validate_index_pattern.test.ts b/src/plugins/data/common/data_views/lib/validate_data_view.test.ts similarity index 94% rename from src/plugins/data/common/data_views/lib/validate_index_pattern.test.ts rename to src/plugins/data/common/data_views/lib/validate_data_view.test.ts index ed90da122484e..edf20440931e3 100644 --- a/src/plugins/data/common/data_views/lib/validate_index_pattern.test.ts +++ b/src/plugins/data/common/data_views/lib/validate_data_view.test.ts @@ -8,7 +8,7 @@ import { CONTAINS_SPACES_KEY, ILLEGAL_CHARACTERS_KEY, ILLEGAL_CHARACTERS_VISIBLE } from './types'; -import { validateDataView } from './validate_index_pattern'; +import { validateDataView } from './validate_data_view'; describe('Index Pattern Utils', () => { describe('Validation', () => { diff --git a/src/plugins/data/common/data_views/lib/validate_index_pattern.ts b/src/plugins/data/common/data_views/lib/validate_data_view.ts similarity index 100% rename from src/plugins/data/common/data_views/lib/validate_index_pattern.ts rename to src/plugins/data/common/data_views/lib/validate_data_view.ts diff --git a/src/plugins/data/common/data_views/mocks.ts b/src/plugins/data/common/data_views/mocks.ts index 6e82118f7b8b8..9585b6e60f923 100644 --- a/src/plugins/data/common/data_views/mocks.ts +++ b/src/plugins/data/common/data_views/mocks.ts @@ -7,4 +7,4 @@ */ export * from './fields/fields.mocks'; -export * from './data_views/index_pattern.stub'; +export * from './data_views/data_view.stub'; diff --git a/src/plugins/data/common/stubs.ts b/src/plugins/data/common/stubs.ts index 36bd3357e7098..5cddcf397f442 100644 --- a/src/plugins/data/common/stubs.ts +++ b/src/plugins/data/common/stubs.ts @@ -7,5 +7,5 @@ */ export * from './data_views/field.stub'; -export * from './data_views/index_pattern.stub'; +export * from './data_views/data_view.stub'; export * from './es_query/stubs'; diff --git a/src/plugins/data/public/data_views/data_views/index_pattern.stub.ts b/src/plugins/data/public/data_views/data_views/data_view.stub.ts similarity index 87% rename from src/plugins/data/public/data_views/data_views/index_pattern.stub.ts rename to src/plugins/data/public/data_views/data_views/data_view.stub.ts index 49d31def92384..b3d8448064c65 100644 --- a/src/plugins/data/public/data_views/data_views/index_pattern.stub.ts +++ b/src/plugins/data/public/data_views/data_views/data_view.stub.ts @@ -10,15 +10,15 @@ import { CoreSetup } from 'kibana/public'; import { FieldFormatsStartCommon } from '../../../../field_formats/common'; import { getFieldFormatsRegistry } from '../../../../field_formats/public/mocks'; import * as commonStubs from '../../../common/stubs'; -import { IndexPattern, IndexPatternSpec } from '../../../common'; +import { DataView, DataViewSpec } from '../../../common'; import { coreMock } from '../../../../../core/public/mocks'; /** - * Create a custom stub index pattern. Use it in your unit tests where an {@link IndexPattern} expected. + * Create a custom stub index pattern. Use it in your unit tests where an {@link DataView} expected. * @param spec - Serialized index pattern object * @param opts - Specify index pattern options * @param deps - Optionally provide dependencies, you can provide a custom field formats implementation, by default client side registry with real formatters implementation is used * - * @returns - an {@link IndexPattern} instance + * @returns - an {@link DataView} instance * * @remark - This is a client side version, a browser-agnostic version is available in {@link commonStubs | common}. * The main difference is that client side version by default uses client side field formats service, where common version uses a dummy field formats mock. @@ -35,12 +35,12 @@ import { coreMock } from '../../../../../core/public/mocks'; * * ``` */ -export const createStubIndexPattern = ({ +export const createStubDataView = ({ spec, opts, deps, }: { - spec: IndexPatternSpec; + spec: DataViewSpec; opts?: { shortDotsEnable?: boolean; metaFields?: string[]; @@ -49,8 +49,8 @@ export const createStubIndexPattern = ({ fieldFormats?: FieldFormatsStartCommon; core?: CoreSetup; }; -}): IndexPattern => { - return commonStubs.createStubIndexPattern({ +}): DataView => { + return commonStubs.createStubDataView({ spec, opts, deps: { diff --git a/src/plugins/data/public/data_views/data_views/index_patterns_api_client.test.mock.ts b/src/plugins/data/public/data_views/data_views/data_views_api_client.test.mock.ts similarity index 100% rename from src/plugins/data/public/data_views/data_views/index_patterns_api_client.test.mock.ts rename to src/plugins/data/public/data_views/data_views/data_views_api_client.test.mock.ts diff --git a/src/plugins/data/public/data_views/data_views/index_patterns_api_client.test.ts b/src/plugins/data/public/data_views/data_views/data_views_api_client.test.ts similarity index 83% rename from src/plugins/data/public/data_views/data_views/index_patterns_api_client.test.ts rename to src/plugins/data/public/data_views/data_views/data_views_api_client.test.ts index a6742852533a0..09ee001c218b5 100644 --- a/src/plugins/data/public/data_views/data_views/index_patterns_api_client.test.ts +++ b/src/plugins/data/public/data_views/data_views/data_views_api_client.test.ts @@ -6,16 +6,16 @@ * Side Public License, v 1. */ -import { http } from './index_patterns_api_client.test.mock'; -import { IndexPatternsApiClient } from './index_patterns_api_client'; +import { http } from './data_views_api_client.test.mock'; +import { DataViewsApiClient } from './data_views_api_client'; describe('IndexPatternsApiClient', () => { let fetchSpy: jest.SpyInstance; - let indexPatternsApiClient: IndexPatternsApiClient; + let indexPatternsApiClient: DataViewsApiClient; beforeEach(() => { fetchSpy = jest.spyOn(http, 'fetch').mockImplementation(() => Promise.resolve({})); - indexPatternsApiClient = new IndexPatternsApiClient(http); + indexPatternsApiClient = new DataViewsApiClient(http); }); test('uses the right URI to fetch fields for time patterns', async function () { diff --git a/src/plugins/data/public/data_views/data_views/index_patterns_api_client.ts b/src/plugins/data/public/data_views/data_views/data_views_api_client.ts similarity index 95% rename from src/plugins/data/public/data_views/data_views/index_patterns_api_client.ts rename to src/plugins/data/public/data_views/data_views/data_views_api_client.ts index 295cd99e7e017..d11ec7cfa003d 100644 --- a/src/plugins/data/public/data_views/data_views/index_patterns_api_client.ts +++ b/src/plugins/data/public/data_views/data_views/data_views_api_client.ts @@ -10,13 +10,13 @@ import { HttpSetup } from 'src/core/public'; import { DataViewMissingIndices } from '../../../common/data_views/lib'; import { GetFieldsOptions, - IIndexPatternsApiClient, + IDataViewsApiClient, GetFieldsOptionsTimePattern, } from '../../../common/data_views/types'; const API_BASE_URL: string = `/api/index_patterns/`; -export class IndexPatternsApiClient implements IIndexPatternsApiClient { +export class DataViewsApiClient implements IDataViewsApiClient { private http: HttpSetup; constructor(http: HttpSetup) { diff --git a/src/plugins/data/public/data_views/data_views/index.ts b/src/plugins/data/public/data_views/data_views/index.ts index 4b31933442893..e0d18d47f39db 100644 --- a/src/plugins/data/public/data_views/data_views/index.ts +++ b/src/plugins/data/public/data_views/data_views/index.ts @@ -8,4 +8,4 @@ export * from '../../../common/data_views/data_views'; export * from './redirect_no_index_pattern'; -export * from './index_patterns_api_client'; +export * from './data_views_api_client'; diff --git a/src/plugins/data/public/data_views/index.ts b/src/plugins/data/public/data_views/index.ts index 02e36d893fa6f..0125b173989fb 100644 --- a/src/plugins/data/public/data_views/index.ts +++ b/src/plugins/data/public/data_views/index.ts @@ -12,7 +12,6 @@ export { ILLEGAL_CHARACTERS_VISIBLE, ILLEGAL_CHARACTERS, validateDataView, - isDefault, } from '../../common/data_views/lib'; export { flattenHitWrapper, formatHitProvider, onRedirectNoIndexPattern } from './data_views'; @@ -22,7 +21,7 @@ export { IndexPatternsService, IndexPatternsContract, IndexPattern, - IndexPatternsApiClient, + DataViewsApiClient, DataViewsService, DataViewsContract, DataView, diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 6480a0a340340..e1f5b98baca9c 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -45,7 +45,6 @@ import { CONTAINS_SPACES_KEY, ILLEGAL_CHARACTERS_VISIBLE, ILLEGAL_CHARACTERS, - isDefault, validateDataView, flattenHitWrapper, } from './data_views'; @@ -58,7 +57,6 @@ export const indexPatterns = { CONTAINS_SPACES_KEY, ILLEGAL_CHARACTERS_VISIBLE, ILLEGAL_CHARACTERS, - isDefault, isFilterable, isNestedField, validate: validateDataView, diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 63f32e50f61ab..aa766f78a5ecb 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -22,9 +22,9 @@ import { SearchService } from './search/search_service'; import { QueryService } from './query'; import { createIndexPatternSelect } from './ui/index_pattern_select'; import { - IndexPatternsService, + DataViewsService, onRedirectNoIndexPattern, - IndexPatternsApiClient, + DataViewsApiClient, UiSettingsPublicToCommon, } from './data_views'; import { @@ -145,10 +145,10 @@ export class DataPublicPlugin setOverlays(overlays); setUiSettings(uiSettings); - const indexPatterns = new IndexPatternsService({ + const indexPatterns = new DataViewsService({ uiSettings: new UiSettingsPublicToCommon(uiSettings), savedObjectsClient: new SavedObjectsClientPublicToCommon(savedObjects.client), - apiClient: new IndexPatternsApiClient(http), + apiClient: new DataViewsApiClient(http), fieldFormats, onNotification: (toastInputFields) => { notifications.toasts.add(toastInputFields); diff --git a/src/plugins/data/public/stubs.ts b/src/plugins/data/public/stubs.ts index 8e790a2991b05..3d160a56bd8cf 100644 --- a/src/plugins/data/public/stubs.ts +++ b/src/plugins/data/public/stubs.ts @@ -7,4 +7,4 @@ */ export * from '../common/stubs'; -export { createStubIndexPattern } from './data_views/data_views/index_pattern.stub'; +export { createStubDataView } from './data_views/data_views/data_view.stub'; diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx index 7e3d7ff10b3a6..c2d09f31e3e0a 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx @@ -23,11 +23,7 @@ import { METRIC_TYPE } from '@kbn/analytics'; import classNames from 'classnames'; import { DiscoverNoResults } from '../no_results'; import { LoadingSpinner } from '../loading_spinner/loading_spinner'; -import { - esFilters, - IndexPatternField, - indexPatterns as indexPatternsUtils, -} from '../../../../../../../data/public'; +import { esFilters, IndexPatternField } from '../../../../../../../data/public'; import { DiscoverSidebarResponsive } from '../sidebar'; import { DiscoverLayoutProps } from './types'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../../../common'; @@ -79,7 +75,7 @@ export function DiscoverLayout({ }, [dataState.fetchStatus]); const timeField = useMemo(() => { - return indexPatternsUtils.isDefault(indexPattern) ? indexPattern.timeFieldName : undefined; + return indexPattern.type !== 'rollup' ? indexPattern.timeFieldName : undefined; }, [indexPattern]); const [isSidebarClosed, setIsSidebarClosed] = useState(false); diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap b/src/plugins/discover/public/application/apps/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap index 3ad902ed22fe8..ebb06e0b2ecd3 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap @@ -106,7 +106,7 @@ exports[`Discover IndexPattern Management renders correctly 1`] = ` } } selectedIndexPattern={ - IndexPattern { + DataView { "allowNoIndex": false, "deleteFieldFormat": [Function], "fieldAttrs": Object {}, diff --git a/src/plugins/discover/public/application/apps/main/discover_main_route.tsx b/src/plugins/discover/public/application/apps/main/discover_main_route.tsx index 53f95f38c96bd..5141908e44ade 100644 --- a/src/plugins/discover/public/application/apps/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/apps/main/discover_main_route.tsx @@ -59,7 +59,7 @@ export function DiscoverMainRoute({ services, history }: DiscoverMainProps) { const savedSearchId = id; async function loadDefaultOrCurrentIndexPattern(usedSavedSearch: SavedSearch) { - await data.indexPatterns.ensureDefaultIndexPattern(); + await data.indexPatterns.ensureDefaultDataView(); const { appStateContainer } = getState({ history, uiSettings: config }); const { index } = appStateContainer.getState(); const ip = await loadIndexPattern(index || '', data.indexPatterns, config); diff --git a/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts b/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts index 5a4a543a1d5c7..4dfcbc7b79712 100644 --- a/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts +++ b/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts @@ -10,7 +10,6 @@ import { SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; import { IndexPattern, ISearchSource } from '../../../../../../data/common'; import { SortOrder } from '../../../../saved_searches/types'; import { DiscoverServices } from '../../../../build_services'; -import { indexPatterns as indexPatternsUtils } from '../../../../../../data/public'; import { getSortForSearchSource } from '../components/doc_table'; /** @@ -52,8 +51,7 @@ export function updateSearchSource( // document-like response. .setPreferredSearchStrategyId('default'); - // this is not the default index pattern, it determines that it's not of type rollup - if (indexPatternsUtils.isDefault(indexPattern)) { + if (indexPattern.type !== 'rollup') { // Set the date range filter fields from timeFilter using the absolute format. Search sessions requires that it be converted from a relative range searchSource.setField('filter', data.query.timefilter.timefilter.createFilter(indexPattern)); } diff --git a/src/plugins/saved_objects/public/saved_object/saved_object.test.ts b/src/plugins/saved_objects/public/saved_object/saved_object.test.ts index 2f45ee211c8c9..bd8d69d6b693e 100644 --- a/src/plugins/saved_objects/public/saved_object/saved_object.test.ts +++ b/src/plugins/saved_objects/public/saved_object/saved_object.test.ts @@ -20,7 +20,7 @@ import { coreMock } from '../../../../core/public/mocks'; import { dataPluginMock, createSearchSourceMock } from '../../../../plugins/data/public/mocks'; import { createStubIndexPattern } from '../../../../plugins/data/common/stubs'; import { SavedObjectAttributes, SimpleSavedObject } from 'kibana/public'; -import { IndexPattern } from '../../../data/common'; +import { DataView } from '../../../data/common'; import { savedObjectsDecoratorRegistryMock } from './decorators/registry.mock'; describe('Saved Object', () => { @@ -725,7 +725,7 @@ describe('Saved Object', () => { type: 'dashboard', afterESResp: afterESRespCallback, searchSource: true, - indexPattern: { id: indexPatternId } as IndexPattern, + indexPattern: { id: indexPatternId } as DataView, }; stubESResponse( @@ -752,7 +752,7 @@ describe('Saved Object', () => { return savedObject.init!().then(() => { expect(afterESRespCallback).toHaveBeenCalled(); const index = savedObject.searchSource!.getField('index'); - expect(index instanceof IndexPattern).toBe(true); + expect(index instanceof DataView).toBe(true); expect(index!.id).toEqual(indexPatternId); }); }); @@ -765,7 +765,7 @@ describe('Saved Object', () => { type: 'dashboard', afterESResp: afterESRespCallback, searchSource: false, - indexPattern: { id: indexPatternId } as IndexPattern, + indexPattern: { id: indexPatternId } as DataView, }; stubESResponse(getMockedDocResponse(indexPatternId)); diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index aef131ce8d530..b128c09209743 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -162,7 +162,7 @@ export class VisualizePlugin pluginsStart.data.indexPatterns.clearCache(); // make sure a default index pattern exists // if not, the page will be redirected to management and visualize won't be rendered - await pluginsStart.data.indexPatterns.ensureDefaultIndexPattern(); + await pluginsStart.data.indexPatterns.ensureDefaultDataView(); appMounted(); diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 5326927d2c6c5..7891b5990989c 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -233,10 +233,10 @@ export class LensPlugin { const getPresentationUtilContext = () => startServices().plugins.presentationUtil.ContextProvider; - const ensureDefaultIndexPattern = async () => { + const ensureDefaultDataView = async () => { // make sure a default index pattern exists // if not, the page will be redirected to management and visualize won't be rendered - await startServices().plugins.data.indexPatterns.ensureDefaultIndexPattern(); + await startServices().plugins.data.indexPatterns.ensureDefaultDataView(); }; core.application.register({ @@ -261,7 +261,7 @@ export class LensPlugin { const frameStart = this.editorFrameService!.start(coreStart, deps); this.stopReportManager = stopReportManager; - await ensureDefaultIndexPattern(); + await ensureDefaultDataView(); return mountApp(core, params, { createEditorFrame: frameStart.createInstance, attributeService: getLensAttributeService(coreStart, deps), From 48b126a2ea9ea46808b3137fbd9bfe01231b4285 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Wed, 22 Sep 2021 11:50:07 -0400 Subject: [PATCH 14/39] Adding cases team to directories (#112835) --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6ae834b58fc89..73d22362345bd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -381,9 +381,9 @@ #CC# /x-pack/plugins/security_solution/ @elastic/security-solution # Security Solution sub teams -/x-pack/plugins/cases @elastic/security-threat-hunting +/x-pack/plugins/cases @elastic/security-threat-hunting-cases /x-pack/plugins/timelines @elastic/security-threat-hunting -/x-pack/test/case_api_integration @elastic/security-threat-hunting +/x-pack/test/case_api_integration @elastic/security-threat-hunting-cases /x-pack/plugins/lists @elastic/security-detections-response ## Security Solution sub teams - security-onboarding-and-lifecycle-mgt From cdc34cf32f7582235483e64385cd5d15b7f4b27c Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Wed, 22 Sep 2021 11:54:45 -0400 Subject: [PATCH 15/39] [Mappings editor] Add multi-field support for ip type (#112477) --- .../components/mappings_editor/lib/utils.test.ts | 15 ++++++++++++++- .../components/mappings_editor/lib/utils.ts | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts index 321500730c82f..162bb59a0528a 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts @@ -10,7 +10,7 @@ jest.mock('../constants', () => { return { MAIN_DATA_TYPE_DEFINITION: {}, TYPE_DEFINITION }; }); -import { stripUndefinedValues, getTypeLabelFromField } from './utils'; +import { stripUndefinedValues, getTypeLabelFromField, getFieldMeta } from './utils'; describe('utils', () => { describe('stripUndefinedValues()', () => { @@ -77,4 +77,17 @@ describe('utils', () => { ).toBe('Other: hyperdrive'); }); }); + + describe('getFieldMeta', () => { + test('returns "canHaveMultiFields:true" for IP data type', () => { + expect(getFieldMeta({ name: 'ip_field', type: 'ip' })).toEqual({ + canHaveChildFields: false, + canHaveMultiFields: true, + childFieldsName: 'fields', + hasChildFields: false, + hasMultiFields: false, + isExpanded: false, + }); + }); + }); }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts index bc02640ba7b78..44461f1b98aef 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts @@ -40,7 +40,7 @@ import { TreeItem } from '../components/tree'; export const getUniqueId = () => uuid.v4(); export const getChildFieldsName = (dataType: DataType): ChildFieldName | undefined => { - if (dataType === 'text' || dataType === 'keyword') { + if (dataType === 'text' || dataType === 'keyword' || dataType === 'ip') { return 'fields'; } else if (dataType === 'object' || dataType === 'nested') { return 'properties'; From a753f83140ba304e255eb1852a85746476da1f61 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 22 Sep 2021 10:07:49 -0600 Subject: [PATCH 16/39] [maps] move registerLayerWizard and registerSource to MapsSetupApi (#112465) * [maps] move registerLayerWizard and registerSource to MapsSetupApi * eslint Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/maps/public/api/index.ts | 1 + x-pack/plugins/maps/public/api/setup_api.ts | 14 ++++++++++++++ x-pack/plugins/maps/public/api/start_api.ts | 4 ---- x-pack/plugins/maps/public/index.ts | 19 +++++++++++++++++-- x-pack/plugins/maps/public/plugin.ts | 10 +++++++--- 5 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 x-pack/plugins/maps/public/api/setup_api.ts diff --git a/x-pack/plugins/maps/public/api/index.ts b/x-pack/plugins/maps/public/api/index.ts index 186fd98c90bf6..feded3e16f375 100644 --- a/x-pack/plugins/maps/public/api/index.ts +++ b/x-pack/plugins/maps/public/api/index.ts @@ -6,6 +6,7 @@ */ export { MapsStartApi } from './start_api'; +export { MapsSetupApi } from './setup_api'; export { createLayerDescriptors } from './create_layer_descriptors'; export { registerLayerWizard, registerSource } from './register'; export { suggestEMSTermJoinConfig } from './ems'; diff --git a/x-pack/plugins/maps/public/api/setup_api.ts b/x-pack/plugins/maps/public/api/setup_api.ts new file mode 100644 index 0000000000000..1b4fee968aad4 --- /dev/null +++ b/x-pack/plugins/maps/public/api/setup_api.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SourceRegistryEntry } from '../classes/sources/source_registry'; +import type { LayerWizard } from '../classes/layers/layer_wizard_registry'; + +export interface MapsSetupApi { + registerLayerWizard(layerWizard: LayerWizard): Promise; + registerSource(entry: SourceRegistryEntry): Promise; +} diff --git a/x-pack/plugins/maps/public/api/start_api.ts b/x-pack/plugins/maps/public/api/start_api.ts index e4213fe07a49c..eea440b8b2afc 100644 --- a/x-pack/plugins/maps/public/api/start_api.ts +++ b/x-pack/plugins/maps/public/api/start_api.ts @@ -6,8 +6,6 @@ */ import type { LayerDescriptor } from '../../common/descriptor_types'; -import type { SourceRegistryEntry } from '../classes/sources/source_registry'; -import type { LayerWizard } from '../classes/layers/layer_wizard_registry'; import type { CreateLayerDescriptorParams } from '../classes/sources/es_search_source'; import type { SampleValuesConfig, EMSTermJoinConfig } from '../ems_autosuggest'; @@ -22,7 +20,5 @@ export interface MapsStartApi { params: CreateLayerDescriptorParams ) => Promise; }; - registerLayerWizard(layerWizard: LayerWizard): Promise; - registerSource(entry: SourceRegistryEntry): Promise; suggestEMSTermJoinConfig(config: SampleValuesConfig): Promise; } diff --git a/x-pack/plugins/maps/public/index.ts b/x-pack/plugins/maps/public/index.ts index 29cc20c706296..19850e004e6b8 100644 --- a/x-pack/plugins/maps/public/index.ts +++ b/x-pack/plugins/maps/public/index.ts @@ -18,11 +18,26 @@ export const plugin: PluginInitializer = ( }; export { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; +export type { PreIndexedShape } from '../common/elasticsearch_util'; -export type { RenderTooltipContentParams } from './classes/tooltips/tooltip_property'; +export type { + ITooltipProperty, + RenderTooltipContentParams, +} from './classes/tooltips/tooltip_property'; -export { MapsStartApi } from './api'; +export type { MapsSetupApi, MapsStartApi } from './api'; export type { MapEmbeddable, MapEmbeddableInput, MapEmbeddableOutput } from './embeddable'; export type { EMSTermJoinConfig, SampleValuesConfig } from './ems_autosuggest'; + +export type { IVectorSource, GeoJsonWithMeta } from './classes/sources/vector_source/vector_source'; +export type { ImmutableSourceProperty, SourceEditorArgs } from './classes/sources/source'; +export type { Attribution } from '../common/descriptor_types'; +export type { + BoundsRequestMeta, + SourceTooltipConfig, +} from './classes/sources/vector_source/vector_source'; +export type { IField } from './classes/fields/field'; +export type { LayerWizard, RenderWizardArguments } from './classes/layers/layer_wizard_registry'; +export type { DataRequest } from './classes/util/data_request'; diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 784f4e8f4b9d9..8f6e74adfc2fd 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -51,6 +51,7 @@ import { createLayerDescriptors, registerLayerWizard, registerSource, + MapsSetupApi, MapsStartApi, suggestEMSTermJoinConfig, } from './api'; @@ -138,7 +139,7 @@ export class MapsPlugin this._initializerContext = initializerContext; } - public setup(core: CoreSetup, plugins: MapsPluginSetupDependencies) { + public setup(core: CoreSetup, plugins: MapsPluginSetupDependencies): MapsSetupApi { registerLicensedFeatures(plugins.licensing); const config = this._initializerContext.config.get(); @@ -194,6 +195,11 @@ export class MapsPlugin plugins.expressions.registerFunction(createTileMapFn); plugins.expressions.registerRenderer(tileMapRenderer); plugins.visualizations.createBaseVisualization(tileMapVisType); + + return { + registerLayerWizard, + registerSource, + }; } public start(core: CoreStart, plugins: MapsPluginStartDependencies): MapsStartApi { @@ -211,8 +217,6 @@ export class MapsPlugin return { createLayerDescriptors, - registerLayerWizard, - registerSource, suggestEMSTermJoinConfig, }; } From 61410a30a5843201260b710ac3bb537165c297df Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 22 Sep 2021 19:08:33 +0300 Subject: [PATCH 17/39] [vega] Handle removal of deprecated date histogram interval (#109090) * [vega] Handle removal of deprecated date histogram interval Fixes: #106352 * fix CI * add deprecation_interval_info * add test * Update vega_info_message.tsx * fix types * Update es_query_parser.ts * apply comments * fix error Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Uladzislau Lasitsa --- .../parse_es_interval.test.ts | 7 + .../date_interval_utils/parse_es_interval.ts | 27 +++- .../deprecated_interval_info.test.tsx | 135 ++++++++++++++++++ .../components/deprecated_interval_info.tsx | 53 +++++++ .../components/experimental_map_vis_info.tsx | 72 ++++------ .../public/components/vega_info_message.tsx | 45 ++++++ .../public/data_model/es_query_parser.test.js | 4 +- .../vega/public/data_model/es_query_parser.ts | 58 +++++--- .../vis_types/vega/public/data_model/types.ts | 1 - .../vis_types/vega/public/vega_type.ts | 2 +- test/functional/apps/visualize/_vega_chart.ts | 3 +- 11 files changed, 332 insertions(+), 75 deletions(-) create mode 100644 src/plugins/vis_types/vega/public/components/deprecated_interval_info.test.tsx create mode 100644 src/plugins/vis_types/vega/public/components/deprecated_interval_info.tsx create mode 100644 src/plugins/vis_types/vega/public/components/vega_info_message.tsx diff --git a/src/plugins/data/common/search/aggs/utils/date_interval_utils/parse_es_interval.test.ts b/src/plugins/data/common/search/aggs/utils/date_interval_utils/parse_es_interval.test.ts index 2e7ffd9d562c3..13d957e7c38bc 100644 --- a/src/plugins/data/common/search/aggs/utils/date_interval_utils/parse_es_interval.test.ts +++ b/src/plugins/data/common/search/aggs/utils/date_interval_utils/parse_es_interval.test.ts @@ -22,6 +22,13 @@ describe('parseEsInterval', () => { expect(parseEsInterval('1y')).toEqual({ value: 1, unit: 'y', type: 'calendar' }); }); + it('should correctly parse an user-friendly intervals', () => { + expect(parseEsInterval('minute')).toEqual({ value: 1, unit: 'm', type: 'calendar' }); + expect(parseEsInterval('hour')).toEqual({ value: 1, unit: 'h', type: 'calendar' }); + expect(parseEsInterval('month')).toEqual({ value: 1, unit: 'M', type: 'calendar' }); + expect(parseEsInterval('year')).toEqual({ value: 1, unit: 'y', type: 'calendar' }); + }); + it('should correctly parse an interval containing unit and multiple value', () => { expect(parseEsInterval('250ms')).toEqual({ value: 250, unit: 'ms', type: 'fixed' }); expect(parseEsInterval('90s')).toEqual({ value: 90, unit: 's', type: 'fixed' }); diff --git a/src/plugins/data/common/search/aggs/utils/date_interval_utils/parse_es_interval.ts b/src/plugins/data/common/search/aggs/utils/date_interval_utils/parse_es_interval.ts index 0280cc0f7c8af..b723c3f45c5a6 100644 --- a/src/plugins/data/common/search/aggs/utils/date_interval_utils/parse_es_interval.ts +++ b/src/plugins/data/common/search/aggs/utils/date_interval_utils/parse_es_interval.ts @@ -7,16 +7,37 @@ */ import dateMath, { Unit } from '@elastic/datemath'; - import { InvalidEsCalendarIntervalError } from './invalid_es_calendar_interval_error'; import { InvalidEsIntervalFormatError } from './invalid_es_interval_format_error'; const ES_INTERVAL_STRING_REGEX = new RegExp( '^([1-9][0-9]*)\\s*(' + dateMath.units.join('|') + ')$' ); - export type ParsedInterval = ReturnType; +/** ES allows to work at user-friendly intervals. + * This method matches between these intervals and the intervals accepted by parseEsInterval. + * @internal **/ +const mapToEquivalentInterval = (interval: string) => { + switch (interval) { + case 'minute': + return '1m'; + case 'hour': + return '1h'; + case 'day': + return '1d'; + case 'week': + return '1w'; + case 'month': + return '1M'; + case 'quarter': + return '1q'; + case 'year': + return '1y'; + } + return interval; +}; + /** * Extracts interval properties from an ES interval string. Disallows unrecognized interval formats * and fractional values. Converts some intervals from "calendar" to "fixed" when the number of @@ -37,7 +58,7 @@ export type ParsedInterval = ReturnType; * */ export function parseEsInterval(interval: string) { - const matches = String(interval).trim().match(ES_INTERVAL_STRING_REGEX); + const matches = String(mapToEquivalentInterval(interval)).trim().match(ES_INTERVAL_STRING_REGEX); if (!matches) { throw new InvalidEsIntervalFormatError(interval); diff --git a/src/plugins/vis_types/vega/public/components/deprecated_interval_info.test.tsx b/src/plugins/vis_types/vega/public/components/deprecated_interval_info.test.tsx new file mode 100644 index 0000000000000..d88cf279881b3 --- /dev/null +++ b/src/plugins/vis_types/vega/public/components/deprecated_interval_info.test.tsx @@ -0,0 +1,135 @@ +/* + * 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 { shouldShowDeprecatedHistogramIntervalInfo } from './deprecated_interval_info'; + +describe('shouldShowDeprecatedHistogramIntervalInfo', () => { + test('should show deprecated histogram interval', () => { + expect( + shouldShowDeprecatedHistogramIntervalInfo({ + data: { + url: { + body: { + aggs: { + test: { + date_histogram: { + interval: 'day', + }, + }, + }, + }, + }, + }, + }) + ).toBeTruthy(); + + expect( + shouldShowDeprecatedHistogramIntervalInfo({ + data: [ + { + url: { + body: { + aggs: { + test: { + date_histogram: { + interval: 'day', + }, + }, + }, + }, + }, + }, + { + url: { + body: { + aggs: { + test: { + date_histogram: { + calendar_interval: 'day', + }, + }, + }, + }, + }, + }, + ], + }) + ).toBeTruthy(); + }); + + test('should not show deprecated histogram interval', () => { + expect( + shouldShowDeprecatedHistogramIntervalInfo({ + data: { + url: { + body: { + aggs: { + test: { + date_histogram: { + interval: { '%autointerval%': true }, + }, + }, + }, + }, + }, + }, + }) + ).toBeFalsy(); + + expect( + shouldShowDeprecatedHistogramIntervalInfo({ + data: { + url: { + body: { + aggs: { + test: { + auto_date_histogram: { + field: 'bytes', + }, + }, + }, + }, + }, + }, + }) + ).toBeFalsy(); + + expect( + shouldShowDeprecatedHistogramIntervalInfo({ + data: [ + { + url: { + body: { + aggs: { + test: { + date_histogram: { + calendar_interval: 'week', + }, + }, + }, + }, + }, + }, + { + url: { + body: { + aggs: { + test: { + date_histogram: { + fixed_interval: '23d', + }, + }, + }, + }, + }, + }, + ], + }) + ).toBeFalsy(); + }); +}); diff --git a/src/plugins/vis_types/vega/public/components/deprecated_interval_info.tsx b/src/plugins/vis_types/vega/public/components/deprecated_interval_info.tsx new file mode 100644 index 0000000000000..23144a4c2084d --- /dev/null +++ b/src/plugins/vis_types/vega/public/components/deprecated_interval_info.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 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 React from 'react'; +import { EuiCallOut, EuiButtonIcon } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { VegaSpec } from '../data_model/types'; +import { getDocLinks } from '../services'; + +import { BUCKET_TYPES } from '../../../../data/public'; + +export const DeprecatedHistogramIntervalInfo = () => ( + + ), + }} + /> + } + iconType="help" + /> +); + +export const shouldShowDeprecatedHistogramIntervalInfo = (spec: VegaSpec) => { + const data = Array.isArray(spec.data) ? spec?.data : [spec.data]; + + return data.some((dataItem = {}) => { + const aggs = dataItem.url?.body?.aggs ?? {}; + + return Object.keys(aggs).some((key) => { + const dateHistogram = aggs[key]?.[BUCKET_TYPES.DATE_HISTOGRAM] || {}; + return 'interval' in dateHistogram && typeof dateHistogram.interval !== 'object'; + }); + }); +}; diff --git a/src/plugins/vis_types/vega/public/components/experimental_map_vis_info.tsx b/src/plugins/vis_types/vega/public/components/experimental_map_vis_info.tsx index 2de6eb490196c..8a1f2c2794974 100644 --- a/src/plugins/vis_types/vega/public/components/experimental_map_vis_info.tsx +++ b/src/plugins/vis_types/vega/public/components/experimental_map_vis_info.tsx @@ -6,55 +6,37 @@ * Side Public License, v 1. */ -import { parse } from 'hjson'; import React from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Vis } from '../../../../visualizations/public'; -function ExperimentalMapLayerInfo() { - const title = ( - - GitHub - - ), - }} - /> - ); - - return ( - - ); -} +import type { VegaSpec } from '../data_model/types'; -export const getInfoMessage = (vis: Vis) => { - if (vis.params.spec) { - try { - const spec = parse(vis.params.spec, { legacyRoot: false, keepWsc: true }); - - if (spec.config?.kibana?.type === 'map') { - return ; - } - } catch (e) { - // spec is invalid +export const ExperimentalMapLayerInfo = () => ( + + GitHub + + ), + }} + /> } - } + iconType="beaker" + /> +); - return null; -}; +export const shouldShowMapLayerInfo = (spec: VegaSpec) => spec.config?.kibana?.type === 'map'; diff --git a/src/plugins/vis_types/vega/public/components/vega_info_message.tsx b/src/plugins/vis_types/vega/public/components/vega_info_message.tsx new file mode 100644 index 0000000000000..265613ef1e6ce --- /dev/null +++ b/src/plugins/vis_types/vega/public/components/vega_info_message.tsx @@ -0,0 +1,45 @@ +/* + * 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 React, { useMemo } from 'react'; +import { parse } from 'hjson'; +import { ExperimentalMapLayerInfo, shouldShowMapLayerInfo } from './experimental_map_vis_info'; +import { + DeprecatedHistogramIntervalInfo, + shouldShowDeprecatedHistogramIntervalInfo, +} from './deprecated_interval_info'; + +import type { Vis } from '../../../../visualizations/public'; +import type { VegaSpec } from '../data_model/types'; + +const parseSpec = (spec: string) => { + if (spec) { + try { + return parse(spec, { legacyRoot: false, keepWsc: true }); + } catch (e) { + // spec is invalid + } + } +}; + +const InfoMessage = ({ spec }: { spec: string }) => { + const vegaSpec: VegaSpec = useMemo(() => parseSpec(spec), [spec]); + + if (!vegaSpec) { + return null; + } + + return ( + <> + {shouldShowMapLayerInfo(vegaSpec) && } + {shouldShowDeprecatedHistogramIntervalInfo(vegaSpec) && } + + ); +}; + +export const getInfoMessage = (vis: Vis) => ; diff --git a/src/plugins/vis_types/vega/public/data_model/es_query_parser.test.js b/src/plugins/vis_types/vega/public/data_model/es_query_parser.test.js index 27ed5aa18a96d..bb3c0276f4cf9 100644 --- a/src/plugins/vis_types/vega/public/data_model/es_query_parser.test.js +++ b/src/plugins/vis_types/vega/public/data_model/es_query_parser.test.js @@ -178,11 +178,11 @@ describe(`EsQueryParser.injectQueryContextVars`, () => { ); test( `%autointerval% = true`, - check({ interval: { '%autointerval%': true } }, { interval: `1h` }, ctxObj) + check({ interval: { '%autointerval%': true } }, { calendar_interval: `1h` }, ctxObj) ); test( `%autointerval% = 10`, - check({ interval: { '%autointerval%': 10 } }, { interval: `3h` }, ctxObj) + check({ interval: { '%autointerval%': 10 } }, { fixed_interval: `3h` }, ctxObj) ); test(`%timefilter% = min`, check({ a: { '%timefilter%': 'min' } }, { a: rangeStart })); test(`%timefilter% = max`, check({ a: { '%timefilter%': 'max' } }, { a: rangeEnd })); diff --git a/src/plugins/vis_types/vega/public/data_model/es_query_parser.ts b/src/plugins/vis_types/vega/public/data_model/es_query_parser.ts index d0c63b8f2a6a0..134e82d676763 100644 --- a/src/plugins/vis_types/vega/public/data_model/es_query_parser.ts +++ b/src/plugins/vis_types/vega/public/data_model/es_query_parser.ts @@ -10,6 +10,7 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { cloneDeep, isPlainObject } from 'lodash'; import type { estypes } from '@elastic/elasticsearch'; +import { Assign } from 'utility-types'; import { TimeCache } from './time_cache'; import { SearchAPI } from './search_api'; import { @@ -22,6 +23,7 @@ import { Query, ContextVarsObject, } from './types'; +import { dateHistogramInterval } from '../../../../data/common'; const TIMEFILTER: string = '%timefilter%'; const AUTOINTERVAL: string = '%autointerval%'; @@ -226,7 +228,15 @@ export class EsQueryParser { * @param {*} obj * @param {boolean} isQuery - if true, the `obj` belongs to the req's query portion */ - _injectContextVars(obj: Query | estypes.SearchRequest['body']['aggs'], isQuery: boolean) { + _injectContextVars( + obj: Assign< + Query | estypes.SearchRequest['body']['aggs'], + { + interval?: { '%autointerval%': true | number } | string; + } + >, + isQuery: boolean + ) { if (obj && typeof obj === 'object') { if (Array.isArray(obj)) { // For arrays, replace MUST_CLAUSE and MUST_NOT_CLAUSE string elements @@ -270,27 +280,33 @@ export class EsQueryParser { const subObj = (obj as ContextVarsObject)[prop]; if (!subObj || typeof obj !== 'object') continue; - // replace "interval": { "%autointerval%": true|integer } with - // auto-generated range based on the timepicker - if (prop === 'interval' && subObj[AUTOINTERVAL]) { - let size = subObj[AUTOINTERVAL]; - if (size === true) { - size = 50; // by default, try to get ~80 values - } else if (typeof size !== 'number') { - throw new Error( - i18n.translate('visTypeVega.esQueryParser.autointervalValueTypeErrorMessage', { - defaultMessage: '{autointerval} must be either {trueValue} or a number', - values: { - autointerval: `"${AUTOINTERVAL}"`, - trueValue: 'true', - }, - }) - ); + // replace "interval" with ES acceptable fixed_interval / calendar_interval + if (prop === 'interval') { + let intervalString: string; + + if (typeof subObj === 'string') { + intervalString = subObj; + } else if (subObj[AUTOINTERVAL]) { + let size = subObj[AUTOINTERVAL]; + if (size === true) { + size = 50; // by default, try to get ~80 values + } else if (typeof size !== 'number') { + throw new Error( + i18n.translate('visTypeVega.esQueryParser.autointervalValueTypeErrorMessage', { + defaultMessage: '{autointerval} must be either {trueValue} or a number', + values: { + autointerval: `"${AUTOINTERVAL}"`, + trueValue: 'true', + }, + }) + ); + } + const { max, min } = this._timeCache.getTimeBounds(); + intervalString = EsQueryParser._roundInterval((max - min) / size); } - const bounds = this._timeCache.getTimeBounds(); - (obj as ContextVarsObject).interval = EsQueryParser._roundInterval( - (bounds.max - bounds.min) / size - ); + + Object.assign(obj, dateHistogramInterval(intervalString)); + delete obj.interval; continue; } diff --git a/src/plugins/vis_types/vega/public/data_model/types.ts b/src/plugins/vis_types/vega/public/data_model/types.ts index 75b1132176d67..d1568bba6c98c 100644 --- a/src/plugins/vis_types/vega/public/data_model/types.ts +++ b/src/plugins/vis_types/vega/public/data_model/types.ts @@ -192,7 +192,6 @@ export type EmsQueryRequest = Requests & { export interface ContextVarsObject { [index: string]: any; prop: ContextVarsObjectProps; - interval: string; } export interface TooltipConfig { diff --git a/src/plugins/vis_types/vega/public/vega_type.ts b/src/plugins/vis_types/vega/public/vega_type.ts index 74899f5cfb3a4..23f0e385d2b33 100644 --- a/src/plugins/vis_types/vega/public/vega_type.ts +++ b/src/plugins/vis_types/vega/public/vega_type.ts @@ -16,7 +16,7 @@ import { getDefaultSpec } from './default_spec'; import { extractIndexPatternsFromSpec } from './lib/extract_index_pattern'; import { createInspectorAdapters } from './vega_inspector'; import { toExpressionAst } from './to_ast'; -import { getInfoMessage } from './components/experimental_map_vis_info'; +import { getInfoMessage } from './components/vega_info_message'; import { VegaVisEditorComponent } from './components/vega_vis_editor_lazy'; import type { VisParams } from './vega_fn'; diff --git a/test/functional/apps/visualize/_vega_chart.ts b/test/functional/apps/visualize/_vega_chart.ts index c52b0e0f8451f..b2692c2a00d78 100644 --- a/test/functional/apps/visualize/_vega_chart.ts +++ b/test/functional/apps/visualize/_vega_chart.ts @@ -41,8 +41,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const retry = getService('retry'); const browser = getService('browser'); - // SKIPPED: https://github.com/elastic/kibana/issues/106352 - describe.skip('vega chart in visualize app', () => { + describe('vega chart in visualize app', () => { before(async () => { await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); From 23c829997f4716f810ff3ba223e53eb99fea0b1e Mon Sep 17 00:00:00 2001 From: Vadim Yakhin Date: Wed, 22 Sep 2021 13:25:33 -0300 Subject: [PATCH 18/39] Copy pass 3 (#112815) * Remove Source config section description * Update copy * Update i18n Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../views/content_sources/components/source_settings.tsx | 3 +-- .../workplace_search/views/content_sources/constants.ts | 9 +-------- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 4 files changed, 2 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx index f723391a01cea..6b3d126ec8c0b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx @@ -45,7 +45,6 @@ import { SOURCE_SETTINGS_DESCRIPTION, SOURCE_NAME_LABEL, SOURCE_CONFIG_TITLE, - SOURCE_CONFIG_DESCRIPTION, SOURCE_CONFIG_LINK, SOURCE_REMOVE_TITLE, SOURCE_REMOVE_DESCRIPTION, @@ -206,7 +205,7 @@ export const SourceSettings: React.FC = () => { {showConfig && ( - + Date: Wed, 22 Sep 2021 12:16:14 -0600 Subject: [PATCH 19/39] [Maps] fix unhandled promise rejections in jest tests (#112712) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../edit_layer_panel.test.tsx.snap | 4 +- .../edit_layer_panel.test.tsx | 3 ++ .../__snapshots__/toc_entry.test.tsx.snap | 50 +++++++++++++++++-- .../layer_toc/toc_entry/toc_entry.test.tsx | 3 ++ 4 files changed, 54 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap index 24f15674a0504..a2ad6fa62dce6 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap @@ -101,9 +101,10 @@ exports[`EditLayerPanel is rendered 1`] = ` "renderSourceSettingsEditor": [Function], "showJoinEditor": [Function], "supportsElasticsearchFilters": [Function], + "supportsFitToBounds": [Function], } } - supportsFitToBounds={false} + supportsFitToBounds={true} />
mockSourceSettings @@ -120,6 +121,7 @@ exports[`EditLayerPanel is rendered 1`] = ` "renderSourceSettingsEditor": [Function], "showJoinEditor": [Function], "supportsElasticsearchFilters": [Function], + "supportsFitToBounds": [Function], } } layerDisplayName="layer 1" diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.test.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.test.tsx index 5b6f9c2ffc834..73e831724e78e 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.test.tsx @@ -76,6 +76,9 @@ const mockLayer = { hasErrors: () => { return false; }, + supportsFitToBounds: () => { + return true; + }, } as unknown as ILayer; const defaultProps = { diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/__snapshots__/toc_entry.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/__snapshots__/toc_entry.test.tsx.snap index 42618d099ffcf..5beaf12029d2f 100644 --- a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/__snapshots__/toc_entry.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/__snapshots__/toc_entry.test.tsx.snap @@ -24,10 +24,11 @@ exports[`TOCEntry is rendered 1`] = ` "isVisible": [Function], "renderLegendDetails": [Function], "showAtZoomLevel": [Function], + "supportsFitToBounds": [Function], } } openLayerSettings={[Function]} - supportsFitToBounds={false} + supportsFitToBounds={true} />
+
+
+
+
+ { return true; }, + supportsFitToBounds: () => { + return true; + }, } as unknown as ILayer; const defaultProps = { From 409d7e2796f75f023c9d166aab889a6fa31687f4 Mon Sep 17 00:00:00 2001 From: Kellen <9484709+goodroot@users.noreply.github.com> Date: Wed, 22 Sep 2021 11:31:49 -0700 Subject: [PATCH 20/39] Removes space, fix build (#112856) --- dev_docs/key_concepts/persistable_state.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev_docs/key_concepts/persistable_state.mdx b/dev_docs/key_concepts/persistable_state.mdx index 77c7e41ff2e53..36ad6f128d6e0 100644 --- a/dev_docs/key_concepts/persistable_state.mdx +++ b/dev_docs/key_concepts/persistable_state.mdx @@ -14,7 +14,7 @@ tags: ['kibana','dev', 'contributor', 'api docs'] Any plugin that exposes state that another plugin might persist should implement interface on their `setup` contract. This will allow plugins persisting the state to easily access migrations and other utilities. Example: Data plugin allows you to generate filters. Those filters can be persisted by applications in their saved -objects or in the URL. In order to allow apps to migrate the filters in case the structure changes in the future, the Data plugin implements `PersistableStateService` on . +objects or in the URL. In order to allow apps to migrate the filters in case the structure changes in the future, the Data plugin implements `PersistableStateService` on . note: There is currently no obvious way for a plugin to know which state is safe to persist. The developer must manually look for a matching `PersistableStateService`, or ad-hoc provided migration utilities (as is the case with Rule Type Parameters). In the future, we hope to make it easier for producers of state to understand when they need to write a migration with changes, and also make it easier for consumers of such state, to understand whether it is safe to persist. From 11f1c44bde35452c8b0d654652d936a55b0c98c4 Mon Sep 17 00:00:00 2001 From: Vadim Yakhin Date: Wed, 22 Sep 2021 15:41:05 -0300 Subject: [PATCH 21/39] Add a handler for a possible promise rejection (#112840) * Add a handler for a possible promise rejection * Improve test coverage --- .../views/account_settings/account_settings.test.tsx | 10 ++++++++++ .../views/account_settings/account_settings.tsx | 7 ++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.test.tsx index 5ff80a7683db6..c675a57ab18d8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.test.tsx @@ -27,6 +27,9 @@ describe('AccountSettings', () => { const mockCurrentUser = (user?: unknown) => (getCurrentUser as jest.Mock).mockReturnValue(Promise.resolve(user)); + const mockCurrentUserError = () => + (getCurrentUser as jest.Mock).mockReturnValue(Promise.reject()); + beforeAll(() => { mockCurrentUser(); }); @@ -44,6 +47,13 @@ describe('AccountSettings', () => { expect(wrapper.isEmptyRender()).toBe(true); }); + it('does not render if the getCurrentUser promise returns error', async () => { + mockCurrentUserError(); + const wrapper = await shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + it('renders the security UI components when the user exists', async () => { mockCurrentUser({ username: 'mock user' }); (getPersonalInfo as jest.Mock).mockReturnValue(
); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx index fb5a3e34a9db7..b26650875f3ef 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx @@ -20,7 +20,12 @@ export const AccountSettings: React.FC = () => { const [currentUser, setCurrentUser] = useState(null); useEffect(() => { - security.authc.getCurrentUser().then(setCurrentUser); + security.authc + .getCurrentUser() + .then(setCurrentUser) + .catch(() => { + setCurrentUser(null); + }); }, [security.authc]); const PersonalInfo = useMemo(() => security.uiApi.components.getPersonalInfo, [security.uiApi]); From 15846b27c01498242ae4b8934aafcd49b3e6580c Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 22 Sep 2021 12:47:35 -0600 Subject: [PATCH 22/39] [Maps] move joins from LayerDescriptor to VectorLayerDescriptor (#112427) * [Maps] move joins from LayerDescriptor to VectorLayerDescriptor * clean up ISource * export isVectorLayer * tslint * tslint for ml plugin * tslint apm plugin Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../VisitorBreakdownMap/useLayerList.ts | 5 +- .../choropleth_map.tsx | 3 +- .../layer_descriptor_types.ts | 5 +- x-pack/plugins/maps/common/index.ts | 1 + .../common/migrations/add_type_to_termjoin.ts | 11 +- .../maps/common/migrations/references.ts | 58 ++++---- .../blended_vector_layer.ts | 5 +- .../maps/public/classes/layers/layer.test.ts | 115 --------------- .../maps/public/classes/layers/layer.tsx | 68 --------- .../tiled_vector_layer/tiled_vector_layer.tsx | 2 +- .../classes/layers/vector_layer/index.ts | 1 + .../layers/vector_layer/vector_layer.test.tsx | 136 ++++++++++++++++++ .../layers/vector_layer/vector_layer.tsx | 85 ++++++++++- .../mvt_single_layer_vector_source.tsx | 8 ++ .../maps/public/classes/sources/source.ts | 10 -- .../sources/vector_source/vector_source.tsx | 6 + .../edit_layer_panel.test.tsx.snap | 2 + .../edit_layer_panel.test.tsx | 3 + .../edit_layer_panel/edit_layer_panel.tsx | 21 +-- .../edit_layer_panel/index.ts | 9 +- .../join_editor/join_editor.test.tsx | 6 +- .../join_editor/join_editor.tsx | 6 +- .../tooltip_control/tooltip_control.tsx | 6 +- .../maps/server/maps_telemetry/util.ts | 4 +- .../application/explorer/anomalies_map.tsx | 3 +- 25 files changed, 321 insertions(+), 258 deletions(-) create mode 100644 x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.test.tsx diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts index f1b5b67da21f1..ce42b530b80f5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts @@ -15,6 +15,7 @@ import { COLOR_MAP_TYPE, FIELD_ORIGIN, LABEL_BORDER_SIZES, + LAYER_TYPE, SOURCE_TYPES, STYLE_TYPE, SYMBOLIZE_AS_TYPES, @@ -154,7 +155,7 @@ export function useLayerList() { maxZoom: 24, alpha: 0.75, visible: true, - type: 'VECTOR', + type: LAYER_TYPE.VECTOR, }; ES_TERM_SOURCE_REGION.whereQuery = getWhereQuery(serviceName!); @@ -178,7 +179,7 @@ export function useLayerList() { maxZoom: 24, alpha: 0.75, visible: true, - type: 'VECTOR', + type: LAYER_TYPE.VECTOR, }; return [ diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/choropleth_map.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/choropleth_map.tsx index c2e1e0b7dffa9..318ff655abb21 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/choropleth_map.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/choropleth_map.tsx @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { FIELD_ORIGIN, + LAYER_TYPE, SOURCE_TYPES, STYLE_TYPE, COLOR_MAP_TYPE, @@ -85,7 +86,7 @@ export const getChoroplethTopValuesLayer = ( }, isTimeAware: true, }, - type: 'VECTOR', + type: LAYER_TYPE.VECTOR, }; }; diff --git a/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts index 740da8493c53c..244ebc59efd17 100644 --- a/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts @@ -23,6 +23,7 @@ import { KBN_IS_TILE_COMPLETE, KBN_METADATA_FEATURE, KBN_VECTOR_SHAPE_TYPE_COUNTS, + LAYER_TYPE, } from '../constants'; export type Attribution = { @@ -56,7 +57,6 @@ export type LayerDescriptor = { alpha?: number; attribution?: Attribution; id: string; - joins?: JoinDescriptor[]; label?: string | null; areLabelsOnTop?: boolean; minZoom?: number; @@ -70,9 +70,12 @@ export type LayerDescriptor = { }; export type VectorLayerDescriptor = LayerDescriptor & { + type: LAYER_TYPE.VECTOR | LAYER_TYPE.TILED_VECTOR | LAYER_TYPE.BLENDED_VECTOR; + joins?: JoinDescriptor[]; style: VectorStyleDescriptor; }; export type HeatmapLayerDescriptor = LayerDescriptor & { + type: LAYER_TYPE.HEATMAP; style: HeatmapStyleDescriptor; }; diff --git a/x-pack/plugins/maps/common/index.ts b/x-pack/plugins/maps/common/index.ts index c1b5d26fca292..8374a4d0dbaa3 100644 --- a/x-pack/plugins/maps/common/index.ts +++ b/x-pack/plugins/maps/common/index.ts @@ -12,6 +12,7 @@ export { FIELD_ORIGIN, INITIAL_LOCATION, LABEL_BORDER_SIZES, + LAYER_TYPE, MAP_SAVED_OBJECT_TYPE, SOURCE_TYPES, STYLE_TYPE, diff --git a/x-pack/plugins/maps/common/migrations/add_type_to_termjoin.ts b/x-pack/plugins/maps/common/migrations/add_type_to_termjoin.ts index d40d85f9b6192..e46bf6a1a6e7f 100644 --- a/x-pack/plugins/maps/common/migrations/add_type_to_termjoin.ts +++ b/x-pack/plugins/maps/common/migrations/add_type_to_termjoin.ts @@ -6,8 +6,8 @@ */ import { MapSavedObjectAttributes } from '../map_saved_object_type'; -import { JoinDescriptor, LayerDescriptor } from '../descriptor_types'; -import { LAYER_TYPE, SOURCE_TYPES } from '../constants'; +import { JoinDescriptor, LayerDescriptor, VectorLayerDescriptor } from '../descriptor_types'; +import { SOURCE_TYPES } from '../constants'; // enforce type property on joins. It's possible older saved-objects do not have this correctly filled in // e.g. sample-data was missing the right.type field. @@ -24,14 +24,15 @@ export function addTypeToTermJoin({ const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); layerList.forEach((layer: LayerDescriptor) => { - if (layer.type !== LAYER_TYPE.VECTOR) { + if (!('joins' in layer)) { return; } - if (!layer.joins) { + const vectorLayer = layer as VectorLayerDescriptor; + if (!vectorLayer.joins) { return; } - layer.joins.forEach((join: JoinDescriptor) => { + vectorLayer.joins.forEach((join: JoinDescriptor) => { if (!join.right) { return; } diff --git a/x-pack/plugins/maps/common/migrations/references.ts b/x-pack/plugins/maps/common/migrations/references.ts index d48be6bd56fbe..41d9dc063fe47 100644 --- a/x-pack/plugins/maps/common/migrations/references.ts +++ b/x-pack/plugins/maps/common/migrations/references.ts @@ -9,7 +9,7 @@ import { SavedObjectReference } from '../../../../../src/core/types'; import { MapSavedObjectAttributes } from '../map_saved_object_type'; -import { LayerDescriptor } from '../descriptor_types'; +import { LayerDescriptor, VectorLayerDescriptor } from '../descriptor_types'; interface IndexPatternReferenceDescriptor { indexPatternId?: string; @@ -44,21 +44,24 @@ export function extractReferences({ 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; - } - }); + if ('joins' in layer) { + // Extract index-pattern references from join + const vectorLayer = layer as VectorLayerDescriptor; + const joins = vectorLayer.joins ? vectorLayer.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 { @@ -99,16 +102,19 @@ export function injectReferences({ 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; - } - }); + if ('joins' in layer) { + // Inject index-pattern references into join + const vectorLayer = layer as VectorLayerDescriptor; + const joins = vectorLayer.joins ? vectorLayer.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 { diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts index d2734265f3bc3..a158892be9d09 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts @@ -33,7 +33,6 @@ import { SizeDynamicOptions, DynamicStylePropertyOptions, StylePropertyOptions, - LayerDescriptor, Timeslice, VectorLayerDescriptor, VectorSourceRequestMeta, @@ -179,7 +178,7 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { mapColors: string[] ): VectorLayerDescriptor { const layerDescriptor = VectorLayer.createDescriptor(options, mapColors); - layerDescriptor.type = BlendedVectorLayer.type; + layerDescriptor.type = LAYER_TYPE.BLENDED_VECTOR; return layerDescriptor; } @@ -256,7 +255,7 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { return false; } - async cloneDescriptor(): Promise { + async cloneDescriptor(): Promise { const clonedDescriptor = await super.cloneDescriptor(); // Use super getDisplayName instead of instance getDisplayName to avoid getting 'Clustered Clone of Clustered' diff --git a/x-pack/plugins/maps/public/classes/layers/layer.test.ts b/x-pack/plugins/maps/public/classes/layers/layer.test.ts index 83a936f377c7f..194b41680872c 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/layer.test.ts @@ -9,21 +9,6 @@ import { AbstractLayer } from './layer'; import { ISource } from '../sources/source'; -import { - AGG_TYPE, - FIELD_ORIGIN, - LAYER_STYLE_TYPE, - SOURCE_TYPES, - VECTOR_STYLES, -} from '../../../common/constants'; -import { ESTermSourceDescriptor, VectorStyleDescriptor } from '../../../common/descriptor_types'; -import { getDefaultDynamicProperties } from '../styles/vector/vector_style_defaults'; - -jest.mock('uuid/v4', () => { - return function () { - return '12345'; - }; -}); class MockLayer extends AbstractLayer {} @@ -36,111 +21,11 @@ class MockSource { return {}; } - getDisplayName() { - return 'mySource'; - } - async supportsFitToBounds() { return this._fitToBounds; } } -describe('cloneDescriptor', () => { - describe('with joins', () => { - const styleDescriptor = { - type: LAYER_STYLE_TYPE.VECTOR, - properties: { - ...getDefaultDynamicProperties(), - }, - } as VectorStyleDescriptor; - // @ts-expect-error - styleDescriptor.properties[VECTOR_STYLES.FILL_COLOR].options.field = { - name: '__kbnjoin__count__557d0f15', - origin: FIELD_ORIGIN.JOIN, - }; - // @ts-expect-error - styleDescriptor.properties[VECTOR_STYLES.LINE_COLOR].options.field = { - name: 'bytes', - origin: FIELD_ORIGIN.SOURCE, - }; - // @ts-expect-error - styleDescriptor.properties[VECTOR_STYLES.LABEL_BORDER_COLOR].options.field = { - name: '__kbnjoin__count__6666666666', - origin: FIELD_ORIGIN.JOIN, - }; - - test('Should update data driven styling properties using join fields', async () => { - const layerDescriptor = AbstractLayer.createDescriptor({ - style: styleDescriptor, - joins: [ - { - leftField: 'iso2', - right: { - id: '557d0f15', - indexPatternId: 'myIndexPattern', - indexPatternTitle: 'logs-*', - metrics: [{ type: AGG_TYPE.COUNT }], - term: 'myTermField', - type: SOURCE_TYPES.ES_TERM_SOURCE, - applyGlobalQuery: true, - applyGlobalTime: true, - applyForceRefresh: true, - }, - }, - ], - }); - const layer = new MockLayer({ - layerDescriptor, - source: new MockSource() as unknown as ISource, - }); - const clonedDescriptor = await layer.cloneDescriptor(); - const clonedStyleProps = (clonedDescriptor.style as VectorStyleDescriptor).properties; - // Should update style field belonging to join - // @ts-expect-error - expect(clonedStyleProps[VECTOR_STYLES.FILL_COLOR].options.field.name).toEqual( - '__kbnjoin__count__12345' - ); - // Should not update style field belonging to source - // @ts-expect-error - expect(clonedStyleProps[VECTOR_STYLES.LINE_COLOR].options.field.name).toEqual('bytes'); - // Should not update style feild belonging to different join - // @ts-expect-error - expect(clonedStyleProps[VECTOR_STYLES.LABEL_BORDER_COLOR].options.field.name).toEqual( - '__kbnjoin__count__6666666666' - ); - }); - - test('Should update data driven styling properties using join fields when metrics are not provided', async () => { - const layerDescriptor = AbstractLayer.createDescriptor({ - style: styleDescriptor, - joins: [ - { - leftField: 'iso2', - right: { - id: '557d0f15', - indexPatternId: 'myIndexPattern', - indexPatternTitle: 'logs-*', - term: 'myTermField', - type: 'joinSource', - } as unknown as ESTermSourceDescriptor, - }, - ], - }); - const layer = new MockLayer({ - layerDescriptor, - source: new MockSource() as unknown as ISource, - }); - const clonedDescriptor = await layer.cloneDescriptor(); - const clonedStyleProps = (clonedDescriptor.style as VectorStyleDescriptor).properties; - // Should update style field belonging to join - // @ts-expect-error - expect(clonedStyleProps[VECTOR_STYLES.FILL_COLOR].options.field.name).toEqual( - '__kbnjoin__count__12345' - ); - }); - }); -}); - describe('isFittable', () => { [ { diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index 0700e54a3fe87..e1043a33f28ad 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -16,23 +16,16 @@ import uuid from 'uuid/v4'; import { FeatureCollection } from 'geojson'; import { DataRequest } from '../util/data_request'; import { - AGG_TYPE, - FIELD_ORIGIN, LAYER_TYPE, MAX_ZOOM, MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER, MIN_ZOOM, SOURCE_BOUNDS_DATA_REQUEST_ID, SOURCE_DATA_REQUEST_ID, - SOURCE_TYPES, - STYLE_TYPE, } from '../../../common/constants'; import { copyPersistentState } from '../../reducers/copy_persistent_state'; import { - AggDescriptor, Attribution, - ESTermSourceDescriptor, - JoinDescriptor, LayerDescriptor, MapExtent, StyleDescriptor, @@ -42,7 +35,6 @@ import { import { ImmutableSourceProperty, ISource, SourceEditorArgs } from '../sources/source'; import { DataRequestContext } from '../../actions'; import { IStyle } from '../styles/style'; -import { getJoinAggKey } from '../../../common/get_agg_key'; import { LICENSED_FEATURES } from '../../licensed_features'; import { IESSource } from '../sources/es_source'; @@ -97,8 +89,6 @@ export interface ILayer { isPreviewLayer: () => boolean; areLabelsOnTop: () => boolean; supportsLabelsOnTop: () => boolean; - showJoinEditor(): boolean; - getJoinsDisabledReason(): string | null; isFittable(): Promise; isIncludeInFitToBounds(): boolean; getLicensedFeatures(): Promise; @@ -177,56 +167,6 @@ export class AbstractLayer implements ILayer { const displayName = await this.getDisplayName(); clonedDescriptor.label = `Clone of ${displayName}`; clonedDescriptor.sourceDescriptor = this.getSource().cloneDescriptor(); - - if (clonedDescriptor.joins) { - clonedDescriptor.joins.forEach((joinDescriptor: JoinDescriptor) => { - if (joinDescriptor.right && joinDescriptor.right.type === SOURCE_TYPES.TABLE_SOURCE) { - throw new Error( - 'Cannot clone table-source. Should only be used in MapEmbeddable, not in UX' - ); - } - const termSourceDescriptor: ESTermSourceDescriptor = - joinDescriptor.right as ESTermSourceDescriptor; - - // todo: must tie this to generic thing - const originalJoinId = joinDescriptor.right.id!; - - // right.id is uuid used to track requests in inspector - joinDescriptor.right.id = uuid(); - - // Update all data driven styling properties using join fields - if (clonedDescriptor.style && 'properties' in clonedDescriptor.style) { - const metrics = - termSourceDescriptor.metrics && termSourceDescriptor.metrics.length - ? termSourceDescriptor.metrics - : [{ type: AGG_TYPE.COUNT }]; - metrics.forEach((metricsDescriptor: AggDescriptor) => { - const originalJoinKey = getJoinAggKey({ - aggType: metricsDescriptor.type, - aggFieldName: 'field' in metricsDescriptor ? metricsDescriptor.field : '', - rightSourceId: originalJoinId, - }); - const newJoinKey = getJoinAggKey({ - aggType: metricsDescriptor.type, - aggFieldName: 'field' in metricsDescriptor ? metricsDescriptor.field : '', - rightSourceId: joinDescriptor.right.id!, - }); - - Object.keys(clonedDescriptor.style.properties).forEach((key) => { - const styleProp = clonedDescriptor.style.properties[key]; - if ( - styleProp.type === STYLE_TYPE.DYNAMIC && - styleProp.options.field && - styleProp.options.field.origin === FIELD_ORIGIN.JOIN && - styleProp.options.field.name === originalJoinKey - ) { - styleProp.options.field.name = newJoinKey; - } - }); - }); - } - }); - } return clonedDescriptor; } @@ -234,14 +174,6 @@ export class AbstractLayer implements ILayer { return `${this.getId()}${MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER}${layerNameSuffix}`; } - showJoinEditor(): boolean { - return this.getSource().showJoinEditor(); - } - - getJoinsDisabledReason() { - return this.getSource().getJoinsDisabledReason(); - } - isPreviewLayer(): boolean { return !!this._descriptor.__isPreviewLayer; } diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx index 30ec789cf8106..9b5298685865a 100644 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx @@ -48,7 +48,7 @@ export class TiledVectorLayer extends VectorLayer { mapColors?: string[] ): VectorLayerDescriptor { const layerDescriptor = super.createDescriptor(descriptor, mapColors); - layerDescriptor.type = TiledVectorLayer.type; + layerDescriptor.type = LAYER_TYPE.TILED_VECTOR; if (!layerDescriptor.style) { const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors ? mapColors : []); diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts index 3c8449c5aa4ae..cb964f77613da 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts @@ -7,6 +7,7 @@ export { addGeoJsonMbSource, getVectorSourceBounds, syncVectorSource } from './utils'; export { + isVectorLayer, IVectorLayer, VectorLayer, VectorLayerArguments, diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.test.tsx new file mode 100644 index 0000000000000..618be0b21cd73 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.test.tsx @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable max-classes-per-file */ + +jest.mock('../../styles/vector/vector_style', () => ({ + VectorStyle: class MockVectorStyle {}, +})); + +jest.mock('uuid/v4', () => { + return function () { + return '12345'; + }; +}); + +import { + AGG_TYPE, + FIELD_ORIGIN, + LAYER_STYLE_TYPE, + SOURCE_TYPES, + VECTOR_STYLES, +} from '../../../../common/constants'; +import { ESTermSourceDescriptor, VectorStyleDescriptor } from '../../../../common/descriptor_types'; +import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults'; +import { IVectorSource } from '../../sources/vector_source'; +import { VectorLayer } from './vector_layer'; + +class MockSource { + cloneDescriptor() { + return {}; + } + + getDisplayName() { + return 'mySource'; + } +} + +describe('cloneDescriptor', () => { + describe('with joins', () => { + const styleDescriptor = { + type: LAYER_STYLE_TYPE.VECTOR, + properties: { + ...getDefaultDynamicProperties(), + }, + } as VectorStyleDescriptor; + // @ts-expect-error + styleDescriptor.properties[VECTOR_STYLES.FILL_COLOR].options.field = { + name: '__kbnjoin__count__557d0f15', + origin: FIELD_ORIGIN.JOIN, + }; + // @ts-expect-error + styleDescriptor.properties[VECTOR_STYLES.LINE_COLOR].options.field = { + name: 'bytes', + origin: FIELD_ORIGIN.SOURCE, + }; + // @ts-expect-error + styleDescriptor.properties[VECTOR_STYLES.LABEL_BORDER_COLOR].options.field = { + name: '__kbnjoin__count__6666666666', + origin: FIELD_ORIGIN.JOIN, + }; + + test('Should update data driven styling properties using join fields', async () => { + const layerDescriptor = VectorLayer.createDescriptor({ + style: styleDescriptor, + joins: [ + { + leftField: 'iso2', + right: { + id: '557d0f15', + indexPatternId: 'myIndexPattern', + indexPatternTitle: 'logs-*', + metrics: [{ type: AGG_TYPE.COUNT }], + term: 'myTermField', + type: SOURCE_TYPES.ES_TERM_SOURCE, + applyGlobalQuery: true, + applyGlobalTime: true, + applyForceRefresh: true, + }, + }, + ], + }); + const layer = new VectorLayer({ + layerDescriptor, + source: new MockSource() as unknown as IVectorSource, + }); + const clonedDescriptor = await layer.cloneDescriptor(); + const clonedStyleProps = (clonedDescriptor.style as VectorStyleDescriptor).properties; + // Should update style field belonging to join + // @ts-expect-error + expect(clonedStyleProps[VECTOR_STYLES.FILL_COLOR].options.field.name).toEqual( + '__kbnjoin__count__12345' + ); + // Should not update style field belonging to source + // @ts-expect-error + expect(clonedStyleProps[VECTOR_STYLES.LINE_COLOR].options.field.name).toEqual('bytes'); + // Should not update style feild belonging to different join + // @ts-expect-error + expect(clonedStyleProps[VECTOR_STYLES.LABEL_BORDER_COLOR].options.field.name).toEqual( + '__kbnjoin__count__6666666666' + ); + }); + + test('Should update data driven styling properties using join fields when metrics are not provided', async () => { + const layerDescriptor = VectorLayer.createDescriptor({ + style: styleDescriptor, + joins: [ + { + leftField: 'iso2', + right: { + id: '557d0f15', + indexPatternId: 'myIndexPattern', + indexPatternTitle: 'logs-*', + term: 'myTermField', + type: 'joinSource', + } as unknown as ESTermSourceDescriptor, + }, + ], + }); + const layer = new VectorLayer({ + layerDescriptor, + source: new MockSource() as unknown as IVectorSource, + }); + const clonedDescriptor = await layer.cloneDescriptor(); + const clonedStyleProps = (clonedDescriptor.style as VectorStyleDescriptor).properties; + // Should update style field belonging to join + // @ts-expect-error + expect(clonedStyleProps[VECTOR_STYLES.FILL_COLOR].options.field.name).toEqual( + '__kbnjoin__count__12345' + ); + }); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index fb310496ac9ed..3faf92715451c 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import uuid from 'uuid/v4'; import type { Map as MbMap, AnyLayer as MbLayer, @@ -19,6 +20,7 @@ import { i18n } from '@kbn/i18n'; import { AbstractLayer } from '../layer'; import { IVectorStyle, VectorStyle } from '../../styles/vector/vector_style'; import { + AGG_TYPE, FEATURE_ID_PROPERTY_NAME, SOURCE_META_DATA_REQUEST_ID, SOURCE_FORMATTERS_DATA_REQUEST_ID, @@ -29,8 +31,11 @@ import { FIELD_ORIGIN, KBN_TOO_MANY_FEATURES_IMAGE_ID, FieldFormatter, + SOURCE_TYPES, + STYLE_TYPE, SUPPORTS_FEATURE_EDITING_REQUEST_ID, KBN_IS_TILE_COMPLETE, + VECTOR_STYLES, } from '../../../../common/constants'; import { JoinTooltipProperty } from '../../tooltips/join_tooltip_property'; import { DataRequestAbortError } from '../../util/data_request'; @@ -48,8 +53,11 @@ import { TimesliceMaskConfig, } from '../../util/mb_filter_expressions'; import { + AggDescriptor, DynamicStylePropertyOptions, DataFilters, + ESTermSourceDescriptor, + JoinDescriptor, StyleMetaDescriptor, Timeslice, VectorLayerDescriptor, @@ -71,6 +79,11 @@ import { ITermJoinSource } from '../../sources/term_join_source'; import { addGeoJsonMbSource, getVectorSourceBounds, syncVectorSource } from './utils'; import { JoinState, performInnerJoins } from './perform_inner_joins'; import { buildVectorRequestMeta } from '../build_vector_request_meta'; +import { getJoinAggKey } from '../../../../common/get_agg_key'; + +export function isVectorLayer(layer: ILayer) { + return (layer as IVectorLayer).canShowTooltip !== undefined; +} export interface VectorLayerArguments { source: IVectorSource; @@ -83,11 +96,13 @@ export interface IVectorLayer extends ILayer { getFields(): Promise; getStyleEditorFields(): Promise; getJoins(): InnerJoin[]; + getJoinsDisabledReason(): string | null; getValidJoins(): InnerJoin[]; getSource(): IVectorSource; getFeatureById(id: string | number): Feature | null; getPropertiesForTooltip(properties: GeoJsonProperties): Promise; hasJoins(): boolean; + showJoinEditor(): boolean; canShowTooltip(): boolean; supportsFeatureEditing(): boolean; getLeftJoinFields(): Promise; @@ -113,8 +128,8 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { options: Partial, mapColors?: string[] ): VectorLayerDescriptor { - const layerDescriptor = super.createDescriptor(options); - layerDescriptor.type = VectorLayer.type; + const layerDescriptor = super.createDescriptor(options) as VectorLayerDescriptor; + layerDescriptor.type = LAYER_TYPE.VECTOR; if (!options.style) { const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors ? mapColors : []); @@ -125,7 +140,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { layerDescriptor.joins = []; } - return layerDescriptor as VectorLayerDescriptor; + return layerDescriptor; } constructor({ @@ -147,6 +162,62 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { ); } + async cloneDescriptor(): Promise { + const clonedDescriptor = (await super.cloneDescriptor()) as VectorLayerDescriptor; + if (clonedDescriptor.joins) { + clonedDescriptor.joins.forEach((joinDescriptor: JoinDescriptor) => { + if (joinDescriptor.right && joinDescriptor.right.type === SOURCE_TYPES.TABLE_SOURCE) { + throw new Error( + 'Cannot clone table-source. Should only be used in MapEmbeddable, not in UX' + ); + } + const termSourceDescriptor: ESTermSourceDescriptor = + joinDescriptor.right as ESTermSourceDescriptor; + + // todo: must tie this to generic thing + const originalJoinId = joinDescriptor.right.id!; + + // right.id is uuid used to track requests in inspector + joinDescriptor.right.id = uuid(); + + // Update all data driven styling properties using join fields + if (clonedDescriptor.style && 'properties' in clonedDescriptor.style) { + const metrics = + termSourceDescriptor.metrics && termSourceDescriptor.metrics.length + ? termSourceDescriptor.metrics + : [{ type: AGG_TYPE.COUNT }]; + metrics.forEach((metricsDescriptor: AggDescriptor) => { + const originalJoinKey = getJoinAggKey({ + aggType: metricsDescriptor.type, + aggFieldName: 'field' in metricsDescriptor ? metricsDescriptor.field : '', + rightSourceId: originalJoinId, + }); + const newJoinKey = getJoinAggKey({ + aggType: metricsDescriptor.type, + aggFieldName: 'field' in metricsDescriptor ? metricsDescriptor.field : '', + rightSourceId: joinDescriptor.right.id!, + }); + + Object.keys(clonedDescriptor.style.properties).forEach((key) => { + const styleProp = clonedDescriptor.style.properties[key as VECTOR_STYLES]; + if ('type' in styleProp && styleProp.type === STYLE_TYPE.DYNAMIC) { + const options = styleProp.options as DynamicStylePropertyOptions; + if ( + options.field && + options.field.origin === FIELD_ORIGIN.JOIN && + options.field.name === originalJoinKey + ) { + options.field.name = newJoinKey; + } + } + }); + }); + } + }); + } + return clonedDescriptor; + } + getSource(): IVectorSource { return super.getSource() as IVectorSource; } @@ -176,6 +247,10 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { return this._joins.slice(); } + getJoinsDisabledReason() { + return this.getSource().getJoinsDisabledReason(); + } + getValidJoins() { return this.getJoins().filter((join) => { return join.hasCompleteConfig(); @@ -192,6 +267,10 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { return this.getValidJoins().length > 0; } + showJoinEditor(): boolean { + return this.getSource().showJoinEditor(); + } + isInitialDataLoadComplete() { const sourceDataRequest = this.getSourceDataRequest(); if (!sourceDataRequest || !sourceDataRequest.hasData()) { diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx index a955cb336e55e..34a30ae9ec977 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx @@ -243,6 +243,14 @@ export class MVTSingleLayerVectorSource async getDefaultFields(): Promise>> { return {}; } + + showJoinEditor(): boolean { + return false; + } + + getJoinsDisabledReason(): string | null { + return null; + } } registerSource({ diff --git a/x-pack/plugins/maps/public/classes/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts index 5b2fc16d18b41..4c2cffcf8b070 100644 --- a/x-pack/plugins/maps/public/classes/sources/source.ts +++ b/x-pack/plugins/maps/public/classes/sources/source.ts @@ -53,8 +53,6 @@ export interface ISource { isESSource(): boolean; renderSourceSettingsEditor(sourceEditorArgs: SourceEditorArgs): ReactElement | null; supportsFitToBounds(): Promise; - showJoinEditor(): boolean; - getJoinsDisabledReason(): string | null; cloneDescriptor(): AbstractSourceDescriptor; getFieldNames(): string[]; getApplyGlobalQuery(): boolean; @@ -155,14 +153,6 @@ export class AbstractSource implements ISource { return 0; } - showJoinEditor(): boolean { - return false; - } - - getJoinsDisabledReason(): string | null { - return null; - } - isESSource(): boolean { return false; } diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx index bc3807a8247b1..3c0adf64216e6 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx @@ -59,6 +59,8 @@ export interface IVectorSource extends ISource { getFields(): Promise; getFieldByName(fieldName: string): IField | null; getLeftJoinFields(): Promise; + showJoinEditor(): boolean; + getJoinsDisabledReason(): string | null; /* * Vector layer avoids unnecessarily re-fetching source data. @@ -122,6 +124,10 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc return []; } + getJoinsDisabledReason(): string | null { + return null; + } + async getGeoJsonWithMeta( layerName: string, searchFilters: VectorSourceRequestMeta, diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap index a2ad6fa62dce6..5fb1cc6f72585 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap @@ -93,6 +93,7 @@ exports[`EditLayerPanel is rendered 1`] = ` { return true; }, + canShowTooltip: () => { + return true; + }, supportsElasticsearchFilters: () => { return false; }, diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.tsx index da72969684216..424c4b8e16bec 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.tsx @@ -33,7 +33,7 @@ import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; import { LAYER_TYPE } from '../../../common/constants'; import { getData, getCore } from '../../kibana_services'; import { ILayer } from '../../classes/layers/layer'; -import { IVectorLayer } from '../../classes/layers/vector_layer'; +import { isVectorLayer, IVectorLayer } from '../../classes/layers/vector_layer'; import { ImmutableSourceProperty, OnSourceChangeArgs } from '../../classes/sources/source'; import { IField } from '../../classes/fields/field'; @@ -111,11 +111,12 @@ export class EditLayerPanel extends Component { }; async _loadLeftJoinFields() { - if ( - !this.props.selectedLayer || - !this.props.selectedLayer.showJoinEditor() || - (this.props.selectedLayer as IVectorLayer).getLeftJoinFields === undefined - ) { + if (!this.props.selectedLayer || !isVectorLayer(this.props.selectedLayer)) { + return; + } + + const vectorLayer = this.props.selectedLayer as IVectorLayer; + if (!vectorLayer.showJoinEditor() || vectorLayer.getLeftJoinFields === undefined) { return; } @@ -182,7 +183,11 @@ export class EditLayerPanel extends Component { } _renderJoinSection() { - if (!this.props.selectedLayer || !this.props.selectedLayer.showJoinEditor()) { + if (!this.props.selectedLayer || !isVectorLayer(this.props.selectedLayer)) { + return; + } + const vectorLayer = this.props.selectedLayer as IVectorLayer; + if (!vectorLayer.showJoinEditor()) { return null; } @@ -190,7 +195,7 @@ export class EditLayerPanel extends Component { diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/index.ts b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/index.ts index b78ffb3874e30..84caa45741a62 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/index.ts +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/index.ts @@ -13,11 +13,18 @@ import { LAYER_TYPE } from '../../../common/constants'; import { getSelectedLayer } from '../../selectors/map_selectors'; import { updateSourceProp } from '../../actions'; import { MapStoreState } from '../../reducers/store'; +import { isVectorLayer, IVectorLayer } from '../../classes/layers/vector_layer'; function mapStateToProps(state: MapStoreState) { const selectedLayer = getSelectedLayer(state); + let key = 'none'; + if (selectedLayer) { + key = isVectorLayer(selectedLayer) + ? `${selectedLayer.getId()}${(selectedLayer as IVectorLayer).showJoinEditor()}` + : selectedLayer.getId(); + } return { - key: selectedLayer ? `${selectedLayer.getId()}${selectedLayer.showJoinEditor()}` : '', + key, selectedLayer, }; } diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.test.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.test.tsx index 27a345cdf2dda..6da05ef0b4092 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { ILayer } from '../../../classes/layers/layer'; +import { IVectorLayer } from '../../../classes/layers/vector_layer'; import { JoinEditor } from './join_editor'; import { shallow } from 'enzyme'; import { JoinDescriptor } from '../../../../common/descriptor_types'; @@ -48,7 +48,7 @@ const defaultProps = { test('Should render join editor', () => { const component = shallow( - + ); expect(component).toMatchSnapshot(); }); @@ -57,7 +57,7 @@ test('Should render callout when joins are disabled', () => { const component = shallow( ); expect(component).toMatchSnapshot(); diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx index e0d630994566d..e99ec6a688092 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx @@ -20,7 +20,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { Join } from './resources/join'; -import { ILayer } from '../../../classes/layers/layer'; +import { IVectorLayer } from '../../../classes/layers/vector_layer'; import { JoinDescriptor } from '../../../../common/descriptor_types'; import { SOURCE_TYPES } from '../../../../common/constants'; @@ -31,10 +31,10 @@ export interface JoinField { export interface Props { joins: JoinDescriptor[]; - layer: ILayer; + layer: IVectorLayer; layerDisplayName: string; leftJoinFields: JoinField[]; - onChange: (layer: ILayer, joins: JoinDescriptor[]) => void; + onChange: (layer: IVectorLayer, joins: JoinDescriptor[]) => void; } export function JoinEditor({ joins, layer, onChange, leftJoinFields, layerDisplayName }: Props) { diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx index 3567501455734..c2ad75d9cb335 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx @@ -35,7 +35,7 @@ import { TooltipPopover } from './tooltip_popover'; import { FeatureGeometryFilterForm } from './features_tooltip'; import { EXCLUDE_TOO_MANY_FEATURES_BOX } from '../../../classes/util/mb_filter_expressions'; import { ILayer } from '../../../classes/layers/layer'; -import { IVectorLayer } from '../../../classes/layers/vector_layer'; +import { IVectorLayer, isVectorLayer } from '../../../classes/layers/vector_layer'; import { RenderToolTipContent } from '../../../classes/tooltips/tooltip_property'; function justifyAnchorLocation( @@ -58,10 +58,6 @@ function justifyAnchorLocation( return popupAnchorLocation; } -function isVectorLayer(layer: ILayer) { - return (layer as IVectorLayer).canShowTooltip !== undefined; -} - export interface Props { addFilters: ((filters: Filter[], actionId: string) => Promise) | null; closeOnClickTooltip: (tooltipId: string) => void; diff --git a/x-pack/plugins/maps/server/maps_telemetry/util.ts b/x-pack/plugins/maps/server/maps_telemetry/util.ts index ff9339fca76cb..27190c9b82142 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/util.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/util.ts @@ -10,6 +10,7 @@ import { ESGeoGridSourceDescriptor, ESSearchSourceDescriptor, LayerDescriptor, + VectorLayerDescriptor, } from '../../common/descriptor_types'; import { GRID_RESOLUTION, @@ -265,8 +266,7 @@ export function getTermJoinsPerCluster( ): TELEMETRY_TERM_JOIN_COUNTS_PER_CLUSTER { return getCountsByCluster(layerLists, (layerDescriptor: LayerDescriptor) => { return layerDescriptor.type === LAYER_TYPE.VECTOR && - layerDescriptor.joins && - layerDescriptor.joins.length + (layerDescriptor as VectorLayerDescriptor)?.joins?.length ? TELEMETRY_TERM_JOIN : null; }); diff --git a/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx b/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx index 182a8a37fadfc..28f346c0148c6 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx +++ b/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx @@ -18,6 +18,7 @@ import { } from '@elastic/eui'; import { FIELD_ORIGIN, + LAYER_TYPE, SOURCE_TYPES, STYLE_TYPE, COLOR_MAP_TYPE, @@ -125,7 +126,7 @@ export const getChoroplethAnomaliesLayer = ( isTimeAware: true, }, visible: false, - type: 'VECTOR', + type: LAYER_TYPE.VECTOR, }; }; From 74bcbc310d1dda294f71fd3b38b204f9440cdc93 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Wed, 22 Sep 2021 14:33:41 -0500 Subject: [PATCH 23/39] Bump chromedriver to 93 (#112847) --- package.json | 2 +- yarn.lock | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index c24f61e556556..588dc984651b7 100644 --- a/package.json +++ b/package.json @@ -664,7 +664,7 @@ "callsites": "^3.1.0", "chai": "3.5.0", "chance": "1.0.18", - "chromedriver": "^92.0.1", + "chromedriver": "^93.0.1", "clean-webpack-plugin": "^3.0.0", "cmd-shim": "^2.1.0", "compression-webpack-plugin": "^4.0.0", diff --git a/yarn.lock b/yarn.lock index a69450d4b628d..545bd43ed1084 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7732,6 +7732,13 @@ axios@^0.21.1: dependencies: follow-redirects "^1.10.0" +axios@^0.21.2: + version "0.21.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== + dependencies: + follow-redirects "^1.14.0" + axobject-query@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.0.2.tgz#ea187abe5b9002b377f925d8bf7d1c561adf38f9" @@ -9441,13 +9448,13 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^92.0.1: - version "92.0.1" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-92.0.1.tgz#3e28b7e0c9fb94d693cf74d51af0c29d57f18dca" - integrity sha512-LptlDVCs1GgyFNVbRoHzzy948JDVzTgGiVPXjNj385qXKQP3hjAVBIgyvb/Hco0xSEW8fjwJfsm1eQRmu6t4pQ== +chromedriver@^93.0.1: + version "93.0.1" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-93.0.1.tgz#3ed1f7baa98a754fc1788c42ac8e4bb1ab27db32" + integrity sha512-KDzbW34CvQLF5aTkm3b5VdlTrvdIt4wEpCzT2p4XJIQWQZEPco5pNce7Lu9UqZQGkhQ4mpZt4Ky6NKVyIS2N8A== dependencies: "@testim/chrome-version" "^1.0.7" - axios "^0.21.1" + axios "^0.21.2" del "^6.0.0" extract-zip "^2.0.1" https-proxy-agent "^5.0.0" @@ -13927,6 +13934,11 @@ follow-redirects@^1.0.0, follow-redirects@^1.10.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA== +follow-redirects@^1.14.0: + version "1.14.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.3.tgz#6ada78118d8d24caee595595accdc0ac6abd022e" + integrity sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw== + font-awesome@4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133" From 4c3f48fcbd64bb51b8d12a496941f7bbcd84c80d Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Wed, 22 Sep 2021 12:41:42 -0700 Subject: [PATCH 24/39] Add ILM URLs to documentation link service (#112748) --- .../public/doc_links/doc_links_service.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index a9b19a6e84050..7b8b1b79572c9 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -159,6 +159,17 @@ export class DocLinksService { docsBase: `${ELASTICSEARCH_DOCS}`, asyncSearch: `${ELASTICSEARCH_DOCS}async-search-intro.html`, dataStreams: `${ELASTICSEARCH_DOCS}data-streams.html`, + deprecationLogging: `${ELASTICSEARCH_DOCS}logging.html#deprecation-logging`, + ilm: `${ELASTICSEARCH_DOCS}index-lifecycle-management.html`, + ilmForceMerge: `${ELASTICSEARCH_DOCS}ilm-forcemerge.html`, + ilmFreeze: `${ELASTICSEARCH_DOCS}ilm-freeze.html`, + ilmPhaseTransitions: `${ELASTICSEARCH_DOCS}ilm-index-lifecycle.html#ilm-phase-transitions`, + ilmReadOnly: `${ELASTICSEARCH_DOCS}ilm-readonly.html`, + ilmRollover: `${ELASTICSEARCH_DOCS}ilm-rollover.html`, + ilmSearchableSnapshot: `${ELASTICSEARCH_DOCS}ilm-searchable-snapshot.html`, + ilmSetPriority: `${ELASTICSEARCH_DOCS}ilm-set-priority.html`, + ilmShrink: `${ELASTICSEARCH_DOCS}ilm-shrink.html`, + ilmWaitForSnapshot: `${ELASTICSEARCH_DOCS}ilm-wait-for-snapshot.html`, indexModules: `${ELASTICSEARCH_DOCS}index-modules.html`, indexSettings: `${ELASTICSEARCH_DOCS}index-modules.html#index-modules-settings`, indexTemplates: `${ELASTICSEARCH_DOCS}index-templates.html`, @@ -199,16 +210,17 @@ export class DocLinksService { mappingStore: `${ELASTICSEARCH_DOCS}mapping-store.html`, mappingTermVector: `${ELASTICSEARCH_DOCS}term-vector.html`, mappingTypesRemoval: `${ELASTICSEARCH_DOCS}removal-of-types.html`, + migrateIndexAllocationFilters: `${ELASTICSEARCH_DOCS}migrate-index-allocation-filters.html`, nodeRoles: `${ELASTICSEARCH_DOCS}modules-node.html#node-roles`, + releaseHighlights: `${ELASTICSEARCH_DOCS}release-highlights.html`, remoteClusters: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html`, remoteClustersProxy: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#proxy-mode`, remoteClusersProxySettings: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#remote-cluster-proxy-settings`, scriptParameters: `${ELASTICSEARCH_DOCS}modules-scripting-using.html#prefer-params`, + setupUpgrade: `${ELASTICSEARCH_DOCS}setup-upgrade.html`, + shardAllocationSettings: `${ELASTICSEARCH_DOCS}modules-cluster.html#cluster-shard-allocation-settings`, transportSettings: `${ELASTICSEARCH_DOCS}modules-network.html#common-network-settings`, typesRemoval: `${ELASTICSEARCH_DOCS}removal-of-types.html`, - deprecationLogging: `${ELASTICSEARCH_DOCS}logging.html#deprecation-logging`, - setupUpgrade: `${ELASTICSEARCH_DOCS}setup-upgrade.html`, - releaseHighlights: `${ELASTICSEARCH_DOCS}release-highlights.html`, }, siem: { guide: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/index.html`, @@ -396,6 +408,7 @@ export class DocLinksService { registerUrl: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html#snapshots-read-only-repository`, restoreSnapshot: `${ELASTICSEARCH_DOCS}snapshots-restore-snapshot.html`, restoreSnapshotApi: `${ELASTICSEARCH_DOCS}restore-snapshot-api.html#restore-snapshot-api-request-body`, + searchableSnapshotSharedCache: `${ELASTICSEARCH_DOCS}searchable-snapshots.html#searchable-snapshots-shared-cache`, }, ingest: { append: `${ELASTICSEARCH_DOCS}append-processor.html`, From 9d7290d9ad264b1bf94c23da70e6b8fb9d2ed065 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Wed, 22 Sep 2021 15:42:01 -0400 Subject: [PATCH 25/39] [CI] Balance CI Groups (#112836) --- .../tests/exception_operators_data_types/index.ts | 5 ----- x-pack/test/functional/apps/ml/index.ts | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts index 5adf31aef5a3e..cebd20b698c26 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts @@ -27,11 +27,6 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./keyword')); loadTestFile(require.resolve('./keyword_array')); loadTestFile(require.resolve('./long')); - }); - - describe('', function () { - this.tags('ciGroup13'); - loadTestFile(require.resolve('./text')); loadTestFile(require.resolve('./text_array')); }); diff --git a/x-pack/test/functional/apps/ml/index.ts b/x-pack/test/functional/apps/ml/index.ts index eaf626618726a..d4bf9a22367bf 100644 --- a/x-pack/test/functional/apps/ml/index.ts +++ b/x-pack/test/functional/apps/ml/index.ts @@ -53,7 +53,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { }); describe('', function () { - this.tags('ciGroup13'); + this.tags('ciGroup8'); before(async () => { await ml.securityCommon.createMlRoles(); From 878b1eeae9cb1e1ba3f9793179b01f709ca9d436 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Wed, 22 Sep 2021 13:58:57 -0600 Subject: [PATCH 26/39] Log deprecation warnings for plugins which won't be disable-able in 8.0 (#112602) --- docs/dev-tools/console/console.asciidoc | 1 + docs/settings/alert-action-settings.asciidoc | 1 + docs/settings/apm-settings.asciidoc | 3 +- docs/settings/dev-settings.asciidoc | 3 + docs/settings/fleet-settings.asciidoc | 3 +- .../general-infra-logs-ui-settings.asciidoc | 3 +- docs/settings/graph-settings.asciidoc | 1 + docs/settings/ml-settings.asciidoc | 3 +- docs/settings/monitoring-settings.asciidoc | 3 +- docs/settings/security-settings.asciidoc | 3 +- docs/settings/spaces-settings.asciidoc | 5 +- docs/settings/url-drilldown-settings.asciidoc | 3 +- docs/setup/docker.asciidoc | 2 +- docs/setup/settings.asciidoc | 12 +- docs/user/plugins.asciidoc | 4 +- .../kbn-config/src/config_service.test.ts | 31 ++++ packages/kbn-config/src/config_service.ts | 27 +++- .../deprecation/deprecation_factory.test.ts | 140 +++++++++++++++++- .../src/deprecation/deprecation_factory.ts | 53 ++++++- packages/kbn-config/src/deprecation/types.ts | 38 +++++ .../kbn-legacy-logging/src/setup_logging.ts | 2 +- src/plugins/apm_oss/server/index.ts | 3 +- src/plugins/console/server/index.ts | 2 +- src/plugins/vis_types/pie/config.ts | 15 ++ src/plugins/vis_types/pie/server/index.ts | 7 + src/plugins/vis_types/pie/tsconfig.json | 3 +- src/plugins/vis_types/xy/config.ts | 15 ++ src/plugins/vis_types/xy/kibana.json | 2 +- src/plugins/vis_types/xy/server/index.ts | 17 +++ src/plugins/vis_types/xy/server/plugin.ts | 19 +++ src/plugins/vis_types/xy/tsconfig.json | 3 +- x-pack/plugins/apm/server/index.ts | 82 +++++----- x-pack/plugins/cases/server/index.ts | 3 +- x-pack/plugins/cloud/server/config.ts | 1 + .../cross_cluster_replication/server/index.ts | 1 + .../encrypted_saved_objects/server/index.ts | 7 +- .../plugins/enterprise_search/server/index.ts | 1 + x-pack/plugins/fleet/server/index.ts | 3 +- x-pack/plugins/graph/server/index.ts | 1 + .../server/index.ts | 1 + .../plugins/index_management/server/index.ts | 5 +- x-pack/plugins/infra/server/plugin.ts | 10 +- x-pack/plugins/lens/server/index.ts | 1 + .../license_management/server/index.ts | 1 + x-pack/plugins/lists/server/index.ts | 7 +- x-pack/plugins/logstash/server/index.ts | 1 + x-pack/plugins/maps/server/index.ts | 3 +- .../plugins/metrics_entities/server/index.ts | 7 +- .../monitoring/server/deprecations.test.js | 3 +- .../plugins/monitoring/server/deprecations.ts | 2 + x-pack/plugins/observability/server/index.ts | 7 +- x-pack/plugins/osquery/server/index.ts | 7 +- .../plugins/remote_clusters/server/config.ts | 1 + x-pack/plugins/rollup/server/index.ts | 1 + x-pack/plugins/rule_registry/server/config.ts | 4 +- .../saved_objects_tagging/server/config.ts | 1 + .../plugins/security_solution/server/index.ts | 3 +- .../plugins/snapshot_restore/server/index.ts | 1 + x-pack/plugins/timelines/server/index.ts | 7 +- .../upgrade_assistant/public/plugin.ts | 6 +- .../plugins/upgrade_assistant/server/index.ts | 2 +- 61 files changed, 509 insertions(+), 98 deletions(-) create mode 100644 src/plugins/vis_types/pie/config.ts create mode 100644 src/plugins/vis_types/xy/config.ts create mode 100644 src/plugins/vis_types/xy/server/index.ts create mode 100644 src/plugins/vis_types/xy/server/plugin.ts diff --git a/docs/dev-tools/console/console.asciidoc b/docs/dev-tools/console/console.asciidoc index e1cd156e6a9e4..f29ddb1a600db 100644 --- a/docs/dev-tools/console/console.asciidoc +++ b/docs/dev-tools/console/console.asciidoc @@ -134,6 +134,7 @@ shortcuts, click *Help*. [[console-settings]] === Disable Console +deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] If you don’t want to use *Console*, you can disable it by setting `console.enabled` to `false` in your `kibana.yml` configuration file. Changing this setting causes the server to regenerate assets on the next startup, diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index 050d14e4992d6..91e6b379a0620 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -32,6 +32,7 @@ Be sure to back up the encryption key value somewhere safe, as your alerting rul ==== Action settings `xpack.actions.enabled`:: +deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] Feature toggle that enables Actions in {kib}. If `false`, all features dependent on Actions are disabled, including the *Observability* and *Security* apps. Default: `true`. diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index 67de6f8d24960..d812576878f2b 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -41,7 +41,8 @@ Changing these settings may disable features of the APM App. [cols="2*<"] |=== | `xpack.apm.enabled` - | Set to `false` to disable the APM app. Defaults to `true`. + | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] + Set to `false` to disable the APM app. Defaults to `true`. | `xpack.apm.maxServiceEnvironments` | Maximum number of unique service environments recognized by the UI. Defaults to `100`. diff --git a/docs/settings/dev-settings.asciidoc b/docs/settings/dev-settings.asciidoc index b7edf36851d91..bcf4420cdadca 100644 --- a/docs/settings/dev-settings.asciidoc +++ b/docs/settings/dev-settings.asciidoc @@ -13,6 +13,7 @@ They are enabled by default. ==== Grok Debugger settings `xpack.grokdebugger.enabled` {ess-icon}:: +deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] Set to `true` to enable the <>. Defaults to `true`. @@ -21,6 +22,7 @@ Set to `true` to enable the <>. Defaults to `t ==== {searchprofiler} settings `xpack.searchprofiler.enabled`:: +deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] Set to `true` to enable the <>. Defaults to `true`. [float] @@ -28,4 +30,5 @@ Set to `true` to enable the <>. Defaults to `tr ==== Painless Lab settings `xpack.painless_lab.enabled`:: +deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] When set to `true`, enables the <>. Defaults to `true`. diff --git a/docs/settings/fleet-settings.asciidoc b/docs/settings/fleet-settings.asciidoc index 3411f39309709..bf5c84324b0b9 100644 --- a/docs/settings/fleet-settings.asciidoc +++ b/docs/settings/fleet-settings.asciidoc @@ -21,7 +21,8 @@ See the {fleet-guide}/index.html[{fleet}] docs for more information. [cols="2*<"] |=== | `xpack.fleet.enabled` {ess-icon} - | Set to `true` (default) to enable {fleet}. + | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] + Set to `true` (default) to enable {fleet}. | `xpack.fleet.agents.enabled` {ess-icon} | Set to `true` (default) to enable {fleet}. |=== diff --git a/docs/settings/general-infra-logs-ui-settings.asciidoc b/docs/settings/general-infra-logs-ui-settings.asciidoc index 2a9d4df1ff43c..282239dcf166c 100644 --- a/docs/settings/general-infra-logs-ui-settings.asciidoc +++ b/docs/settings/general-infra-logs-ui-settings.asciidoc @@ -1,7 +1,8 @@ [cols="2*<"] |=== | `xpack.infra.enabled` - | Set to `false` to disable the Logs and Metrics app plugin {kib}. Defaults to `true`. + | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] + Set to `false` to disable the Logs and Metrics app plugin {kib}. Defaults to `true`. | `xpack.infra.sources.default.logAlias` | Index pattern for matching indices that contain log data. Defaults to `filebeat-*,kibana_sample_data_logs*`. To match multiple wildcard patterns, use a comma to separate the names, with no space after the comma. For example, `logstash-app1-*,default-logs-*`. diff --git a/docs/settings/graph-settings.asciidoc b/docs/settings/graph-settings.asciidoc index 093edb0d08547..793a8aae73158 100644 --- a/docs/settings/graph-settings.asciidoc +++ b/docs/settings/graph-settings.asciidoc @@ -8,4 +8,5 @@ You do not need to configure any settings to use the {graph-features}. `xpack.graph.enabled` {ess-icon}:: +deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] Set to `false` to disable the {graph-features}. diff --git a/docs/settings/ml-settings.asciidoc b/docs/settings/ml-settings.asciidoc index 92d0c0b491ce7..59fa236e08275 100644 --- a/docs/settings/ml-settings.asciidoc +++ b/docs/settings/ml-settings.asciidoc @@ -14,7 +14,8 @@ enabled by default. [cols="2*<"] |=== | `xpack.ml.enabled` {ess-icon} - | Set to `true` (default) to enable {kib} {ml-features}. + + | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] + Set to `true` (default) to enable {kib} {ml-features}. + + If set to `false` in `kibana.yml`, the {ml} icon is hidden in this {kib} instance. If `xpack.ml.enabled` is set to `true` in `elasticsearch.yml`, however, diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 31148f0abf4e1..03c11007c64c4 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -32,7 +32,8 @@ For more information, see [cols="2*<"] |=== | `monitoring.enabled` - | Set to `true` (default) to enable the {monitor-features} in {kib}. Unlike the + | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] + Set to `true` (default) to enable the {monitor-features} in {kib}. Unlike the <> setting, when this setting is `false`, the monitoring back-end does not run and {kib} stats are not sent to the monitoring cluster. diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index 455ee76deefe3..906af1dfbb28e 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -15,7 +15,8 @@ You do not need to configure any additional settings to use the [cols="2*<"] |=== | `xpack.security.enabled` - | By default, {kib} automatically detects whether to enable the + | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] + By default, {kib} automatically detects whether to enable the {security-features} based on the license and whether {es} {security-features} are enabled. + + diff --git a/docs/settings/spaces-settings.asciidoc b/docs/settings/spaces-settings.asciidoc index 3b643f76f0c09..8504464da1dfb 100644 --- a/docs/settings/spaces-settings.asciidoc +++ b/docs/settings/spaces-settings.asciidoc @@ -15,8 +15,9 @@ roles when Security is enabled. [cols="2*<"] |=== | `xpack.spaces.enabled` - | Set to `true` (default) to enable Spaces in {kib}. - This setting is deprecated. Starting in 8.0, it will not be possible to disable this plugin. + | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] + Set to `true` (default) to enable Spaces in {kib}. + This setting is deprecated. Starting in 8.0, it will not be possible to disable this plugin. | `xpack.spaces.maxSpaces` | The maximum amount of Spaces that can be used with this instance of {kib}. Some operations diff --git a/docs/settings/url-drilldown-settings.asciidoc b/docs/settings/url-drilldown-settings.asciidoc index 8be3a21bfbffc..ca414d4f650e9 100644 --- a/docs/settings/url-drilldown-settings.asciidoc +++ b/docs/settings/url-drilldown-settings.asciidoc @@ -9,7 +9,8 @@ Configure the URL drilldown settings in your `kibana.yml` configuration file. [cols="2*<"] |=== | [[url-drilldown-enabled]] `url_drilldown.enabled` - | When `true`, enables URL drilldowns on your {kib} instance. + | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] + When `true`, enables URL drilldowns on your {kib} instance. | [[external-URL-policy]] `externalUrl.policy` | Configures the external URL policies. URL drilldowns respect the global *External URL* service, which you can use to deny or allow external URLs. diff --git a/docs/setup/docker.asciidoc b/docs/setup/docker.asciidoc index cc14e79c54f15..01fc6e2e76f92 100644 --- a/docs/setup/docker.asciidoc +++ b/docs/setup/docker.asciidoc @@ -125,7 +125,7 @@ Some example translations are shown here: **Environment Variable**:: **Kibana Setting** `SERVER_NAME`:: `server.name` `SERVER_BASEPATH`:: `server.basePath` -`MONITORING_ENABLED`:: `monitoring.enabled` +`ELASTICSEARCH_HOSTS`:: `elasticsearch.hosts` In general, any setting listed in <> can be configured with this technique. diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 78b776c85c937..c098fb697de04 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -21,7 +21,8 @@ configuration using `${MY_ENV_VAR}` syntax. |=== | `console.enabled:` - | Toggling this causes the server to regenerate assets on the next startup, + | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] +Toggling this causes the server to regenerate assets on the next startup, which may cause a delay before pages start being served. Set to `false` to disable Console. *Default: `true`* @@ -706,12 +707,13 @@ sources and images. When false, Vega can only get data from {es}. *Default: `fal | Enables you to view the underlying documents in a data series from a dashboard panel. *Default: `false`* | `xpack.license_management.enabled` - | Set this value to false to -disable the License Management UI. *Default: `true`* + | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] +Set this value to false to disable the License Management UI. +*Default: `true`* | `xpack.rollup.enabled:` - | Set this value to false to disable the -Rollup UI. *Default: true* + | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] +Set this value to false to disable the Rollup UI. *Default: true* | `i18n.locale` {ess-icon} | Set this value to change the {kib} interface language. diff --git a/docs/user/plugins.asciidoc b/docs/user/plugins.asciidoc index 0ef5d1a237510..c604526d6c933 100644 --- a/docs/user/plugins.asciidoc +++ b/docs/user/plugins.asciidoc @@ -149,6 +149,8 @@ NOTE: Removing a plugin will result in an "optimize" run which will delay the ne [[disable-plugin]] == Disable plugins +deprecated:[7.16.0,"In 8.0 and later, this setting will only be supported for a subset of plugins that have opted in to the behavior."] + Use the following command to disable a plugin: [source,shell] @@ -158,7 +160,7 @@ Use the following command to disable a plugin: NOTE: Disabling or enabling a plugin will result in an "optimize" run which will delay the start of {kib}. -<1> You can find a plugin's plugin ID as the value of the `name` property in the plugin's `package.json` file. +<1> You can find a plugin's plugin ID as the value of the `name` property in the plugin's `kibana.json` file. [float] [[configure-plugin-manager]] diff --git a/packages/kbn-config/src/config_service.test.ts b/packages/kbn-config/src/config_service.test.ts index aa520e7189e54..754de1c0a99f5 100644 --- a/packages/kbn-config/src/config_service.test.ts +++ b/packages/kbn-config/src/config_service.test.ts @@ -364,6 +364,37 @@ test('read "enabled" even if its schema is not present', async () => { expect(isEnabled).toBe(true); }); +test('logs deprecation if schema is not present and "enabled" is used', async () => { + const initialConfig = { + foo: { + enabled: true, + }, + }; + + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); + + await configService.isEnabledAtPath('foo'); + expect(configService.getHandledDeprecatedConfigs()).toMatchInlineSnapshot(` + Array [ + Array [ + "foo", + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Remove \\"foo.enabled\\" from the Kibana config file, CLI flag, or environment variable (in Docker only) before upgrading to 8.0.0.", + ], + }, + "message": "Configuring \\"foo.enabled\\" is deprecated and will be removed in 8.0.0.", + "title": "Setting \\"foo.enabled\\" is deprecated", + }, + ], + ], + ] + `); +}); + test('allows plugins to specify "enabled" flag via validation schema', async () => { const initialConfig = {}; diff --git a/packages/kbn-config/src/config_service.ts b/packages/kbn-config/src/config_service.ts index 514992891ad1b..5883ce8ab513c 100644 --- a/packages/kbn-config/src/config_service.ts +++ b/packages/kbn-config/src/config_service.ts @@ -177,6 +177,23 @@ export class ConfigService { // if plugin hasn't got a config schema, we try to read "enabled" directly const isEnabled = validatedConfig?.enabled ?? config.get(enabledPath); + // if we implicitly added an `enabled` config to a plugin without a schema, + // we log a deprecation warning, as this will not be supported in 8.0 + if (validatedConfig?.enabled === undefined && isEnabled !== undefined) { + const deprecationPath = pathToString(enabledPath); + const deprecatedConfigDetails: DeprecatedConfigDetails = { + title: `Setting "${deprecationPath}" is deprecated`, + message: `Configuring "${deprecationPath}" is deprecated and will be removed in 8.0.0.`, + correctiveActions: { + manualSteps: [ + `Remove "${deprecationPath}" from the Kibana config file, CLI flag, or environment variable (in Docker only) before upgrading to 8.0.0.`, + ], + }, + }; + this.deprecationLog.warn(deprecatedConfigDetails.message); + this.markDeprecatedConfigAsHandled(namespace, deprecatedConfigDetails); + } + // not declared. consider that plugin is enabled by default if (isEnabled === undefined) { return true; @@ -220,9 +237,7 @@ export class ConfigService { if (!context.silent) { deprecationMessages.push(context.message); } - const handledDeprecatedConfig = this.handledDeprecatedConfigs.get(domainId) || []; - handledDeprecatedConfig.push(context); - this.handledDeprecatedConfigs.set(domainId, handledDeprecatedConfig); + this.markDeprecatedConfigAsHandled(domainId, context); }; applyDeprecations(rawConfig, deprecations, createAddDeprecation); @@ -260,6 +275,12 @@ export class ConfigService { this.log.debug(`Marking config path as handled: ${path}`); this.handledPaths.add(path); } + + private markDeprecatedConfigAsHandled(domainId: string, config: DeprecatedConfigDetails) { + const handledDeprecatedConfig = this.handledDeprecatedConfigs.get(domainId) || []; + handledDeprecatedConfig.push(config); + this.handledDeprecatedConfigs.set(domainId, handledDeprecatedConfig); + } } const createPluginEnabledPath = (configPath: string | string[]) => { diff --git a/packages/kbn-config/src/deprecation/deprecation_factory.test.ts b/packages/kbn-config/src/deprecation/deprecation_factory.test.ts index 0a605cbc1c532..dfd6b8fac681f 100644 --- a/packages/kbn-config/src/deprecation/deprecation_factory.test.ts +++ b/packages/kbn-config/src/deprecation/deprecation_factory.test.ts @@ -10,7 +10,8 @@ import { DeprecatedConfigDetails } from './types'; import { configDeprecationFactory } from './deprecation_factory'; describe('DeprecationFactory', () => { - const { rename, unused, renameFromRoot, unusedFromRoot } = configDeprecationFactory; + const { deprecate, deprecateFromRoot, rename, renameFromRoot, unused, unusedFromRoot } = + configDeprecationFactory; const addDeprecation = jest.fn(); @@ -18,6 +19,139 @@ describe('DeprecationFactory', () => { addDeprecation.mockClear(); }); + describe('deprecate', () => { + it('logs a warning when property is present', () => { + const rawConfig = { + myplugin: { + deprecated: 'deprecated', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const commands = deprecate('deprecated', '8.0.0')(rawConfig, 'myplugin', addDeprecation); + expect(commands).toBeUndefined(); + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Remove \\"myplugin.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only) before upgrading to 8.0.0.", + ], + }, + "message": "Configuring \\"myplugin.deprecated\\" is deprecated and will be removed in 8.0.0.", + "title": "Setting \\"myplugin.deprecated\\" is deprecated", + }, + ], + ] + `); + }); + + it('handles deeply nested keys', () => { + const rawConfig = { + myplugin: { + section: { + deprecated: 'deprecated', + }, + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const commands = deprecate('section.deprecated', '8.0.0')( + rawConfig, + 'myplugin', + addDeprecation + ); + expect(commands).toBeUndefined(); + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Remove \\"myplugin.section.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only) before upgrading to 8.0.0.", + ], + }, + "message": "Configuring \\"myplugin.section.deprecated\\" is deprecated and will be removed in 8.0.0.", + "title": "Setting \\"myplugin.section.deprecated\\" is deprecated", + }, + ], + ] + `); + }); + + it('does not log if unused property is not present', () => { + const rawConfig = { + myplugin: { + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const commands = deprecate('deprecated', '8.0.0')(rawConfig, 'myplugin', addDeprecation); + expect(commands).toBeUndefined(); + expect(addDeprecation).toBeCalledTimes(0); + }); + }); + + describe('deprecateFromRoot', () => { + it('logs a warning when property is present', () => { + const rawConfig = { + myplugin: { + deprecated: 'deprecated', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const commands = deprecateFromRoot('myplugin.deprecated', '8.0.0')( + rawConfig, + 'does-not-matter', + addDeprecation + ); + expect(commands).toBeUndefined(); + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Remove \\"myplugin.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only) before upgrading to 8.0.0.", + ], + }, + "message": "Configuring \\"myplugin.deprecated\\" is deprecated and will be removed in 8.0.0.", + "title": "Setting \\"myplugin.deprecated\\" is deprecated", + }, + ], + ] + `); + }); + + it('does not log if unused property is not present', () => { + const rawConfig = { + myplugin: { + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const commands = deprecateFromRoot('myplugin.deprecated', '8.0.0')( + rawConfig, + 'does-not-matter', + addDeprecation + ); + expect(commands).toBeUndefined(); + expect(addDeprecation).toBeCalledTimes(0); + }); + }); + describe('rename', () => { it('moves the property to rename and logs a warning if old property exist and new one does not', () => { const rawConfig = { @@ -132,7 +266,7 @@ describe('DeprecationFactory', () => { "Remove \\"myplugin.deprecated\\" from the config.", ], }, - "message": "Setting \\"$myplugin.deprecated\\" has been replaced by \\"$myplugin.renamed\\". However, both keys are present. Ignoring \\"$myplugin.deprecated\\"", + "message": "Setting \\"myplugin.deprecated\\" has been replaced by \\"myplugin.renamed\\". However, both keys are present. Ignoring \\"myplugin.deprecated\\"", "title": "Setting \\"myplugin.deprecated\\" is deprecated", }, ], @@ -269,7 +403,7 @@ describe('DeprecationFactory', () => { "Remove \\"myplugin.deprecated\\" from the config.", ], }, - "message": "Setting \\"$myplugin.deprecated\\" has been replaced by \\"$myplugin.renamed\\". However, both keys are present. Ignoring \\"$myplugin.deprecated\\"", + "message": "Setting \\"myplugin.deprecated\\" has been replaced by \\"myplugin.renamed\\". However, both keys are present. Ignoring \\"myplugin.deprecated\\"", "title": "Setting \\"myplugin.deprecated\\" is deprecated", }, ], diff --git a/packages/kbn-config/src/deprecation/deprecation_factory.ts b/packages/kbn-config/src/deprecation/deprecation_factory.ts index 119b9b11237dc..1d61733715bd9 100644 --- a/packages/kbn-config/src/deprecation/deprecation_factory.ts +++ b/packages/kbn-config/src/deprecation/deprecation_factory.ts @@ -24,6 +24,37 @@ const getDeprecationTitle = (deprecationPath: string) => { }); }; +const _deprecate = ( + config: Record, + rootPath: string, + addDeprecation: AddConfigDeprecation, + deprecatedKey: string, + removeBy: string, + details?: Partial +): void => { + const fullPath = getPath(rootPath, deprecatedKey); + if (get(config, fullPath) === undefined) { + return; + } + addDeprecation({ + title: getDeprecationTitle(fullPath), + message: i18n.translate('kbnConfig.deprecations.deprecatedSettingMessage', { + defaultMessage: 'Configuring "{fullPath}" is deprecated and will be removed in {removeBy}.', + values: { fullPath, removeBy }, + }), + correctiveActions: { + manualSteps: [ + i18n.translate('kbnConfig.deprecations.deprecatedSetting.manualStepOneMessage', { + defaultMessage: + 'Remove "{fullPath}" from the Kibana config file, CLI flag, or environment variable (in Docker only) before upgrading to {removeBy}.', + values: { fullPath, removeBy }, + }), + ], + }, + ...details, + }); +}; + const _rename = ( config: Record, rootPath: string, @@ -67,7 +98,7 @@ const _rename = ( title: getDeprecationTitle(fullOldPath), message: i18n.translate('kbnConfig.deprecations.conflictSettingMessage', { defaultMessage: - 'Setting "${fullOldPath}" has been replaced by "${fullNewPath}". However, both keys are present. Ignoring "${fullOldPath}"', + 'Setting "{fullOldPath}" has been replaced by "{fullNewPath}". However, both keys are present. Ignoring "{fullOldPath}"', values: { fullOldPath, fullNewPath }, }), correctiveActions: { @@ -125,6 +156,24 @@ const _unused = ( }; }; +const deprecate = + ( + unusedKey: string, + removeBy: string, + details?: Partial + ): ConfigDeprecation => + (config, rootPath, addDeprecation) => + _deprecate(config, rootPath, addDeprecation, unusedKey, removeBy, details); + +const deprecateFromRoot = + ( + unusedKey: string, + removeBy: string, + details?: Partial + ): ConfigDeprecation => + (config, rootPath, addDeprecation) => + _deprecate(config, '', addDeprecation, unusedKey, removeBy, details); + const rename = (oldKey: string, newKey: string, details?: Partial): ConfigDeprecation => (config, rootPath, addDeprecation) => @@ -154,6 +203,8 @@ const getPath = (rootPath: string, subPath: string) => * @internal */ export const configDeprecationFactory: ConfigDeprecationFactory = { + deprecate, + deprecateFromRoot, rename, renameFromRoot, unused, diff --git a/packages/kbn-config/src/deprecation/types.ts b/packages/kbn-config/src/deprecation/types.ts index 0e1f36121e50e..47a31b9e6725a 100644 --- a/packages/kbn-config/src/deprecation/types.ts +++ b/packages/kbn-config/src/deprecation/types.ts @@ -91,6 +91,7 @@ export interface ConfigDeprecationCommand { * @example * ```typescript * const provider: ConfigDeprecationProvider = ({ rename, unused }) => [ + * deprecate('deprecatedKey', '8.0.0'), * rename('oldKey', 'newKey'), * unused('deprecatedKey'), * (config, path) => ({ unset: [{ key: 'path.to.key' }] }) @@ -119,6 +120,43 @@ export type ConfigDeprecationProvider = (factory: ConfigDeprecationFactory) => C */ export interface ConfigDeprecationFactory { + /** + * Deprecate a configuration property from inside a plugin's configuration path. + * Will log a deprecation warning if the deprecatedKey was found. + * + * @example + * Log a deprecation warning indicating 'myplugin.deprecatedKey' should be removed by `8.0.0` + * ```typescript + * const provider: ConfigDeprecationProvider = ({ deprecate }) => [ + * deprecate('deprecatedKey', '8.0.0'), + * ] + * ``` + */ + deprecate( + deprecatedKey: string, + removeBy: string, + details?: Partial + ): ConfigDeprecation; + /** + * Deprecate a configuration property from the root configuration. + * Will log a deprecation warning if the deprecatedKey was found. + * + * This should be only used when deprecating properties from different configuration's path. + * To deprecate properties from inside a plugin's configuration, use 'deprecate' instead. + * + * @example + * Log a deprecation warning indicating 'myplugin.deprecatedKey' should be removed by `8.0.0` + * ```typescript + * const provider: ConfigDeprecationProvider = ({ deprecate }) => [ + * deprecateFromRoot('deprecatedKey', '8.0.0'), + * ] + * ``` + */ + deprecateFromRoot( + deprecatedKey: string, + removeBy: string, + details?: Partial + ): ConfigDeprecation; /** * Rename a configuration property from inside a plugin's configuration path. * Will log a deprecation warning if the oldKey was found and deprecation applied. diff --git a/packages/kbn-legacy-logging/src/setup_logging.ts b/packages/kbn-legacy-logging/src/setup_logging.ts index 800ed2e523274..a045469e81251 100644 --- a/packages/kbn-legacy-logging/src/setup_logging.ts +++ b/packages/kbn-legacy-logging/src/setup_logging.ts @@ -23,7 +23,7 @@ export async function setupLogging( // thrown every time we start the server. // In order to keep using the legacy logger until we remove it I'm just adding // a new hard limit here. - process.stdout.setMaxListeners(40); + process.stdout.setMaxListeners(60); return await server.register({ plugin: good, diff --git a/src/plugins/apm_oss/server/index.ts b/src/plugins/apm_oss/server/index.ts index bf6baf1876074..f2f6777672e33 100644 --- a/src/plugins/apm_oss/server/index.ts +++ b/src/plugins/apm_oss/server/index.ts @@ -10,7 +10,8 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { ConfigDeprecationProvider, PluginInitializerContext } from '../../../core/server'; import { APMOSSPlugin } from './plugin'; -const deprecations: ConfigDeprecationProvider = ({ unused }) => [ +const deprecations: ConfigDeprecationProvider = ({ deprecate, unused }) => [ + deprecate('enabled', '8.0.0'), unused('fleetMode'), unused('indexPattern'), ]; diff --git a/src/plugins/console/server/index.ts b/src/plugins/console/server/index.ts index 736a7e1ae3c97..cd05652c62838 100644 --- a/src/plugins/console/server/index.ts +++ b/src/plugins/console/server/index.ts @@ -16,6 +16,6 @@ export { ConsoleSetup, ConsoleStart } from './types'; export const plugin = (ctx: PluginInitializerContext) => new ConsoleServerPlugin(ctx); export const config: PluginConfigDescriptor = { - deprecations: ({ unused }) => [unused('ssl')], + deprecations: ({ deprecate, unused, rename }) => [deprecate('enabled', '8.0.0'), unused('ssl')], schema: configSchema, }; diff --git a/src/plugins/vis_types/pie/config.ts b/src/plugins/vis_types/pie/config.ts new file mode 100644 index 0000000000000..b831d26854c30 --- /dev/null +++ b/src/plugins/vis_types/pie/config.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 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 { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/vis_types/pie/server/index.ts b/src/plugins/vis_types/pie/server/index.ts index 201071fbb5fca..1e92bedb3d11c 100644 --- a/src/plugins/vis_types/pie/server/index.ts +++ b/src/plugins/vis_types/pie/server/index.ts @@ -5,6 +5,13 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + +import { PluginConfigDescriptor } from 'src/core/server'; +import { configSchema, ConfigSchema } from '../config'; import { VisTypePieServerPlugin } from './plugin'; +export const config: PluginConfigDescriptor = { + schema: configSchema, +}; + export const plugin = () => new VisTypePieServerPlugin(); diff --git a/src/plugins/vis_types/pie/tsconfig.json b/src/plugins/vis_types/pie/tsconfig.json index 9a0a3418d72db..99e25a4eba632 100644 --- a/src/plugins/vis_types/pie/tsconfig.json +++ b/src/plugins/vis_types/pie/tsconfig.json @@ -9,7 +9,8 @@ "include": [ "common/**/*", "public/**/*", - "server/**/*" + "server/**/*", + "*.ts" ], "references": [ { "path": "../../../core/tsconfig.json" }, diff --git a/src/plugins/vis_types/xy/config.ts b/src/plugins/vis_types/xy/config.ts new file mode 100644 index 0000000000000..b831d26854c30 --- /dev/null +++ b/src/plugins/vis_types/xy/config.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 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 { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/vis_types/xy/kibana.json b/src/plugins/vis_types/xy/kibana.json index 1666a346e3482..1606af5944ad3 100644 --- a/src/plugins/vis_types/xy/kibana.json +++ b/src/plugins/vis_types/xy/kibana.json @@ -2,7 +2,7 @@ "id": "visTypeXy", "version": "kibana", "ui": true, - "server": false, + "server": true, "requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection"], "requiredBundles": ["kibanaUtils", "visDefaultEditor"], "extraPublicDirs": ["common/index"], diff --git a/src/plugins/vis_types/xy/server/index.ts b/src/plugins/vis_types/xy/server/index.ts new file mode 100644 index 0000000000000..9dfa405ee27b8 --- /dev/null +++ b/src/plugins/vis_types/xy/server/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { PluginConfigDescriptor } from 'src/core/server'; +import { configSchema, ConfigSchema } from '../config'; +import { VisTypeXYServerPlugin } from './plugin'; + +export const config: PluginConfigDescriptor = { + schema: configSchema, +}; + +export const plugin = () => new VisTypeXYServerPlugin(); diff --git a/src/plugins/vis_types/xy/server/plugin.ts b/src/plugins/vis_types/xy/server/plugin.ts new file mode 100644 index 0000000000000..5cb0687cf1889 --- /dev/null +++ b/src/plugins/vis_types/xy/server/plugin.ts @@ -0,0 +1,19 @@ +/* + * 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 { Plugin } from 'src/core/server'; + +export class VisTypeXYServerPlugin implements Plugin { + public setup() { + return {}; + } + + public start() { + return {}; + } +} diff --git a/src/plugins/vis_types/xy/tsconfig.json b/src/plugins/vis_types/xy/tsconfig.json index f1f65b6218e82..ab3f3d1252ed8 100644 --- a/src/plugins/vis_types/xy/tsconfig.json +++ b/src/plugins/vis_types/xy/tsconfig.json @@ -9,7 +9,8 @@ "include": [ "common/**/*", "public/**/*", - "server/**/*" + "server/**/*", + "*.ts" ], "references": [ { "path": "../../../core/tsconfig.json" }, diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index 6ba412bd22029..2c21ff17f779b 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -6,56 +6,62 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { PluginInitializerContext } from 'src/core/server'; +import { + PluginInitializerContext, + PluginConfigDescriptor, +} from 'src/core/server'; import { APMOSSConfig } from 'src/plugins/apm_oss/server'; import { APMPlugin } from './plugin'; import { SearchAggregatedTransactionSetting } from '../common/aggregated_transactions'; +const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), + serviceMapEnabled: schema.boolean({ defaultValue: true }), + serviceMapFingerprintBucketSize: schema.number({ defaultValue: 100 }), + serviceMapTraceIdBucketSize: schema.number({ defaultValue: 65 }), + serviceMapFingerprintGlobalBucketSize: schema.number({ + defaultValue: 1000, + }), + serviceMapTraceIdGlobalBucketSize: schema.number({ defaultValue: 6 }), + serviceMapMaxTracesPerRequest: schema.number({ defaultValue: 50 }), + autocreateApmIndexPattern: schema.boolean({ defaultValue: true }), + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + transactionGroupBucketSize: schema.number({ defaultValue: 1000 }), + maxTraceItems: schema.number({ defaultValue: 1000 }), + }), + searchAggregatedTransactions: schema.oneOf( + [ + schema.literal(SearchAggregatedTransactionSetting.auto), + schema.literal(SearchAggregatedTransactionSetting.always), + schema.literal(SearchAggregatedTransactionSetting.never), + ], + { defaultValue: SearchAggregatedTransactionSetting.auto } + ), + telemetryCollectionEnabled: schema.boolean({ defaultValue: true }), + metricsInterval: schema.number({ defaultValue: 30 }), + maxServiceEnvironments: schema.number({ defaultValue: 100 }), + maxServiceSelection: schema.number({ defaultValue: 50 }), + profilingEnabled: schema.boolean({ defaultValue: false }), + agent: schema.object({ + migrations: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + }), + }), +}); + // plugin config -export const config = { +export const config: PluginConfigDescriptor = { + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], exposeToBrowser: { serviceMapEnabled: true, ui: true, profilingEnabled: true, }, - schema: schema.object({ - enabled: schema.boolean({ defaultValue: true }), - serviceMapEnabled: schema.boolean({ defaultValue: true }), - serviceMapFingerprintBucketSize: schema.number({ defaultValue: 100 }), - serviceMapTraceIdBucketSize: schema.number({ defaultValue: 65 }), - serviceMapFingerprintGlobalBucketSize: schema.number({ - defaultValue: 1000, - }), - serviceMapTraceIdGlobalBucketSize: schema.number({ defaultValue: 6 }), - serviceMapMaxTracesPerRequest: schema.number({ defaultValue: 50 }), - autocreateApmIndexPattern: schema.boolean({ defaultValue: true }), - ui: schema.object({ - enabled: schema.boolean({ defaultValue: true }), - transactionGroupBucketSize: schema.number({ defaultValue: 1000 }), - maxTraceItems: schema.number({ defaultValue: 1000 }), - }), - searchAggregatedTransactions: schema.oneOf( - [ - schema.literal(SearchAggregatedTransactionSetting.auto), - schema.literal(SearchAggregatedTransactionSetting.always), - schema.literal(SearchAggregatedTransactionSetting.never), - ], - { defaultValue: SearchAggregatedTransactionSetting.auto } - ), - telemetryCollectionEnabled: schema.boolean({ defaultValue: true }), - metricsInterval: schema.number({ defaultValue: 30 }), - maxServiceEnvironments: schema.number({ defaultValue: 100 }), - maxServiceSelection: schema.number({ defaultValue: 50 }), - profilingEnabled: schema.boolean({ defaultValue: false }), - agent: schema.object({ - migrations: schema.object({ - enabled: schema.boolean({ defaultValue: false }), - }), - }), - }), + schema: configSchema, }; -export type APMXPackConfig = TypeOf; +export type APMXPackConfig = TypeOf; export type APMConfig = ReturnType; // plugin config and ui indices settings diff --git a/x-pack/plugins/cases/server/index.ts b/x-pack/plugins/cases/server/index.ts index 5e433b46b80e5..ad76724eb49f7 100644 --- a/x-pack/plugins/cases/server/index.ts +++ b/x-pack/plugins/cases/server/index.ts @@ -15,7 +15,8 @@ export const config: PluginConfigDescriptor = { exposeToBrowser: { markdownPlugins: true, }, - deprecations: ({ renameFromRoot }) => [ + deprecations: ({ deprecate, renameFromRoot }) => [ + deprecate('enabled', '8.0.0'), renameFromRoot('xpack.case.enabled', 'xpack.cases.enabled'), ], }; diff --git a/x-pack/plugins/cloud/server/config.ts b/x-pack/plugins/cloud/server/config.ts index 4b83071bf473a..2cc413178c3ae 100644 --- a/x-pack/plugins/cloud/server/config.ts +++ b/x-pack/plugins/cloud/server/config.ts @@ -52,5 +52,6 @@ export const config: PluginConfigDescriptor = { organization_url: true, full_story: true, }, + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], schema: configSchema, }; diff --git a/x-pack/plugins/cross_cluster_replication/server/index.ts b/x-pack/plugins/cross_cluster_replication/server/index.ts index b1803950614d8..a6a3ec0fe5753 100644 --- a/x-pack/plugins/cross_cluster_replication/server/index.ts +++ b/x-pack/plugins/cross_cluster_replication/server/index.ts @@ -17,4 +17,5 @@ export const config: PluginConfigDescriptor = { exposeToBrowser: { ui: true, }, + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], }; diff --git a/x-pack/plugins/encrypted_saved_objects/server/index.ts b/x-pack/plugins/encrypted_saved_objects/server/index.ts index 2706da22d108b..b765f1fcaf6fa 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { PluginInitializerContext } from 'src/core/server'; +import type { PluginConfigDescriptor, PluginInitializerContext } from 'src/core/server'; import { ConfigSchema } from './config'; import { EncryptedSavedObjectsPlugin } from './plugin'; @@ -15,6 +15,9 @@ export { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart } fr export { EncryptedSavedObjectsClient } from './saved_objects'; export type { IsMigrationNeededPredicate } from './create_migration'; -export const config = { schema: ConfigSchema }; +export const config: PluginConfigDescriptor = { + schema: ConfigSchema, + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], +}; export const plugin = (initializerContext: PluginInitializerContext) => new EncryptedSavedObjectsPlugin(initializerContext); diff --git a/x-pack/plugins/enterprise_search/server/index.ts b/x-pack/plugins/enterprise_search/server/index.ts index ecd068c8bdbd9..dae584a883bd7 100644 --- a/x-pack/plugins/enterprise_search/server/index.ts +++ b/x-pack/plugins/enterprise_search/server/index.ts @@ -37,4 +37,5 @@ export const config: PluginConfigDescriptor = { exposeToBrowser: { host: true, }, + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], }; diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index 05ad8a9a9c83f..accd5e040f4f0 100644 --- a/x-pack/plugins/fleet/server/index.ts +++ b/x-pack/plugins/fleet/server/index.ts @@ -42,7 +42,8 @@ export const config: PluginConfigDescriptor = { epm: true, agents: true, }, - deprecations: ({ renameFromRoot, unused, unusedFromRoot }) => [ + deprecations: ({ deprecate, renameFromRoot, unused, unusedFromRoot }) => [ + deprecate('enabled', '8.0.0'), // Fleet plugin was named ingestManager before renameFromRoot('xpack.ingestManager.enabled', 'xpack.fleet.enabled'), renameFromRoot('xpack.ingestManager.registryUrl', 'xpack.fleet.registryUrl'), diff --git a/x-pack/plugins/graph/server/index.ts b/x-pack/plugins/graph/server/index.ts index 10ddca631a898..528e122da9a4d 100644 --- a/x-pack/plugins/graph/server/index.ts +++ b/x-pack/plugins/graph/server/index.ts @@ -18,4 +18,5 @@ export const config: PluginConfigDescriptor = { savePolicy: true, }, schema: configSchema, + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], }; diff --git a/x-pack/plugins/index_lifecycle_management/server/index.ts b/x-pack/plugins/index_lifecycle_management/server/index.ts index e90518dbfa357..1f8b01913fd3e 100644 --- a/x-pack/plugins/index_lifecycle_management/server/index.ts +++ b/x-pack/plugins/index_lifecycle_management/server/index.ts @@ -17,4 +17,5 @@ export const config: PluginConfigDescriptor = { exposeToBrowser: { ui: true, }, + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], }; diff --git a/x-pack/plugins/index_management/server/index.ts b/x-pack/plugins/index_management/server/index.ts index 507401398a407..14b67e2ffd581 100644 --- a/x-pack/plugins/index_management/server/index.ts +++ b/x-pack/plugins/index_management/server/index.ts @@ -5,15 +5,16 @@ * 2.0. */ -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; import { IndexMgmtServerPlugin } from './plugin'; import { configSchema } from './config'; export const plugin = (context: PluginInitializerContext) => new IndexMgmtServerPlugin(context); -export const config = { +export const config: PluginConfigDescriptor = { schema: configSchema, + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], }; /** @public */ diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index de445affc178e..b77b81cf41ee1 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -9,7 +9,12 @@ import { Server } from '@hapi/hapi'; import { schema, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { Logger } from '@kbn/logging'; -import { CoreSetup, PluginInitializerContext, Plugin } from 'src/core/server'; +import { + CoreSetup, + PluginInitializerContext, + Plugin, + PluginConfigDescriptor, +} from 'src/core/server'; import { LOGS_FEATURE_ID, METRICS_FEATURE_ID } from '../common/constants'; import { InfraStaticSourceConfiguration } from '../common/source_configuration/source_configuration'; import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; @@ -36,7 +41,7 @@ import { createGetLogQueryFields } from './services/log_queries/get_log_query_fi import { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; import { RulesService } from './services/rules'; -export const config = { +export const config: PluginConfigDescriptor = { schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), inventory: schema.object({ @@ -63,6 +68,7 @@ export const config = { }) ), }), + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], }; export type InfraConfig = TypeOf; diff --git a/x-pack/plugins/lens/server/index.ts b/x-pack/plugins/lens/server/index.ts index 08f1eb1562739..e2117506e9b72 100644 --- a/x-pack/plugins/lens/server/index.ts +++ b/x-pack/plugins/lens/server/index.ts @@ -19,6 +19,7 @@ import { configSchema, ConfigSchema } from '../config'; export const config: PluginConfigDescriptor = { schema: configSchema, + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], }; export const plugin = (initializerContext: PluginInitializerContext) => diff --git a/x-pack/plugins/license_management/server/index.ts b/x-pack/plugins/license_management/server/index.ts index 7a24845c981e9..e78ffe07b50c0 100644 --- a/x-pack/plugins/license_management/server/index.ts +++ b/x-pack/plugins/license_management/server/index.ts @@ -17,4 +17,5 @@ export const config: PluginConfigDescriptor = { exposeToBrowser: { ui: true, }, + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], }; diff --git a/x-pack/plugins/lists/server/index.ts b/x-pack/plugins/lists/server/index.ts index 250b5e79ed109..7e1283927aa86 100644 --- a/x-pack/plugins/lists/server/index.ts +++ b/x-pack/plugins/lists/server/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { PluginInitializerContext } from '../../../../src/core/server'; +import { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server'; import { ConfigSchema } from './config'; import { ListPlugin } from './plugin'; @@ -19,6 +19,9 @@ export { export { ExceptionListClient } from './services/exception_lists/exception_list_client'; export type { ListPluginSetup, ListsApiRequestHandlerContext } from './types'; -export const config = { schema: ConfigSchema }; +export const config: PluginConfigDescriptor = { + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], + schema: ConfigSchema, +}; export const plugin = (initializerContext: PluginInitializerContext): ListPlugin => new ListPlugin(initializerContext); diff --git a/x-pack/plugins/logstash/server/index.ts b/x-pack/plugins/logstash/server/index.ts index 4606a518fa8c5..33f3777297f63 100644 --- a/x-pack/plugins/logstash/server/index.ts +++ b/x-pack/plugins/logstash/server/index.ts @@ -15,4 +15,5 @@ export const config: PluginConfigDescriptor = { schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), }), + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], }; diff --git a/x-pack/plugins/maps/server/index.ts b/x-pack/plugins/maps/server/index.ts index b57f9ec9c29b1..603273efe0d95 100644 --- a/x-pack/plugins/maps/server/index.ts +++ b/x-pack/plugins/maps/server/index.ts @@ -22,7 +22,8 @@ export const config: PluginConfigDescriptor = { preserveDrawingBuffer: true, }, schema: configSchema, - deprecations: () => [ + deprecations: ({ deprecate }) => [ + deprecate('enabled', '8.0.0'), ( completeConfig: Record, rootPath: string, diff --git a/x-pack/plugins/metrics_entities/server/index.ts b/x-pack/plugins/metrics_entities/server/index.ts index b4d35eb90f486..c8f9d81347bdb 100644 --- a/x-pack/plugins/metrics_entities/server/index.ts +++ b/x-pack/plugins/metrics_entities/server/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { PluginInitializerContext } from '../../../../src/core/server'; +import { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server'; import { ConfigSchema } from './config'; import { MetricsEntitiesPlugin } from './plugin'; @@ -13,7 +13,10 @@ import { MetricsEntitiesPlugin } from './plugin'; // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. -export const config = { schema: ConfigSchema }; +export const config: PluginConfigDescriptor = { + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], + schema: ConfigSchema, +}; export const plugin = (initializerContext: PluginInitializerContext): MetricsEntitiesPlugin => { return new MetricsEntitiesPlugin(initializerContext); }; diff --git a/x-pack/plugins/monitoring/server/deprecations.test.js b/x-pack/plugins/monitoring/server/deprecations.test.js index 2931f704a4478..4c12979e97804 100644 --- a/x-pack/plugins/monitoring/server/deprecations.test.js +++ b/x-pack/plugins/monitoring/server/deprecations.test.js @@ -10,12 +10,13 @@ import { deprecations as deprecationsModule } from './deprecations'; describe('monitoring plugin deprecations', function () { let transformDeprecations; + const deprecate = jest.fn(() => jest.fn()); const rename = jest.fn(() => jest.fn()); const renameFromRoot = jest.fn(() => jest.fn()); const fromPath = 'monitoring'; beforeAll(function () { - const deprecations = deprecationsModule({ rename, renameFromRoot }); + const deprecations = deprecationsModule({ deprecate, rename, renameFromRoot }); transformDeprecations = (settings, fromPath, addDeprecation = noop) => { deprecations.forEach((deprecation) => deprecation(settings, fromPath, addDeprecation)); }; diff --git a/x-pack/plugins/monitoring/server/deprecations.ts b/x-pack/plugins/monitoring/server/deprecations.ts index 3e4d1627b0ae2..cb09bbdb5a87c 100644 --- a/x-pack/plugins/monitoring/server/deprecations.ts +++ b/x-pack/plugins/monitoring/server/deprecations.ts @@ -18,10 +18,12 @@ import { CLUSTER_ALERTS_ADDRESS_CONFIG_KEY } from '../common/constants'; * @return {Array} array of rename operations and callback function for rename logging */ export const deprecations = ({ + deprecate, rename, renameFromRoot, }: ConfigDeprecationFactory): ConfigDeprecation[] => { return [ + deprecate('enabled', '8.0.0'), // This order matters. The "blanket rename" needs to happen at the end renameFromRoot('xpack.monitoring.max_bucket_size', 'monitoring.ui.max_bucket_size'), renameFromRoot('xpack.monitoring.min_interval_seconds', 'monitoring.ui.min_interval_seconds'), diff --git a/x-pack/plugins/observability/server/index.ts b/x-pack/plugins/observability/server/index.ts index 9a62602859c54..97a17b0d11153 100644 --- a/x-pack/plugins/observability/server/index.ts +++ b/x-pack/plugins/observability/server/index.ts @@ -9,7 +9,7 @@ /* eslint-disable @kbn/eslint/no_export_all */ import { schema, TypeOf } from '@kbn/config-schema'; -import { PluginInitializerContext } from 'src/core/server'; +import { PluginConfigDescriptor, PluginInitializerContext } from 'src/core/server'; import { ObservabilityPlugin, ObservabilityPluginSetup } from './plugin'; import { createOrUpdateIndex, Mappings } from './utils/create_or_update_index'; import { ScopedAnnotationsClient } from './lib/annotations/bootstrap_annotations'; @@ -18,9 +18,9 @@ export { rangeQuery, kqlQuery } from './utils/queries'; export * from './types'; -export const config = { +export const config: PluginConfigDescriptor = { exposeToBrowser: { - unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } }, + unsafe: true, }, schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), @@ -33,6 +33,7 @@ export const config = { cases: schema.object({ enabled: schema.boolean({ defaultValue: false }) }), }), }), + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], }; export type ObservabilityConfig = TypeOf; diff --git a/x-pack/plugins/osquery/server/index.ts b/x-pack/plugins/osquery/server/index.ts index 30bc5ed5bd835..385515c285093 100644 --- a/x-pack/plugins/osquery/server/index.ts +++ b/x-pack/plugins/osquery/server/index.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { PluginInitializerContext } from '../../../../src/core/server'; +import { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server'; import { OsqueryPlugin } from './plugin'; -import { ConfigSchema } from './config'; +import { ConfigSchema, ConfigType } from './config'; -export const config = { +export const config: PluginConfigDescriptor = { + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], schema: ConfigSchema, exposeToBrowser: { enabled: true, diff --git a/x-pack/plugins/remote_clusters/server/config.ts b/x-pack/plugins/remote_clusters/server/config.ts index e0fadea5d41f7..8f379ec5613c8 100644 --- a/x-pack/plugins/remote_clusters/server/config.ts +++ b/x-pack/plugins/remote_clusters/server/config.ts @@ -18,6 +18,7 @@ export const configSchema = schema.object({ export type ConfigType = TypeOf; export const config: PluginConfigDescriptor = { + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], schema: configSchema, exposeToBrowser: { ui: true, diff --git a/x-pack/plugins/rollup/server/index.ts b/x-pack/plugins/rollup/server/index.ts index aa96f3ae0aac3..e77e0e6f15d72 100644 --- a/x-pack/plugins/rollup/server/index.ts +++ b/x-pack/plugins/rollup/server/index.ts @@ -13,5 +13,6 @@ export const plugin = (pluginInitializerContext: PluginInitializerContext) => new RollupPlugin(pluginInitializerContext); export const config: PluginConfigDescriptor = { + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], schema: configSchema, }; diff --git a/x-pack/plugins/rule_registry/server/config.ts b/x-pack/plugins/rule_registry/server/config.ts index 481c5fe3cce8b..62f29a9e06294 100644 --- a/x-pack/plugins/rule_registry/server/config.ts +++ b/x-pack/plugins/rule_registry/server/config.ts @@ -6,8 +6,10 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from 'src/core/server'; -export const config = { +export const config: PluginConfigDescriptor = { + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), write: schema.object({ diff --git a/x-pack/plugins/saved_objects_tagging/server/config.ts b/x-pack/plugins/saved_objects_tagging/server/config.ts index f4f0bd1cf1aa0..183779aa6f229 100644 --- a/x-pack/plugins/saved_objects_tagging/server/config.ts +++ b/x-pack/plugins/saved_objects_tagging/server/config.ts @@ -16,6 +16,7 @@ const configSchema = schema.object({ export type SavedObjectsTaggingConfigType = TypeOf; export const config: PluginConfigDescriptor = { + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], schema: configSchema, exposeToBrowser: { cache_refresh_interval: true, diff --git a/x-pack/plugins/security_solution/server/index.ts b/x-pack/plugins/security_solution/server/index.ts index 7e3da726f6ebe..b72a21c0da643 100644 --- a/x-pack/plugins/security_solution/server/index.ts +++ b/x-pack/plugins/security_solution/server/index.ts @@ -20,7 +20,8 @@ export const config: PluginConfigDescriptor = { enableExperimental: true, }, schema: configSchema, - deprecations: ({ renameFromRoot }) => [ + deprecations: ({ deprecate, renameFromRoot }) => [ + deprecate('enabled', '8.0.0'), renameFromRoot('xpack.siem.enabled', 'xpack.securitySolution.enabled'), renameFromRoot( 'xpack.siem.maxRuleImportExportSize', diff --git a/x-pack/plugins/snapshot_restore/server/index.ts b/x-pack/plugins/snapshot_restore/server/index.ts index d85d03923df1f..e10bffd6073d2 100644 --- a/x-pack/plugins/snapshot_restore/server/index.ts +++ b/x-pack/plugins/snapshot_restore/server/index.ts @@ -12,6 +12,7 @@ import { configSchema, SnapshotRestoreConfig } from './config'; export const plugin = (ctx: PluginInitializerContext) => new SnapshotRestoreServerPlugin(ctx); export const config: PluginConfigDescriptor = { + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], schema: configSchema, exposeToBrowser: { slm_ui: true, diff --git a/x-pack/plugins/timelines/server/index.ts b/x-pack/plugins/timelines/server/index.ts index 8ad2bafdcc13a..229a257d8f549 100644 --- a/x-pack/plugins/timelines/server/index.ts +++ b/x-pack/plugins/timelines/server/index.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { PluginInitializerContext } from '../../../../src/core/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from '../../../../src/core/server'; import { TimelinesPlugin } from './plugin'; -import { ConfigSchema } from './config'; +import { ConfigSchema, ConfigType } from './config'; -export const config = { +export const config: PluginConfigDescriptor = { + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], schema: ConfigSchema, exposeToBrowser: { enabled: true, diff --git a/x-pack/plugins/upgrade_assistant/public/plugin.ts b/x-pack/plugins/upgrade_assistant/public/plugin.ts index 3aa712573f0c2..5edb638e1bc5b 100644 --- a/x-pack/plugins/upgrade_assistant/public/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/public/plugin.ts @@ -17,11 +17,7 @@ export class UpgradeAssistantUIPlugin { constructor(private ctx: PluginInitializerContext) {} setup(coreSetup: CoreSetup, { management, cloud }: SetupDependencies) { - const { enabled, readonly } = this.ctx.config.get(); - - if (!enabled) { - return; - } + const { readonly } = this.ctx.config.get(); const appRegistrar = management.sections.section.stack; const kibanaVersion = new SemVer(this.ctx.env.packageInfo.version); diff --git a/x-pack/plugins/upgrade_assistant/server/index.ts b/x-pack/plugins/upgrade_assistant/server/index.ts index 035a6515de152..5591276b2fa34 100644 --- a/x-pack/plugins/upgrade_assistant/server/index.ts +++ b/x-pack/plugins/upgrade_assistant/server/index.ts @@ -14,9 +14,9 @@ export const plugin = (ctx: PluginInitializerContext) => { }; export const config: PluginConfigDescriptor = { + deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], schema: configSchema, exposeToBrowser: { - enabled: true, readonly: true, }, }; From 1b93b00212aa1ff66e23314bcdb676257f3d38f5 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Wed, 22 Sep 2021 14:15:55 -0600 Subject: [PATCH 27/39] [Monitoring] Add KQL filter bar to alerts (#111663) * [Monitoring] Add KQL filter bar to alerts * Finish adding filters to UI * Adding filterQuery to all the backend alert functions * removing unused translations * fixing types * Moving alerting code to async imports Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../{server/lib => common}/ccs_utils.test.js | 0 .../{server/lib => common}/ccs_utils.ts | 3 +- .../plugins/monitoring/common/types/alerts.ts | 4 + .../ccr_read_exceptions_alert/index.tsx | 11 +- .../param_details_form/expression.tsx | 57 +++- .../use_derived_index_pattern.tsx | 44 +++ .../cpu_usage_alert/cpu_usage_alert.tsx | 11 +- .../public/alerts/disk_usage_alert/index.tsx | 11 +- .../alerts/large_shard_size_alert/index.tsx | 11 +- .../public/alerts/legacy_alert/expression.tsx | 56 ++++ .../alerts/legacy_alert/legacy_alert.tsx | 21 +- .../alerts/memory_usage_alert/index.tsx | 11 +- .../thread_pool_rejections_alert/index.tsx | 6 +- .../kuery_bar/autocomplete_field.tsx | 316 ++++++++++++++++++ .../public/components/kuery_bar/index.tsx | 98 ++++++ .../components/kuery_bar/suggestion_item.tsx | 119 +++++++ .../kuery_bar/with_kuery_autocompletion.tsx | 111 ++++++ x-pack/plugins/monitoring/public/lib/kuery.ts | 23 ++ .../monitoring/public/lib/typed_react.tsx | 82 +++++ x-pack/plugins/monitoring/public/plugin.ts | 52 +-- .../server/alerts/ccr_read_exceptions_rule.ts | 5 +- .../server/alerts/cluster_health_rule.ts | 7 +- .../server/alerts/cpu_usage_rule.ts | 5 +- .../server/alerts/disk_usage_rule.ts | 5 +- .../elasticsearch_version_mismatch_rule.ts | 3 +- .../alerts/kibana_version_mismatch_rule.ts | 3 +- .../server/alerts/large_shard_size_rule.ts | 5 +- .../server/alerts/license_expiration_rule.ts | 2 +- .../alerts/logstash_version_mismatch_rule.ts | 3 +- .../server/alerts/memory_usage_rule.ts | 5 +- .../alerts/missing_monitoring_data_rule.ts | 5 +- .../server/alerts/nodes_changed_rule.ts | 3 +- .../thread_pool_rejections_rule_base.ts | 5 +- .../lib/fetch_stack_product_usage.ts | 2 +- .../lib/get_stack_products_usage.ts | 2 +- .../lib/alerts/fetch_ccr_read_exceptions.ts | 12 +- .../server/lib/alerts/fetch_cluster_health.ts | 12 +- .../alerts/fetch_cpu_usage_node_stats.test.ts | 7 +- .../lib/alerts/fetch_cpu_usage_node_stats.ts | 12 +- .../lib/alerts/fetch_disk_usage_node_stats.ts | 12 +- .../alerts/fetch_elasticsearch_versions.ts | 12 +- .../lib/alerts/fetch_index_shard_size.ts | 12 +- .../lib/alerts/fetch_kibana_versions.ts | 12 +- .../server/lib/alerts/fetch_licenses.ts | 12 +- .../lib/alerts/fetch_logstash_versions.ts | 12 +- .../alerts/fetch_memory_usage_node_stats.ts | 12 +- .../alerts/fetch_missing_monitoring_data.ts | 12 +- .../alerts/fetch_nodes_from_cluster_stats.ts | 12 +- .../fetch_thread_pool_rejections_stats.ts | 12 +- .../server/lib/cluster/get_clusters_stats.ts | 2 +- .../server/lib/cluster/get_index_patterns.ts | 2 +- .../server/lib/logs/init_infra_source.ts | 2 +- .../server/routes/api/v1/apm/instance.js | 2 +- .../server/routes/api/v1/apm/instances.js | 2 +- .../server/routes/api/v1/apm/overview.js | 2 +- .../server/routes/api/v1/beats/beat_detail.js | 2 +- .../server/routes/api/v1/beats/beats.js | 2 +- .../server/routes/api/v1/beats/overview.js | 2 +- .../server/routes/api/v1/elasticsearch/ccr.ts | 2 +- .../routes/api/v1/elasticsearch/ccr_shard.ts | 2 +- .../api/v1/elasticsearch/index_detail.js | 2 +- .../routes/api/v1/elasticsearch/indices.js | 2 +- .../routes/api/v1/elasticsearch/ml_jobs.js | 2 +- .../api/v1/elasticsearch/node_detail.js | 2 +- .../routes/api/v1/elasticsearch/nodes.js | 2 +- .../routes/api/v1/elasticsearch/overview.js | 2 +- .../check/internal_monitoring.ts | 2 +- .../server/routes/api/v1/kibana/instance.ts | 2 +- .../server/routes/api/v1/kibana/instances.js | 2 +- .../server/routes/api/v1/kibana/overview.js | 2 +- .../server/routes/api/v1/logstash/node.js | 2 +- .../server/routes/api/v1/logstash/nodes.js | 2 +- .../server/routes/api/v1/logstash/overview.js | 2 +- .../server/routes/api/v1/logstash/pipeline.js | 2 +- .../pipelines/cluster_pipeline_ids.js | 2 +- .../logstash/pipelines/cluster_pipelines.js | 2 +- .../v1/logstash/pipelines/node_pipelines.js | 2 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 79 files changed, 1208 insertions(+), 120 deletions(-) rename x-pack/plugins/monitoring/{server/lib => common}/ccs_utils.test.js (100%) rename x-pack/plugins/monitoring/{server/lib => common}/ccs_utils.ts (96%) create mode 100644 x-pack/plugins/monitoring/public/alerts/components/param_details_form/use_derived_index_pattern.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/legacy_alert/expression.tsx create mode 100644 x-pack/plugins/monitoring/public/components/kuery_bar/autocomplete_field.tsx create mode 100644 x-pack/plugins/monitoring/public/components/kuery_bar/index.tsx create mode 100644 x-pack/plugins/monitoring/public/components/kuery_bar/suggestion_item.tsx create mode 100644 x-pack/plugins/monitoring/public/components/kuery_bar/with_kuery_autocompletion.tsx create mode 100644 x-pack/plugins/monitoring/public/lib/kuery.ts create mode 100644 x-pack/plugins/monitoring/public/lib/typed_react.tsx diff --git a/x-pack/plugins/monitoring/server/lib/ccs_utils.test.js b/x-pack/plugins/monitoring/common/ccs_utils.test.js similarity index 100% rename from x-pack/plugins/monitoring/server/lib/ccs_utils.test.js rename to x-pack/plugins/monitoring/common/ccs_utils.test.js diff --git a/x-pack/plugins/monitoring/server/lib/ccs_utils.ts b/x-pack/plugins/monitoring/common/ccs_utils.ts similarity index 96% rename from x-pack/plugins/monitoring/server/lib/ccs_utils.ts rename to x-pack/plugins/monitoring/common/ccs_utils.ts index 1d899456913b9..7efe6e43ddbbd 100644 --- a/x-pack/plugins/monitoring/server/lib/ccs_utils.ts +++ b/x-pack/plugins/monitoring/common/ccs_utils.ts @@ -6,7 +6,8 @@ */ import { isFunction, get } from 'lodash'; -import type { MonitoringConfig } from '../config'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { MonitoringConfig } from '../server/config'; type Config = Partial & { get?: (key: string) => any; diff --git a/x-pack/plugins/monitoring/common/types/alerts.ts b/x-pack/plugins/monitoring/common/types/alerts.ts index 17bbffce19a18..1f68b0c55a046 100644 --- a/x-pack/plugins/monitoring/common/types/alerts.ts +++ b/x-pack/plugins/monitoring/common/types/alerts.ts @@ -48,12 +48,16 @@ export interface CommonAlertParams { duration: string; threshold?: number; limit?: string; + filterQuery?: string; + filterQueryText?: string; [key: string]: unknown; } export interface ThreadPoolRejectionsAlertParams { threshold: number; duration: string; + filterQuery?: string; + filterQueryText?: string; } export interface AlertEnableAction { diff --git a/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx index 1de9a175026a6..64eab04cbd5ce 100644 --- a/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx @@ -15,6 +15,7 @@ import { RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; import { AlertTypeParams } from '../../../../alerting/common'; +import { MonitoringConfig } from '../../types'; interface ValidateOptions extends AlertTypeParams { duration: string; @@ -36,7 +37,9 @@ const validate = (inputValues: ValidateOptions): ValidationResult => { return validationResult; }; -export function createCCRReadExceptionsAlertType(): AlertTypeModel { +export function createCCRReadExceptionsAlertType( + config: MonitoringConfig +): AlertTypeModel { return { id: RULE_CCR_READ_EXCEPTIONS, description: RULE_DETAILS[RULE_CCR_READ_EXCEPTIONS].description, @@ -45,7 +48,11 @@ export function createCCRReadExceptionsAlertType(): AlertTypeModel ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', diff --git a/x-pack/plugins/monitoring/public/alerts/components/param_details_form/expression.tsx b/x-pack/plugins/monitoring/public/alerts/components/param_details_form/expression.tsx index df17ce1a911a0..827eed955d535 100644 --- a/x-pack/plugins/monitoring/public/alerts/components/param_details_form/expression.tsx +++ b/x-pack/plugins/monitoring/public/alerts/components/param_details_form/expression.tsx @@ -5,14 +5,23 @@ * 2.0. */ -import React, { Fragment } from 'react'; -import { EuiForm, EuiSpacer } from '@elastic/eui'; +import React, { Fragment, useCallback } from 'react'; +import { EuiForm, EuiFormRow, EuiSpacer } from '@elastic/eui'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { debounce } from 'lodash'; +import { i18n } from '@kbn/i18n'; import { CommonAlertParamDetails } from '../../../../common/types/alerts'; import { AlertParamDuration } from '../../flyout_expressions/alert_param_duration'; import { AlertParamType } from '../../../../common/enums'; import { AlertParamPercentage } from '../../flyout_expressions/alert_param_percentage'; import { AlertParamNumber } from '../../flyout_expressions/alert_param_number'; import { AlertParamTextField } from '../../flyout_expressions/alert_param_textfield'; +import { MonitoringConfig } from '../../../types'; +import { useDerivedIndexPattern } from './use_derived_index_pattern'; +import { KueryBar } from '../../../components/kuery_bar'; +import { convertKueryToElasticSearchQuery } from '../../../lib/kuery'; + +const FILTER_TYPING_DEBOUNCE_MS = 500; export interface Props { alertParams: { [property: string]: any }; @@ -20,10 +29,14 @@ export interface Props { setAlertProperty: (property: string, value: any) => void; errors: { [key: string]: string[] }; paramDetails: CommonAlertParamDetails; + data: DataPublicPluginStart; + config?: MonitoringConfig; } export const Expression: React.FC = (props) => { - const { alertParams, paramDetails, setAlertParams, errors } = props; + const { alertParams, paramDetails, setAlertParams, errors, config, data } = props; + + const { derivedIndexPattern } = useDerivedIndexPattern(data, config); const alertParamsUi = Object.keys(paramDetails).map((alertParamName) => { const details = paramDetails[alertParamName]; @@ -77,10 +90,44 @@ export const Expression: React.FC = (props) => { } }); + const onFilterChange = useCallback( + (filter: string) => { + setAlertParams('filterQueryText', filter); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(filter, derivedIndexPattern) || '' + ); + }, + [setAlertParams, derivedIndexPattern] + ); + + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + const debouncedOnFilterChange = useCallback(debounce(onFilterChange, FILTER_TYPING_DEBOUNCE_MS), [ + onFilterChange, + ]); + return ( - {alertParamsUi} - + + {alertParamsUi} + + + + + + ); }; diff --git a/x-pack/plugins/monitoring/public/alerts/components/param_details_form/use_derived_index_pattern.tsx b/x-pack/plugins/monitoring/public/alerts/components/param_details_form/use_derived_index_pattern.tsx new file mode 100644 index 0000000000000..1a4d88d690b84 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/components/param_details_form/use_derived_index_pattern.tsx @@ -0,0 +1,44 @@ +/* + * 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 { useEffect, useState } from 'react'; +import { DataPublicPluginStart, IFieldType, IIndexPattern } from 'src/plugins/data/public'; +import { + INDEX_PATTERN_BEATS, + INDEX_PATTERN_ELASTICSEARCH, + INDEX_PATTERN_KIBANA, + INDEX_PATTERN_LOGSTASH, +} from '../../../../common/constants'; +import { prefixIndexPattern } from '../../../../common/ccs_utils'; +import { MonitoringConfig } from '../../../types'; + +const INDEX_PATTERNS = `${INDEX_PATTERN_ELASTICSEARCH},${INDEX_PATTERN_KIBANA},${INDEX_PATTERN_LOGSTASH},${INDEX_PATTERN_BEATS}`; + +export const useDerivedIndexPattern = ( + data: DataPublicPluginStart, + config?: MonitoringConfig +): { loading: boolean; derivedIndexPattern: IIndexPattern } => { + const indexPattern = prefixIndexPattern(config || ({} as MonitoringConfig), INDEX_PATTERNS, '*'); + const [loading, setLoading] = useState(true); + const [fields, setFields] = useState([]); + useEffect(() => { + (async function fetchData() { + const result = await data.indexPatterns.getFieldsForWildcard({ + pattern: indexPattern, + }); + setFields(result); + setLoading(false); + })(); + }, [indexPattern, data.indexPatterns]); + return { + loading, + derivedIndexPattern: { + title: indexPattern, + fields, + }, + }; +}; diff --git a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx index ec7a583ec2ba1..f0e0a413435f9 100644 --- a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx @@ -11,8 +11,11 @@ import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; import { RULE_CPU_USAGE, RULE_DETAILS, RULE_REQUIRES_APP_CONTEXT } from '../../../common/constants'; import { validate, MonitoringAlertTypeParams } from '../components/param_details_form/validation'; import { Expression, Props } from '../components/param_details_form/expression'; +import { MonitoringConfig } from '../../types'; -export function createCpuUsageAlertType(): AlertTypeModel { +export function createCpuUsageAlertType( + config: MonitoringConfig +): AlertTypeModel { return { id: RULE_CPU_USAGE, description: RULE_DETAILS[RULE_CPU_USAGE].description, @@ -21,7 +24,11 @@ export function createCpuUsageAlertType(): AlertTypeModel ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', diff --git a/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx index 779945da0c9e0..5f9f9536ae567 100644 --- a/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx @@ -16,8 +16,11 @@ import { RULE_DETAILS, RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; +import { MonitoringConfig } from '../../types'; -export function createDiskUsageAlertType(): AlertTypeModel { +export function createDiskUsageAlertType( + config: MonitoringConfig +): AlertTypeModel { return { id: RULE_DISK_USAGE, description: RULE_DETAILS[RULE_DISK_USAGE].description, @@ -26,7 +29,11 @@ export function createDiskUsageAlertType(): AlertTypeModel ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', diff --git a/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx index e0f595abe7602..afaf20d60d882 100644 --- a/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx @@ -15,6 +15,7 @@ import { RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; import { AlertTypeParams } from '../../../../alerting/common'; +import { MonitoringConfig } from '../../types'; interface ValidateOptions extends AlertTypeParams { indexPattern: string; @@ -36,7 +37,9 @@ const validate = (inputValues: ValidateOptions): ValidationResult => { return validationResult; }; -export function createLargeShardSizeAlertType(): AlertTypeModel { +export function createLargeShardSizeAlertType( + config: MonitoringConfig +): AlertTypeModel { return { id: RULE_LARGE_SHARD_SIZE, description: RULE_DETAILS[RULE_LARGE_SHARD_SIZE].description, @@ -45,7 +48,11 @@ export function createLargeShardSizeAlertType(): AlertTypeModel return `${docLinks.links.monitoring.alertsKibanaLargeShardSize}`; }, alertParamsExpression: (props: Props) => ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', diff --git a/x-pack/plugins/monitoring/public/alerts/legacy_alert/expression.tsx b/x-pack/plugins/monitoring/public/alerts/legacy_alert/expression.tsx new file mode 100644 index 0000000000000..fe6adf66c1d4f --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/legacy_alert/expression.tsx @@ -0,0 +1,56 @@ +/* + * 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, { useCallback } from 'react'; +import { debounce } from 'lodash'; +import { EuiSpacer, EuiForm, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useDerivedIndexPattern } from '../components/param_details_form/use_derived_index_pattern'; +import { convertKueryToElasticSearchQuery } from '../../lib/kuery'; +import { KueryBar } from '../../components/kuery_bar'; +import { Props } from '../components/param_details_form/expression'; + +const FILTER_TYPING_DEBOUNCE_MS = 500; + +export const Expression = ({ alertParams, config, setAlertParams, data }: Props) => { + const { derivedIndexPattern } = useDerivedIndexPattern(data, config); + const onFilterChange = useCallback( + (filter: string) => { + setAlertParams('filterQueryText', filter); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(filter, derivedIndexPattern) || '' + ); + }, + [setAlertParams, derivedIndexPattern] + ); + + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + const debouncedOnFilterChange = useCallback(debounce(onFilterChange, FILTER_TYPING_DEBOUNCE_MS), [ + onFilterChange, + ]); + return ( + + + + + + + ); +}; diff --git a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx index cac4873bc0c79..a6c22035c5a5a 100644 --- a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx @@ -5,9 +5,7 @@ * 2.0. */ -import React, { Fragment } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiTextColor, EuiSpacer } from '@elastic/eui'; +import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; import { @@ -15,8 +13,11 @@ import { LEGACY_RULE_DETAILS, RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; +import { MonitoringConfig } from '../../types'; +import { Expression } from './expression'; +import { Props } from '../components/param_details_form/expression'; -export function createLegacyAlertTypes(): AlertTypeModel[] { +export function createLegacyAlertTypes(config: MonitoringConfig): AlertTypeModel[] { return LEGACY_RULES.map((legacyAlert) => { return { id: legacyAlert, @@ -25,17 +26,7 @@ export function createLegacyAlertTypes(): AlertTypeModel[] { documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaClusterAlerts}`; }, - alertParamsExpression: () => ( - - - - {i18n.translate('xpack.monitoring.alerts.legacyAlert.expressionText', { - defaultMessage: 'There is nothing to configure.', - })} - - - - ), + alertParamsExpression: (props: Props) => , defaultActionMessage: '{{context.internalFullMessage}}', validate: () => ({ errors: {} }), requiresAppContext: RULE_REQUIRES_APP_CONTEXT, diff --git a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx index 3e55b6d5454ff..2fe0c9b77c0eb 100644 --- a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx @@ -16,8 +16,11 @@ import { RULE_DETAILS, RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; +import { MonitoringConfig } from '../../types'; -export function createMemoryUsageAlertType(): AlertTypeModel { +export function createMemoryUsageAlertType( + config: MonitoringConfig +): AlertTypeModel { return { id: RULE_MEMORY_USAGE, description: RULE_DETAILS[RULE_MEMORY_USAGE].description, @@ -26,7 +29,11 @@ export function createMemoryUsageAlertType(): AlertTypeModel ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', diff --git a/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx index 7fd9438e1cea3..e8a15ad835581 100644 --- a/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx @@ -13,6 +13,7 @@ import { Expression, Props } from '../components/param_details_form/expression'; import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; import { CommonAlertParamDetails } from '../../../common/types/alerts'; import { RULE_REQUIRES_APP_CONTEXT } from '../../../common/constants'; +import { MonitoringConfig } from '../../types'; interface ThreadPoolTypes { [key: string]: unknown; @@ -26,7 +27,8 @@ interface ThreadPoolRejectionAlertDetails { export function createThreadPoolRejectionsAlertType( alertId: string, - threadPoolAlertDetails: ThreadPoolRejectionAlertDetails + threadPoolAlertDetails: ThreadPoolRejectionAlertDetails, + config: MonitoringConfig ): AlertTypeModel { return { id: alertId, @@ -38,7 +40,7 @@ export function createThreadPoolRejectionsAlertType( alertParamsExpression: (props: Props) => ( <> - + ), validate: (inputValues: ThreadPoolTypes) => { diff --git a/x-pack/plugins/monitoring/public/components/kuery_bar/autocomplete_field.tsx b/x-pack/plugins/monitoring/public/components/kuery_bar/autocomplete_field.tsx new file mode 100644 index 0000000000000..522256ea49b98 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/kuery_bar/autocomplete_field.tsx @@ -0,0 +1,316 @@ +/* + * 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 { EuiFieldSearch, EuiOutsideClickDetector, EuiPanel } from '@elastic/eui'; +import React from 'react'; +import { QuerySuggestion } from '../../../../../../src/plugins/data/public'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; +import { composeStateUpdaters } from '../../lib/typed_react'; +import { SuggestionItem } from './suggestion_item'; + +interface AutocompleteFieldProps { + isLoadingSuggestions: boolean; + isValid: boolean; + loadSuggestions: (value: string, cursorPosition: number, maxCount?: number) => void; + onSubmit?: (value: string) => void; + onChange?: (value: string) => void; + placeholder?: string; + suggestions: QuerySuggestion[]; + value: string; + disabled?: boolean; + autoFocus?: boolean; + 'aria-label'?: string; +} + +interface AutocompleteFieldState { + areSuggestionsVisible: boolean; + isFocused: boolean; + selectedIndex: number | null; +} + +export class AutocompleteField extends React.Component< + AutocompleteFieldProps, + AutocompleteFieldState +> { + public readonly state: AutocompleteFieldState = { + areSuggestionsVisible: false, + isFocused: false, + selectedIndex: null, + }; + + private inputElement: HTMLInputElement | null = null; + + public render() { + const { + suggestions, + isLoadingSuggestions, + isValid, + placeholder, + value, + disabled, + 'aria-label': ariaLabel, + } = this.props; + const { areSuggestionsVisible, selectedIndex } = this.state; + + return ( + + + + {areSuggestionsVisible && !isLoadingSuggestions && suggestions.length > 0 ? ( + + {suggestions.map((suggestion, suggestionIndex) => ( + + ))} + + ) : null} + + + ); + } + + public componentDidMount() { + if (this.inputElement && this.props.autoFocus) { + this.inputElement.focus(); + } + } + + public componentDidUpdate(prevProps: AutocompleteFieldProps) { + const hasNewValue = prevProps.value !== this.props.value; + const hasNewSuggestions = prevProps.suggestions !== this.props.suggestions; + + if (hasNewValue) { + this.updateSuggestions(); + } + + if (hasNewValue && this.props.value === '') { + this.submit(); + } + + if (hasNewSuggestions && this.state.isFocused) { + this.showSuggestions(); + } + } + + private handleChangeInputRef = (element: HTMLInputElement | null) => { + this.inputElement = element; + }; + + private handleChange = (evt: React.ChangeEvent) => { + this.changeValue(evt.currentTarget.value); + }; + + private handleKeyDown = (evt: React.KeyboardEvent) => { + const { suggestions } = this.props; + + switch (evt.key) { + case 'ArrowUp': + evt.preventDefault(); + if (suggestions.length > 0) { + this.setState( + composeStateUpdaters(withSuggestionsVisible, withPreviousSuggestionSelected) + ); + } + break; + case 'ArrowDown': + evt.preventDefault(); + if (suggestions.length > 0) { + this.setState(composeStateUpdaters(withSuggestionsVisible, withNextSuggestionSelected)); + } else { + this.updateSuggestions(); + } + break; + case 'Enter': + evt.preventDefault(); + if (this.state.selectedIndex !== null) { + this.applySelectedSuggestion(); + } else { + this.submit(); + } + break; + case 'Escape': + evt.preventDefault(); + this.setState(withSuggestionsHidden); + break; + } + }; + + private handleKeyUp = (evt: React.KeyboardEvent) => { + switch (evt.key) { + case 'ArrowLeft': + case 'ArrowRight': + case 'Home': + case 'End': + this.updateSuggestions(); + break; + } + }; + + private handleFocus = () => { + this.setState(composeStateUpdaters(withSuggestionsVisible, withFocused)); + }; + + private handleBlur = () => { + this.setState(composeStateUpdaters(withSuggestionsHidden, withUnfocused)); + }; + + private selectSuggestionAt = (index: number) => () => { + this.setState(withSuggestionAtIndexSelected(index)); + }; + + private applySelectedSuggestion = () => { + if (this.state.selectedIndex !== null) { + this.applySuggestionAt(this.state.selectedIndex)(); + } + }; + + private applySuggestionAt = (index: number) => () => { + const { value, suggestions } = this.props; + const selectedSuggestion = suggestions[index]; + + if (!selectedSuggestion) { + return; + } + + const newValue = + value.substr(0, selectedSuggestion.start) + + selectedSuggestion.text + + value.substr(selectedSuggestion.end); + + this.setState(withSuggestionsHidden); + this.changeValue(newValue); + this.focusInputElement(); + }; + + private changeValue = (value: string) => { + const { onChange } = this.props; + + if (onChange) { + onChange(value); + } + }; + + private focusInputElement = () => { + if (this.inputElement) { + this.inputElement.focus(); + } + }; + + private showSuggestions = () => { + this.setState(withSuggestionsVisible); + }; + + private submit = () => { + const { isValid, onSubmit, value } = this.props; + + if (isValid && onSubmit) { + onSubmit(value); + } + + this.setState(withSuggestionsHidden); + }; + + private updateSuggestions = () => { + const inputCursorPosition = this.inputElement ? this.inputElement.selectionStart || 0 : 0; + this.props.loadSuggestions(this.props.value, inputCursorPosition, 200); + }; +} + +const withPreviousSuggestionSelected = ( + state: AutocompleteFieldState, + props: AutocompleteFieldProps +): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : state.selectedIndex !== null + ? (state.selectedIndex + props.suggestions.length - 1) % props.suggestions.length + : Math.max(props.suggestions.length - 1, 0), +}); + +const withNextSuggestionSelected = ( + state: AutocompleteFieldState, + props: AutocompleteFieldProps +): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : state.selectedIndex !== null + ? (state.selectedIndex + 1) % props.suggestions.length + : 0, +}); + +const withSuggestionAtIndexSelected = + (suggestionIndex: number) => + (state: AutocompleteFieldState, props: AutocompleteFieldProps): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : suggestionIndex >= 0 && suggestionIndex < props.suggestions.length + ? suggestionIndex + : 0, + }); + +const withSuggestionsVisible = (state: AutocompleteFieldState) => ({ + ...state, + areSuggestionsVisible: true, +}); + +const withSuggestionsHidden = (state: AutocompleteFieldState) => ({ + ...state, + areSuggestionsVisible: false, + selectedIndex: null, +}); + +const withFocused = (state: AutocompleteFieldState) => ({ + ...state, + isFocused: true, +}); + +const withUnfocused = (state: AutocompleteFieldState) => ({ + ...state, + isFocused: false, +}); + +const AutocompleteContainer = euiStyled.div` + position: relative; +`; + +const SuggestionsPanel = euiStyled(EuiPanel).attrs(() => ({ + paddingSize: 'none', + hasShadow: true, +}))` + position: absolute; + width: 100%; + margin-top: 2px; + overflow-x: hidden; + overflow-y: scroll; + z-index: ${(props) => props.theme.eui.euiZLevel1}; + max-height: 322px; +`; diff --git a/x-pack/plugins/monitoring/public/components/kuery_bar/index.tsx b/x-pack/plugins/monitoring/public/components/kuery_bar/index.tsx new file mode 100644 index 0000000000000..ca0a8122772f3 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/kuery_bar/index.tsx @@ -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 { i18n } from '@kbn/i18n'; + +import React, { useEffect, useState } from 'react'; +import { WithKueryAutocompletion } from './with_kuery_autocompletion'; +import { AutocompleteField } from './autocomplete_field'; +import { esKuery, IIndexPattern, QuerySuggestion } from '../../../../../../src/plugins/data/public'; + +type LoadSuggestionsFn = ( + e: string, + p: number, + m?: number, + transform?: (s: QuerySuggestion[]) => QuerySuggestion[] +) => void; +export type CurryLoadSuggestionsType = (loadSuggestions: LoadSuggestionsFn) => LoadSuggestionsFn; + +interface Props { + derivedIndexPattern: IIndexPattern; + onSubmit: (query: string) => void; + onChange?: (query: string) => void; + value?: string | null; + placeholder?: string; + curryLoadSuggestions?: CurryLoadSuggestionsType; +} + +function validateQuery(query: string) { + try { + esKuery.fromKueryExpression(query); + } catch (err) { + return false; + } + return true; +} + +export const KueryBar = ({ + derivedIndexPattern, + onSubmit, + onChange, + value, + placeholder, + curryLoadSuggestions = defaultCurryLoadSuggestions, +}: Props) => { + const [draftQuery, setDraftQuery] = useState(value || ''); + const [isValid, setValidation] = useState(true); + + // This ensures that if value changes out side this component it will update. + useEffect(() => { + if (value) { + setDraftQuery(value); + } + }, [value]); + + const handleChange = (query: string) => { + setValidation(validateQuery(query)); + setDraftQuery(query); + if (onChange) { + onChange(query); + } + }; + + const filteredDerivedIndexPattern = { + ...derivedIndexPattern, + fields: derivedIndexPattern.fields, + }; + + const defaultPlaceholder = i18n.translate('xpack.monitoring.alerts.kqlSearchFieldPlaceholder', { + defaultMessage: 'Search for monitoring data', + }); + + return ( + + {({ isLoadingSuggestions, loadSuggestions, suggestions }) => ( + + )} + + ); +}; + +const defaultCurryLoadSuggestions: CurryLoadSuggestionsType = + (loadSuggestions) => + (...args) => + loadSuggestions(...args); diff --git a/x-pack/plugins/monitoring/public/components/kuery_bar/suggestion_item.tsx b/x-pack/plugins/monitoring/public/components/kuery_bar/suggestion_item.tsx new file mode 100644 index 0000000000000..3681bf26987cc --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/kuery_bar/suggestion_item.tsx @@ -0,0 +1,119 @@ +/* + * 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 { EuiIcon } from '@elastic/eui'; +import { transparentize } from 'polished'; +import React from 'react'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; +import { QuerySuggestion, QuerySuggestionTypes } from '../../../../../../src/plugins/data/public'; + +interface Props { + isSelected?: boolean; + onClick?: React.MouseEventHandler; + onMouseEnter?: React.MouseEventHandler; + suggestion: QuerySuggestion; +} + +export const SuggestionItem: React.FC = (props) => { + const { isSelected, onClick, onMouseEnter, suggestion } = props; + + return ( + + + + + {suggestion.text} + {suggestion.description} + + ); +}; + +SuggestionItem.defaultProps = { + isSelected: false, +}; + +const SuggestionItemContainer = euiStyled.div<{ + isSelected?: boolean; +}>` + display: flex; + flex-direction: row; + font-size: ${(props) => props.theme.eui.euiFontSizeS}; + height: ${(props) => props.theme.eui.euiSizeXL}; + white-space: nowrap; + background-color: ${(props) => + props.isSelected ? props.theme.eui.euiColorLightestShade : 'transparent'}; +`; + +const SuggestionItemField = euiStyled.div` + align-items: center; + cursor: pointer; + display: flex; + flex-direction: row; + height: ${(props) => props.theme.eui.euiSizeXL}; + padding: ${(props) => props.theme.eui.euiSizeXS}; +`; + +const SuggestionItemIconField = euiStyled(SuggestionItemField)<{ + suggestionType: QuerySuggestionTypes; +}>` + background-color: ${(props) => + transparentize(0.9, getEuiIconColor(props.theme, props.suggestionType))}; + color: ${(props) => getEuiIconColor(props.theme, props.suggestionType)}; + flex: 0 0 auto; + justify-content: center; + width: ${(props) => props.theme.eui.euiSizeXL}; +`; + +const SuggestionItemTextField = euiStyled(SuggestionItemField)` + flex: 2 0 0; + font-family: ${(props) => props.theme.eui.euiCodeFontFamily}; +`; + +const SuggestionItemDescriptionField = euiStyled(SuggestionItemField)` + flex: 3 0 0; + + p { + display: inline; + + span { + font-family: ${(props) => props.theme.eui.euiCodeFontFamily}; + } + } +`; + +const getEuiIconType = (suggestionType: QuerySuggestionTypes) => { + switch (suggestionType) { + case QuerySuggestionTypes.Field: + return 'kqlField'; + case QuerySuggestionTypes.Value: + return 'kqlValue'; + case QuerySuggestionTypes.RecentSearch: + return 'search'; + case QuerySuggestionTypes.Conjunction: + return 'kqlSelector'; + case QuerySuggestionTypes.Operator: + return 'kqlOperand'; + default: + return 'empty'; + } +}; + +const getEuiIconColor = (theme: any, suggestionType: QuerySuggestionTypes): string => { + switch (suggestionType) { + case QuerySuggestionTypes.Field: + return theme?.eui.euiColorVis7; + case QuerySuggestionTypes.Value: + return theme?.eui.euiColorVis0; + case QuerySuggestionTypes.Operator: + return theme?.eui.euiColorVis1; + case QuerySuggestionTypes.Conjunction: + return theme?.eui.euiColorVis2; + case QuerySuggestionTypes.RecentSearch: + default: + return theme?.eui.euiColorMediumShade; + } +}; diff --git a/x-pack/plugins/monitoring/public/components/kuery_bar/with_kuery_autocompletion.tsx b/x-pack/plugins/monitoring/public/components/kuery_bar/with_kuery_autocompletion.tsx new file mode 100644 index 0000000000000..8d79bf4039846 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/kuery_bar/with_kuery_autocompletion.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { QuerySuggestion, IIndexPattern, DataPublicPluginStart } from 'src/plugins/data/public'; +import { + withKibana, + KibanaReactContextValue, + KibanaServices, +} from '../../../../../../src/plugins/kibana_react/public'; +import { RendererFunction } from '../../lib/typed_react'; + +interface WithKueryAutocompletionLifecycleProps { + kibana: KibanaReactContextValue<{ data: DataPublicPluginStart } & KibanaServices>; + children: RendererFunction<{ + isLoadingSuggestions: boolean; + loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void; + suggestions: QuerySuggestion[]; + }>; + indexPattern: IIndexPattern; +} + +interface WithKueryAutocompletionLifecycleState { + // lacking cancellation support in the autocompletion api, + // this is used to keep older, slower requests from clobbering newer ones + currentRequest: { + expression: string; + cursorPosition: number; + } | null; + suggestions: QuerySuggestion[]; +} + +class WithKueryAutocompletionComponent extends React.Component< + WithKueryAutocompletionLifecycleProps, + WithKueryAutocompletionLifecycleState +> { + public readonly state: WithKueryAutocompletionLifecycleState = { + currentRequest: null, + suggestions: [], + }; + + public render() { + const { currentRequest, suggestions } = this.state; + + return this.props.children({ + isLoadingSuggestions: currentRequest !== null, + loadSuggestions: this.loadSuggestions, + suggestions, + }); + } + + private loadSuggestions = async ( + expression: string, + cursorPosition: number, + maxSuggestions?: number, + transformSuggestions?: (s: QuerySuggestion[]) => QuerySuggestion[] + ) => { + const { indexPattern } = this.props; + const language = 'kuery'; + const hasQuerySuggestions = + this.props.kibana.services.data?.autocomplete.hasQuerySuggestions(language); + + if (!hasQuerySuggestions) { + return; + } + + this.setState({ + currentRequest: { + expression, + cursorPosition, + }, + suggestions: [], + }); + + const suggestions = + (await this.props.kibana.services.data.autocomplete.getQuerySuggestions({ + language, + query: expression, + selectionStart: cursorPosition, + selectionEnd: cursorPosition, + indexPatterns: [indexPattern], + boolFilter: [], + })) || []; + + const transformedSuggestions = transformSuggestions + ? transformSuggestions(suggestions) + : suggestions; + + this.setState((state) => + state.currentRequest && + state.currentRequest.expression !== expression && + state.currentRequest.cursorPosition !== cursorPosition + ? state // ignore this result, since a newer request is in flight + : { + ...state, + currentRequest: null, + suggestions: maxSuggestions + ? transformedSuggestions.slice(0, maxSuggestions) + : transformedSuggestions, + } + ); + }; +} + +export const WithKueryAutocompletion = withKibana( + WithKueryAutocompletionComponent +); diff --git a/x-pack/plugins/monitoring/public/lib/kuery.ts b/x-pack/plugins/monitoring/public/lib/kuery.ts new file mode 100644 index 0000000000000..19706d7664c22 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/kuery.ts @@ -0,0 +1,23 @@ +/* + * 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 { esKuery, IIndexPattern } from '../../../../../src/plugins/data/public'; + +export const convertKueryToElasticSearchQuery = ( + kueryExpression: string, + indexPattern: IIndexPattern +) => { + try { + return kueryExpression + ? JSON.stringify( + esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) + ) + : ''; + } catch (err) { + return ''; + } +}; diff --git a/x-pack/plugins/monitoring/public/lib/typed_react.tsx b/x-pack/plugins/monitoring/public/lib/typed_react.tsx new file mode 100644 index 0000000000000..b5b7a440c117c --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/typed_react.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { omit } from 'lodash'; +import React from 'react'; +import { InferableComponentEnhancerWithProps, ConnectedComponent } from 'react-redux'; + +export type RendererResult = React.ReactElement | null; +export type RendererFunction = (args: RenderArgs) => Result; + +export type ChildFunctionRendererProps = { + children: RendererFunction; + initializeOnMount?: boolean; + resetOnUnmount?: boolean; +} & RenderArgs; + +interface ChildFunctionRendererOptions { + onInitialize?: (props: RenderArgs) => void; + onCleanup?: (props: RenderArgs) => void; +} + +export const asChildFunctionRenderer = ( + hoc: InferableComponentEnhancerWithProps, + { onInitialize, onCleanup }: ChildFunctionRendererOptions = {} +): ConnectedComponent< + React.ComponentClass<{}>, + { + children: RendererFunction; + initializeOnMount?: boolean; + resetOnUnmount?: boolean; + } & OwnProps +> => + hoc( + class ChildFunctionRenderer extends React.Component> { + public displayName = 'ChildFunctionRenderer'; + + public componentDidMount() { + if (this.props.initializeOnMount && onInitialize) { + onInitialize(this.getRendererArgs()); + } + } + + public componentWillUnmount() { + if (this.props.resetOnUnmount && onCleanup) { + onCleanup(this.getRendererArgs()); + } + } + + public render() { + return (this.props.children as ChildFunctionRendererProps['children'])( + this.getRendererArgs() + ); + } + + private getRendererArgs = () => + omit(this.props, ['children', 'initializeOnMount', 'resetOnUnmount']) as Pick< + ChildFunctionRendererProps, + keyof InjectedProps + >; + } as any + ); + +export type StateUpdater = ( + prevState: Readonly, + prevProps: Readonly +) => State | null; + +export type PropsOfContainer = Container extends InferableComponentEnhancerWithProps< + infer InjectedProps, + any +> + ? InjectedProps + : never; + +export function composeStateUpdaters(...updaters: Array>) { + return (state: State, props: Props) => + updaters.reduce((currentState, updater) => updater(currentState, props) || currentState, state); +} diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index ad71cdbeb106c..aee5072947531 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -28,14 +28,6 @@ import { RULE_THREAD_POOL_WRITE_REJECTIONS, RULE_DETAILS, } from '../common/constants'; -import { createCpuUsageAlertType } from './alerts/cpu_usage_alert'; -import { createMissingMonitoringDataAlertType } from './alerts/missing_monitoring_data_alert'; -import { createLegacyAlertTypes } from './alerts/legacy_alert'; -import { createDiskUsageAlertType } from './alerts/disk_usage_alert'; -import { createThreadPoolRejectionsAlertType } from './alerts/thread_pool_rejections_alert'; -import { createMemoryUsageAlertType } from './alerts/memory_usage_alert'; -import { createCCRReadExceptionsAlertType } from './alerts/ccr_read_exceptions_alert'; -import { createLargeShardSizeAlertType } from './alerts/large_shard_size_alert'; import { setConfig } from './external_config'; interface MonitoringSetupPluginDependencies { @@ -49,11 +41,11 @@ const HASH_CHANGE = 'hashchange'; export class MonitoringPlugin implements - Plugin + Plugin { constructor(private initializerContext: PluginInitializerContext) {} - public setup( + public async setup( core: CoreSetup, plugins: MonitoringSetupPluginDependencies ) { @@ -86,7 +78,7 @@ export class MonitoringPlugin }); } - this.registerAlerts(plugins); + await this.registerAlerts(plugins, monitoring); const app: App = { id, @@ -152,7 +144,6 @@ export class MonitoringPlugin }; core.application.register(app); - return true; } public start(core: CoreStart, plugins: any) {} @@ -192,29 +183,48 @@ export class MonitoringPlugin ]; } - private registerAlerts(plugins: MonitoringSetupPluginDependencies) { + private async registerAlerts( + plugins: MonitoringSetupPluginDependencies, + config: MonitoringConfig + ) { const { triggersActionsUi: { ruleTypeRegistry }, } = plugins; - ruleTypeRegistry.register(createCpuUsageAlertType()); - ruleTypeRegistry.register(createDiskUsageAlertType()); - ruleTypeRegistry.register(createMemoryUsageAlertType()); + + const { createCpuUsageAlertType } = await import('./alerts/cpu_usage_alert'); + const { createMissingMonitoringDataAlertType } = await import( + './alerts/missing_monitoring_data_alert' + ); + const { createLegacyAlertTypes } = await import('./alerts/legacy_alert'); + const { createDiskUsageAlertType } = await import('./alerts/disk_usage_alert'); + const { createThreadPoolRejectionsAlertType } = await import( + './alerts/thread_pool_rejections_alert' + ); + const { createMemoryUsageAlertType } = await import('./alerts/memory_usage_alert'); + const { createCCRReadExceptionsAlertType } = await import('./alerts/ccr_read_exceptions_alert'); + const { createLargeShardSizeAlertType } = await import('./alerts/large_shard_size_alert'); + + ruleTypeRegistry.register(createCpuUsageAlertType(config)); + ruleTypeRegistry.register(createDiskUsageAlertType(config)); + ruleTypeRegistry.register(createMemoryUsageAlertType(config)); ruleTypeRegistry.register(createMissingMonitoringDataAlertType()); ruleTypeRegistry.register( createThreadPoolRejectionsAlertType( RULE_THREAD_POOL_SEARCH_REJECTIONS, - RULE_DETAILS[RULE_THREAD_POOL_SEARCH_REJECTIONS] + RULE_DETAILS[RULE_THREAD_POOL_SEARCH_REJECTIONS], + config ) ); ruleTypeRegistry.register( createThreadPoolRejectionsAlertType( RULE_THREAD_POOL_WRITE_REJECTIONS, - RULE_DETAILS[RULE_THREAD_POOL_WRITE_REJECTIONS] + RULE_DETAILS[RULE_THREAD_POOL_WRITE_REJECTIONS], + config ) ); - ruleTypeRegistry.register(createCCRReadExceptionsAlertType()); - ruleTypeRegistry.register(createLargeShardSizeAlertType()); - const legacyAlertTypes = createLegacyAlertTypes(); + ruleTypeRegistry.register(createCCRReadExceptionsAlertType(config)); + ruleTypeRegistry.register(createLargeShardSizeAlertType(config)); + const legacyAlertTypes = createLegacyAlertTypes(config); for (const legacyAlertType of legacyAlertTypes) { ruleTypeRegistry.register(legacyAlertType); } diff --git a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts index 3a50aca7d4b84..e3a3537ea2eaf 100644 --- a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts @@ -88,7 +88,8 @@ export class CCRReadExceptionsRule extends BaseRule { esIndexPattern, startMs, endMs, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return stats.map((stat) => { @@ -278,7 +279,7 @@ export class CCRReadExceptionsRule extends BaseRule { state: AlertingDefaults.ALERT_STATE.firing, remoteCluster, followerIndex, - /* continue to send "remoteClusters" and "followerIndices" values for users still using it though + /* continue to send "remoteClusters" and "followerIndices" values for users still using it though we have replaced it with "remoteCluster" and "followerIndex" in the template due to alerts per index instead of all indices see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts index 7fac3b74a1b66..b9b9b90845eea 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts @@ -73,7 +73,12 @@ export class ClusterHealthRule extends BaseRule { if (availableCcs) { esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); } - const healths = await fetchClusterHealth(esClient, clusters, esIndexPattern); + const healths = await fetchClusterHealth( + esClient, + clusters, + esIndexPattern, + params.filterQuery + ); return healths.map((clusterHealth) => { const shouldFire = clusterHealth.health !== AlertClusterHealthType.Green; const severity = diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts index 2e57a3c22de1b..7e38efcb819ea 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts @@ -76,7 +76,8 @@ export class CpuUsageRule extends BaseRule { esIndexPattern, startMs, endMs, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return stats.map((stat) => { if (Globals.app.config.ui.container.elasticsearch.enabled) { @@ -203,7 +204,7 @@ export class CpuUsageRule extends BaseRule { internalShortMessage, internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, state: AlertingDefaults.ALERT_STATE.firing, - /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 + /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ nodes: `${firingNode.nodeName}:${firingNode.cpuUsage}`, diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts index ae3025c1db92c..bac70baebb4e2 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts @@ -72,7 +72,8 @@ export class DiskUsageRule extends BaseRule { clusters, esIndexPattern, duration as string, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return stats.map((stat) => { @@ -212,7 +213,7 @@ export class DiskUsageRule extends BaseRule { internalShortMessage, internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, state: AlertingDefaults.ALERT_STATE.firing, - /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 + /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ nodes: `${firingNode.nodeName}:${firingNode.diskUsage}`, diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts index 6a5abcb4975f4..352cac531f8e8 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts @@ -66,7 +66,8 @@ export class ElasticsearchVersionMismatchRule extends BaseRule { esClient, clusters, esIndexPattern, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return elasticsearchVersions.map((elasticsearchVersion) => { diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts index 90275ea4d23a8..6d9410ed0e5a0 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts @@ -79,7 +79,8 @@ export class KibanaVersionMismatchRule extends BaseRule { esClient, clusters, kibanaIndexPattern, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return kibanaVersions.map((kibanaVersion) => { diff --git a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts index 86f96daa3b21d..b0370a23159d7 100644 --- a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts @@ -75,7 +75,8 @@ export class LargeShardSizeRule extends BaseRule { esIndexPattern, threshold!, shardIndexPatterns, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return stats.map((stat) => { @@ -211,7 +212,7 @@ export class LargeShardSizeRule extends BaseRule { internalShortMessage, internalFullMessage, state: AlertingDefaults.ALERT_STATE.firing, - /* continue to send "shardIndices" values for users still using it though + /* continue to send "shardIndices" values for users still using it though we have replaced it with shardIndex in the template due to alerts per index instead of all indices see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts index 67ea8bd57b491..c26929b05ab26 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts @@ -87,7 +87,7 @@ export class LicenseExpirationRule extends BaseRule { if (availableCcs) { esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); } - const licenses = await fetchLicenses(esClient, clusters, esIndexPattern); + const licenses = await fetchLicenses(esClient, clusters, esIndexPattern, params.filterQuery); return licenses.map((license) => { const { clusterUuid, type, expiryDateMS, status, ccs } = license; diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts index 0f9ad4dd4b117..e59ed9efbefb2 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts @@ -66,7 +66,8 @@ export class LogstashVersionMismatchRule extends BaseRule { esClient, clusters, logstashIndexPattern, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return logstashVersions.map((logstashVersion) => { diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts index 384610e659d47..d94e1234ce813 100644 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts @@ -82,7 +82,8 @@ export class MemoryUsageRule extends BaseRule { esIndexPattern, startMs, endMs, - Globals.app.config.ui.max_bucket_size + Globals.app.config.ui.max_bucket_size, + params.filterQuery ); return stats.map((stat) => { @@ -223,7 +224,7 @@ export class MemoryUsageRule extends BaseRule { internalShortMessage, internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, state: AlertingDefaults.ALERT_STATE.firing, - /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 + /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ nodes: `${firingNode.nodeName}:${firingNode.memoryUsage.toFixed(2)}`, diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts index 32e4ff738c71b..1b45b19fe89f8 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts @@ -75,7 +75,8 @@ export class MissingMonitoringDataRule extends BaseRule { indexPattern, Globals.app.config.ui.max_bucket_size, now, - now - limit - LIMIT_BUFFER + now - limit - LIMIT_BUFFER, + params.filterQuery ); return missingData.map((missing) => { return { @@ -198,7 +199,7 @@ export class MissingMonitoringDataRule extends BaseRule { internalShortMessage, internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, state: AlertingDefaults.ALERT_STATE.firing, - /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 + /* continue to send "nodes" and "count" values for users before https://github.com/elastic/kibana/pull/102544 see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ nodes: `node: ${firingNode.nodeName}`, diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts index 90bd70f32c8cb..6645466f30c73 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts @@ -114,7 +114,8 @@ export class NodesChangedRule extends BaseRule { const nodesFromClusterStats = await fetchNodesFromClusterStats( esClient, clusters, - esIndexPattern + esIndexPattern, + params.filterQuery ); return nodesFromClusterStats.map((nodes) => { const { removed, added, restarted } = getNodeStates(nodes); diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts index c478b2f687c02..678f8b429167f 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts @@ -86,7 +86,8 @@ export class ThreadPoolRejectionsRuleBase extends BaseRule { esIndexPattern, Globals.app.config.ui.max_bucket_size, this.threadPoolType, - duration + duration, + params.filterQuery ); return stats.map((stat) => { @@ -257,7 +258,7 @@ export class ThreadPoolRejectionsRuleBase extends BaseRule { internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, threadPoolType: type, state: AlertingDefaults.ALERT_STATE.firing, - /* continue to send "count" value for users before https://github.com/elastic/kibana/pull/102544 + /* continue to send "count" value for users before https://github.com/elastic/kibana/pull/102544 see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ count: 1, diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.ts index 527ed503c8faf..0d3aab8283688 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.ts @@ -10,7 +10,7 @@ import { ElasticsearchClient } from 'src/core/server'; import { estypes } from '@elastic/elasticsearch'; import { MonitoringConfig } from '../../../config'; // @ts-ignore -import { prefixIndexPattern } from '../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../common/ccs_utils'; import { StackProductUsage } from '../types'; interface ESResponse { diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.ts index 7cce1b392112f..25a1892a9f38d 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.ts @@ -12,7 +12,7 @@ import { MonitoringConfig } from '../../../config'; // @ts-ignore import { getIndexPatterns } from '../../../lib/cluster/get_index_patterns'; // @ts-ignore -import { prefixIndexPattern } from '../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../common/ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH, INDEX_PATTERN_KIBANA, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts index 560751d1297d5..e7a5923207d60 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts @@ -14,7 +14,8 @@ export async function fetchCCRReadExceptions( index: string, startMs: number, endMs: number, - size: number + size: number, + filterQuery?: string ): Promise { const params = { index, @@ -93,6 +94,15 @@ export async function fetchCCRReadExceptions( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const stats: CCRReadExceptionsStats[] = []; // @ts-expect-error declare aggegations type explicitly diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts index 85bfbd9dbd049..b2004f0c7c710 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts @@ -11,7 +11,8 @@ import { ElasticsearchSource, ElasticsearchResponse } from '../../../common/type export async function fetchClusterHealth( esClient: ElasticsearchClient, clusters: AlertCluster[], - index: string + index: string, + filterQuery?: string ): Promise { const params = { index, @@ -59,6 +60,15 @@ export async function fetchClusterHealth( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const result = await esClient.search(params); const response: ElasticsearchResponse = result.body as ElasticsearchResponse; return (response.hits?.hits ?? []).map((hit) => { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts index 90cd456f18037..8f0083f1f533f 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts @@ -201,7 +201,9 @@ describe('fetchCpuUsageNodeStats', () => { {} as estypes.SearchResponse ); }); - await fetchCpuUsageNodeStats(esClient, clusters, index, startMs, endMs, size); + const filterQuery = + '{"bool":{"should":[{"exists":{"field":"cluster_uuid"}}],"minimum_should_match":1}}'; + await fetchCpuUsageNodeStats(esClient, clusters, index, startMs, endMs, size, filterQuery); expect(params).toStrictEqual({ index: '.monitoring-es-*', filter_path: ['aggregations'], @@ -213,6 +215,9 @@ describe('fetchCpuUsageNodeStats', () => { { terms: { cluster_uuid: ['abc123'] } }, { term: { type: 'node_stats' } }, { range: { timestamp: { format: 'epoch_millis', gte: 0, lte: 0 } } }, + { + bool: { should: [{ exists: { field: 'cluster_uuid' } }], minimum_should_match: 1 }, + }, ], }, }, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts index 6f7d27916a7b1..2ad42870e9958 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts @@ -29,7 +29,8 @@ export async function fetchCpuUsageNodeStats( index: string, startMs: number, endMs: number, - size: number + size: number, + filterQuery?: string ): Promise { // Using pure MS didn't seem to work well with the date_histogram interval // but minutes does @@ -140,6 +141,15 @@ export async function fetchCpuUsageNodeStats( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const stats: AlertCpuUsageNodeStats[] = []; const clusterBuckets = get( diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts index 70f05991d4229..2d4872c0bd895 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts @@ -14,7 +14,8 @@ export async function fetchDiskUsageNodeStats( clusters: AlertCluster[], index: string, duration: string, - size: number + size: number, + filterQuery?: string ): Promise { const clustersIds = clusters.map((cluster) => cluster.clusterUuid); const params = { @@ -99,6 +100,15 @@ export async function fetchDiskUsageNodeStats( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const stats: AlertDiskUsageNodeStats[] = []; // @ts-expect-error declare type for aggregations explicitly diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts index f2f311ac870a5..6ca2e89048df9 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts @@ -12,7 +12,8 @@ export async function fetchElasticsearchVersions( esClient: ElasticsearchClient, clusters: AlertCluster[], index: string, - size: number + size: number, + filterQuery?: string ): Promise { const params = { index, @@ -60,6 +61,15 @@ export async function fetchElasticsearchVersions( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const result = await esClient.search(params); const response: ElasticsearchResponse = result.body as ElasticsearchResponse; return (response.hits?.hits ?? []).map((hit) => { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts index 7e7ea5e6bfdd2..98bb546b43ab9 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts @@ -35,7 +35,8 @@ export async function fetchIndexShardSize( index: string, threshold: number, shardIndexPatterns: string, - size: number + size: number, + filterQuery?: string ): Promise { const params = { index, @@ -104,6 +105,15 @@ export async function fetchIndexShardSize( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.must.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); // @ts-expect-error declare aggegations type explicitly const { buckets: clusterBuckets } = response.aggregations?.clusters; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts index e57b45e2570fa..71813f3a526de 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts @@ -16,7 +16,8 @@ export async function fetchKibanaVersions( esClient: ElasticsearchClient, clusters: AlertCluster[], index: string, - size: number + size: number, + filterQuery?: string ): Promise { const params = { index, @@ -89,6 +90,15 @@ export async function fetchKibanaVersions( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const indexName = get(response, 'aggregations.index.buckets[0].key', ''); const clusterList = get(response, 'aggregations.cluster.buckets', []) as ESAggResponse[]; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts index 38ff82cf29832..b7bdf2fb6be72 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts @@ -11,7 +11,8 @@ import { ElasticsearchSource } from '../../../common/types/es'; export async function fetchLicenses( esClient: ElasticsearchClient, clusters: AlertCluster[], - index: string + index: string, + filterQuery?: string ): Promise { const params = { index, @@ -59,6 +60,15 @@ export async function fetchLicenses( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); return ( response?.hits?.hits.map((hit) => { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts index 774ee2551ec07..112c2fe798b10 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts @@ -16,7 +16,8 @@ export async function fetchLogstashVersions( esClient: ElasticsearchClient, clusters: AlertCluster[], index: string, - size: number + size: number, + filterQuery?: string ): Promise { const params = { index, @@ -89,6 +90,15 @@ export async function fetchLogstashVersions( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const indexName = get(response, 'aggregations.index.buckets[0].key', ''); const clusterList = get(response, 'aggregations.cluster.buckets', []) as ESAggResponse[]; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts index f34a8dcff1db7..9403ae5d79a70 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts @@ -15,7 +15,8 @@ export async function fetchMemoryUsageNodeStats( index: string, startMs: number, endMs: number, - size: number + size: number, + filterQuery?: string ): Promise { const clustersIds = clusters.map((cluster) => cluster.clusterUuid); const params = { @@ -92,6 +93,15 @@ export async function fetchMemoryUsageNodeStats( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const stats: AlertMemoryUsageNodeStats[] = []; // @ts-expect-error declare type for aggregations explicitly diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts index 856ca7c919885..cdf0f21b52b09 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts @@ -47,7 +47,8 @@ export async function fetchMissingMonitoringData( index: string, size: number, nowInMs: number, - startMs: number + startMs: number, + filterQuery?: string ): Promise { const endMs = nowInMs; const params = { @@ -117,6 +118,15 @@ export async function fetchMissingMonitoringData( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const clusterBuckets = get( response, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts index dcc8e6516c69b..3dc3e315318fc 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts @@ -26,7 +26,8 @@ function formatNode( export async function fetchNodesFromClusterStats( esClient: ElasticsearchClient, clusters: AlertCluster[], - index: string + index: string, + filterQuery?: string ): Promise { const params = { index, @@ -88,6 +89,15 @@ export async function fetchNodesFromClusterStats( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const nodes: AlertClusterStatsNodes[] = []; // @ts-expect-error declare type for aggregations explicitly diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts index 132f7692a7579..0d1d052b5f866 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts @@ -36,7 +36,8 @@ export async function fetchThreadPoolRejectionStats( index: string, size: number, threadType: string, - duration: string + duration: string, + filterQuery?: string ): Promise { const clustersIds = clusters.map((cluster) => cluster.clusterUuid); const params = { @@ -94,6 +95,15 @@ export async function fetchThreadPoolRejectionStats( }, }; + try { + if (filterQuery) { + const filterQueryObject = JSON.parse(filterQuery); + params.body.query.bool.filter.push(filterQueryObject); + } + } catch (e) { + // meh + } + const { body: response } = await esClient.search(params); const stats: AlertThreadPoolRejectionsStats[] = []; // @ts-expect-error declare type for aggregations explicitly diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts index 6eb21165d7256..a2201ca958e35 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_stats.ts @@ -12,7 +12,7 @@ import { createQuery } from '../create_query'; // @ts-ignore import { ElasticsearchMetric } from '../metrics'; // @ts-ignore -import { parseCrossClusterPrefix } from '../ccs_utils'; +import { parseCrossClusterPrefix } from '../../../common/ccs_utils'; import { getClustersState } from './get_clusters_state'; import { ElasticsearchResponse, ElasticsearchModifiedSource } from '../../../common/types/es'; import { LegacyRequest } from '../../types'; diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts index d908d6180772e..ccfe380edec09 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts @@ -6,7 +6,7 @@ */ import { LegacyServer } from '../../types'; -import { prefixIndexPattern } from '../ccs_utils'; +import { prefixIndexPattern } from '../../../common/ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH, INDEX_PATTERN_KIBANA, diff --git a/x-pack/plugins/monitoring/server/lib/logs/init_infra_source.ts b/x-pack/plugins/monitoring/server/lib/logs/init_infra_source.ts index c0fa931676870..727e47b62bc92 100644 --- a/x-pack/plugins/monitoring/server/lib/logs/init_infra_source.ts +++ b/x-pack/plugins/monitoring/server/lib/logs/init_infra_source.ts @@ -6,7 +6,7 @@ */ // @ts-ignore -import { prefixIndexPattern } from '../ccs_utils'; +import { prefixIndexPattern } from '../../../common/ccs_utils'; import { INFRA_SOURCE_ID } from '../../../common/constants'; import { MonitoringConfig } from '../../config'; import { InfraPluginSetup } from '../../../../infra/server'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.js b/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.js index 4884b8151f61f..a0b00167101fe 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { metricSet } from './metric_set_instance'; import { handleError } from '../../../../lib/errors'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/apm/instances.js b/x-pack/plugins/monitoring/server/routes/api/v1/apm/instances.js index 53afa4c3f01b4..95f378ff5b98d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/apm/instances.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/apm/instances.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getStats, getApms } from '../../../../lib/apm'; import { handleError } from '../../../../lib/errors'; import { INDEX_PATTERN_BEATS } from '../../../../../common/constants'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/apm/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/apm/overview.js index 7a772594b4bc2..ea7f3f41b842e 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/apm/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/apm/overview.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { metricSet } from './metric_set_overview'; import { handleError } from '../../../../lib/errors'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.js b/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.js index 919efe98f3df3..851380fede77d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getBeatSummary } from '../../../../lib/beats'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { handleError } from '../../../../lib/errors'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/beats/beats.js b/x-pack/plugins/monitoring/server/routes/api/v1/beats/beats.js index 57b24e59e66ab..fa35ccb9371c2 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/beats/beats.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/beats/beats.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getStats, getBeats } from '../../../../lib/beats'; import { handleError } from '../../../../lib/errors'; import { INDEX_PATTERN_BEATS } from '../../../../../common/constants'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.js index 5f1bb1778bc9a..4abf46b3ad1ce 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { getLatestStats, getStats } from '../../../../lib/beats'; import { handleError } from '../../../../lib/errors'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts index 73b646126ce98..898cfc82463d9 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr.ts @@ -11,7 +11,7 @@ import { get, groupBy } from 'lodash'; // @ts-ignore import { handleError } from '../../../../lib/errors/handle_error'; // @ts-ignore -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { ElasticsearchResponse, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts index 5ecb84d97618b..d07a660222407 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ccr_shard.ts @@ -10,7 +10,7 @@ import { schema } from '@kbn/config-schema'; // @ts-ignore import { handleError } from '../../../../lib/errors/handle_error'; // @ts-ignore -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; // @ts-ignore import { getMetrics } from '../../../../lib/details/get_metrics'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.js index 89ca911f44268..e99ae04ab282c 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.js @@ -12,7 +12,7 @@ import { getIndexSummary } from '../../../../lib/elasticsearch/indices'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { getShardAllocation, getShardStats } from '../../../../lib/elasticsearch/shards'; import { handleError } from '../../../../lib/errors/handle_error'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { metricSet } from './metric_set_index_detail'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getLogs } from '../../../../lib/logs/get_logs'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/indices.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/indices.js index 8099ecf3462cc..76e769ac030ba 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/indices.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/indices.js @@ -10,7 +10,7 @@ import { getClusterStats } from '../../../../lib/cluster/get_cluster_stats'; import { getClusterStatus } from '../../../../lib/cluster/get_cluster_status'; import { getIndices } from '../../../../lib/elasticsearch/indices'; import { handleError } from '../../../../lib/errors/handle_error'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getIndicesUnassignedShardStats } from '../../../../lib/elasticsearch/shards/get_indices_unassigned_shard_stats'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ml_jobs.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ml_jobs.js index e23c23f7a819d..5853cc3d6ee9d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ml_jobs.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/ml_jobs.js @@ -10,7 +10,7 @@ import { getClusterStats } from '../../../../lib/cluster/get_cluster_stats'; import { getClusterStatus } from '../../../../lib/cluster/get_cluster_status'; import { getMlJobs } from '../../../../lib/elasticsearch/get_ml_jobs'; import { handleError } from '../../../../lib/errors/handle_error'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getIndicesUnassignedShardStats } from '../../../../lib/elasticsearch/shards/get_indices_unassigned_shard_stats'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.js index 2122f8ceb2215..5f77d0394a4f1 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.js @@ -12,7 +12,7 @@ import { getNodeSummary } from '../../../../lib/elasticsearch/nodes'; import { getShardStats, getShardAllocation } from '../../../../lib/elasticsearch/shards'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { handleError } from '../../../../lib/errors/handle_error'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { metricSets } from './metric_set_node_detail'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getLogs } from '../../../../lib/logs/get_logs'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/nodes.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/nodes.js index db12e28916b65..7ea2e6e1e1440 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/nodes.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/nodes.js @@ -11,7 +11,7 @@ import { getClusterStatus } from '../../../../lib/cluster/get_cluster_status'; import { getNodes } from '../../../../lib/elasticsearch/nodes'; import { getNodesShardCount } from '../../../../lib/elasticsearch/shards/get_nodes_shard_count'; import { handleError } from '../../../../lib/errors/handle_error'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getPaginatedNodes } from '../../../../lib/elasticsearch/nodes/get_nodes/get_paginated_nodes'; import { LISTING_METRICS_NAMES } from '../../../../lib/elasticsearch/nodes/get_nodes/nodes_listing_metrics'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/overview.js index c76513df721ba..a0fc524768eb9 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/overview.js @@ -11,7 +11,7 @@ import { getClusterStatus } from '../../../../lib/cluster/get_cluster_status'; import { getLastRecovery } from '../../../../lib/elasticsearch/get_last_recovery'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { handleError } from '../../../../lib/errors/handle_error'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { metricSet } from './metric_set_overview'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getLogs } from '../../../../lib/logs'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts index d05d60866d119..3cd2b8b73b315 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts @@ -14,7 +14,7 @@ import { INDEX_PATTERN_LOGSTASH, } from '../../../../../../common/constants'; // @ts-ignore -import { prefixIndexPattern } from '../../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../../common/ccs_utils'; // @ts-ignore import { handleError } from '../../../../../lib/errors'; import { RouteDependencies, LegacyServer } from '../../../../../types'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instance.ts b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instance.ts index d16f568b475b4..613ca39275c2d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instance.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instance.ts @@ -12,7 +12,7 @@ import { handleError } from '../../../../lib/errors'; // @ts-ignore import { getMetrics } from '../../../../lib/details/get_metrics'; // @ts-ignore -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; // @ts-ignore import { metricSet } from './metric_set_instance'; import { INDEX_PATTERN_KIBANA } from '../../../../../common/constants'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instances.js b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instances.js index 59618f0a217b5..f9b3498cd684e 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instances.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/instances.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getKibanaClusterStatus } from './_get_kibana_cluster_status'; import { getKibanas } from '../../../../lib/kibana/get_kibanas'; import { handleError } from '../../../../lib/errors'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/overview.js index cca36d2aad1a7..f9a9443c3533b 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/kibana/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/kibana/overview.js @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { getKibanaClusterStatus } from './_get_kibana_cluster_status'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { metricSet } from './metric_set_overview'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/node.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/node.js index b81b4ea796c63..d3ecea95430ca 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/node.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/node.js @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { getNodeInfo } from '../../../../lib/logstash/get_node_info'; import { handleError } from '../../../../lib/errors'; import { getMetrics } from '../../../../lib/details/get_metrics'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { metricSets } from './metric_set_node'; import { INDEX_PATTERN_LOGSTASH } from '../../../../../common/constants'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/nodes.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/nodes.js index 74b89ab41be92..051fb7d38fd41 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/nodes.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/nodes.js @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { getClusterStatus } from '../../../../lib/logstash/get_cluster_status'; import { getNodes } from '../../../../lib/logstash/get_nodes'; import { handleError } from '../../../../lib/errors'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { INDEX_PATTERN_LOGSTASH } from '../../../../../common/constants'; /* diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/overview.js index 23dd64a1afb74..89a6a93fb207d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/overview.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/overview.js @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { getClusterStatus } from '../../../../lib/logstash/get_cluster_status'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { handleError } from '../../../../lib/errors'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { metricSet } from './metric_set_overview'; import { INDEX_PATTERN_LOGSTASH } from '../../../../../common/constants'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipeline.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipeline.js index 4243b2d6c3a5c..6b81059f0c256 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipeline.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipeline.js @@ -10,7 +10,7 @@ import { handleError } from '../../../../lib/errors'; import { getPipelineVersions } from '../../../../lib/logstash/get_pipeline_versions'; import { getPipeline } from '../../../../lib/logstash/get_pipeline'; import { getPipelineVertex } from '../../../../lib/logstash/get_pipeline_vertex'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; import { INDEX_PATTERN_LOGSTASH } from '../../../../../common/constants'; function getPipelineVersion(versions, pipelineHash) { diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipeline_ids.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipeline_ids.js index c881ff7b3d23c..7f14b74da207d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipeline_ids.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipeline_ids.js @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { handleError } from '../../../../../lib/errors'; -import { prefixIndexPattern } from '../../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../../common/ccs_utils'; import { INDEX_PATTERN_LOGSTASH } from '../../../../../../common/constants'; import { getLogstashPipelineIds } from '../../../../../lib/logstash/get_pipeline_ids'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipelines.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipelines.js index 1f7a5e1d436b1..b7d86e86e7a07 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipelines.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/cluster_pipelines.js @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { getClusterStatus } from '../../../../../lib/logstash/get_cluster_status'; import { handleError } from '../../../../../lib/errors'; -import { prefixIndexPattern } from '../../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../../common/ccs_utils'; import { INDEX_PATTERN_LOGSTASH } from '../../../../../../common/constants'; import { getPaginatedPipelines } from '../../../../../lib/logstash/get_paginated_pipelines'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/node_pipelines.js b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/node_pipelines.js index 47b8fd81a4d44..f31e88b5b8b08 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/node_pipelines.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/pipelines/node_pipelines.js @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { getNodeInfo } from '../../../../../lib/logstash/get_node_info'; import { handleError } from '../../../../../lib/errors'; -import { prefixIndexPattern } from '../../../../../lib/ccs_utils'; +import { prefixIndexPattern } from '../../../../../../common/ccs_utils'; import { INDEX_PATTERN_LOGSTASH } from '../../../../../../common/constants'; import { getPaginatedPipelines } from '../../../../../lib/logstash/get_paginated_pipelines'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a66873882245e..d36304ab06f25 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -17458,7 +17458,6 @@ "xpack.monitoring.alerts.kibanaVersionMismatch.label": "Kibana バージョン不一致", "xpack.monitoring.alerts.kibanaVersionMismatch.shortAction": "すべてのインスタンスのバージョンが同じことを確認してください。", "xpack.monitoring.alerts.kibanaVersionMismatch.ui.firingMessage": "このクラスターでは、複数のバージョンの Kibana({versions})が実行されています。", - "xpack.monitoring.alerts.legacyAlert.expressionText": "構成するものがありません。", "xpack.monitoring.alerts.licenseExpiration.action": "ライセンスを更新してください。", "xpack.monitoring.alerts.licenseExpiration.actionVariables.clusterName": "ライセンスが属しているクラスター。", "xpack.monitoring.alerts.licenseExpiration.actionVariables.expiredDate": "ライセンスの有効期限。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e529af35140be..ec935797fe534 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -17732,7 +17732,6 @@ "xpack.monitoring.alerts.kibanaVersionMismatch.label": "Kibana 版本不匹配", "xpack.monitoring.alerts.kibanaVersionMismatch.shortAction": "确认所有实例具有相同的版本。", "xpack.monitoring.alerts.kibanaVersionMismatch.ui.firingMessage": "在此集群中正运行着多个 Kibana 版本 ({versions})。", - "xpack.monitoring.alerts.legacyAlert.expressionText": "没有可配置的内容。", "xpack.monitoring.alerts.licenseExpiration.action": "请更新您的许可证。", "xpack.monitoring.alerts.licenseExpiration.actionVariables.clusterName": "许可证所属的集群。", "xpack.monitoring.alerts.licenseExpiration.actionVariables.expiredDate": "许可证过期日期。", From 791fed5b82d14797fed0a574da25a33e2bdca0d9 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Wed, 22 Sep 2021 13:36:17 -0700 Subject: [PATCH 28/39] [data.search] Do not send ignore_throttled when search:includeFrozen is disabled (#112755) * Do not send ignore_throttled when search:includeFrozen is disabled * Fix tests --- .../search/strategies/eql_search/eql_search_strategy.test.ts | 1 - .../server/search/strategies/ese_search/request_utils.test.ts | 4 ++-- .../data/server/search/strategies/ese_search/request_utils.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.test.ts b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.test.ts index 58a5e875f7c93..d32080928d630 100644 --- a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.test.ts +++ b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.test.ts @@ -132,7 +132,6 @@ describe('EQL search strategy', () => { expect(request).toEqual( expect.objectContaining({ ignore_unavailable: true, - ignore_throttled: true, }) ); }); diff --git a/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts b/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts index 272e41e8bf82d..91b323de7c07b 100644 --- a/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts +++ b/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts @@ -31,12 +31,12 @@ const getMockSearchSessionsConfig = ({ describe('request utils', () => { describe('getIgnoreThrottled', () => { - test('returns `ignore_throttled` as `true` when `includeFrozen` is `false`', async () => { + test('does not return `ignore_throttled` when `includeFrozen` is `false`', async () => { const mockUiSettingsClient = getMockUiSettingsClient({ [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false, }); const result = await getIgnoreThrottled(mockUiSettingsClient); - expect(result.ignore_throttled).toBe(true); + expect(result).not.toHaveProperty('ignore_throttled'); }); test('returns `ignore_throttled` as `false` when `includeFrozen` is `true`', async () => { diff --git a/src/plugins/data/server/search/strategies/ese_search/request_utils.ts b/src/plugins/data/server/search/strategies/ese_search/request_utils.ts index 8bf4473355ccf..e224215571ca9 100644 --- a/src/plugins/data/server/search/strategies/ese_search/request_utils.ts +++ b/src/plugins/data/server/search/strategies/ese_search/request_utils.ts @@ -23,7 +23,7 @@ export async function getIgnoreThrottled( uiSettingsClient: IUiSettingsClient ): Promise> { const includeFrozen = await uiSettingsClient.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); - return { ignore_throttled: !includeFrozen }; + return includeFrozen ? { ignore_throttled: false } : {}; } /** From f4c2a934f0e1b78268d80e81e8679082afcfed3f Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Wed, 22 Sep 2021 13:36:27 -0700 Subject: [PATCH 29/39] [Search sessions] Don't show incomplete warning if search requests aren't in session (#112364) * [Search sessions] Don't show incomplete warning if search requests aren't in session" * Update src/plugins/data/public/search/search_interceptor/search_interceptor.ts Co-authored-by: Anton Dosov * Fix lint Co-authored-by: Anton Dosov Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../search_interceptor.test.ts | 40 ++++++++++++++++++- .../search_interceptor/search_interceptor.ts | 10 ++++- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts index 155638250a2a4..7186938816d5f 100644 --- a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts @@ -501,12 +501,12 @@ describe('SearchInterceptor', () => { opts: { isRestore?: boolean; isStored?: boolean; - sessionId: string; + sessionId?: string; } | null ) => { const sessionServiceMock = sessionService as jest.Mocked; sessionServiceMock.getSearchOptions.mockImplementation(() => - opts + opts && opts.sessionId ? { sessionId: opts.sessionId, isRestore: opts.isRestore ?? false, @@ -515,6 +515,7 @@ describe('SearchInterceptor', () => { : null ); sessionServiceMock.isRestore.mockReturnValue(!!opts?.isRestore); + sessionServiceMock.getSessionId.mockImplementation(() => opts?.sessionId); fetchMock.mockResolvedValue({ result: 200 }); }; @@ -606,6 +607,41 @@ describe('SearchInterceptor', () => { expect(SearchSessionIncompleteWarning).toBeCalledTimes(0); }); + test('should not show warning if a search outside of session is running', async () => { + setup({ + isRestore: false, + isStored: false, + }); + + const responses = [ + { + time: 10, + value: { + isPartial: false, + isRunning: false, + isRestored: false, + id: 1, + rawResponse: { + took: 1, + }, + }, + }, + ]; + mockFetchImplementation(responses); + + const response = searchInterceptor.search( + {}, + { + sessionId: undefined, + } + ); + response.subscribe({ next, error, complete }); + + await timeTravel(10); + + expect(SearchSessionIncompleteWarning).toBeCalledTimes(0); + }); + test('should show warning once if a search is not available during restore', async () => { setup({ isRestore: true, diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts index ff3c173fd18cf..180e826b5bc4e 100644 --- a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts @@ -352,8 +352,14 @@ export class SearchInterceptor { ); }), tap((response) => { - if (this.deps.session.isRestore() && response.isRestored === false) { - this.showRestoreWarning(this.deps.session.getSessionId()); + const isSearchInScopeOfSession = + sessionId && sessionId === this.deps.session.getSessionId(); + if ( + isSearchInScopeOfSession && + this.deps.session.isRestore() && + response.isRestored === false + ) { + this.showRestoreWarning(sessionId); } }), finalize(() => { From 02b2a4d0860bcbc36c0bc7678b4f4f1598f57282 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Wed, 22 Sep 2021 13:36:39 -0700 Subject: [PATCH 30/39] [data.search.aggs] Use fields instead of _source in top_hits agg (#109531) * [data.search] Handle warnings inside of headers * Update docs * Add tests * Remove isWarningResponse * [data.search.aggs] Use fields instead of _source in top_hits agg Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../search/aggs/metrics/top_hit.test.ts | 24 +++++++++---------- .../common/search/aggs/metrics/top_hit.ts | 6 ++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/plugins/data/common/search/aggs/metrics/top_hit.test.ts b/src/plugins/data/common/search/aggs/metrics/top_hit.test.ts index 04d382f1aa6d1..37ce9c4edb8d1 100644 --- a/src/plugins/data/common/search/aggs/metrics/top_hit.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/top_hit.test.ts @@ -133,28 +133,28 @@ describe('Top hit metric', () => { }); it('should request the _source field', () => { - init({ field: '_source' }); - expect(aggDsl.top_hits._source).toBeTruthy(); - expect(aggDsl.top_hits.docvalue_fields).toBeUndefined(); + init({ fieldName: '_source' }); + expect(aggDsl.top_hits._source).toBe(true); + expect(aggDsl.top_hits.fields).toBeUndefined(); }); - it('requests both source and docvalues_fields for non-text aggregatable fields', () => { + it('requests fields for non-text aggregatable fields', () => { init({ fieldName: 'bytes', readFromDocValues: true }); - expect(aggDsl.top_hits._source).toBe('bytes'); - expect(aggDsl.top_hits.docvalue_fields).toEqual([{ field: 'bytes' }]); + expect(aggDsl.top_hits._source).toBe(false); + expect(aggDsl.top_hits.fields).toEqual([{ field: 'bytes' }]); }); - it('requests both source and docvalues_fields for date aggregatable fields', () => { + it('requests fields for date aggregatable fields', () => { init({ fieldName: '@timestamp', readFromDocValues: true, fieldType: KBN_FIELD_TYPES.DATE }); - expect(aggDsl.top_hits._source).toBe('@timestamp'); - expect(aggDsl.top_hits.docvalue_fields).toEqual([{ field: '@timestamp', format: 'date_time' }]); + expect(aggDsl.top_hits._source).toBe(false); + expect(aggDsl.top_hits.fields).toEqual([{ field: '@timestamp', format: 'date_time' }]); }); - it('requests just source for aggregatable text fields', () => { + it('requests fields for aggregatable text fields', () => { init({ fieldName: 'machine.os' }); - expect(aggDsl.top_hits._source).toBe('machine.os'); - expect(aggDsl.top_hits.docvalue_fields).toBeUndefined(); + expect(aggDsl.top_hits._source).toBe(false); + expect(aggDsl.top_hits.fields).toEqual([{ field: 'machine.os' }]); }); describe('try to get the value from the top hit', () => { diff --git a/src/plugins/data/common/search/aggs/metrics/top_hit.ts b/src/plugins/data/common/search/aggs/metrics/top_hit.ts index 094b5cda9a46d..a4bd99d6b210d 100644 --- a/src/plugins/data/common/search/aggs/metrics/top_hit.ts +++ b/src/plugins/data/common/search/aggs/metrics/top_hit.ts @@ -78,8 +78,8 @@ export const getTopHitMetricAgg = () => { }, }; } else { - if (field.readFromDocValues) { - output.params.docvalue_fields = [ + if (field.name !== '_source') { + output.params.fields = [ { field: field.name, // always format date fields as date_time to avoid @@ -89,7 +89,7 @@ export const getTopHitMetricAgg = () => { }, ]; } - output.params._source = field.name === '_source' ? true : field.name; + output.params._source = field.name === '_source'; } }, }, From 56f5c179e02c746e4635642eea1816625b03aa05 Mon Sep 17 00:00:00 2001 From: Kellen <9484709+goodroot@users.noreply.github.com> Date: Wed, 22 Sep 2021 14:38:20 -0700 Subject: [PATCH 31/39] Fix the other one... (#112873) --- dev_docs/key_concepts/persistable_state.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev_docs/key_concepts/persistable_state.mdx b/dev_docs/key_concepts/persistable_state.mdx index 36ad6f128d6e0..4368417170eed 100644 --- a/dev_docs/key_concepts/persistable_state.mdx +++ b/dev_docs/key_concepts/persistable_state.mdx @@ -11,7 +11,7 @@ tags: ['kibana','dev', 'contributor', 'api docs'] ## Exposing state that can be persisted -Any plugin that exposes state that another plugin might persist should implement interface on their `setup` contract. This will allow plugins persisting the state to easily access migrations and other utilities. +Any plugin that exposes state that another plugin might persist should implement interface on their `setup` contract. This will allow plugins persisting the state to easily access migrations and other utilities. Example: Data plugin allows you to generate filters. Those filters can be persisted by applications in their saved objects or in the URL. In order to allow apps to migrate the filters in case the structure changes in the future, the Data plugin implements `PersistableStateService` on . From 97e232f105304df69248951de0620e140969c463 Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Wed, 22 Sep 2021 17:14:35 -0700 Subject: [PATCH 32/39] test/functional/apps/management/_test_huge_fields.js (#112878) --- test/functional/apps/management/_test_huge_fields.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/apps/management/_test_huge_fields.js b/test/functional/apps/management/_test_huge_fields.js index c8710a79e4fc8..7b75683940928 100644 --- a/test/functional/apps/management/_test_huge_fields.js +++ b/test/functional/apps/management/_test_huge_fields.js @@ -14,7 +14,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'home', 'settings']); // FLAKY: https://github.com/elastic/kibana/issues/89031 - describe.skip('test large number of fields', function () { + describe('test large number of fields', function () { this.tags(['skipCloud']); const EXPECTED_FIELD_COUNT = '10006'; From 5b73ab1432ce8e86a4188d47707e25ef61d5ed2b Mon Sep 17 00:00:00 2001 From: Adam Locke Date: Wed, 22 Sep 2021 23:44:12 -0400 Subject: [PATCH 33/39] [DOCS] Update remote cluster and security links (#112874) * [DOCS] Update remote cluster and security links * Updating test link * Update URL for failing test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/core/public/doc_links/doc_links_service.ts | 8 ++++---- .../public/application/components/health_check.test.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 7b8b1b79572c9..e6f7974beac2f 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -213,9 +213,9 @@ export class DocLinksService { migrateIndexAllocationFilters: `${ELASTICSEARCH_DOCS}migrate-index-allocation-filters.html`, nodeRoles: `${ELASTICSEARCH_DOCS}modules-node.html#node-roles`, releaseHighlights: `${ELASTICSEARCH_DOCS}release-highlights.html`, - remoteClusters: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html`, - remoteClustersProxy: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#proxy-mode`, - remoteClusersProxySettings: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#remote-cluster-proxy-settings`, + remoteClusters: `${ELASTICSEARCH_DOCS}remote-clusters.html`, + remoteClustersProxy: `${ELASTICSEARCH_DOCS}remote-clusters.html#proxy-mode`, + remoteClusersProxySettings: `${ELASTICSEARCH_DOCS}remote-clusters-settings.html#remote-cluster-proxy-settings`, scriptParameters: `${ELASTICSEARCH_DOCS}modules-scripting-using.html#prefer-params`, setupUpgrade: `${ELASTICSEARCH_DOCS}setup-upgrade.html`, shardAllocationSettings: `${ELASTICSEARCH_DOCS}modules-cluster.html#cluster-shard-allocation-settings`, @@ -346,7 +346,7 @@ export class DocLinksService { elasticsearchSettings: `${ELASTICSEARCH_DOCS}security-settings.html`, elasticsearchEnableSecurity: `${ELASTICSEARCH_DOCS}configuring-stack-security.html`, indicesPrivileges: `${ELASTICSEARCH_DOCS}security-privileges.html#privileges-list-indices`, - kibanaTLS: `${KIBANA_DOCS}configuring-tls.html`, + kibanaTLS: `${ELASTICSEARCH_DOCS}security-basic-setup.html#encrypt-internode-communication`, kibanaPrivileges: `${KIBANA_DOCS}kibana-privileges.html`, mappingRoles: `${ELASTICSEARCH_DOCS}mapping-roles.html`, mappingRolesFieldRules: `${ELASTICSEARCH_DOCS}role-mapping-resources.html#mapping-roles-rule-field`, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx index b998067424edd..ff5992a6542b7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx @@ -114,7 +114,7 @@ describe('health check', () => { ); expect(action.getAttribute('href')).toMatchInlineSnapshot( - `"https://www.elastic.co/guide/en/kibana/mocked-test-branch/configuring-tls.html"` + `"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-basic-setup.html#encrypt-internode-communication"` ); }); From c06d6047855c8a92c10e41b3c944e8335475dc6f Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Thu, 23 Sep 2021 08:44:16 +0200 Subject: [PATCH 34/39] [Expressions] Fix setup and start contracts (#110841) * Refactor executor forking to implement state inheritance * Fix setup and start contracts typings * Add support of named forks * Add expressions service life-cycle assertions --- .../expressions/common/execution/execution.ts | 2 +- .../expressions/common/executor/executor.ts | 117 +++---- .../service/expressions_services.test.ts | 52 +++- .../common/service/expressions_services.ts | 288 +++++++++++------- src/plugins/expressions/public/loader.test.ts | 2 + src/plugins/expressions/public/mocks.tsx | 10 +- src/plugins/expressions/public/plugin.test.ts | 10 - src/plugins/expressions/server/mocks.ts | 39 +-- src/plugins/expressions/server/plugin.test.ts | 10 - x-pack/plugins/canvas/public/application.tsx | 5 +- x-pack/plugins/canvas/public/plugin.tsx | 3 +- .../canvas/public/services/expressions.ts | 4 +- .../public/services/kibana/expressions.ts | 2 +- .../saved_objects/workpad_references.ts | 7 +- 14 files changed, 320 insertions(+), 231 deletions(-) diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index 0c4185c82dc3c..0bb12951202a5 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -180,7 +180,7 @@ export class Execution< const ast = execution.ast || parseExpression(this.expression); this.state = createExecutionContainer({ - ...executor.state.get(), + ...executor.state, state: 'not-started', ast, }); diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index 55d3a7b897864..ce411ea94eafe 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -49,7 +49,7 @@ export class TypesRegistry implements IRegistry { } public get(id: string): ExpressionType | null { - return this.executor.state.selectors.getType(id); + return this.executor.getType(id) ?? null; } public toJS(): Record { @@ -71,7 +71,7 @@ export class FunctionsRegistry implements IRegistry { } public get(id: string): ExpressionFunction | null { - return this.executor.state.selectors.getFunction(id); + return this.executor.getFunction(id) ?? null; } public toJS(): Record { @@ -95,22 +95,44 @@ export class Executor = Record; + public readonly container: ExecutorContainer; /** * @deprecated */ - public readonly functions: FunctionsRegistry; + public readonly functions = new FunctionsRegistry(this); /** * @deprecated */ - public readonly types: TypesRegistry; + public readonly types = new TypesRegistry(this); + + protected parent?: Executor; constructor(state?: ExecutorState) { - this.state = createExecutorContainer(state); - this.functions = new FunctionsRegistry(this); - this.types = new TypesRegistry(this); + this.container = createExecutorContainer(state); + } + + public get state(): ExecutorState { + const parent = this.parent?.state; + const state = this.container.get(); + + return { + ...(parent ?? {}), + ...state, + types: { + ...(parent?.types ?? {}), + ...state.types, + }, + functions: { + ...(parent?.functions ?? {}), + ...state.functions, + }, + context: { + ...(parent?.context ?? {}), + ...state.context, + }, + }; } public registerFunction( @@ -119,15 +141,18 @@ export class Executor = Record { - return { ...this.state.get().functions }; + return { + ...(this.parent?.getFunctions() ?? {}), + ...this.container.get().functions, + }; } public registerType( @@ -136,23 +161,30 @@ export class Executor = Record { - return { ...this.state.get().types }; + return { + ...(this.parent?.getTypes() ?? {}), + ...this.container.get().types, + }; } public extendContext(extraContext: Record) { - this.state.transitions.extendContext(extraContext); + this.container.transitions.extendContext(extraContext); } public get context(): Record { - return this.state.selectors.getContext(); + return { + ...(this.parent?.context ?? {}), + ...this.container.selectors.getContext(), + }; } /** @@ -199,18 +231,15 @@ export class Executor = Record { - return asts.map((arg) => { - if (arg && typeof arg === 'object') { - return this.walkAst(arg, action); - } - return arg; - }); - }); + link.arguments = mapValues(fnArgs, (asts) => + asts.map((arg) => + arg != null && typeof arg === 'object' ? this.walkAst(arg, action) : arg + ) + ); action(fn, link); } @@ -275,39 +304,19 @@ export class Executor = Record { - if (!fn.migrations[version]) return link; - const updatedAst = fn.migrations[version](link) as ExpressionAstFunction; - link.arguments = updatedAst.arguments; - link.type = updatedAst.type; + if (!fn.migrations[version]) { + return; + } + + ({ arguments: link.arguments, type: link.type } = fn.migrations[version]( + link + ) as ExpressionAstFunction); }); } public fork(): Executor { - const initialState = this.state.get(); - const fork = new Executor(initialState); - - /** - * Synchronize registry state - make any new types, functions and context - * also available in the forked instance of `Executor`. - */ - this.state.state$.subscribe(({ types, functions, context }) => { - const state = fork.state.get(); - fork.state.set({ - ...state, - types: { - ...types, - ...state.types, - }, - functions: { - ...functions, - ...state.functions, - }, - context: { - ...context, - ...state.context, - }, - }); - }); + const fork = new Executor(); + fork.parent = this; return fork; } diff --git a/src/plugins/expressions/common/service/expressions_services.test.ts b/src/plugins/expressions/common/service/expressions_services.test.ts index db73d300e1273..620917dc64d4d 100644 --- a/src/plugins/expressions/common/service/expressions_services.test.ts +++ b/src/plugins/expressions/common/service/expressions_services.test.ts @@ -17,11 +17,16 @@ describe('ExpressionsService', () => { const expressions = new ExpressionsService(); expect(expressions.setup()).toMatchObject({ + getFunction: expect.any(Function), getFunctions: expect.any(Function), + getRenderer: expect.any(Function), + getRenderers: expect.any(Function), + getType: expect.any(Function), + getTypes: expect.any(Function), registerFunction: expect.any(Function), registerType: expect.any(Function), registerRenderer: expect.any(Function), - run: expect.any(Function), + fork: expect.any(Function), }); }); @@ -30,7 +35,16 @@ describe('ExpressionsService', () => { expressions.setup(); expect(expressions.start()).toMatchObject({ + getFunction: expect.any(Function), getFunctions: expect.any(Function), + getRenderer: expect.any(Function), + getRenderers: expect.any(Function), + getType: expect.any(Function), + getTypes: expect.any(Function), + registerFunction: expect.any(Function), + registerType: expect.any(Function), + registerRenderer: expect.any(Function), + execute: expect.any(Function), run: expect.any(Function), }); }); @@ -54,21 +68,21 @@ describe('ExpressionsService', () => { const service = new ExpressionsService(); const fork = service.fork(); - expect(fork.executor.state.get().types).toEqual(service.executor.state.get().types); + expect(fork.getTypes()).toEqual(service.getTypes()); }); test('fork keeps all functions of the origin service', () => { const service = new ExpressionsService(); const fork = service.fork(); - expect(fork.executor.state.get().functions).toEqual(service.executor.state.get().functions); + expect(fork.getFunctions()).toEqual(service.getFunctions()); }); test('fork keeps context of the origin service', () => { const service = new ExpressionsService(); const fork = service.fork(); - expect(fork.executor.state.get().context).toEqual(service.executor.state.get().context); + expect(fork.executor.state.context).toEqual(service.executor.state.context); }); test('newly registered functions in origin are also available in fork', () => { @@ -82,7 +96,7 @@ describe('ExpressionsService', () => { fn: () => {}, }); - expect(fork.executor.state.get().functions).toEqual(service.executor.state.get().functions); + expect(fork.getFunctions()).toEqual(service.getFunctions()); }); test('newly registered functions in fork are NOT available in origin', () => { @@ -96,14 +110,15 @@ describe('ExpressionsService', () => { fn: () => {}, }); - expect(Object.values(fork.executor.state.get().functions)).toHaveLength( - Object.values(service.executor.state.get().functions).length + 1 + expect(Object.values(fork.getFunctions())).toHaveLength( + Object.values(service.getFunctions()).length + 1 ); }); test('fork can execute an expression with newly registered function', async () => { const service = new ExpressionsService(); const fork = service.fork(); + fork.start(); service.registerFunction({ name: '__test__', @@ -118,5 +133,28 @@ describe('ExpressionsService', () => { expect(result).toBe('123'); }); + + test('throw on fork if the service is already started', async () => { + const service = new ExpressionsService(); + service.start(); + + expect(() => service.fork()).toThrow(); + }); + }); + + describe('.execute()', () => { + test('throw if the service is not started', () => { + const expressions = new ExpressionsService(); + + expect(() => expressions.execute('foo', null)).toThrow(); + }); + }); + + describe('.run()', () => { + test('throw if the service is not started', () => { + const expressions = new ExpressionsService(); + + expect(() => expressions.run('foo', null)).toThrow(); + }); }); }); diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index 2be4f5207bb82..f21eaa34d7868 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -41,22 +41,86 @@ import { * The public contract that `ExpressionsService` provides to other plugins * in Kibana Platform in *setup* life-cycle. */ -export type ExpressionsServiceSetup = Pick< - ExpressionsService, - | 'getFunction' - | 'getFunctions' - | 'getRenderer' - | 'getRenderers' - | 'getType' - | 'getTypes' - | 'registerFunction' - | 'registerRenderer' - | 'registerType' - | 'run' - | 'fork' - | 'extract' - | 'inject' ->; +export interface ExpressionsServiceSetup { + /** + * Get a registered `ExpressionFunction` by its name, which was registered + * using the `registerFunction` method. The returned `ExpressionFunction` + * instance is an internal representation of the function in Expressions + * service - do not mutate that object. + * @deprecated Use start contract instead. + */ + getFunction(name: string): ReturnType; + + /** + * Returns POJO map of all registered expression functions, where keys are + * names of the functions and values are `ExpressionFunction` instances. + * @deprecated Use start contract instead. + */ + getFunctions(): ReturnType; + + /** + * Returns POJO map of all registered expression types, where keys are + * names of the types and values are `ExpressionType` instances. + * @deprecated Use start contract instead. + */ + getTypes(): ReturnType; + + /** + * Create a new instance of `ExpressionsService`. The new instance inherits + * all state of the original `ExpressionsService`, including all expression + * types, expression functions and context. Also, all new types and functions + * registered in the original services AFTER the forking event will be + * available in the forked instance. However, all new types and functions + * registered in the forked instances will NOT be available to the original + * service. + * @param name A fork name that can be used to get fork instance later. + */ + fork(name?: string): ExpressionsService; + + /** + * Register an expression function, which will be possible to execute as + * part of the expression pipeline. + * + * Below we register a function which simply sleeps for given number of + * milliseconds to delay the execution and outputs its input as-is. + * + * ```ts + * expressions.registerFunction({ + * name: 'sleep', + * args: { + * time: { + * aliases: ['_'], + * help: 'Time in milliseconds for how long to sleep', + * types: ['number'], + * }, + * }, + * help: '', + * fn: async (input, args, context) => { + * await new Promise(r => setTimeout(r, args.time)); + * return input; + * }, + * } + * ``` + * + * The actual function is defined in the `fn` key. The function can be *async*. + * It receives three arguments: (1) `input` is the output of the previous function + * or the initial input of the expression if the function is first in chain; + * (2) `args` are function arguments as defined in expression string, that can + * be edited by user (e.g in case of Canvas); (3) `context` is a shared object + * passed to all functions that can be used for side-effects. + */ + registerFunction( + functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition) + ): void; + + registerType( + typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition) + ): void; + + registerRenderer( + definition: AnyExpressionRenderDefinition | (() => AnyExpressionRenderDefinition) + ): void; +} export interface ExpressionExecutionParams { searchContext?: SerializableRecord; @@ -97,7 +161,13 @@ export interface ExpressionsServiceStart { * instance is an internal representation of the function in Expressions * service - do not mutate that object. */ - getFunction: (name: string) => ReturnType; + getFunction(name: string): ReturnType; + + /** + * Returns POJO map of all registered expression functions, where keys are + * names of the functions and values are `ExpressionFunction` instances. + */ + getFunctions(): ReturnType; /** * Get a registered `ExpressionRenderer` by its name, which was registered @@ -105,7 +175,13 @@ export interface ExpressionsServiceStart { * instance is an internal representation of the renderer in Expressions * service - do not mutate that object. */ - getRenderer: (name: string) => ReturnType; + getRenderer(name: string): ReturnType; + + /** + * Returns POJO map of all registered expression renderers, where keys are + * names of the renderers and values are `ExpressionRenderer` instances. + */ + getRenderers(): ReturnType; /** * Get a registered `ExpressionType` by its name, which was registered @@ -113,7 +189,13 @@ export interface ExpressionsServiceStart { * instance is an internal representation of the type in Expressions * service - do not mutate that object. */ - getType: (name: string) => ReturnType; + getType(name: string): ReturnType; + + /** + * Returns POJO map of all registered expression types, where keys are + * names of the types and values are `ExpressionType` instances. + */ + getTypes(): ReturnType; /** * Executes expression string or a parsed expression AST and immediately @@ -139,34 +221,23 @@ export interface ExpressionsServiceStart { * expressions.run('...', null, { elasticsearchClient }); * ``` */ - run: ( + run( ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams - ) => Observable>; + ): Observable>; /** * Starts expression execution and immediately returns `ExecutionContract` * instance that tracks the progress of the execution and can be used to * interact with the execution. */ - execute: ( + execute( ast: string | ExpressionAstExpression, // This any is for legacy reasons. input: Input, params?: ExpressionExecutionParams - ) => ExecutionContract; - - /** - * Create a new instance of `ExpressionsService`. The new instance inherits - * all state of the original `ExpressionsService`, including all expression - * types, expression functions and context. Also, all new types and functions - * registered in the original services AFTER the forking event will be - * available in the forked instance. However, all new types and functions - * registered in the forked instances will NOT be available to the original - * service. - */ - fork: () => ExpressionsService; + ): ExecutionContract; } export interface ExpressionServiceParams { @@ -193,7 +264,19 @@ export interface ExpressionServiceParams { * * so that JSDoc appears in developers IDE when they use those `plugins.expressions.registerFunction(`. */ -export class ExpressionsService implements PersistableStateService { +export class ExpressionsService + implements + PersistableStateService, + ExpressionsServiceSetup, + ExpressionsServiceStart +{ + /** + * @note Workaround since the expressions service is frozen. + */ + private static started = new WeakSet(); + private children = new Map(); + private parent?: ExpressionsService; + public readonly executor: Executor; public readonly renderers: ExpressionRendererRegistry; @@ -205,94 +288,85 @@ export class ExpressionsService implements PersistableStateService { - * await new Promise(r => setTimeout(r, args.time)); - * return input; - * }, - * } - * ``` - * - * The actual function is defined in the `fn` key. The function can be *async*. - * It receives three arguments: (1) `input` is the output of the previous function - * or the initial input of the expression if the function is first in chain; - * (2) `args` are function arguments as defined in expression string, that can - * be edited by user (e.g in case of Canvas); (3) `context` is a shared object - * passed to all functions that can be used for side-effects. - */ - public readonly registerFunction = ( - functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition) - ): void => this.executor.registerFunction(functionDefinition); - - public readonly registerType = ( - typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition) - ): void => this.executor.registerType(typeDefinition); + private isStarted(): boolean { + return !!(ExpressionsService.started.has(this) || this.parent?.isStarted()); + } - public readonly registerRenderer = ( - definition: AnyExpressionRenderDefinition | (() => AnyExpressionRenderDefinition) - ): void => this.renderers.register(definition); + private assertSetup() { + if (this.isStarted()) { + throw new Error('The expression service is already started and can no longer be configured.'); + } + } - public readonly run: ExpressionsServiceStart['run'] = (ast, input, params) => - this.executor.run(ast, input, params); + private assertStart() { + if (!this.isStarted()) { + throw new Error('The expressions service has not started yet.'); + } + } public readonly getFunction: ExpressionsServiceStart['getFunction'] = (name) => this.executor.getFunction(name); - /** - * Returns POJO map of all registered expression functions, where keys are - * names of the functions and values are `ExpressionFunction` instances. - */ - public readonly getFunctions = (): ReturnType => + public readonly getFunctions: ExpressionsServiceStart['getFunctions'] = () => this.executor.getFunctions(); - public readonly getRenderer: ExpressionsServiceStart['getRenderer'] = (name) => - this.renderers.get(name); + public readonly getRenderer: ExpressionsServiceStart['getRenderer'] = (name) => { + this.assertStart(); - /** - * Returns POJO map of all registered expression renderers, where keys are - * names of the renderers and values are `ExpressionRenderer` instances. - */ - public readonly getRenderers = (): ReturnType => - this.renderers.toJS(); + return this.renderers.get(name); + }; - public readonly getType: ExpressionsServiceStart['getType'] = (name) => - this.executor.getType(name); + public readonly getRenderers: ExpressionsServiceStart['getRenderers'] = () => { + this.assertStart(); - /** - * Returns POJO map of all registered expression types, where keys are - * names of the types and values are `ExpressionType` instances. - */ - public readonly getTypes = (): ReturnType => this.executor.getTypes(); + return this.renderers.toJS(); + }; + + public readonly getType: ExpressionsServiceStart['getType'] = (name) => { + this.assertStart(); + + return this.executor.getType(name); + }; + + public readonly getTypes: ExpressionsServiceStart['getTypes'] = () => this.executor.getTypes(); + + public readonly registerFunction: ExpressionsServiceSetup['registerFunction'] = ( + functionDefinition + ) => this.executor.registerFunction(functionDefinition); + + public readonly registerType: ExpressionsServiceSetup['registerType'] = (typeDefinition) => + this.executor.registerType(typeDefinition); + + public readonly registerRenderer: ExpressionsServiceSetup['registerRenderer'] = (definition) => + this.renderers.register(definition); + + public readonly fork: ExpressionsServiceSetup['fork'] = (name) => { + this.assertSetup(); + + const executor = this.executor.fork(); + const renderers = this.renderers; + const fork = new (this.constructor as typeof ExpressionsService)({ executor, renderers }); + fork.parent = this; + + if (name) { + this.children.set(name, fork); + } + + return fork; + }; public readonly execute: ExpressionsServiceStart['execute'] = ((ast, input, params) => { + this.assertStart(); const execution = this.executor.createExecution(ast, params); execution.start(input); + return execution.contract; }) as ExpressionsServiceStart['execute']; - public readonly fork = () => { - const executor = this.executor.fork(); - const renderers = this.renderers; - const fork = new (this.constructor as typeof ExpressionsService)({ executor, renderers }); + public readonly run: ExpressionsServiceStart['run'] = (ast, input, params) => { + this.assertStart(); - return fork; + return this.executor.run(ast, input, params); }; /** @@ -371,8 +445,12 @@ export class ExpressionsService implements PersistableStateService { service.registerFunction(func); } + service.start(); + const moduleMock = { __execution: undefined, __getLastExecution: () => moduleMock.__execution, diff --git a/src/plugins/expressions/public/mocks.tsx b/src/plugins/expressions/public/mocks.tsx index 3a5450fc02837..f2f6a6807f339 100644 --- a/src/plugins/expressions/public/mocks.tsx +++ b/src/plugins/expressions/public/mocks.tsx @@ -16,19 +16,13 @@ export type Start = jest.Mocked; const createSetupContract = (): Setup => { const setupContract: Setup = { - extract: jest.fn(), fork: jest.fn(), getFunction: jest.fn(), getFunctions: jest.fn(), - getRenderer: jest.fn(), - getRenderers: jest.fn(), - getType: jest.fn(), getTypes: jest.fn(), - inject: jest.fn(), registerFunction: jest.fn(), registerRenderer: jest.fn(), registerType: jest.fn(), - run: jest.fn(), }; return setupContract; }; @@ -38,10 +32,12 @@ const createStartContract = (): Start => { execute: jest.fn(), ExpressionLoader: jest.fn(), ExpressionRenderHandler: jest.fn(), - fork: jest.fn(), getFunction: jest.fn(), + getFunctions: jest.fn(), getRenderer: jest.fn(), + getRenderers: jest.fn(), getType: jest.fn(), + getTypes: jest.fn(), loader: jest.fn(), ReactExpressionRenderer: jest.fn((props) => <>), render: jest.fn(), diff --git a/src/plugins/expressions/public/plugin.test.ts b/src/plugins/expressions/public/plugin.test.ts index 1963eb1f1b3f7..61ff0d8b54033 100644 --- a/src/plugins/expressions/public/plugin.test.ts +++ b/src/plugins/expressions/public/plugin.test.ts @@ -32,16 +32,6 @@ describe('ExpressionsPublicPlugin', () => { expect(setup.getFunctions().add.name).toBe('add'); }); }); - - describe('.run()', () => { - test('can execute simple expression', async () => { - const { setup } = await expressionsPluginMock.createPlugin(); - const { result } = await setup - .run('var_set name="foo" value="bar" | var name="foo"', null) - .toPromise(); - expect(result).toBe('bar'); - }); - }); }); describe('start contract', () => { diff --git a/src/plugins/expressions/server/mocks.ts b/src/plugins/expressions/server/mocks.ts index f4379145f6a6c..bf36ab3c5daa9 100644 --- a/src/plugins/expressions/server/mocks.ts +++ b/src/plugins/expressions/server/mocks.ts @@ -13,37 +13,24 @@ import { coreMock } from '../../../core/server/mocks'; export type Setup = jest.Mocked; export type Start = jest.Mocked; -const createSetupContract = (): Setup => { - const setupContract: Setup = { - extract: jest.fn(), - fork: jest.fn(), - getFunction: jest.fn(), - getFunctions: jest.fn(), - getRenderer: jest.fn(), - getRenderers: jest.fn(), - getType: jest.fn(), - getTypes: jest.fn(), - inject: jest.fn(), - registerFunction: jest.fn(), - registerRenderer: jest.fn(), - registerType: jest.fn(), - run: jest.fn(), - }; - return setupContract; -}; - -const createStartContract = (): Start => { - const startContract: Start = { +const createSetupContract = (): Setup => ({ + fork: jest.fn(), + getFunction: jest.fn(), + getFunctions: jest.fn(), + getTypes: jest.fn(), + registerFunction: jest.fn(), + registerRenderer: jest.fn(), + registerType: jest.fn(), +}); + +const createStartContract = (): Start => + ({ execute: jest.fn(), - fork: jest.fn(), getFunction: jest.fn(), getRenderer: jest.fn(), getType: jest.fn(), run: jest.fn(), - }; - - return startContract; -}; + } as unknown as Start); const createPlugin = async () => { const pluginInitializerContext = coreMock.createPluginInitializerContext(); diff --git a/src/plugins/expressions/server/plugin.test.ts b/src/plugins/expressions/server/plugin.test.ts index c41cda36e7623..52ecf1ff9979e 100644 --- a/src/plugins/expressions/server/plugin.test.ts +++ b/src/plugins/expressions/server/plugin.test.ts @@ -24,15 +24,5 @@ describe('ExpressionsServerPlugin', () => { expect(setup.getFunctions().add.name).toBe('add'); }); }); - - describe('.run()', () => { - test('can execute simple expression', async () => { - const { setup } = await expressionsPluginMock.createPlugin(); - const { result } = await setup - .run('var_set name="foo" value="bar" | var name="foo"', null) - .toPromise(); - expect(result).toBe('bar'); - }); - }); }); }); diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index 30b2d78a6b1fe..f2fe944bfd45d 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -98,11 +98,10 @@ export const initializeCanvas = async ( setupPlugins: CanvasSetupDeps, startPlugins: CanvasStartDeps, registries: SetupRegistries, - appUpdater: BehaviorSubject, - pluginServices: PluginServices + appUpdater: BehaviorSubject ) => { await startLegacyServices(coreSetup, coreStart, setupPlugins, startPlugins, appUpdater); - const { expressions } = pluginServices.getServices(); + const { expressions } = setupPlugins; // Adding these functions here instead of in plugin.ts. // Some of these functions have deep dependencies into Canvas, which was bulking up the size diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index 555cedb6b16a1..bd5d884f1485c 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -132,8 +132,7 @@ export class CanvasPlugin setupPlugins, startPlugins, registries, - this.appUpdater, - pluginServices + this.appUpdater ); const unmount = renderApp({ coreStart, startPlugins, params, canvasStore, pluginServices }); diff --git a/x-pack/plugins/canvas/public/services/expressions.ts b/x-pack/plugins/canvas/public/services/expressions.ts index a1af0fba50a5c..01bb0adb17711 100644 --- a/x-pack/plugins/canvas/public/services/expressions.ts +++ b/x-pack/plugins/canvas/public/services/expressions.ts @@ -5,6 +5,6 @@ * 2.0. */ -import { ExpressionsService } from '../../../../../src/plugins/expressions/public'; +import { ExpressionsServiceStart } from '../../../../../src/plugins/expressions/public'; -export type CanvasExpressionsService = ExpressionsService; +export type CanvasExpressionsService = ExpressionsServiceStart; diff --git a/x-pack/plugins/canvas/public/services/kibana/expressions.ts b/x-pack/plugins/canvas/public/services/kibana/expressions.ts index 4e3bb52a5d449..780de5309d97e 100644 --- a/x-pack/plugins/canvas/public/services/kibana/expressions.ts +++ b/x-pack/plugins/canvas/public/services/kibana/expressions.ts @@ -16,4 +16,4 @@ export type CanvasExpressionsServiceFactory = KibanaPluginServiceFactory< >; export const expressionsServiceFactory: CanvasExpressionsServiceFactory = ({ startPlugins }) => - startPlugins.expressions.fork(); + startPlugins.expressions; diff --git a/x-pack/plugins/canvas/server/saved_objects/workpad_references.ts b/x-pack/plugins/canvas/server/saved_objects/workpad_references.ts index b0d20add2f79a..e9eefa1bdb3f4 100644 --- a/x-pack/plugins/canvas/server/saved_objects/workpad_references.ts +++ b/x-pack/plugins/canvas/server/saved_objects/workpad_references.ts @@ -6,14 +6,15 @@ */ import { fromExpression, toExpression } from '@kbn/interpreter/common'; +import { PersistableStateService } from '../../../../../src/plugins/kibana_utils/common'; import { SavedObjectReference } from '../../../../../src/core/server'; import { WorkpadAttributes } from '../routes/workpad/workpad_attributes'; -import { ExpressionsServerSetup } from '../../../../../src/plugins/expressions/server'; +import type { ExpressionAstExpression } from '../../../../../src/plugins/expressions'; export const extractReferences = ( workpad: WorkpadAttributes, - expressions: ExpressionsServerSetup + expressions: PersistableStateService ): { workpad: WorkpadAttributes; references: SavedObjectReference[] } => { // We need to find every element in the workpad and extract references const references: SavedObjectReference[] = []; @@ -42,7 +43,7 @@ export const extractReferences = ( export const injectReferences = ( workpad: WorkpadAttributes, references: SavedObjectReference[], - expressions: ExpressionsServerSetup + expressions: PersistableStateService ) => { const pages = workpad.pages.map((page) => { const elements = page.elements.map((element) => { From 1803511ec439c5e4203c65e873ea0556a4b62f9a Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Thu, 23 Sep 2021 09:54:43 +0300 Subject: [PATCH 35/39] Updates the TSVB docs for v8 (#112778) * Updates the TSVB docs for v8 * Update docs/management/advanced-options.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/user/dashboard/tsvb.asciidoc Co-authored-by: Kaarina Tungseth Co-authored-by: Kaarina Tungseth Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/management/advanced-options.asciidoc | 3 +++ .../tsvb_index_pattern_selection_mode.png | Bin 130582 -> 153809 bytes docs/user/dashboard/tsvb.asciidoc | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 872fcbbf83385..6fd83e8af3d15 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -142,6 +142,9 @@ The default placeholder value to use in Fields that exist outside of `_source`. Kibana merges these fields into the document when displaying it. +[[metrics:allowStringIndices]]`metrics:allowStringIndices`:: +Enables you to use {es} indices in *TSVB* visualizations. + [[metrics-maxbuckets]]`metrics:max_buckets`:: Affects the *TSVB* histogram density. Must be set higher than `histogram:maxBars`. diff --git a/docs/user/dashboard/images/tsvb_index_pattern_selection_mode.png b/docs/user/dashboard/images/tsvb_index_pattern_selection_mode.png index ef72f291850e48f9f5c9e943f6ba8e9d4e70e867..4a790650ae0602693050cbb500d47ef7a5ab5530 100644 GIT binary patch literal 153809 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%0DAT)Z1=5QYjw4yFeqOUG}ux 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 P2Kc8Srz#7VF?#!d!GQ-w diff --git a/docs/user/dashboard/tsvb.asciidoc b/docs/user/dashboard/tsvb.asciidoc index 5eb818b0ea3e4..80138990c4f6e 100644 --- a/docs/user/dashboard/tsvb.asciidoc +++ b/docs/user/dashboard/tsvb.asciidoc @@ -53,8 +53,8 @@ When you use only {data-sources}, you are able to: * Improve performance -IMPORTANT: Creating *TSVB* visualizations with an {es} index string is deprecated and will be removed in a future release. -It is the default one for new visualizations but it can also be switched for the old implementations: +IMPORTANT: Creating *TSVB* visualizations with an {es} index string is deprecated. To use an {es} index string, contact your administrator, or go to <> and set `metrics:allowStringIndices` to `true`. Creating *TSVB* visualizations with an {es} index string will be removed in a future release. +Creating visualizations with only {data-sources} is the default one for new visualizations but it can also be switched for the old implementations: . Click *Panel options*, then open the *Index pattern selection mode* options next to the *Index pattern* dropdown. From 5c4f5195637bc98905c7ddc208ceb5e835f7226a Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Thu, 23 Sep 2021 09:55:18 +0300 Subject: [PATCH 36/39] Updates the VEGA docs for v8.0 (#112781) * Update VEGA docs for v8.0 * Update docs/user/dashboard/vega-reference.asciidoc Co-authored-by: Kaarina Tungseth Co-authored-by: Kaarina Tungseth Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/user/dashboard/vega-reference.asciidoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/user/dashboard/vega-reference.asciidoc b/docs/user/dashboard/vega-reference.asciidoc index 0881afd1003da..1277797417c9e 100644 --- a/docs/user/dashboard/vega-reference.asciidoc +++ b/docs/user/dashboard/vega-reference.asciidoc @@ -102,6 +102,8 @@ Tokens include the following: * `"%dashboard_context-filter_clause%"`: String replaced by an object containing filters * `"%dashboard_context-must_not_clause%"`: String replaced by an object containing filters +NOTE: Vega supports the `interval` parameter, which is unsupported {es} 8.0.0 and later. To use intervals, use `fixed_interval` or `calendar_interval` instead. + For example, the following query counts the number of documents in a specific index: [source,yaml] From fa624567d6ad44649973974b45e52d9eb32e5b70 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 23 Sep 2021 10:13:55 +0200 Subject: [PATCH 37/39] fix permissions for cloud test (#112568) --- test/functional/apps/visualize/_timelion.ts | 6 ++++++ test/functional/apps/visualize/_tsvb_time_series.ts | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/test/functional/apps/visualize/_timelion.ts b/test/functional/apps/visualize/_timelion.ts index 85dbf7cc5ca96..bb85b6821df31 100644 --- a/test/functional/apps/visualize/_timelion.ts +++ b/test/functional/apps/visualize/_timelion.ts @@ -18,6 +18,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'timelion', 'common', ]); + const security = getService('security'); const monacoEditor = getService('monacoEditor'); const kibanaServer = getService('kibanaServer'); const elasticChart = getService('elasticChart'); @@ -26,6 +27,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('Timelion visualization', () => { before(async () => { + await security.testUser.setRoles([ + 'kibana_admin', + 'long_window_logstash', + 'test_logstash_reader', + ]); await kibanaServer.uiSettings.update({ 'timelion:legacyChartsLibrary': false, }); diff --git a/test/functional/apps/visualize/_tsvb_time_series.ts b/test/functional/apps/visualize/_tsvb_time_series.ts index 09dc61e9f5f62..009e4a07cd42a 100644 --- a/test/functional/apps/visualize/_tsvb_time_series.ts +++ b/test/functional/apps/visualize/_tsvb_time_series.ts @@ -17,6 +17,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'timeToVisualize', 'dashboard', ]); + const security = getService('security'); const testSubjects = getService('testSubjects'); const retry = getService('retry'); const filterBar = getService('filterBar'); @@ -27,6 +28,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('visual builder', function describeIndexTests() { before(async () => { + await security.testUser.setRoles([ + 'kibana_admin', + 'long_window_logstash', + 'test_logstash_reader', + ]); await visualize.initTests(); }); beforeEach(async () => { From 9dcacf77f7155b867b211caf7067e6b6534bde37 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Thu, 23 Sep 2021 10:54:31 +0200 Subject: [PATCH 38/39] [Vega] Better error explanation for EsErrors and inspector now showing error responses (#112634) * :bug: Fix EsError not showing info * :bug: Record error response in the inspector * :label: Fix type issue * :ok_hand: Integrate feedback * :ok_hand: Integrated latest feedback * :ok_hand: i18n unknwon message * :ok_hand: Integrate feedback * :bug: Fix syntax error * :label: Fix type issue Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/search/errors/es_error.test.tsx | 27 +++++++++++++++++++ .../data/public/search/errors/es_error.tsx | 8 +++++- .../data/public/search/errors/utils.ts | 16 ++++++++--- .../vega/public/data_model/search_api.ts | 11 +++++++- 4 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/plugins/data/public/search/errors/es_error.test.tsx b/src/plugins/data/public/search/errors/es_error.test.tsx index fd1100ba34afc..4d1bc8b03b8f2 100644 --- a/src/plugins/data/public/search/errors/es_error.test.tsx +++ b/src/plugins/data/public/search/errors/es_error.test.tsx @@ -25,4 +25,31 @@ describe('EsError', () => { expect(typeof esError.attributes).toEqual('object'); expect(esError.attributes).toEqual(error.attributes); }); + + it('contains some explanation of the error in the message', () => { + // error taken from Vega's issue + const error = { + message: + 'x_content_parse_exception: [x_content_parse_exception] Reason: [1:78] [date_histogram] failed to parse field [calendar_interval]', + statusCode: 400, + attributes: { + root_cause: [ + { + type: 'x_content_parse_exception', + reason: '[1:78] [date_histogram] failed to parse field [calendar_interval]', + }, + ], + type: 'x_content_parse_exception', + reason: '[1:78] [date_histogram] failed to parse field [calendar_interval]', + caused_by: { + type: 'illegal_argument_exception', + reason: 'The supplied interval [2q] could not be parsed as a calendar interval.', + }, + }, + } as any; + const esError = new EsError(error); + expect(esError.message).toEqual( + 'EsError: The supplied interval [2q] could not be parsed as a calendar interval.' + ); + }); }); diff --git a/src/plugins/data/public/search/errors/es_error.tsx b/src/plugins/data/public/search/errors/es_error.tsx index 3303d48bf2adb..71c11af48830f 100644 --- a/src/plugins/data/public/search/errors/es_error.tsx +++ b/src/plugins/data/public/search/errors/es_error.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { EuiCodeBlock, EuiSpacer } from '@elastic/eui'; import { ApplicationStart } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; import { KbnError } from '../../../../kibana_utils/common'; import { IEsError } from './types'; import { getRootCause } from './utils'; @@ -17,7 +18,12 @@ export class EsError extends KbnError { readonly attributes: IEsError['attributes']; constructor(protected readonly err: IEsError) { - super('EsError'); + super( + `EsError: ${ + getRootCause(err)?.reason || + i18n.translate('data.esError.unknownRootCause', { defaultMessage: 'unknown' }) + }` + ); this.attributes = err.attributes; } diff --git a/src/plugins/data/public/search/errors/utils.ts b/src/plugins/data/public/search/errors/utils.ts index aba4e965d64c8..cb3e83dc8001c 100644 --- a/src/plugins/data/public/search/errors/utils.ts +++ b/src/plugins/data/public/search/errors/utils.ts @@ -5,8 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - -import { FailedShard } from './types'; +import type { ErrorCause } from '@elastic/elasticsearch/api/types'; +import type { FailedShard, Reason } from './types'; import { KibanaServerError } from '../../../../kibana_utils/common'; export function getFailedShards(err: KibanaServerError): FailedShard | undefined { @@ -15,6 +15,16 @@ export function getFailedShards(err: KibanaServerError): FailedShard | unde return failedShards ? failedShards[0] : undefined; } +function getNestedCause(err: KibanaServerError | ErrorCause): Reason { + const attr = ((err as KibanaServerError).attributes || err) as ErrorCause; + const { type, reason, caused_by: causedBy } = attr; + if (causedBy) { + return getNestedCause(causedBy); + } + return { type, reason }; +} + export function getRootCause(err: KibanaServerError) { - return getFailedShards(err)?.reason; + // Give shard failures priority, then try to get the error navigating nested objects + return getFailedShards(err)?.reason || getNestedCause(err); } diff --git a/src/plugins/vis_types/vega/public/data_model/search_api.ts b/src/plugins/vis_types/vega/public/data_model/search_api.ts index e00cf647930a8..11302ad65d56b 100644 --- a/src/plugins/vis_types/vega/public/data_model/search_api.ts +++ b/src/plugins/vis_types/vega/public/data_model/search_api.ts @@ -95,7 +95,16 @@ export class SearchAPI { } ) .pipe( - tap((data) => this.inspectSearchResult(data, requestResponders[requestId])), + tap( + (data) => this.inspectSearchResult(data, requestResponders[requestId]), + (err) => + this.inspectSearchResult( + { + rawResponse: err?.err, + }, + requestResponders[requestId] + ) + ), map((data) => ({ name: requestId, rawResponse: data.rawResponse, From 8cd6df6d959deb041d92907edab360f67599a596 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 23 Sep 2021 11:07:10 +0200 Subject: [PATCH 39/39] [ML] APM Correlations: Fix element key, fix to not pass on undefined (#112641) - Adds key attribute for AreaSeries element of TransactionDistributionChart. - Avoids passing on undefined as progress values. --- .../shared/charts/transaction_distribution_chart/index.tsx | 1 + x-pack/plugins/apm/public/hooks/use_search_strategy.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx index e8a159f23ee3d..535fb777166bb 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx @@ -256,6 +256,7 @@ export function TransactionDistributionChart({ /> {data.map((d, i) => (