diff --git a/examples/embeddable_examples/kibana.jsonc b/examples/embeddable_examples/kibana.jsonc index b3b63480e1103e..f0d4e829747f76 100644 --- a/examples/embeddable_examples/kibana.jsonc +++ b/examples/embeddable_examples/kibana.jsonc @@ -18,6 +18,6 @@ "developerExamples", "dataViewFieldEditor" ], - "requiredBundles": ["presentationUtil", "kibanaUtils", "kibanaReact"] + "requiredBundles": ["dashboard", "presentationUtil", "kibanaUtils", "kibanaReact"] } } diff --git a/examples/embeddable_examples/public/app/register_embeddable.tsx b/examples/embeddable_examples/public/app/register_embeddable.tsx index e9116a611d0ed1..95bac13ee0023b 100644 --- a/examples/embeddable_examples/public/app/register_embeddable.tsx +++ b/examples/embeddable_examples/public/app/register_embeddable.tsx @@ -12,11 +12,16 @@ import { EuiCodeBlock, EuiSpacer, EuiText } from '@elastic/eui'; import registerSearchEmbeddableSource from '!!raw-loader!../react_embeddables/search/register_search_embeddable'; // @ts-ignore import registerAttachActionSource from '!!raw-loader!../react_embeddables/search/register_add_search_panel_action'; +// @ts-ignore +import registerFieldListEmbeddableSource from '!!raw-loader!../react_embeddables/field_list/register_field_list_embeddable'; +// @ts-ignore +import registerReactEmbeddableSavedObjectSource from '!!raw-loader!../react_embeddables/register_saved_object_example'; export const RegisterEmbeddable = () => { return ( <> +

Register a new embeddable type

This plugin registers several embeddable types with{' '} registerReactEmbeddableFactory during plugin start. The code example @@ -24,7 +29,7 @@ export const RegisterEmbeddable = () => { asynchronously to limit initial page load size.

- + {registerSearchEmbeddableSource} @@ -36,6 +41,12 @@ export const RegisterEmbeddable = () => { Run the example embeddables by creating a dashboard, clicking Add panel button, and then selecting Embeddable examples group.

+ + + + + +

Show embeddables in the Add panel menu

Add your own embeddables to Add panel menu by attaching an action to the{' '} ADD_PANEL_TRIGGER trigger. Notice usage of grouping to @@ -43,10 +54,45 @@ export const RegisterEmbeddable = () => { @elastic/kibana-presentation team to coordinate menu updates.

- + {registerAttachActionSource} + + + + +

Configure initial dashboard placement (optional)

+

+ Add an entry to registerDashboardPanelPlacementSetting to configure + initial dashboard placement. Panel placement lets you configure the width, height, and + placement strategy when panels get added to a dashboard. In the example below, the Field + List embeddable will be added to dashboards as a narrow and tall panel. +

+
+ + + + {registerFieldListEmbeddableSource} + + + + + +

Saved object embeddables

+

+ Embeddable factories, such as Lens, Maps, Links, that can reference saved objects should + register their saved object types using{' '} + registerReactEmbeddableSavedObject. The Add from library flyout + on Dashboards uses this registry to list saved objects. The example function below could + be called from the public start contract for a plugin. +

+
+ + + + {registerReactEmbeddableSavedObjectSource} + ); }; diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts index e588919591724f..6939271e373413 100644 --- a/examples/embeddable_examples/public/plugin.ts +++ b/examples/embeddable_examples/public/plugin.ts @@ -24,8 +24,9 @@ import { DATA_TABLE_ID } from './react_embeddables/data_table/constants'; import { registerCreateDataTableAction } from './react_embeddables/data_table/create_data_table_action'; import { EUI_MARKDOWN_ID } from './react_embeddables/eui_markdown/constants'; import { registerCreateEuiMarkdownAction } from './react_embeddables/eui_markdown/create_eui_markdown_action'; -import { FIELD_LIST_ID } from './react_embeddables/field_list/constants'; import { registerCreateFieldListAction } from './react_embeddables/field_list/create_field_list_action'; +import { FIELD_LIST_ID } from './react_embeddables/field_list/constants'; +import { registerFieldListPanelPlacementSetting } from './react_embeddables/field_list/register_field_list_embeddable'; import { registerAddSearchPanelAction } from './react_embeddables/search/register_add_search_panel_action'; import { registerSearchEmbeddable } from './react_embeddables/search/register_search_embeddable'; @@ -58,6 +59,7 @@ export class EmbeddableExamplesPlugin implements Plugin { diff --git a/examples/embeddable_examples/public/react_embeddables/field_list/field_list_react_embeddable.tsx b/examples/embeddable_examples/public/react_embeddables/field_list/field_list_react_embeddable.tsx index 0c7d3d127efb93..2931760310c2b4 100644 --- a/examples/embeddable_examples/public/react_embeddables/field_list/field_list_react_embeddable.tsx +++ b/examples/embeddable_examples/public/react_embeddables/field_list/field_list_react_embeddable.tsx @@ -8,17 +8,11 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { css } from '@emotion/react'; -import { ChartsPluginStart } from '@kbn/charts-plugin/public'; import { Reference } from '@kbn/content-management-utils'; import { CoreStart } from '@kbn/core-lifecycle-browser'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DataView } from '@kbn/data-views-plugin/common'; -import { - DataViewsPublicPluginStart, - DATA_VIEW_SAVED_OBJECT_TYPE, -} from '@kbn/data-views-plugin/public'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/public'; import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public'; -import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { i18n } from '@kbn/i18n'; import { initializeTitles, useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; import { LazyDataViewPicker, withSuspense } from '@kbn/presentation-util-plugin/public'; @@ -31,7 +25,7 @@ import { cloneDeep } from 'lodash'; import React, { useEffect } from 'react'; import { BehaviorSubject, skip, Subscription, switchMap } from 'rxjs'; import { FIELD_LIST_DATA_VIEW_REF_NAME, FIELD_LIST_ID } from './constants'; -import { FieldListApi, FieldListSerializedStateState } from './types'; +import { FieldListApi, Services, FieldListSerializedStateState } from './types'; const DataViewPicker = withSuspense(LazyDataViewPicker, null); @@ -48,17 +42,7 @@ const getCreationOptions: UnifiedFieldListSidebarContainerProps['getCreationOpti export const getFieldListFactory = ( core: CoreStart, - { - dataViews, - data, - charts, - fieldFormats, - }: { - dataViews: DataViewsPublicPluginStart; - data: DataPublicPluginStart; - charts: ChartsPluginStart; - fieldFormats: FieldFormatsStart; - } + { dataViews, data, charts, fieldFormats }: Services ) => { const fieldListEmbeddableFactory: ReactEmbeddableFactory< FieldListSerializedStateState, diff --git a/examples/embeddable_examples/public/react_embeddables/field_list/register_field_list_embeddable.ts b/examples/embeddable_examples/public/react_embeddables/field_list/register_field_list_embeddable.ts new file mode 100644 index 00000000000000..e31df63b62f378 --- /dev/null +++ b/examples/embeddable_examples/public/react_embeddables/field_list/register_field_list_embeddable.ts @@ -0,0 +1,27 @@ +/* + * 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 { + registerDashboardPanelPlacementSetting, + PanelPlacementStrategy, +} from '@kbn/dashboard-plugin/public'; +import { FIELD_LIST_ID } from './constants'; +import { FieldListSerializedStateState } from './types'; + +const getPanelPlacementSetting = (serializedState?: FieldListSerializedStateState) => { + // Consider using the serialized state to determine the width, height, and strategy + return { + width: 12, + height: 36, + strategy: PanelPlacementStrategy.placeAtTop, + }; +}; + +export function registerFieldListPanelPlacementSetting() { + registerDashboardPanelPlacementSetting(FIELD_LIST_ID, getPanelPlacementSetting); +} diff --git a/examples/embeddable_examples/public/react_embeddables/field_list/types.ts b/examples/embeddable_examples/public/react_embeddables/field_list/types.ts index 0dd6651c577069..0da67bcd0f70b2 100644 --- a/examples/embeddable_examples/public/react_embeddables/field_list/types.ts +++ b/examples/embeddable_examples/public/react_embeddables/field_list/types.ts @@ -6,7 +6,11 @@ * Side Public License, v 1. */ +import { ChartsPluginStart } from '@kbn/charts-plugin/public'; +import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public'; +import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { PublishesDataViews, SerializedTitles } from '@kbn/presentation-publishing'; import { PublishesSelectedFields } from './publishes_selected_fields'; @@ -16,3 +20,10 @@ export type FieldListSerializedStateState = SerializedTitles & { }; export type FieldListApi = DefaultEmbeddableApi & PublishesSelectedFields & PublishesDataViews; + +export interface Services { + dataViews: DataViewsPublicPluginStart; + data: DataPublicPluginStart; + charts: ChartsPluginStart; + fieldFormats: FieldFormatsStart; +} diff --git a/examples/embeddable_examples/public/react_embeddables/register_saved_object_example.ts b/examples/embeddable_examples/public/react_embeddables/register_saved_object_example.ts new file mode 100644 index 00000000000000..d2263240519933 --- /dev/null +++ b/examples/embeddable_examples/public/react_embeddables/register_saved_object_example.ts @@ -0,0 +1,27 @@ +/* + * 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 { registerReactEmbeddableSavedObject } from '@kbn/embeddable-plugin/public'; + +const MY_EMBEDDABLE_TYPE = 'myEmbeddableType'; +const MY_SAVED_OBJECT_TYPE = 'mySavedObjectType'; +const APP_ICON = 'logoKibana'; + +export const registerMyEmbeddableSavedObject = () => + registerReactEmbeddableSavedObject({ + onAdd: (container, savedObject) => { + container.addNewPanel({ + panelType: MY_EMBEDDABLE_TYPE, + initialState: savedObject.attributes, + }); + }, + embeddableType: MY_EMBEDDABLE_TYPE, + savedObjectType: MY_SAVED_OBJECT_TYPE, + savedObjectName: 'Some saved object', + getIconForSavedObject: () => APP_ICON, + }); diff --git a/examples/embeddable_examples/tsconfig.json b/examples/embeddable_examples/tsconfig.json index 30ac39afe90b75..b356083a20546d 100644 --- a/examples/embeddable_examples/tsconfig.json +++ b/examples/embeddable_examples/tsconfig.json @@ -15,6 +15,7 @@ "kbn_references": [ "@kbn/core", "@kbn/ui-actions-plugin", + "@kbn/dashboard-plugin", "@kbn/embeddable-plugin", "@kbn/presentation-publishing", "@kbn/ui-theme", diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 017d5c6b233054..689f8a5e9859e1 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -480,6 +480,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D privileges: `${SECURITY_SOLUTION_DOCS}endpoint-management-req.html`, manageDetectionRules: `${SECURITY_SOLUTION_DOCS}rules-ui-management.html`, createEsqlRuleType: `${SECURITY_SOLUTION_DOCS}rules-ui-create.html#create-esql-rule`, + ruleUiAdvancedParams: `${SECURITY_SOLUTION_DOCS}rules-ui-create.html#rule-ui-advanced-params`, entityAnalytics: { riskScorePrerequisites: `${SECURITY_SOLUTION_DOCS}ers-requirements.html`, hostRiskScore: `${SECURITY_SOLUTION_DOCS}host-risk-score.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index bd8f353c1c5915..261165dcd7ec97 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -355,6 +355,7 @@ export interface DocLinks { readonly privileges: string; readonly manageDetectionRules: string; readonly createEsqlRuleType: string; + readonly ruleUiAdvancedParams: string; readonly entityAnalytics: { readonly riskScorePrerequisites: string; readonly hostRiskScore: string; diff --git a/packages/kbn-eslint-config/typescript.js b/packages/kbn-eslint-config/typescript.js index d62314c97d547c..d1503a196439b3 100644 --- a/packages/kbn-eslint-config/typescript.js +++ b/packages/kbn-eslint-config/typescript.js @@ -16,12 +16,7 @@ module.exports = { files: ['**/*.{ts,tsx}'], parser: '@typescript-eslint/parser', - plugins: [ - '@typescript-eslint', - 'ban', - 'import', - 'eslint-comments' - ], + plugins: ['@typescript-eslint', 'ban', 'import', 'eslint-comments'], env: { es6: true, @@ -34,7 +29,7 @@ module.exports = { sourceType: 'module', ecmaVersion: 2018, ecmaFeatures: { - jsx: true + jsx: true, }, // NOTE: That is to avoid a known performance issue related with the `ts.Program` used by // typescript eslint. As we are not using rules that need types information, we can safely @@ -43,7 +38,7 @@ module.exports = { // https://github.com/typescript-eslint/typescript-eslint/issues/389 // https://github.com/typescript-eslint/typescript-eslint/issues/243 // https://github.com/typescript-eslint/typescript-eslint/pull/361 - project: undefined + project: undefined, }, // NOTE: we can't override the extends option here to apply @@ -60,32 +55,38 @@ module.exports = { // // Old recommended tslint rules '@typescript-eslint/adjacent-overload-signatures': 'error', - '@typescript-eslint/array-type': ['error', { default: 'array-simple', readonly: 'array-simple' }], - '@typescript-eslint/ban-types': ['error', { - types: { - SFC: { - message: 'Use FC or FunctionComponent instead.', - fixWith: 'FC' - }, - 'React.SFC': { - message: 'Use FC or FunctionComponent instead.', - fixWith: 'React.FC' - }, - StatelessComponent: { - message: 'Use FunctionComponent instead.', - fixWith: 'FunctionComponent' - }, - 'React.StatelessComponent': { - message: 'Use FunctionComponent instead.', - fixWith: 'React.FunctionComponent' + '@typescript-eslint/array-type': [ + 'error', + { default: 'array-simple', readonly: 'array-simple' }, + ], + '@typescript-eslint/ban-types': [ + 'error', + { + types: { + SFC: { + message: 'Use FC or FunctionComponent instead.', + fixWith: 'FC', + }, + 'React.SFC': { + message: 'Use FC or FunctionComponent instead.', + fixWith: 'React.FC', + }, + StatelessComponent: { + message: 'Use FunctionComponent instead.', + fixWith: 'FunctionComponent', + }, + 'React.StatelessComponent': { + message: 'Use FunctionComponent instead.', + fixWith: 'React.FunctionComponent', + }, + // used in the codebase in the wild + '{}': false, + object: false, + Function: false, }, - // used in the codebase in the wild - '{}': false, - 'object': false, - 'Function': false, - } - }], - 'camelcase': 'off', + }, + ], + camelcase: 'off', '@typescript-eslint/naming-convention': [ 'error', { @@ -93,8 +94,8 @@ module.exports = { format: ['camelCase'], filter: { regex: allowedNameRegexp, - match: false - } + match: false, + }, }, { selector: 'variable', @@ -105,19 +106,16 @@ module.exports = { ], filter: { regex: allowedNameRegexp, - match: false - } + match: false, + }, }, { selector: 'parameter', - format: [ - 'camelCase', - 'PascalCase', - ], + format: ['camelCase', 'PascalCase'], filter: { regex: allowedNameRegexp, - match: false - } + match: false, + }, }, { selector: 'memberLike', @@ -125,23 +123,23 @@ module.exports = { 'camelCase', 'PascalCase', 'snake_case', // keys in elasticsearch requests / responses - 'UPPER_CASE' + 'UPPER_CASE', ], filter: { regex: allowedNameRegexp, - match: false - } + match: false, + }, }, { selector: 'function', format: [ 'camelCase', - 'PascalCase' // React.FunctionComponent = + 'PascalCase', // React.FunctionComponent = ], filter: { regex: allowedNameRegexp, - match: false - } + match: false, + }, }, { selector: 'typeLike', @@ -164,27 +162,31 @@ module.exports = { 'objectLiteralMethod', 'typeMethod', 'accessor', - 'enumMember' + 'enumMember', ], format: null, - modifiers: ['requiresQuotes'] - } + modifiers: ['requiresQuotes'], + }, ], - '@typescript-eslint/explicit-member-accessibility': ['error', + '@typescript-eslint/explicit-member-accessibility': [ + 'error', { accessibility: 'off', overrides: { accessors: 'explicit', constructors: 'no-public', - parameterProperties: 'explicit' - } - } + parameterProperties: 'explicit', + }, + }, ], '@typescript-eslint/prefer-function-type': 'error', '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], - '@typescript-eslint/member-ordering': ['error', { - 'default': ['public-static-field', 'static-field', 'instance-field'] - }], + '@typescript-eslint/member-ordering': [ + 'error', + { + default: ['public-static-field', 'static-field', 'instance-field'], + }, + ], '@typescript-eslint/consistent-type-assertions': 'error', '@typescript-eslint/no-empty-interface': 'error', '@typescript-eslint/no-extra-non-null-assertion': 'error', @@ -195,24 +197,26 @@ module.exports = { '@typescript-eslint/no-undef': 'off', 'no-undef': 'off', - '@typescript-eslint/triple-slash-reference': ['error', { - path: 'never', - types: 'never', - lib: 'never' - }], + '@typescript-eslint/triple-slash-reference': [ + 'error', + { + path: 'never', + types: 'never', + lib: 'never', + }, + ], '@typescript-eslint/no-var-requires': 'error', '@typescript-eslint/unified-signatures': 'error', 'constructor-super': 'error', 'dot-notation': 'error', - 'eqeqeq': ['error', 'always', {'null': 'ignore'}], + eqeqeq: ['error', 'always', { null: 'ignore' }], 'guard-for-in': 'error', - 'import/order': ['error', { - 'groups': [ - ['external', 'builtin'], - 'internal', - ['parent', 'sibling', 'index'], - ], - }], + 'import/order': [ + 'error', + { + groups: [['external', 'builtin'], 'internal', ['parent', 'sibling', 'index']], + }, + ], 'max-classes-per-file': ['error', 1], 'no-bitwise': 'error', 'no-caller': 'error', @@ -233,22 +237,27 @@ module.exports = { 'no-unused-labels': 'error', 'no-var': 'error', 'object-shorthand': 'error', - 'one-var': [ 'error', 'never' ], + 'one-var': ['error', 'never'], 'prefer-const': 'error', 'prefer-rest-params': 'error', - 'radix': 'error', - 'spaced-comment': ["error", "always", { - "exceptions": ["/"] - }], + radix: 'error', + 'spaced-comment': [ + 'error', + 'always', + { + exceptions: ['/'], + }, + ], 'use-isnan': 'error', // Old tslint yml override or defined rules 'ban/ban': [ 2, - {'name': ['describe', 'only'], 'message': 'No exclusive suites.'}, - {'name': ['it', 'only'], 'message': 'No exclusive tests.'}, - {'name': ['test', 'only'], 'message': 'No exclusive tests.'}, - + { name: ['describe', 'only'], message: 'No exclusive suites.' }, + { name: ['it', 'only'], message: 'No exclusive tests.' }, + { name: ['test', 'only'], message: 'No exclusive tests.' }, + { name: ['testSuggestions', 'only'], message: 'No exclusive tests.' }, + { name: ['testErrorsAndWarnings', 'only'], message: 'No exclusive tests.' }, ], 'import/no-default-export': 'error', @@ -257,13 +266,13 @@ module.exports = { 'no-restricted-syntax': [ 'error', { - "selector": "TSEnumDeclaration[const=true]", - "message": "Do not use `const` with enum declarations" - } - ] + selector: 'TSEnumDeclaration[const=true]', + message: 'Do not use `const` with enum declarations', + }, + ], }, eslintConfigPrettierRules - ) + ), }, - ] + ], }; diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts index 4be855868d39b9..1f4353b9df836c 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts @@ -12,10 +12,9 @@ import { builtinFunctions } from '../definitions/builtin'; import { statsAggregationFunctionDefinitions } from '../definitions/aggs'; import { chronoLiterals, timeLiterals } from '../definitions/literals'; import { commandDefinitions } from '../definitions/commands'; -import { TRIGGER_SUGGESTION_COMMAND } from './factories'; +import { getUnitDuration, TRIGGER_SUGGESTION_COMMAND } from './factories'; import { camelCase } from 'lodash'; import { getAstAndSyntaxErrors } from '@kbn/esql-ast'; -import { SuggestionRawDefinition } from './types'; import { groupingFunctionDefinitions } from '../definitions/grouping'; const triggerCharacters = [',', '(', '=', ' ']; @@ -231,14 +230,14 @@ function getPolicyFields(policyName: string) { describe('autocomplete', () => { type TestArgs = [ string, - Array>, + string[], (string | number)?, Parameters? ]; const testSuggestionsFn = ( statement: string, - expected: Array>, + expected: string[], triggerCharacter: string | number = '', customCallbacksArgs: Parameters = [ undefined, @@ -271,28 +270,20 @@ describe('autocomplete', () => { async (text) => (text ? getAstAndSyntaxErrors(text) : { ast: [], errors: [] }), callbackMocks ); - const suggestionInertTextSorted = suggestions - // simulate the editor behaviour for sorting suggestions - // copied from https://github.com/microsoft/vscode/blob/0a141d23179c76c5771df25a43546d9d9b6ed71c/src/vs/workbench/contrib/testing/browser/testingDecorations.ts#L971-L972 - // still not sure how accurate this is... - .sort((a, b) => (a.sortText || a.label).localeCompare(b.sortText || b.label)); - expect(suggestionInertTextSorted).toHaveLength(expected.length); - for (const [index, receivedSuggestion] of suggestionInertTextSorted.entries()) { - if (typeof expected[index] !== 'object') { - expect(receivedSuggestion.text).toEqual(expected[index]); - } else { - // check all properties that are defined in the expected suggestion - for (const [key, value] of Object.entries(expected[index])) { - expect(receivedSuggestion[key as keyof SuggestionRawDefinition]).toEqual(value); - } - } - } + const sortedSuggestions = suggestions.map((suggestion) => suggestion.text).sort(); + const sortedExpected = expected.sort(); + + expect(sortedSuggestions).toEqual(sortedExpected); } ); }; // Enrich the function to work with .only and .skip as regular test function + // + // DO NOT CHANGE THE NAME OF THIS FUNCTION WITHOUT ALSO CHANGING + // THE LINTER RULE IN packages/kbn-eslint-config/typescript.js + // const testSuggestions = Object.assign(testSuggestionsFn, { skip: (...args: TestArgs) => { const paddingArgs = ['', [undefined, undefined, undefined]].slice(args.length - 2); @@ -616,6 +607,16 @@ describe('autocomplete', () => { 'any', { evalMath: true, + grouping: false, + }, + undefined, + undefined, + 'by' + ); + const allGroupingFunctions = getFunctionSignaturesByReturnType( + 'stats', + 'any', + { grouping: true, }, undefined, @@ -623,25 +624,26 @@ describe('autocomplete', () => { 'by' ); testSuggestions('from a | stats ', ['var0 =', ...allAggFunctions, ...allEvaFunctions]); - testSuggestions('from a | stats a ', [ - { text: '= $0', asSnippet: true, command: TRIGGER_SUGGESTION_COMMAND }, - ]); + testSuggestions('from a | stats a ', ['= $0']); testSuggestions('from a | stats a=', [...allAggFunctions, ...allEvaFunctions]); - testSuggestions.only('from a | stats a=max(b) by ', [ + testSuggestions('from a | stats a=max(b) by ', [ 'var0 =', ...getFieldNamesByType('any'), ...allEvaFunctions, + ...allGroupingFunctions, ]); testSuggestions('from a | stats a=max(b) BY ', [ 'var0 =', ...getFieldNamesByType('any'), ...allEvaFunctions, + ...allGroupingFunctions, ]); testSuggestions('from a | stats a=c by d ', [',', '|']); testSuggestions('from a | stats a=c by d, ', [ 'var0 =', ...getFieldNamesByType('any'), ...allEvaFunctions, + ...allGroupingFunctions, ]); testSuggestions('from a | stats a=max(b), ', [ 'var0 =', @@ -663,6 +665,7 @@ describe('autocomplete', () => { 'var0 =', ...getFieldNamesByType('any'), ...allEvaFunctions, + ...allGroupingFunctions, ]); testSuggestions('from a | stats a=min(b),', ['var0 =', ...allAggFunctions, ...allEvaFunctions]); testSuggestions('from a | stats var0=min(b),var1=c,', [ @@ -705,19 +708,23 @@ describe('autocomplete', () => { ...getFieldNamesByType('number'), '`avg(b)`', ...getFunctionSignaturesByReturnType('eval', 'number', { evalMath: true }), + ...allGroupingFunctions, ]); testSuggestions('from a | stats avg(b) by var0 = ', [ ...getFieldNamesByType('any'), ...allEvaFunctions, + ...allGroupingFunctions, ]); testSuggestions('from a | stats avg(b) by c, ', [ 'var0 =', ...getFieldNamesByType('any'), ...getFunctionSignaturesByReturnType('eval', 'any', { evalMath: true }), + ...allGroupingFunctions, ]); testSuggestions('from a | stats avg(b) by c, var0 = ', [ ...getFieldNamesByType('any'), ...allEvaFunctions, + ...allGroupingFunctions, ]); testSuggestions('from a | stats avg(b) by numberField % 2 ', [',', '|']); @@ -1160,7 +1167,7 @@ describe('autocomplete', () => { } } - testSuggestions('from a | eval var0 = bucket(@timestamp,', []); + testSuggestions('from a | eval var0 = bucket(@timestamp,', getUnitDuration(1)); describe('date math', () => { const dateSuggestions = timeLiterals.map(({ name }) => name); diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts index 7b28ae5add262c..481399edfb4d53 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts @@ -289,7 +289,7 @@ export const buildNoPoliciesAvailableDefinition = (): SuggestionRawDefinition => }, }); -function getUnitDuration(unit: number = 1) { +export function getUnitDuration(unit: number = 1) { const filteredTimeLiteral = timeLiterals.filter(({ name }) => { const result = /s$/.test(name); return unit > 1 ? result : !result; @@ -297,6 +297,19 @@ function getUnitDuration(unit: number = 1) { return filteredTimeLiteral.map(({ name }) => `${unit} ${name}`); } +/** + * Given information about the current command and the parameter type, suggest + * some literals that may make sense. + * + * TODO — this currently tries to cover both command-specific suggestions and type + * suggestions. We could consider separating the two... or just using parameter types + * and forgetting about command-specific suggestions altogether. + * + * Another thought... should literal suggestions be defined in the definitions file? + * That approach might allow for greater specificity in the suggestions and remove some + * "magical" logic. Maybe this is really the same thing as the literalOptions parameter + * definition property... + */ export function getCompatibleLiterals(commandName: string, types: string[], names?: string[]) { const suggestions: SuggestionRawDefinition[] = []; if (types.includes('number')) { diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts b/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts index dc37f5b2fd1ff6..73a30e5833aeee 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts @@ -362,6 +362,10 @@ describe('validation logic', () => { type TestArgs = [string, string[], string[]?]; // Make only and skip work with our custom wrapper + // + // DO NOT CHANGE THE NAME OF THIS FUNCTION WITHOUT ALSO CHANGING + // THE LINTER RULE IN packages/kbn-eslint-config/typescript.js + // const testErrorsAndWarnings = Object.assign(testErrorsAndWarningsFn, { skip: (...args: TestArgs) => { const warningArgs = [[]].slice(args.length - 2); diff --git a/src/plugins/dashboard/public/dashboard_constants.ts b/src/plugins/dashboard/public/dashboard_constants.ts index 965ee67355c580..6af0e391f2308e 100644 --- a/src/plugins/dashboard/public/dashboard_constants.ts +++ b/src/plugins/dashboard/public/dashboard_constants.ts @@ -65,6 +65,13 @@ export const DEFAULT_PANEL_WIDTH = DASHBOARD_GRID_COLUMN_COUNT / 2; export const CHANGE_CHECK_DEBOUNCE = 100; +export enum PanelPlacementStrategy { + /** Place on the very top of the Dashboard, add the height of this panel to all other panels. */ + placeAtTop = 'placeAtTop', + /** Look for the smallest y and x value where the default panel will fit. */ + findTopLeftMostOpenSpace = 'findTopLeftMostOpenSpace', +} + // ------------------------------------------------------------------ // Content Management // ------------------------------------------------------------------ diff --git a/src/plugins/dashboard/public/dashboard_container/_dashboard_container_strings.ts b/src/plugins/dashboard/public/dashboard_container/_dashboard_container_strings.ts index 8bccc6ce4f5a9e..399f3c6128e3d2 100644 --- a/src/plugins/dashboard/public/dashboard_container/_dashboard_container_strings.ts +++ b/src/plugins/dashboard/public/dashboard_container/_dashboard_container_strings.ts @@ -105,3 +105,16 @@ export const backupServiceStrings = { values: { message }, }), }; + +export const panelPlacementStrings = { + getUnknownStrategyError: (strategy: string) => + i18n.translate('dashboard.panelPlacement.unknownStrategyError', { + defaultMessage: 'Unknown panel placement strategy: {strategy}', + values: { strategy }, + }), + getPanelPlacementSettingsExistsError: (panelType: string) => + i18n.translate('dashboard.panelPlacement.panelPlacementSettingsExistsError', { + defaultMessage: 'Panel placement settings for embeddable type {panelType} already exists', + values: { panelType }, + }), +}; diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_new_panel_strategies.ts b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_new_panel_strategies.ts index 8a8c8a83193ebf..168dcfa72a491a 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_new_panel_strategies.ts +++ b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_new_panel_strategies.ts @@ -7,98 +7,98 @@ */ import { cloneDeep } from 'lodash'; -import { DASHBOARD_GRID_COLUMN_COUNT } from '../../../dashboard_constants'; +import { DASHBOARD_GRID_COLUMN_COUNT, PanelPlacementStrategy } from '../../../dashboard_constants'; +import { panelPlacementStrings } from '../../_dashboard_container_strings'; import { PanelPlacementProps, PanelPlacementReturn } from './types'; -export const panelPlacementStrategies = { - // Place on the very top of the Dashboard, add the height of this panel to all other panels. - placeAtTop: ({ width, height, currentPanels }: PanelPlacementProps): PanelPlacementReturn => { - const otherPanels = { ...currentPanels }; - for (const [id, panel] of Object.entries(currentPanels)) { - const currentPanel = cloneDeep(panel); - currentPanel.gridData.y = currentPanel.gridData.y + height; - otherPanels[id] = currentPanel; - } - return { - newPanelPlacement: { x: 0, y: 0, w: width, h: height }, - otherPanels, - }; - }, - - // Look for the smallest y and x value where the default panel will fit. - findTopLeftMostOpenSpace: ({ - width, - height, - currentPanels, - }: PanelPlacementProps): PanelPlacementReturn => { - let maxY = -1; - - const currentPanelsArray = Object.values(currentPanels); - currentPanelsArray.forEach((panel) => { - maxY = Math.max(panel.gridData.y + panel.gridData.h, maxY); - }); - - // Handle case of empty grid. - if (maxY < 0) { +export const runPanelPlacementStrategy = ( + strategy: PanelPlacementStrategy, + { width, height, currentPanels }: PanelPlacementProps +): PanelPlacementReturn => { + switch (strategy) { + case PanelPlacementStrategy.placeAtTop: + const otherPanels = { ...currentPanels }; + for (const [id, panel] of Object.entries(currentPanels)) { + const currentPanel = cloneDeep(panel); + currentPanel.gridData.y = currentPanel.gridData.y + height; + otherPanels[id] = currentPanel; + } return { newPanelPlacement: { x: 0, y: 0, w: width, h: height }, - otherPanels: currentPanels, + otherPanels, }; - } - const grid = new Array(maxY); - for (let y = 0; y < maxY; y++) { - grid[y] = new Array(DASHBOARD_GRID_COLUMN_COUNT).fill(0); - } + case PanelPlacementStrategy.findTopLeftMostOpenSpace: + let maxY = -1; + + const currentPanelsArray = Object.values(currentPanels); + currentPanelsArray.forEach((panel) => { + maxY = Math.max(panel.gridData.y + panel.gridData.h, maxY); + }); + + // Handle case of empty grid. + if (maxY < 0) { + return { + newPanelPlacement: { x: 0, y: 0, w: width, h: height }, + otherPanels: currentPanels, + }; + } - currentPanelsArray.forEach((panel) => { - for (let x = panel.gridData.x; x < panel.gridData.x + panel.gridData.w; x++) { - for (let y = panel.gridData.y; y < panel.gridData.y + panel.gridData.h; y++) { - const row = grid[y]; - if (row === undefined) { - throw new Error( - `Attempted to access a row that doesn't exist at ${y} for panel ${JSON.stringify( - panel - )}` - ); + const grid = new Array(maxY); + for (let y = 0; y < maxY; y++) { + grid[y] = new Array(DASHBOARD_GRID_COLUMN_COUNT).fill(0); + } + + currentPanelsArray.forEach((panel) => { + for (let x = panel.gridData.x; x < panel.gridData.x + panel.gridData.w; x++) { + for (let y = panel.gridData.y; y < panel.gridData.y + panel.gridData.h; y++) { + const row = grid[y]; + if (row === undefined) { + throw new Error( + `Attempted to access a row that doesn't exist at ${y} for panel ${JSON.stringify( + panel + )}` + ); + } + grid[y][x] = 1; } - grid[y][x] = 1; } - } - }); + }); - for (let y = 0; y < maxY; y++) { - for (let x = 0; x < DASHBOARD_GRID_COLUMN_COUNT; x++) { - if (grid[y][x] === 1) { - // Space is filled - continue; - } else { - for (let h = y; h < Math.min(y + height, maxY); h++) { - for (let w = x; w < Math.min(x + width, DASHBOARD_GRID_COLUMN_COUNT); w++) { - const spaceIsEmpty = grid[h][w] === 0; - const fitsPanelWidth = w === x + width - 1; - // If the panel is taller than any other panel in the current grid, it can still fit in the space, hence - // we check the minimum of maxY and the panel height. - const fitsPanelHeight = h === Math.min(y + height - 1, maxY - 1); + for (let y = 0; y < maxY; y++) { + for (let x = 0; x < DASHBOARD_GRID_COLUMN_COUNT; x++) { + if (grid[y][x] === 1) { + // Space is filled + continue; + } else { + for (let h = y; h < Math.min(y + height, maxY); h++) { + for (let w = x; w < Math.min(x + width, DASHBOARD_GRID_COLUMN_COUNT); w++) { + const spaceIsEmpty = grid[h][w] === 0; + const fitsPanelWidth = w === x + width - 1; + // If the panel is taller than any other panel in the current grid, it can still fit in the space, hence + // we check the minimum of maxY and the panel height. + const fitsPanelHeight = h === Math.min(y + height - 1, maxY - 1); - if (spaceIsEmpty && fitsPanelWidth && fitsPanelHeight) { - // Found space - return { - newPanelPlacement: { x, y, w: width, h: height }, - otherPanels: currentPanels, - }; - } else if (grid[h][w] === 1) { - // x, y spot doesn't work, break. - break; + if (spaceIsEmpty && fitsPanelWidth && fitsPanelHeight) { + // Found space + return { + newPanelPlacement: { x, y, w: width, h: height }, + otherPanels: currentPanels, + }; + } else if (grid[h][w] === 1) { + // x, y spot doesn't work, break. + break; + } } } } } } - } - return { - newPanelPlacement: { x: 0, y: maxY, w: width, h: height }, - otherPanels: currentPanels, - }; - }, -} as const; + return { + newPanelPlacement: { x: 0, y: maxY, w: width, h: height }, + otherPanels: currentPanels, + }; + default: + throw new Error(panelPlacementStrings.getUnknownStrategyError(strategy)); + } +}; diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_panel.ts b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_panel.ts index a65c4fca9c1158..c440c0fe93e103 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_panel.ts +++ b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_panel.ts @@ -9,9 +9,13 @@ import { PanelState, EmbeddableInput, EmbeddableFactory } from '@kbn/embeddable-plugin/public'; import { DashboardPanelState } from '../../../../common'; -import { panelPlacementStrategies } from './place_new_panel_strategies'; -import { IProvidesPanelPlacementSettings, PanelPlacementSettings } from './types'; -import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../../../dashboard_constants'; +import { IProvidesPanelPlacementSettings } from './types'; +import { runPanelPlacementStrategy } from './place_new_panel_strategies'; +import { + DEFAULT_PANEL_HEIGHT, + DEFAULT_PANEL_WIDTH, + PanelPlacementStrategy, +} from '../../../dashboard_constants'; export const providesPanelPlacementSettings = ( value: unknown @@ -28,10 +32,10 @@ export function placePanel( newPanel: DashboardPanelState; otherPanels: { [key: string]: DashboardPanelState }; } { - let placementSettings: PanelPlacementSettings = { + let placementSettings = { width: DEFAULT_PANEL_WIDTH, height: DEFAULT_PANEL_HEIGHT, - strategy: 'findTopLeftMostOpenSpace', + strategy: PanelPlacementStrategy.findTopLeftMostOpenSpace, }; if (providesPanelPlacementSettings(factory)) { placementSettings = { @@ -41,7 +45,7 @@ export function placePanel( } const { width, height, strategy } = placementSettings; - const { newPanelPlacement, otherPanels } = panelPlacementStrategies[strategy]({ + const { newPanelPlacement, otherPanels } = runPanelPlacementStrategy(strategy, { currentPanels, height, width, diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel_placement/types.ts b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/types.ts index 7fb20b469c1a92..d5fdbf705f4431 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/panel_placement/types.ts +++ b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/types.ts @@ -9,14 +9,12 @@ import { EmbeddableInput } from '@kbn/embeddable-plugin/public'; import { DashboardPanelState } from '../../../../common'; import { GridData } from '../../../../common/content_management'; -import { panelPlacementStrategies } from './place_new_panel_strategies'; - -export type PanelPlacementStrategy = keyof typeof panelPlacementStrategies; +import { PanelPlacementStrategy } from '../../../dashboard_constants'; export interface PanelPlacementSettings { - strategy: PanelPlacementStrategy; - height: number; - width: number; + strategy?: PanelPlacementStrategy; + height?: number; + width?: number; } export interface PanelPlacementReturn { diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts index 23d677b3e47c56..88ece952a49b55 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts @@ -39,13 +39,14 @@ import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH, GLOBAL_STATE_STORAGE_KEY, + PanelPlacementStrategy, } from '../../../dashboard_constants'; import { LoadDashboardReturn, SavedDashboardInput, } from '../../../services/dashboard_content_management/types'; import { pluginServices } from '../../../services/plugin_services'; -import { panelPlacementStrategies } from '../../component/panel_placement/place_new_panel_strategies'; +import { runPanelPlacementStrategy } from '../../component/panel_placement/place_new_panel_strategies'; import { startDiffingDashboardState } from '../../state/diffing/dashboard_diffing_integration'; import { DashboardPublicState } from '../../types'; import { DashboardContainer } from '../dashboard_container'; @@ -353,12 +354,14 @@ export const initializeDashboard = async ({ const { width, height } = incomingEmbeddable.size; const currentPanels = container.getInput().panels; const embeddableId = incomingEmbeddable.embeddableId ?? v4(); - const { findTopLeftMostOpenSpace } = panelPlacementStrategies; - const { newPanelPlacement } = findTopLeftMostOpenSpace({ - width: width ?? DEFAULT_PANEL_WIDTH, - height: height ?? DEFAULT_PANEL_HEIGHT, - currentPanels, - }); + const { newPanelPlacement } = runPanelPlacementStrategy( + PanelPlacementStrategy.findTopLeftMostOpenSpace, + { + width: width ?? DEFAULT_PANEL_WIDTH, + height: height ?? DEFAULT_PANEL_HEIGHT, + currentPanels, + } + ); const newPanelState: DashboardPanelState = { explicitInput: { ...incomingEmbeddable.input, id: embeddableId }, type: incomingEmbeddable.type, diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx index e54753b2b85399..69fdf28647c092 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx @@ -57,14 +57,16 @@ import { DASHBOARD_UI_METRIC_ID, DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH, + PanelPlacementStrategy, } from '../../dashboard_constants'; import { DashboardAnalyticsService } from '../../services/analytics/types'; import { DashboardCapabilitiesService } from '../../services/dashboard_capabilities/types'; import { pluginServices } from '../../services/plugin_services'; import { placePanel } from '../component/panel_placement'; -import { panelPlacementStrategies } from '../component/panel_placement/place_new_panel_strategies'; +import { runPanelPlacementStrategy } from '../component/panel_placement/place_new_panel_strategies'; import { DashboardViewport } from '../component/viewport/dashboard_viewport'; import { DashboardExternallyAccessibleApi } from '../external_api/dashboard_api'; +import { getDashboardPanelPlacementSetting } from '../external_api/dashboard_panel_placement_registry'; import { dashboardContainerReducers } from '../state/dashboard_container_reducers'; import { getDiffingMiddleware } from '../state/diffing/dashboard_diffing_integration'; import { @@ -498,10 +500,20 @@ export class DashboardContainer } if (reactEmbeddableRegistryHasKey(panelPackage.panelType)) { const newId = v4(); - const { newPanelPlacement, otherPanels } = panelPlacementStrategies.findTopLeftMostOpenSpace({ - currentPanels: this.getInput().panels, - height: DEFAULT_PANEL_HEIGHT, + + const placementSettings = { width: DEFAULT_PANEL_WIDTH, + height: DEFAULT_PANEL_HEIGHT, + strategy: PanelPlacementStrategy.findTopLeftMostOpenSpace, + ...getDashboardPanelPlacementSetting(panelPackage.panelType)?.(panelPackage.initialState), + }; + + const { width, height, strategy } = placementSettings; + + const { newPanelPlacement, otherPanels } = runPanelPlacementStrategy(strategy, { + currentPanels: this.getInput().panels, + height, + width, }); const newPanel: DashboardPanelState = { type: panelPackage.panelType, diff --git a/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_panel_placement_registry.ts b/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_panel_placement_registry.ts new file mode 100644 index 00000000000000..e21fedb9aabe1e --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_panel_placement_registry.ts @@ -0,0 +1,30 @@ +/* + * 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 { PanelPlacementSettings } from '../component/panel_placement/types'; +import { panelPlacementStrings } from '../_dashboard_container_strings'; + +type GetPanelPlacementSettings = ( + serializedState?: SerializedState +) => PanelPlacementSettings; + +const registry = new Map>(); + +export const registerDashboardPanelPlacementSetting = ( + embeddableType: string, + getPanelPlacementSettings: GetPanelPlacementSettings +) => { + if (registry.has(embeddableType)) { + throw new Error(panelPlacementStrings.getPanelPlacementSettingsExistsError(embeddableType)); + } + registry.set(embeddableType, getPanelPlacementSettings); +}; + +export const getDashboardPanelPlacementSetting = (embeddableType: string) => { + return registry.get(embeddableType); +}; diff --git a/src/plugins/dashboard/public/dashboard_container/index.ts b/src/plugins/dashboard/public/dashboard_container/index.ts index e0b421d65fd156..c8944968e6cb7c 100644 --- a/src/plugins/dashboard/public/dashboard_container/index.ts +++ b/src/plugins/dashboard/public/dashboard_container/index.ts @@ -22,4 +22,5 @@ export { export { DashboardRenderer } from './external_api/dashboard_renderer'; export type { DashboardAPI, AwaitingDashboardAPI } from './external_api/dashboard_api'; +export { registerDashboardPanelPlacementSetting } from './external_api/dashboard_panel_placement_registry'; export type { DashboardLocatorParams } from './types'; diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index 03cd4e03d52a51..3b1bafe2e1fd47 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -14,8 +14,10 @@ export { DASHBOARD_APP_ID, LEGACY_DASHBOARD_APP_ID, DASHBOARD_GRID_COLUMN_COUNT, + PanelPlacementStrategy, } from './dashboard_constants'; export { + registerDashboardPanelPlacementSetting, type DashboardAPI, type AwaitingDashboardAPI, DashboardRenderer, diff --git a/src/plugins/links/public/embeddable/links_embeddable_factory.ts b/src/plugins/links/public/embeddable/links_embeddable_factory.ts index fa489ff4ee9777..df629c9b1d0534 100644 --- a/src/plugins/links/public/embeddable/links_embeddable_factory.ts +++ b/src/plugins/links/public/embeddable/links_embeddable_factory.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { DASHBOARD_GRID_COLUMN_COUNT } from '@kbn/dashboard-plugin/public'; +import { DASHBOARD_GRID_COLUMN_COUNT, PanelPlacementStrategy } from '@kbn/dashboard-plugin/public'; import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container'; import { IProvidesPanelPlacementSettings } from '@kbn/dashboard-plugin/public/dashboard_container/component/panel_placement/types'; import { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common'; @@ -76,7 +76,7 @@ export class LinksFactoryDefinition const isHorizontal = attributes.layout === 'horizontal'; const width = isHorizontal ? DASHBOARD_GRID_COLUMN_COUNT : 8; const height = isHorizontal ? 4 : (attributes.links?.length ?? 1 * 3) + 4; - return { width, height, strategy: 'placeAtTop' }; + return { width, height, strategy: PanelPlacementStrategy.placeAtTop }; }; public async isEditable() { diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx index c205ef7f825258..ae1a1cb755afc9 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx @@ -549,7 +549,7 @@ export const CspPolicyTemplateForm = memo )} diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/setup_technology_selector/use_setup_technology.ts b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/setup_technology_selector/use_setup_technology.ts index 1eb3334d09103f..eb18a90b72936d 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/setup_technology_selector/use_setup_technology.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/setup_technology_selector/use_setup_technology.ts @@ -38,8 +38,15 @@ export const useSetupTechnology = ({ return SetupTechnology.AGENT_BASED; }); + const [isDirty, setIsDirty] = useState(false); + + const updateSetupTechnology = (value: SetupTechnology) => { + setSetupTechnology(value); + setIsDirty(true); + }; + useEffect(() => { - if (isEditPage) { + if (isEditPage || isDirty) { return; } @@ -58,7 +65,7 @@ export const useSetupTechnology = ({ } else { setSetupTechnology(SetupTechnology.AGENT_BASED); } - }, [agentPolicyId, agentlessPolicyId, isAgentlessAvailable, isEditPage]); + }, [agentPolicyId, agentlessPolicyId, isAgentlessAvailable, isDirty, isEditPage]); useEffect(() => { if (isEditPage) { @@ -74,5 +81,6 @@ export const useSetupTechnology = ({ isAgentlessAvailable, setupTechnology, setSetupTechnology, + updateSetupTechnology, }; }; diff --git a/x-pack/plugins/enterprise_search/server/lib/pipelines/create_pipeline_definitions.ts b/x-pack/plugins/enterprise_search/server/lib/pipelines/create_pipeline_definitions.ts index 94353b7ee35653..744369ca188068 100644 --- a/x-pack/plugins/enterprise_search/server/lib/pipelines/create_pipeline_definitions.ts +++ b/x-pack/plugins/enterprise_search/server/lib/pipelines/create_pipeline_definitions.ts @@ -30,7 +30,7 @@ export const createIndexPipelineDefinitions = async ( let result: Record = {}; try { const mlPipeline = { - description: `Enterprise Search Machine Learning Inference pipeline for the '${indexName}' index`, + description: `Machine Learning Inference pipeline for the '${indexName}' index`, id: getInferencePipelineNameFromIndexName(indexName), processors: [], version: 1, @@ -38,7 +38,7 @@ export const createIndexPipelineDefinitions = async ( await esClient.ingest.putPipeline(mlPipeline); result = { ...result, [mlPipeline.id]: mlPipeline }; const customPipeline = { - description: `Enterprise Search customizable ingest pipeline for the '${indexName}' index`, + description: `Customizable ingest pipeline for the '${indexName}' index`, id: `${indexName}@custom`, processors: [], version: 1, @@ -48,9 +48,9 @@ export const createIndexPipelineDefinitions = async ( const ingestPipeline = { _meta: { managed: true, - managed_by: 'Enterprise Search', + managed_by: 'Search', }, - description: `Enterprise Search ingest pipeline for the '${indexName}' index`, + description: `Ingest pipeline for the '${indexName}' index`, id: `${indexName}`, processors: [ { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts index 7088bdd37f0439..2eab867e2ae121 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts @@ -37,7 +37,7 @@ export function useSetupTechnology({ }: { updateNewAgentPolicy: (policy: NewAgentPolicy) => void; newAgentPolicy: NewAgentPolicy; - updateAgentPolicy: (policy: AgentPolicy) => void; + updateAgentPolicy: (policy: AgentPolicy | undefined) => void; setSelectedPolicyTab: (tab: SelectedPolicyTab) => void; }) { const { isAgentlessEnabled } = useAgentlessPolicy(); @@ -75,8 +75,8 @@ export function useSetupTechnology({ } else if (setupTechnology === SetupTechnology.AGENT_BASED) { updateNewAgentPolicy(newAgentPolicy); setSelectedPolicyTab(SelectedPolicyTab.NEW); + updateAgentPolicy(undefined); } - setSelectedSetupTechnology(setupTechnology); }, [ diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx index 6093f7b327b17d..572b7a4136ea54 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx @@ -226,10 +226,15 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ } }; + if (selectedPolicyTab === SelectedPolicyTab.NEW) { + setAgentCount(0); + return; + } + if (isFleetEnabled && agentPolicyId) { getAgentCount(); } - }, [agentPolicyId, isFleetEnabled]); + }, [agentPolicyId, selectedPolicyTab, isFleetEnabled]); const handleExtensionViewOnChange = useCallback< PackagePolicyEditExtensionComponentProps['onChange'] diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v8_14_0.test.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v8_14_0.test.ts index 2ad57f32f9cf9e..a5b5ac8424597d 100644 --- a/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v8_14_0.test.ts +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v8_14_0.test.ts @@ -85,8 +85,31 @@ describe('8.14.0 Endpoint Package Policy migration', () => { }); describe('backfilling `on_write_scan`', () => { - it('should backfill `on_write_scan` field to malware protections on Kibana update', () => { + it('should backfill `on_write_scan` field as `true` for `malware prevent` on Kibana update', () => { const originalPolicyConfigSO = cloneDeep(policyDoc); + const originalPolicyConfig = originalPolicyConfigSO.attributes.inputs[0].config?.policy.value; + originalPolicyConfig.windows.malware.mode = 'prevent'; + originalPolicyConfig.mac.malware.mode = 'prevent'; + originalPolicyConfig.linux.malware.mode = 'prevent'; + + const migratedPolicyConfigSO = migrator.migrate({ + document: originalPolicyConfigSO, + fromVersion: 5, + toVersion: 6, + }); + + const migratedPolicyConfig = migratedPolicyConfigSO.attributes.inputs[0].config?.policy.value; + expect(migratedPolicyConfig.windows.malware.on_write_scan).toBe(true); + expect(migratedPolicyConfig.mac.malware.on_write_scan).toBe(true); + expect(migratedPolicyConfig.linux.malware.on_write_scan).toBe(true); + }); + + it('should backfill `on_write_scan` field as `true` for `malware detect` on Kibana update', () => { + const originalPolicyConfigSO = cloneDeep(policyDoc); + const originalPolicyConfig = originalPolicyConfigSO.attributes.inputs[0].config?.policy.value; + originalPolicyConfig.windows.malware.mode = 'detect'; + originalPolicyConfig.mac.malware.mode = 'detect'; + originalPolicyConfig.linux.malware.mode = 'detect'; const migratedPolicyConfigSO = migrator.migrate({ document: originalPolicyConfigSO, @@ -100,6 +123,25 @@ describe('8.14.0 Endpoint Package Policy migration', () => { expect(migratedPolicyConfig.linux.malware.on_write_scan).toBe(true); }); + it('should backfill `on_write_scan` field as `false` for `malware off` on Kibana update', () => { + const originalPolicyConfigSO = cloneDeep(policyDoc); + const originalPolicyConfig = originalPolicyConfigSO.attributes.inputs[0].config?.policy.value; + originalPolicyConfig.windows.malware.mode = 'off'; + originalPolicyConfig.mac.malware.mode = 'off'; + originalPolicyConfig.linux.malware.mode = 'off'; + + const migratedPolicyConfigSO = migrator.migrate({ + document: originalPolicyConfigSO, + fromVersion: 5, + toVersion: 6, + }); + + const migratedPolicyConfig = migratedPolicyConfigSO.attributes.inputs[0].config?.policy.value; + expect(migratedPolicyConfig.windows.malware.on_write_scan).toBe(false); + expect(migratedPolicyConfig.mac.malware.on_write_scan).toBe(false); + expect(migratedPolicyConfig.linux.malware.on_write_scan).toBe(false); + }); + it('should not backfill `on_write_scan` field if already present due to user edit before migration is performed on serverless', () => { const originalPolicyConfigSO = cloneDeep(policyDoc); const originalPolicyConfig = originalPolicyConfigSO.attributes.inputs[0].config?.policy.value; diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v8_14_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v8_14_0.ts index c94332d22afa8f..89a0715900ebe3 100644 --- a/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v8_14_0.ts +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v8_14_0.ts @@ -28,9 +28,12 @@ export const migratePackagePolicyToV8140: SavedObjectModelDataBackfillFn< if (input && input.config) { const policy = input.config.policy.value; - policy.windows.malware.on_write_scan ??= ON_WRITE_SCAN_DEFAULT_VALUE; - policy.mac.malware.on_write_scan ??= ON_WRITE_SCAN_DEFAULT_VALUE; - policy.linux.malware.on_write_scan ??= ON_WRITE_SCAN_DEFAULT_VALUE; + policy.windows.malware.on_write_scan ??= + policy.windows.malware.mode === 'off' ? false : ON_WRITE_SCAN_DEFAULT_VALUE; + policy.mac.malware.on_write_scan ??= + policy.mac.malware.mode === 'off' ? false : ON_WRITE_SCAN_DEFAULT_VALUE; + policy.linux.malware.on_write_scan ??= + policy.linux.malware.mode === 'off' ? false : ON_WRITE_SCAN_DEFAULT_VALUE; } return { attributes: updatedPackagePolicyDoc.attributes }; diff --git a/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts b/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts index 912bc637120be4..a97f578cfadf88 100644 --- a/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts +++ b/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts @@ -36,16 +36,18 @@ spec: # Uncomment if using hints feature #initContainers: # - name: k8s-templates-downloader - # image: busybox:1.28 - # command: ['sh'] + # image: docker.elastic.co/beats/elastic-agent:VERSION + # command: ['bash'] # args: # - -c # - >- - # mkdir -p /etc/elastic-agent/inputs.d && - # wget -O - https://github.com/elastic/elastic-agent/archive/8.15.tar.gz | tar xz -C /etc/elastic-agent/inputs.d --strip=5 "elastic-agent-8.15/deploy/kubernetes/elastic-agent/templates.d" + # mkdir -p /usr/share/elastic-agent/state/inputs.d && + # curl -sL https://github.com/elastic/elastic-agent/archive/8.15.tar.gz | tar xz -C /usr/share/elastic-agent/state/inputs.d --strip=5 "elastic-agent-8.15/deploy/kubernetes/elastic-agent/templates.d" + # securityContext: + # runAsUser: 0 # volumeMounts: - # - name: external-inputs - # mountPath: /etc/elastic-agent/inputs.d + # - name: elastic-agent-state + # mountPath: /usr/share/elastic-agent/state containers: - name: elastic-agent image: docker.elastic.co/beats/elastic-agent:VERSION @@ -66,8 +68,6 @@ spec: valueFrom: fieldRef: fieldPath: metadata.name - - name: STATE_PATH - value: "/etc/elastic-agent" # The following ELASTIC_NETINFO:false variable will disable the netinfo.enabled option of add-host-metadata processor. This will remove fields host.ip and host.mac. # For more info: https://www.elastic.co/guide/en/beats/metricbeat/current/add-host-metadata.html - name: ELASTIC_NETINFO @@ -101,9 +101,6 @@ spec: mountPath: /etc/elastic-agent/agent.yml readOnly: true subPath: agent.yml - # Uncomment if using hints feature - #- name: external-inputs - # mountPath: /etc/elastic-agent/inputs.d - name: proc mountPath: /hostfs/proc readOnly: true @@ -134,9 +131,6 @@ spec: configMap: defaultMode: 0640 name: agent-node-datastreams - # Uncomment if using hints feature - #- name: external-inputs - # emptyDir: {} - name: proc hostPath: path: /proc diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.helpers.ts index c816e9a5ed2e37..5a15772d5e16ab 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.helpers.ts @@ -41,6 +41,8 @@ export interface IndexDetailsPageTestBed extends TestBed { getActiveTabContent: () => string; mappings: { addNewMappingFieldNameAndType: (mappingFields?: MappingField[]) => Promise; + clickFilterByFieldType: () => Promise; + selectFilterFieldType: (fieldType: string) => Promise; clickAddFieldButton: () => Promise; clickSaveMappingsButton: () => Promise; getCodeBlockContent: () => string; @@ -51,6 +53,8 @@ export interface IndexDetailsPageTestBed extends TestBed { getTreeViewContent: (fieldName: string) => string; clickToggleViewButton: () => Promise; isSearchBarDisabled: () => boolean; + setSearchBarValue: (searchValue: string) => Promise; + findSearchResult: () => string; isSemanticTextBannerVisible: () => boolean; }; settings: { @@ -216,9 +220,35 @@ export const setup = async ({ }); component.update(); }, + clickFilterByFieldType: async () => { + expect(exists('indexDetailsMappingsFilterByFieldTypeButton')).toBe(true); + await act(async () => { + find('indexDetailsMappingsFilterByFieldTypeButton').simulate('click'); + }); + component.update(); + }, + selectFilterFieldType: async (fieldType: string) => { + expect(testBed.exists('indexDetailsMappingsSelectFilter-text')).toBe(true); + await act(async () => { + find(fieldType).simulate('click'); + }); + component.update(); + }, isSearchBarDisabled: () => { return find('indexDetailsMappingsFieldSearch').prop('disabled'); }, + setSearchBarValue: async (searchValue: string) => { + await act(async () => { + testBed + .find('indexDetailsMappingsFieldSearch') + .simulate('change', { target: { value: searchValue } }); + }); + component.update(); + }, + findSearchResult: () => { + expect(testBed.exists('fieldName')).toBe(true); + return testBed.find('fieldName').text(); + }, isSemanticTextBannerVisible: () => { return exists('indexDetailsMappingsSemanticTextBanner'); }, diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.test.tsx index d5cb6f90e8b43a..37e2799678e79b 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.test.tsx @@ -471,10 +471,11 @@ describe('', () => { requestOptions ); }); - it('searchbar, toggle button, add field button exists', async () => { + it('filter, searchbar, toggle button, add field button exists', async () => { expect(testBed.exists('indexDetailsMappingsAddField')).toBe(true); expect(testBed.exists('indexDetailsMappingsToggleViewButton')).toBe(true); expect(testBed.exists('indexDetailsMappingsFieldSearch')).toBe(true); + expect(testBed.exists('indexDetailsMappingsFilter')).toBe(true); }); it('displays the mappings in the table view', async () => { @@ -508,6 +509,57 @@ describe('', () => { 'https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/mapping.html' ); }); + describe('Filter field by filter Type', () => { + const mockIndexMappingResponse: any = { + ...testIndexMappings.mappings, + properties: { + ...testIndexMappings.mappings.properties, + name: { + type: 'text', + }, + }, + }; + beforeEach(async () => { + httpRequestsMockHelpers.setLoadIndexMappingResponse(testIndexName, { + mappings: mockIndexMappingResponse, + }); + await act(async () => { + testBed = await setup({ httpSetup }); + }); + testBed.component.update(); + await testBed.actions.clickIndexDetailsTab(IndexDetailsSection.Mappings); + }); + test('popover is visible and shows list of available field types', async () => { + await testBed.actions.mappings.clickFilterByFieldType(); + expect(testBed.exists('euiSelectableList')).toBe(true); + expect(testBed.exists('indexDetailsMappingsFilterByFieldTypeSearch')).toBe(true); + expect(testBed.exists('euiSelectableList')).toBe(true); + }); + test('can select a field type and list view changes', async () => { + await testBed.actions.mappings.clickFilterByFieldType(); + await testBed.actions.mappings.selectFilterFieldType( + 'indexDetailsMappingsSelectFilter-text' + ); + expect(testBed.actions.mappings.getTreeViewContent('nameField-fieldName')).toContain( + 'name' + ); + expect(testBed.find('@timestampField-fieldName')).not.toContain('@timestamp'); + }); + test('can search field with filter', async () => { + expect(testBed.find('fieldName')).toHaveLength(2); + + // set filter + await testBed.actions.mappings.clickFilterByFieldType(); + await testBed.actions.mappings.selectFilterFieldType( + 'indexDetailsMappingsSelectFilter-text' + ); + + await testBed.actions.mappings.setSearchBarValue('na'); + expect(testBed.find('fieldName')).toHaveLength(1); + expect(testBed.actions.mappings.findSearchResult()).not.toBe('@timestamp'); + expect(testBed.actions.mappings.findSearchResult()).toBe('name'); + }); + }); describe('Add a new field ', () => { const mockIndexMappingResponse: any = { ...testIndexMappings.mappings, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result.tsx index e61bcd86399197..2adcf441749c26 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result.tsx @@ -18,6 +18,7 @@ interface Props { result: SearchResultType[]; documentFieldsState: State['documentFields']; style?: React.CSSProperties; + onClearSearch?: () => void; } const ITEM_HEIGHT = 64; @@ -50,12 +51,21 @@ const Row = React.memo(({ data, index, style }) => { }, areEqual); export const SearchResult = React.memo( - ({ result, documentFieldsState: { status, fieldToEdit }, style: virtualListStyle }: Props) => { + ({ + result, + documentFieldsState: { status, fieldToEdit }, + style: virtualListStyle, + onClearSearch, + }: Props) => { const dispatch = useDispatch(); const listHeight = Math.min(result.length * ITEM_HEIGHT, 600); const clearSearch = () => { - dispatch({ type: 'search:update', value: '' }); + if (onClearSearch !== undefined) { + onClearSearch(); + } else { + dispatch({ type: 'search:update', value: '' }); + } }; const itemData = useMemo( diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/index.ts index 1619271d652c5d..c14c408fee24ec 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/index.ts @@ -27,6 +27,9 @@ export { stripUndefinedValues, normalizeRuntimeFields, deNormalizeRuntimeFields, + getAllFieldTypesFromState, + getFieldsFromState, + getFieldsMatchingFilterFromState, } from './utils'; export * from './serializers'; 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 1f1ca1b34deef2..be00b71b58ea12 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,143 @@ jest.mock('../constants', () => { return { MAIN_DATA_TYPE_DEFINITION: {}, TYPE_DEFINITION }; }); -import { stripUndefinedValues, getTypeLabelFromField, getFieldMeta } from './utils'; +import { Fields, NormalizedFields, State } from '../types'; +import { + stripUndefinedValues, + getTypeLabelFromField, + getFieldMeta, + getFieldsFromState, + getAllFieldTypesFromState, + getFieldsMatchingFilterFromState, +} from './utils'; + +const fieldsWithnestedFields: NormalizedFields = { + byId: { + '4459e8f2-3bec-4d17-b50c-f62d1fcbcca0': { + id: '4459e8f2-3bec-4d17-b50c-f62d1fcbcca0', + parentId: 'dd0dd3aa-52c9-472b-a23d-ecec0a1b2420', + nestedDepth: 1, + isMultiField: false, + path: ['multifield', 'flag'], + source: { + name: 'flag', + type: 'boolean', + }, + childFieldsName: 'fields', + canHaveChildFields: false, + hasChildFields: false, + canHaveMultiFields: true, + hasMultiFields: false, + isExpanded: false, + }, + '20fffee6-2a94-4aa6-b7e4-1cd745d6f775': { + id: '20fffee6-2a94-4aa6-b7e4-1cd745d6f775', + parentId: '97399281-b0b2-4490-9931-6cc92676b305', + nestedDepth: 3, + isMultiField: false, + path: ['multifield', 'points', 'entity', 'entity_1'], + source: { + name: 'entity_1', + type: 'keyword', + }, + childFieldsName: 'fields', + canHaveChildFields: false, + hasChildFields: false, + canHaveMultiFields: true, + hasMultiFields: false, + isExpanded: false, + }, + '97399281-b0b2-4490-9931-6cc92676b305': { + id: '97399281-b0b2-4490-9931-6cc92676b305', + parentId: '9031c735-a445-491f-948d-41989b51a1a3', + nestedDepth: 2, + isMultiField: false, + path: ['multifield', 'points', 'entity'], + source: { + name: 'entity', + type: 'object', + }, + childFieldsName: 'properties', + canHaveChildFields: true, + hasChildFields: true, + canHaveMultiFields: false, + hasMultiFields: false, + isExpanded: false, + childFields: ['20fffee6-2a94-4aa6-b7e4-1cd745d6f775'], + }, + 'af9b7a29-8c44-4dbe-baa0-c29eb1760d96': { + id: 'af9b7a29-8c44-4dbe-baa0-c29eb1760d96', + parentId: '9031c735-a445-491f-948d-41989b51a1a3', + nestedDepth: 2, + isMultiField: false, + path: ['multifield', 'points', 'name'], + source: { + name: 'name', + type: 'text', + }, + childFieldsName: 'fields', + canHaveChildFields: false, + hasChildFields: false, + canHaveMultiFields: true, + hasMultiFields: false, + isExpanded: false, + }, + '9031c735-a445-491f-948d-41989b51a1a3': { + id: '9031c735-a445-491f-948d-41989b51a1a3', + parentId: 'dd0dd3aa-52c9-472b-a23d-ecec0a1b2420', + nestedDepth: 1, + isMultiField: false, + path: ['multifield', 'points'], + source: { + name: 'points', + type: 'object', + }, + childFieldsName: 'properties', + canHaveChildFields: true, + hasChildFields: true, + canHaveMultiFields: false, + hasMultiFields: false, + isExpanded: false, + childFields: ['97399281-b0b2-4490-9931-6cc92676b305', 'af9b7a29-8c44-4dbe-baa0-c29eb1760d96'], + }, + 'dd0dd3aa-52c9-472b-a23d-ecec0a1b2420': { + id: 'dd0dd3aa-52c9-472b-a23d-ecec0a1b2420', + nestedDepth: 0, + isMultiField: false, + path: ['multifield'], + source: { + name: 'multifield', + type: 'object', + }, + childFieldsName: 'properties', + canHaveChildFields: true, + hasChildFields: true, + canHaveMultiFields: false, + hasMultiFields: false, + isExpanded: false, + childFields: ['4459e8f2-3bec-4d17-b50c-f62d1fcbcca0', '9031c735-a445-491f-948d-41989b51a1a3'], + }, + '54204b52-c6a0-4de4-8f82-3e1ea9ad533a': { + id: '54204b52-c6a0-4de4-8f82-3e1ea9ad533a', + nestedDepth: 0, + isMultiField: false, + path: ['title'], + source: { + name: 'title', + type: 'text', + }, + childFieldsName: 'fields', + canHaveChildFields: false, + hasChildFields: false, + canHaveMultiFields: true, + hasMultiFields: false, + isExpanded: false, + }, + }, + aliases: {}, + rootLevelFields: ['dd0dd3aa-52c9-472b-a23d-ecec0a1b2420', '54204b52-c6a0-4de4-8f82-3e1ea9ad533a'], + maxNestedDepth: 3, +}; describe('utils', () => { describe('stripUndefinedValues()', () => { @@ -101,4 +237,210 @@ describe('utils', () => { ).toEqual(false); }); }); + describe('getFieldsFromState', () => { + test('returns all the fields', () => { + expect(getFieldsFromState(fieldsWithnestedFields)).toEqual([ + { + id: 'dd0dd3aa-52c9-472b-a23d-ecec0a1b2420', + nestedDepth: 0, + isMultiField: false, + path: ['multifield'], + source: { + name: 'multifield', + type: 'object', + }, + childFieldsName: 'properties', + canHaveChildFields: true, + hasChildFields: true, + canHaveMultiFields: false, + hasMultiFields: false, + isExpanded: false, + childFields: [ + '4459e8f2-3bec-4d17-b50c-f62d1fcbcca0', + '9031c735-a445-491f-948d-41989b51a1a3', + ], + }, + { + id: '54204b52-c6a0-4de4-8f82-3e1ea9ad533a', + nestedDepth: 0, + isMultiField: false, + path: ['title'], + source: { + name: 'title', + type: 'text', + }, + childFieldsName: 'fields', + canHaveChildFields: false, + hasChildFields: false, + canHaveMultiFields: true, + hasMultiFields: false, + isExpanded: false, + }, + ]); + }); + test('returns only text fields matching filter', () => { + expect(getFieldsFromState(fieldsWithnestedFields, ['Text'])).toEqual([ + { + id: 'af9b7a29-8c44-4dbe-baa0-c29eb1760d96', + parentId: '9031c735-a445-491f-948d-41989b51a1a3', + nestedDepth: 2, + isMultiField: false, + path: ['multifield', 'points', 'name'], + source: { + name: 'name', + type: 'text', + }, + childFieldsName: 'fields', + canHaveChildFields: false, + hasChildFields: false, + canHaveMultiFields: true, + hasMultiFields: false, + isExpanded: false, + }, + { + id: '54204b52-c6a0-4de4-8f82-3e1ea9ad533a', + nestedDepth: 0, + isMultiField: false, + path: ['title'], + source: { + name: 'title', + type: 'text', + }, + childFieldsName: 'fields', + canHaveChildFields: false, + hasChildFields: false, + canHaveMultiFields: true, + hasMultiFields: false, + isExpanded: false, + }, + ]); + }); + }); + describe('getallFieldsIncludingNestedFields', () => { + const fields: Fields = { + nested_field: { + properties: { + flag: { type: 'boolean' }, + points: { + properties: { + name: { type: 'text' }, + entity: { + type: 'object', + properties: { + entity_1: { type: 'keyword' }, + }, + }, + }, + type: 'object', + }, + }, + type: 'object', + }, + }; + test('returns all the data types including nested fields types', () => { + expect(getAllFieldTypesFromState(fields)).toEqual(['object', 'boolean', 'text', 'keyword']); + }); + }); + + describe('getFieldsMatchingFilterFromState', () => { + const sampleState: State = { + isValid: true, + configuration: { + defaultValue: {}, + data: { + internal: {}, + format: () => ({}), + }, + validate: () => Promise.resolve(true), + }, + templates: { + defaultValue: {}, + data: { + internal: {}, + format: () => ({}), + }, + validate: () => Promise.resolve(true), + }, + fields: fieldsWithnestedFields, + documentFields: { + status: 'disabled', + editor: 'default', + }, + runtimeFields: {}, + runtimeFieldsList: { + status: 'idle', + }, + fieldsJsonEditor: { + format: () => ({}), + isValid: true, + }, + search: { + term: 'f', + result: [], + }, + filter: { + filteredFields: [ + { + id: 'eb903187-c99e-4773-9274-cbefc68bb3f1', + parentId: '5c6287de-7ed0-48f8-bc08-c401bcc26e40', + nestedDepth: 1, + isMultiField: false, + path: ['multifield', 'flag'], + source: { + name: 'flag', + type: 'boolean', + }, + childFieldsName: 'fields', + canHaveChildFields: false, + hasChildFields: false, + canHaveMultiFields: true, + hasMultiFields: false, + isExpanded: false, + }, + ], + selectedOptions: [ + { + label: 'Object', + 'data-test-subj': 'indexDetailsMappingsSelectFilter-object', + }, + { + checked: 'on', + label: 'Boolean', + 'data-test-subj': 'indexDetailsMappingsSelectFilter-boolean', + }, + { + label: 'Keyword', + 'data-test-subj': 'indexDetailsMappingsSelectFilter-keyword', + }, + { + label: 'Text', + 'data-test-subj': 'indexDetailsMappingsSelectFilter-text', + }, + ], + selectedDataTypes: ['Boolean'], + }, + inferenceToModelIdMap: {}, + }; + test('returns list of matching fields with search term', () => { + expect(getFieldsMatchingFilterFromState(sampleState, ['Boolean'])).toEqual({ + '4459e8f2-3bec-4d17-b50c-f62d1fcbcca0': { + id: '4459e8f2-3bec-4d17-b50c-f62d1fcbcca0', + parentId: 'dd0dd3aa-52c9-472b-a23d-ecec0a1b2420', + nestedDepth: 1, + isMultiField: false, + path: ['multifield', 'flag'], + source: { + name: 'flag', + type: 'boolean', + }, + childFieldsName: 'fields', + canHaveChildFields: false, + hasChildFields: false, + canHaveMultiFields: true, + 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 f432cfdead2fa5..cb95da745a30d2 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 @@ -19,6 +19,7 @@ import { NormalizedField, NormalizedFields, NormalizedRuntimeFields, + State, ParameterName, RuntimeFields, SubType, @@ -602,3 +603,85 @@ export const deNormalizeRuntimeFields = (fields: NormalizedRuntimeFields): Runti }; }, {} as RuntimeFields); }; + +/** + * get all the fields from given state which matches selected DataTypes from filter + * + * @param state The state that we are using depending on the context (when adding new fields, static state is used) + * @param filteredDataTypes data types array from which fields are filtered from given state + */ + +export const getFieldsMatchingFilterFromState = ( + state: State, + filteredDataTypes: string[] +): { + [id: string]: NormalizedField; +} => { + return Object.fromEntries( + Object.entries(state.fields.byId).filter(([_, fieldId]) => + filteredDataTypes.includes(TYPE_DEFINITION[state.fields.byId[fieldId.id].source.type].label) + ) + ); +}; + +/** accepts Generics argument and returns value, if value is not null or undefined + * @param value + */ +function isNotNullish(value: T | null | undefined): value is T { + return value !== null && value !== undefined; +} + +/** returns normalized field that matches the dataTypes from the filteredDataTypes array + * @param normalizedFields fields that we are using, depending on the context (when adding new fields, static state is used) + * @param filteredDataTypes data types array from which fields are filtered from given state. When there are no filter selected, array would be undefined + */ +export const getFieldsFromState = ( + normalizedFields: NormalizedFields, + filteredDataTypes?: string[] +): NormalizedField[] => { + const getField = (fieldId: string) => { + if (filteredDataTypes) { + if ( + filteredDataTypes.includes( + TYPE_DEFINITION[normalizedFields.byId[fieldId].source.type].label + ) + ) { + return normalizedFields.byId[fieldId]; + } + } else { + return normalizedFields.byId[fieldId]; + } + }; + const fields: Array = filteredDataTypes + ? Object.entries(normalizedFields.byId).map(([key, _]) => getField(key)) + : normalizedFields.rootLevelFields.map((id) => getField(id)); + return fields.filter(isNotNullish); +}; +/** + * returns true if given value is first occurence of array + * useful when filtering unique values of an array + */ +function filterUnique(value: T, index: number, array: T[]) { + return array.indexOf(value) === index; +} +/** + * returns array consisting of all field types from state's fields including nested fields + * @param fields + */ +const getallFieldsIncludingNestedFields = (fields: Fields, fieldsArray: DataType[]) => { + const fieldsValue = Object.values(fields); + for (const field of fieldsValue) { + if (field.type) fieldsArray.push(field.type); + if (field.fields) getallFieldsIncludingNestedFields(field.fields, fieldsArray); + if (field.properties) getallFieldsIncludingNestedFields(field.properties, fieldsArray); + } + return fieldsArray; +}; + +/** returns all field types from the fields, including multifield and child fields + * @param allFields fields from state + */ +export const getAllFieldTypesFromState = (allFields: Fields): DataType[] => { + const fields: DataType[] = []; + return getallFieldsIncludingNestedFields(allFields, fields).filter(filterUnique); +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx index ac41436f26c9dc..2b132d377918d5 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state_context.tsx @@ -54,6 +54,11 @@ export const StateProvider: React.FC<{ children?: React.ReactNode }> = ({ childr term: '', result: [], }, + filter: { + filteredFields: [], + selectedOptions: [], + selectedDataTypes: [], + }, inferenceToModelIdMap: {}, }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts index b2b8deeff5555a..0e972e9900b9cb 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts @@ -9,6 +9,8 @@ import { PARAMETERS_DEFINITION } from './constants'; import { getAllChildFields, getFieldMeta, + getFieldsFromState, + getFieldsMatchingFilterFromState, getMaxNestedDepth, getUniqueId, normalize, @@ -205,6 +207,11 @@ export const reducer = (state: State, action: Action): State => { term: '', result: [], }, + filter: { + filteredFields: action.value.filter.filteredFields, + selectedOptions: action.value.filter.selectedOptions, + selectedDataTypes: action.value.filter.selectedDataTypes, + }, }; } case 'configuration.update': { @@ -604,7 +611,12 @@ export const reducer = (state: State, action: Action): State => { ...state, search: { term: action.value, - result: searchFields(action.value, state.fields.byId), + result: searchFields( + action.value, + state.filter.selectedDataTypes.length > 0 + ? getFieldsMatchingFilterFromState(state, state.filter.selectedDataTypes) + : state.fields.byId + ), }, }; } @@ -614,6 +626,22 @@ export const reducer = (state: State, action: Action): State => { isValid: action.value, }; } + case 'filter:update': { + const selectedDataTypes: string[] = action.value.selectedOptions + .filter((option) => option.checked === 'on') + .map((option) => option.label); + return { + ...state, + filter: { + filteredFields: getFieldsFromState( + state.fields, + selectedDataTypes.length > 0 ? selectedDataTypes : undefined + ), + selectedOptions: action.value.selectedOptions, + selectedDataTypes, + }, + }; + } case 'inferenceToModelIdMap.update': { return { ...state, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts index 555709008b8dd5..d08c80835c6201 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts @@ -5,10 +5,12 @@ * 2.0. */ +import { EuiSelectableOption } from '@elastic/eui'; import { InferenceToModelIdMap } from '../components/document_fields/fields'; import { FormHook, OnFormUpdateArg, RuntimeField } from '../shared_imports'; import { Field, + NormalizedField, NormalizedFields, NormalizedRuntimeField, NormalizedRuntimeFields, @@ -94,6 +96,11 @@ export interface State { format(): MappingsFields; isValid: boolean; }; + filter: { + filteredFields: NormalizedField[]; + selectedOptions: EuiSelectableOption[]; + selectedDataTypes: string[]; + }; search: { term: string; result: SearchResult[]; @@ -130,6 +137,7 @@ export type Action = | { type: 'runtimeField.edit'; value: NormalizedRuntimeField } | { type: 'fieldsJsonEditor.update'; value: { json: { [key: string]: any }; isValid: boolean } } | { type: 'search:update'; value: string } - | { type: 'validity:update'; value: boolean }; + | { type: 'validity:update'; value: boolean } + | { type: 'filter:update'; value: { selectedOptions: EuiSelectableOption[] } }; export type Dispatch = (action: Action) => void; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx index 889ccf1f7ffafd..1bd58bcc8e5f09 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx @@ -7,6 +7,7 @@ import { useEffect, useMemo } from 'react'; +import { EuiSelectableOption } from '@elastic/eui'; import { DocumentFieldsStatus, Field, @@ -22,8 +23,11 @@ import { stripUndefinedValues, normalizeRuntimeFields, deNormalizeRuntimeFields, + getAllFieldTypesFromState, + getFieldsFromState, } from './lib'; import { useMappingsState, useDispatch } from './mappings_state_context'; +import { TYPE_DEFINITION } from './constants'; interface Args { onChange?: OnUpdateHandler; @@ -47,6 +51,14 @@ export const useMappingsStateListener = ({ onChange, value, status }: Args) => { () => normalizeRuntimeFields(runtimeFields), [runtimeFields] ); + const fieldTypesOptions: EuiSelectableOption[] = useMemo(() => { + const allFieldsTypes = getAllFieldTypesFromState(deNormalize(normalize(mappedFields))); + return allFieldsTypes.map((dataType) => ({ + checked: undefined, + label: TYPE_DEFINITION[dataType].label, + 'data-test-subj': `indexDetailsMappingsSelectFilter-${dataType}`, + })); + }, [mappedFields]); const calculateStatus = (fieldStatus: string | undefined, rootLevelFields: string | any[]) => { if (fieldStatus) return fieldStatus; @@ -163,7 +175,19 @@ export const useMappingsStateListener = ({ onChange, value, status }: Args) => { editor: 'default', }, runtimeFields: parsedRuntimeFieldsDefaultValue, + filter: { + selectedOptions: fieldTypesOptions, + filteredFields: getFieldsFromState(parsedFieldsDefaultValue), + selectedDataTypes: [], + }, }, }); - }, [value, parsedFieldsDefaultValue, dispatch, status, parsedRuntimeFieldsDefaultValue]); + }, [ + value, + parsedFieldsDefaultValue, + dispatch, + status, + parsedRuntimeFieldsDefaultValue, + fieldTypesOptions, + ]); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_filter_fields.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_filter_fields.tsx new file mode 100644 index 00000000000000..41c4e4e95c1866 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_filter_fields.tsx @@ -0,0 +1,160 @@ +/* + * 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 { + EuiFilterButton, + EuiFilterGroup, + EuiPopover, + EuiPopoverTitle, + EuiSelectable, + EuiSelectableOption, +} from '@elastic/eui'; +import React, { useCallback, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { useDispatch } from '../../../../components/mappings_editor/mappings_state_context'; +import { State } from '../../../../components/mappings_editor/types'; +import { + getFieldsFromState, + getFieldsMatchingFilterFromState, + searchFields, +} from '../../../../components/mappings_editor/lib'; + +interface Props { + isAddingFields: boolean; + isJSONVisible: boolean; + previousState: State; + setPreviousState: (state: State) => void; + state: State; +} +export const MappingsFilter: React.FC = ({ + isAddingFields, + isJSONVisible, + previousState, + setPreviousState, + state, +}) => { + const [isFilterByPopoverVisible, setIsFilterPopoverVisible] = useState(false); + const dispatch = useDispatch(); + const setSelectedOptions = useCallback( + (options) => { + dispatch({ + type: 'filter:update', + value: { + selectedOptions: options, + }, + }); + dispatch({ + type: 'search:update', + value: state.search.term, + }); + }, + [dispatch, state.search.term] + ); + const setPreviousStateSelectedOptions = useCallback( + (options: EuiSelectableOption[]) => { + const selectedDataTypes: string[] = options + .filter((option) => option.checked === 'on') + .map((option) => option.label); + + setPreviousState({ + ...previousState, + filter: { + filteredFields: getFieldsFromState( + previousState.fields, + selectedDataTypes.length > 0 ? selectedDataTypes : undefined + ), + selectedOptions: options, + selectedDataTypes, + }, + search: { + term: previousState.search.term, + result: searchFields( + previousState.search.term, + selectedDataTypes.length > 0 + ? getFieldsMatchingFilterFromState(previousState, selectedDataTypes) + : previousState.fields.byId + ), + }, + }); + }, + [previousState, setPreviousState] + ); + const filterByFieldTypeButton = ( + setIsFilterPopoverVisible(!isFilterByPopoverVisible)} + numFilters={ + !isAddingFields + ? state.filter.selectedOptions.length + : previousState.filter.selectedOptions.length + } + hasActiveFilters={ + !isAddingFields + ? state.filter.selectedDataTypes.length > 0 + : previousState.filter.selectedDataTypes.length > 0 + } + numActiveFilters={ + !isAddingFields + ? state.filter.selectedDataTypes.length + : previousState.filter.selectedDataTypes.length + } + isSelected={isFilterByPopoverVisible} + data-test-subj="indexDetailsMappingsFilterByFieldTypeButton" + > + {i18n.translate('xpack.idxMgmt.indexDetails.mappings.filterByFieldType.button', { + defaultMessage: 'Field types', + })} + + ); + return ( + + setIsFilterPopoverVisible(!isFilterByPopoverVisible)} + anchorPosition="downCenter" + data-test-subj="indexDetailsMappingsFilter" + > + { + if (!isAddingFields) { + setSelectedOptions(options); + } else { + setPreviousStateSelectedOptions(options); + } + }} + > + {(list, search) => ( +
+ + {search} + + {list} +
+ )} +
+
+
+ ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_mappings_content.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_mappings_content.tsx index a5f5cccd025d42..71181796f6cbfc 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_mappings_content.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_mappings_content.tsx @@ -42,10 +42,12 @@ import { useMappingsState, } from '../../../../components/mappings_editor/mappings_state_context'; import { - NormalizedField, - NormalizedFields, - State, -} from '../../../../components/mappings_editor/types'; + getFieldsFromState, + getFieldsMatchingFilterFromState, +} from '../../../../components/mappings_editor/lib'; +import { NormalizedFields, State } from '../../../../components/mappings_editor/types'; +import { MappingsFilter } from './details_page_filter_fields'; + import { useMappingsStateListener } from '../../../../components/mappings_editor/use_state_listener'; import { documentationService } from '../../../../services'; import { updateIndexMappings } from '../../../../services/api'; @@ -54,16 +56,6 @@ import { SemanticTextBanner } from './semantic_text_banner'; import { TrainedModelsDeploymentModal } from './trained_models_deployment_modal'; import { parseMappings } from '../../../../shared/parse_mappings'; -const getFieldsFromState = (state: State) => { - const getField = (fieldId: string) => { - return state.fields.byId[fieldId]; - }; - const fields = () => { - return state.fields.rootLevelFields.map((id) => getField(id)); - }; - return fields(); -}; - export const DetailsPageMappingsContent: FunctionComponent<{ index: Index; data: string; @@ -111,9 +103,13 @@ export const DetailsPageMappingsContent: FunctionComponent<{ }, [state.fields.byId]); const [previousState, setPreviousState] = useState(state); - const [previousStateFields, setPreviousStateFields] = useState( - getFieldsFromState(state) - ); + + const previousStateSelectedDataTypes: string[] = useMemo(() => { + return previousState.filter.selectedOptions + .filter((option) => option.checked === 'on') + .map((option) => option.label); + }, [previousState.filter.selectedOptions]); + const [saveMappingError, setSaveMappingError] = useState(undefined); const [isJSONVisible, setIsJSONVisible] = useState(false); const onToggleChange = () => { @@ -151,7 +147,6 @@ export const DetailsPageMappingsContent: FunctionComponent<{ setAddingFields(!isAddingFields); // when adding new field, save previous state. This state is then used by FieldsList component to show only saved mappings. - setPreviousStateFields(getFieldsFromState(state)); setPreviousState(state); // reset mappings and change status to create field. @@ -160,6 +155,11 @@ export const DetailsPageMappingsContent: FunctionComponent<{ value: { ...state, fields: { ...state.fields, byId: {}, rootLevelFields: [] } as NormalizedFields, + filter: { + filteredFields: [], + selectedOptions: [], + selectedDataTypes: [], + }, documentFields: { status: 'creatingField', editor: 'default', @@ -235,16 +235,39 @@ export const DetailsPageMappingsContent: FunctionComponent<{ ...previousState, search: { term: value, - result: searchFields(value, previousState.fields.byId), + result: searchFields( + value, + previousStateSelectedDataTypes.length > 0 + ? getFieldsMatchingFilterFromState(previousState, previousStateSelectedDataTypes) + : previousState.fields.byId + ), }, }); } else { dispatch({ type: 'search:update', value }); } }, - [dispatch, previousState, isAddingFields] + [dispatch, previousState, isAddingFields, previousStateSelectedDataTypes] ); + const onClearSearch = useCallback(() => { + setPreviousState({ + ...previousState, + search: { + term: '', + result: searchFields( + '', + previousState.filter.selectedDataTypes.length > 0 + ? getFieldsMatchingFilterFromState( + previousState, + previousState.filter.selectedDataTypes + ) + : previousState.fields.byId + ), + }, + }); + }, [previousState]); + const searchTerm = isAddingFields ? previousState.search.term.trim() : state.search.term.trim(); const jsonBlock = ( @@ -263,6 +286,7 @@ export const DetailsPageMappingsContent: FunctionComponent<{ ) : ( @@ -270,13 +294,25 @@ export const DetailsPageMappingsContent: FunctionComponent<{ const fieldsListComponent = isAddingFields ? ( 0 + ? previousState.filter.filteredFields + : getFieldsFromState(previousState.fields) + } state={previousState} setPreviousState={setPreviousState} isAddingFields={isAddingFields} /> ) : ( - + 0 + ? state.filter.filteredFields + : getFieldsFromState(state.fields) + } + state={state} + isAddingFields={isAddingFields} + /> ); const fieldSearchComponent = isAddingFields ? ( + + + {fieldSearchComponent} {!index.hidden && ( diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 9e8dfe0b1eb985..27d158dfb130f4 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -254,7 +254,7 @@ class TimeseriesChartIntl extends Component { } this.rowMouseenterSubscriber = mlTableService.rowMouseenter$.subscribe( - tableRecordMousenterListener + tableRecordMousenterListener.bind(this) ); this.rowMouseleaveSubscriber = mlTableService.rowMouseleave$.subscribe( tableRecordMouseleaveListener diff --git a/x-pack/plugins/observability_solution/apm/server/plugin.ts b/x-pack/plugins/observability_solution/apm/server/plugin.ts index 2c2392b8454158..7e93a5f3c33246 100644 --- a/x-pack/plugins/observability_solution/apm/server/plugin.ts +++ b/x-pack/plugins/observability_solution/apm/server/plugin.ts @@ -40,7 +40,7 @@ import { createApmSourceMapIndexTemplate } from './routes/source_maps/create_apm import { addApiKeysToEveryPackagePolicyIfMissing } from './routes/fleet/api_keys/add_api_keys_to_policies_if_missing'; import { apmTutorialCustomIntegration } from '../common/tutorial/tutorials'; import { registerAssistantFunctions } from './assistant_functions'; -import { getAlertDetailsContextHandler } from './routes/assistant_functions/get_observability_alert_details_context/get_alert_details_context_handler'; +import { getAlertDetailsContextHandler } from './routes/assistant_functions/get_observability_alert_details_context'; export class APMPlugin implements Plugin diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_changepoints/index.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_changepoints/index.ts index 2ffbdc30a1c525..99ea7ec691dabe 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_changepoints/index.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_changepoints/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import moment from 'moment'; import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; import { ApmTimeseriesType, getApmTimeseries, TimeseriesChangePoint } from '../get_apm_timeseries'; @@ -18,14 +17,16 @@ export interface ChangePointGrouping { export async function getServiceChangePoints({ apmEventClient, - alertStartedAt, + start, + end, serviceName, serviceEnvironment, transactionType, transactionName, }: { apmEventClient: APMEventClient; - alertStartedAt: string; + start: string; + end: string; serviceName: string | undefined; serviceEnvironment: string | undefined; transactionType: string | undefined; @@ -38,8 +39,8 @@ export async function getServiceChangePoints({ const res = await getApmTimeseries({ apmEventClient, arguments: { - start: moment(alertStartedAt).subtract(12, 'hours').toISOString(), - end: alertStartedAt, + start, + end, stats: [ { title: 'Latency', @@ -95,12 +96,14 @@ export async function getServiceChangePoints({ export async function getExitSpanChangePoints({ apmEventClient, - alertStartedAt, + start, + end, serviceName, serviceEnvironment, }: { apmEventClient: APMEventClient; - alertStartedAt: string; + start: string; + end: string; serviceName: string | undefined; serviceEnvironment: string | undefined; }): Promise { @@ -111,8 +114,8 @@ export async function getExitSpanChangePoints({ const res = await getApmTimeseries({ apmEventClient, arguments: { - start: moment(alertStartedAt).subtract(30, 'minute').toISOString(), - end: alertStartedAt, + start, + end, stats: [ { title: 'Exit span latency', diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_log_categories/index.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_log_categories/index.ts index 990b63f412f764..e3e570c05e13ed 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_log_categories/index.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_log_categories/index.ts @@ -17,13 +17,11 @@ import { } from '../../../../common/es_fields/apm'; import { getTypedSearch } from '../../../utils/create_typed_es_client'; -export type LogCategories = - | Array<{ - errorCategory: string; - docCount: number; - sampleMessage: string; - }> - | undefined; +export interface LogCategory { + errorCategory: string; + docCount: number; + sampleMessage: string; +} export async function getLogCategories({ esClient, @@ -40,7 +38,7 @@ export async function getLogCategories({ 'container.id'?: string; 'kubernetes.pod.name'?: string; }; -}): Promise { +}): Promise { const start = datemath.parse(args.start)?.valueOf()!; const end = datemath.parse(args.end)?.valueOf()!; diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_alert_details_context_handler.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_alert_details_context_handler.ts deleted file mode 100644 index cd1a56d56f45e4..00000000000000 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_alert_details_context_handler.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Logger } from '@kbn/core/server'; -import { - AlertDetailsContextualInsightsHandlerQuery, - AlertDetailsContextualInsightsRequestContext, -} from '@kbn/observability-plugin/server/services'; -import { getApmAlertsClient } from '../../../lib/helpers/get_apm_alerts_client'; -import { getApmEventClient } from '../../../lib/helpers/get_apm_event_client'; -import { getMlClient } from '../../../lib/helpers/get_ml_client'; -import { getRandomSampler } from '../../../lib/helpers/get_random_sampler'; -import { getObservabilityAlertDetailsContext } from '.'; -import { APMRouteHandlerResources } from '../../apm_routes/register_apm_server_routes'; - -export const getAlertDetailsContextHandler = ( - resourcePlugins: APMRouteHandlerResources['plugins'], - logger: Logger -) => { - return async ( - requestContext: AlertDetailsContextualInsightsRequestContext, - query: AlertDetailsContextualInsightsHandlerQuery - ) => { - const resources = { - getApmIndices: async () => { - const coreContext = await requestContext.core; - return resourcePlugins.apmDataAccess.setup.getApmIndices(coreContext.savedObjects.client); - }, - request: requestContext.request, - params: { query: { _inspect: false } }, - plugins: resourcePlugins, - context: { - core: requestContext.core, - licensing: requestContext.licensing, - alerting: resourcePlugins.alerting!.start().then((startContract) => { - return { - getRulesClient() { - return startContract.getRulesClientWithRequest(requestContext.request); - }, - }; - }), - rac: resourcePlugins.ruleRegistry.start().then((startContract) => { - return { - getAlertsClient() { - return startContract.getRacClientWithRequest(requestContext.request); - }, - }; - }), - }, - }; - - const [apmEventClient, annotationsClient, apmAlertsClient, coreContext, mlClient] = - await Promise.all([ - getApmEventClient(resources), - resourcePlugins.observability.setup.getScopedAnnotationsClient( - resources.context, - requestContext.request - ), - getApmAlertsClient(resources), - requestContext.core, - getMlClient(resources), - getRandomSampler({ - security: resourcePlugins.security, - probability: 1, - request: requestContext.request, - }), - ]); - const esClient = coreContext.elasticsearch.client.asCurrentUser; - - return getObservabilityAlertDetailsContext({ - coreContext, - apmEventClient, - annotationsClient, - apmAlertsClient, - mlClient, - esClient, - query, - logger, - }); - }; -}; diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_apm_alert_details_context_prompt.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_apm_alert_details_context_prompt.ts deleted file mode 100644 index 4a28a0460ebbdd..00000000000000 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_apm_alert_details_context_prompt.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { isEmpty } from 'lodash'; -import { AlertDetailsContextualInsight } from '@kbn/observability-plugin/server/services'; -import { APMDownstreamDependency } from '../get_apm_downstream_dependencies'; -import { ServiceSummary } from '../get_apm_service_summary'; -import { LogCategories } from '../get_log_categories'; -import { ApmAnomalies } from '../get_apm_service_summary/get_anomalies'; -import { ChangePointGrouping } from '../get_changepoints'; - -export function getApmAlertDetailsContextPrompt({ - serviceName, - serviceEnvironment, - serviceSummary, - downstreamDependencies, - logCategories, - serviceChangePoints, - exitSpanChangePoints, - anomalies, -}: { - serviceName?: string; - serviceEnvironment?: string; - serviceSummary?: ServiceSummary; - downstreamDependencies?: APMDownstreamDependency[]; - logCategories: LogCategories; - serviceChangePoints?: ChangePointGrouping[]; - exitSpanChangePoints?: ChangePointGrouping[]; - anomalies?: ApmAnomalies; -}): AlertDetailsContextualInsight[] { - const prompt: AlertDetailsContextualInsight[] = []; - if (!isEmpty(serviceSummary)) { - prompt.push({ - key: 'serviceSummary', - description: 'Metadata for the service where the alert occurred', - data: serviceSummary, - }); - } - - if (!isEmpty(downstreamDependencies)) { - prompt.push({ - key: 'downstreamDependencies', - description: `Downstream dependencies from the service "${serviceName}". Problems in these services can negatively affect the performance of "${serviceName}"`, - data: downstreamDependencies, - }); - } - - if (!isEmpty(serviceChangePoints)) { - prompt.push({ - key: 'serviceChangePoints', - description: `Significant change points for "${serviceName}". Use this to spot dips and spikes in throughput, latency and failure rate`, - data: serviceChangePoints, - }); - } - - if (!isEmpty(exitSpanChangePoints)) { - prompt.push({ - key: 'exitSpanChangePoints', - description: `Significant change points for the dependencies of "${serviceName}". Use this to spot dips or spikes in throughput, latency and failure rate for downstream dependencies`, - data: exitSpanChangePoints, - }); - } - - if (!isEmpty(logCategories)) { - prompt.push({ - key: 'logCategories', - description: `Log events occurring around the time of the alert`, - data: logCategories, - }); - } - - if (!isEmpty(anomalies)) { - prompt.push({ - key: 'anomalies', - description: `Anomalies for services running in the environment "${serviceEnvironment}"`, - data: anomalies, - }); - } - - return prompt; -} diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_container_id_from_signals.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_container_id_from_signals.ts index 22679dd55ded02..638903e8135452 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_container_id_from_signals.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_container_id_from_signals.ts @@ -12,7 +12,7 @@ import { rangeQuery, typedSearch } from '@kbn/observability-plugin/server/utils/ import * as t from 'io-ts'; import moment from 'moment'; import { ESSearchRequest } from '@kbn/es-types'; -import { observabilityAlertDetailsContextRt } from '@kbn/observability-plugin/server/services'; +import { alertDetailsContextRt } from '@kbn/observability-plugin/server/services'; import { ApmDocumentType } from '../../../../common/document_type'; import { APMEventClient, @@ -26,7 +26,7 @@ export async function getContainerIdFromSignals({ coreContext, apmEventClient, }: { - query: t.TypeOf; + query: t.TypeOf; esClient: ElasticsearchClient; coreContext: Pick; apmEventClient: APMEventClient; diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_service_name_from_signals.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_service_name_from_signals.ts index bd62b998bee994..284c286766c769 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_service_name_from_signals.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_service_name_from_signals.ts @@ -12,7 +12,7 @@ import { rangeQuery, termQuery, typedSearch } from '@kbn/observability-plugin/se import * as t from 'io-ts'; import moment from 'moment'; import { ESSearchRequest } from '@kbn/es-types'; -import { observabilityAlertDetailsContextRt } from '@kbn/observability-plugin/server/services'; +import { alertDetailsContextRt } from '@kbn/observability-plugin/server/services'; import { ApmDocumentType } from '../../../../common/document_type'; import { APMEventClient, @@ -26,7 +26,7 @@ export async function getServiceNameFromSignals({ coreContext, apmEventClient, }: { - query: t.TypeOf; + query: t.TypeOf; esClient: ElasticsearchClient; coreContext: Pick; apmEventClient: APMEventClient; diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/index.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/index.ts index d6022876c9f3b9..85032b2f05b531 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/index.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/index.ts @@ -5,176 +5,236 @@ * 2.0. */ -import type { ScopedAnnotationsClient } from '@kbn/observability-plugin/server'; -import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -import type { CoreRequestHandlerContext, Logger } from '@kbn/core/server'; +import { Logger } from '@kbn/core/server'; import { - AlertDetailsContextualInsight, AlertDetailsContextualInsightsHandlerQuery, + AlertDetailsContextualInsightsRequestContext, } from '@kbn/observability-plugin/server/services'; import moment from 'moment'; -import type { MlClient } from '../../../lib/helpers/get_ml_client'; -import type { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; -import type { ApmAlertsClient } from '../../../lib/helpers/get_apm_alerts_client'; +import { isEmpty } from 'lodash'; +import { getApmAlertsClient } from '../../../lib/helpers/get_apm_alerts_client'; +import { getApmEventClient } from '../../../lib/helpers/get_apm_event_client'; +import { getMlClient } from '../../../lib/helpers/get_ml_client'; +import { getRandomSampler } from '../../../lib/helpers/get_random_sampler'; import { getApmServiceSummary } from '../get_apm_service_summary'; import { getAssistantDownstreamDependencies } from '../get_apm_downstream_dependencies'; import { getLogCategories } from '../get_log_categories'; import { getAnomalies } from '../get_apm_service_summary/get_anomalies'; import { getServiceNameFromSignals } from './get_service_name_from_signals'; import { getContainerIdFromSignals } from './get_container_id_from_signals'; -import { getApmAlertDetailsContextPrompt } from './get_apm_alert_details_context_prompt'; import { getExitSpanChangePoints, getServiceChangePoints } from '../get_changepoints'; +import { APMRouteHandlerResources } from '../../apm_routes/register_apm_server_routes'; -export async function getObservabilityAlertDetailsContext({ - coreContext, - annotationsClient, - apmAlertsClient, - apmEventClient, - esClient, - logger, - mlClient, - query, -}: { - coreContext: Pick; - annotationsClient?: ScopedAnnotationsClient; - apmAlertsClient: ApmAlertsClient; - apmEventClient: APMEventClient; - esClient: ElasticsearchClient; - logger: Logger; - mlClient?: MlClient; - query: AlertDetailsContextualInsightsHandlerQuery; -}): Promise { - const alertStartedAt = query.alert_started_at; - const serviceEnvironment = query['service.environment']; - const hostName = query['host.name']; - const kubernetesPodName = query['kubernetes.pod.name']; - const [serviceName, containerId] = await Promise.all([ - getServiceNameFromSignals({ - query, - esClient, - coreContext, - apmEventClient, - }), - getContainerIdFromSignals({ - query, - esClient, - coreContext, - apmEventClient, - }), - ]); +export const getAlertDetailsContextHandler = ( + resourcePlugins: APMRouteHandlerResources['plugins'], + logger: Logger +) => { + return async ( + requestContext: AlertDetailsContextualInsightsRequestContext, + query: AlertDetailsContextualInsightsHandlerQuery + ) => { + const resources = { + getApmIndices: async () => { + const coreContext = await requestContext.core; + return resourcePlugins.apmDataAccess.setup.getApmIndices(coreContext.savedObjects.client); + }, + request: requestContext.request, + params: { query: { _inspect: false } }, + plugins: resourcePlugins, + context: { + core: requestContext.core, + licensing: requestContext.licensing, + alerting: resourcePlugins.alerting!.start().then((startContract) => { + return { + getRulesClient() { + return startContract.getRulesClientWithRequest(requestContext.request); + }, + }; + }), + rac: resourcePlugins.ruleRegistry.start().then((startContract) => { + return { + getAlertsClient() { + return startContract.getRacClientWithRequest(requestContext.request); + }, + }; + }), + }, + }; + + const [apmEventClient, annotationsClient, apmAlertsClient, coreContext, mlClient] = + await Promise.all([ + getApmEventClient(resources), + resourcePlugins.observability.setup.getScopedAnnotationsClient( + resources.context, + requestContext.request + ), + getApmAlertsClient(resources), + requestContext.core, + getMlClient(resources), + getRandomSampler({ + security: resourcePlugins.security, + probability: 1, + request: requestContext.request, + }), + ]); + const esClient = coreContext.elasticsearch.client.asCurrentUser; + + const alertStartedAt = query.alert_started_at; + const serviceEnvironment = query['service.environment']; + const hostName = query['host.name']; + const kubernetesPodName = query['kubernetes.pod.name']; + const [serviceName, containerId] = await Promise.all([ + getServiceNameFromSignals({ + query, + esClient, + coreContext, + apmEventClient, + }), + getContainerIdFromSignals({ + query, + esClient, + coreContext, + apmEventClient, + }), + ]); - async function handleError(cb: () => Promise): Promise { - try { - return await cb(); - } catch (error) { - logger.error('Error while fetching observability alert details context'); - logger.error(error); - return; + async function handleError(cb: () => Promise): Promise { + try { + return await cb(); + } catch (error) { + logger.error('Error while fetching observability alert details context'); + logger.error(error); + return; + } } - } - const serviceSummaryPromise = serviceName - ? handleError(() => - getApmServiceSummary({ - apmEventClient, - annotationsClient, - esClient, - apmAlertsClient, - mlClient, - logger, - arguments: { - 'service.name': serviceName, - 'service.environment': serviceEnvironment, - start: moment(alertStartedAt).subtract(5, 'minute').toISOString(), - end: alertStartedAt, - }, - }) - ) - : undefined; + const serviceSummaryPromise = serviceName + ? handleError(() => + getApmServiceSummary({ + apmEventClient, + annotationsClient, + esClient, + apmAlertsClient, + mlClient, + logger, + arguments: { + 'service.name': serviceName, + 'service.environment': serviceEnvironment, + start: moment(alertStartedAt).subtract(5, 'minute').toISOString(), + end: alertStartedAt, + }, + }) + ) + : undefined; - const downstreamDependenciesPromise = serviceName - ? handleError(() => - getAssistantDownstreamDependencies({ - apmEventClient, - arguments: { - 'service.name': serviceName, - 'service.environment': serviceEnvironment, - start: moment(alertStartedAt).subtract(5, 'minute').toISOString(), - end: alertStartedAt, - }, - }) - ) - : undefined; + const downstreamDependenciesPromise = serviceName + ? handleError(() => + getAssistantDownstreamDependencies({ + apmEventClient, + arguments: { + 'service.name': serviceName, + 'service.environment': serviceEnvironment, + start: moment(alertStartedAt).subtract(15, 'minute').toISOString(), + end: alertStartedAt, + }, + }) + ) + : undefined; - const logCategoriesPromise = handleError(() => - getLogCategories({ - esClient, - coreContext, - arguments: { - start: moment(alertStartedAt).subtract(5, 'minute').toISOString(), - end: alertStartedAt, - 'service.name': serviceName, - 'host.name': hostName, - 'container.id': containerId, - 'kubernetes.pod.name': kubernetesPodName, - }, - }) - ); + const logCategoriesPromise = handleError(() => + getLogCategories({ + esClient, + coreContext, + arguments: { + start: moment(alertStartedAt).subtract(15, 'minute').toISOString(), + end: alertStartedAt, + 'service.name': serviceName, + 'host.name': hostName, + 'container.id': containerId, + 'kubernetes.pod.name': kubernetesPodName, + }, + }) + ); - const serviceChangePointsPromise = handleError(() => - getServiceChangePoints({ - apmEventClient, - alertStartedAt, - serviceName, - serviceEnvironment, - transactionType: query['transaction.type'], - transactionName: query['transaction.name'], - }) - ); + const serviceChangePointsPromise = handleError(() => + getServiceChangePoints({ + apmEventClient, + start: moment(alertStartedAt).subtract(6, 'hours').toISOString(), + end: alertStartedAt, + serviceName, + serviceEnvironment, + transactionType: query['transaction.type'], + transactionName: query['transaction.name'], + }) + ); - const exitSpanChangePointsPromise = handleError(() => - getExitSpanChangePoints({ - apmEventClient, - alertStartedAt, - serviceName, - serviceEnvironment, - }) - ); + const exitSpanChangePointsPromise = handleError(() => + getExitSpanChangePoints({ + apmEventClient, + start: moment(alertStartedAt).subtract(6, 'hours').toISOString(), + end: alertStartedAt, + serviceName, + serviceEnvironment, + }) + ); - const anomaliesPromise = handleError(() => - getAnomalies({ - start: moment(alertStartedAt).subtract(1, 'hour').valueOf(), - end: moment(alertStartedAt).valueOf(), - environment: serviceEnvironment, - mlClient, - logger, - }) - ); + const anomaliesPromise = handleError(() => + getAnomalies({ + start: moment(alertStartedAt).subtract(1, 'hour').valueOf(), + end: moment(alertStartedAt).valueOf(), + environment: serviceEnvironment, + mlClient, + logger, + }) + ); - const [ - serviceSummary, - downstreamDependencies, - logCategories, - serviceChangePoints, - exitSpanChangePoints, - anomalies, - ] = await Promise.all([ - serviceSummaryPromise, - downstreamDependenciesPromise, - logCategoriesPromise, - serviceChangePointsPromise, - exitSpanChangePointsPromise, - anomaliesPromise, - ]); + const [ + serviceSummary, + downstreamDependencies, + logCategories, + serviceChangePoints, + exitSpanChangePoints, + anomalies, + ] = await Promise.all([ + serviceSummaryPromise, + downstreamDependenciesPromise, + logCategoriesPromise, + serviceChangePointsPromise, + exitSpanChangePointsPromise, + anomaliesPromise, + ]); - return getApmAlertDetailsContextPrompt({ - serviceName, - serviceEnvironment, - serviceSummary, - downstreamDependencies, - logCategories, - serviceChangePoints, - exitSpanChangePoints, - anomalies, - }); -} + return [ + { + key: 'serviceSummary', + description: 'Metadata for the service where the alert occurred', + data: serviceSummary, + }, + { + key: 'downstreamDependencies', + description: `Downstream dependencies from the service "${serviceName}". Problems in these services can negatively affect the performance of "${serviceName}"`, + data: downstreamDependencies, + }, + { + key: 'serviceChangePoints', + description: `Significant change points for "${serviceName}". Use this to spot dips and spikes in throughput, latency and failure rate`, + data: serviceChangePoints, + }, + { + key: 'exitSpanChangePoints', + description: `Significant change points for the dependencies of "${serviceName}". Use this to spot dips or spikes in throughput, latency and failure rate for downstream dependencies`, + data: exitSpanChangePoints, + }, + { + key: 'logCategories', + description: `Log events occurring around the time of the alert`, + data: logCategories, + }, + { + key: 'anomalies', + description: `Anomalies for services running in the environment "${serviceEnvironment}"`, + data: anomalies, + }, + ].filter(({ data }) => !isEmpty(data)); + }; +}; diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/route.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/route.ts index af3dfac613bd57..68b7362f85d855 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/route.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/route.ts @@ -6,16 +6,8 @@ */ import * as t from 'io-ts'; import { omit } from 'lodash'; -import { - AlertDetailsContextualInsight, - observabilityAlertDetailsContextRt, -} from '@kbn/observability-plugin/server/services'; -import { getApmAlertsClient } from '../../lib/helpers/get_apm_alerts_client'; import { getApmEventClient } from '../../lib/helpers/get_apm_event_client'; -import { getMlClient } from '../../lib/helpers/get_ml_client'; -import { getRandomSampler } from '../../lib/helpers/get_random_sampler'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; -import { getObservabilityAlertDetailsContext } from './get_observability_alert_details_context'; import { downstreamDependenciesRouteRt, @@ -24,49 +16,6 @@ import { } from './get_apm_downstream_dependencies'; import { getApmTimeseries, getApmTimeseriesRt, type ApmTimeseries } from './get_apm_timeseries'; -const getObservabilityAlertDetailsContextRoute = createApmServerRoute({ - endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', - options: { - tags: ['access:apm'], - }, - - params: t.type({ - query: observabilityAlertDetailsContextRt, - }), - handler: async (resources): Promise<{ context: AlertDetailsContextualInsight[] }> => { - const { context, request, plugins, logger, params } = resources; - const { query } = params; - - const [apmEventClient, annotationsClient, coreContext, apmAlertsClient, mlClient] = - await Promise.all([ - getApmEventClient(resources), - plugins.observability.setup.getScopedAnnotationsClient(context, request), - context.core, - getApmAlertsClient(resources), - getMlClient(resources), - getRandomSampler({ - security: resources.plugins.security, - probability: 1, - request: resources.request, - }), - ]); - const esClient = coreContext.elasticsearch.client.asCurrentUser; - - const obsAlertContext = await getObservabilityAlertDetailsContext({ - coreContext, - annotationsClient, - apmAlertsClient, - apmEventClient, - esClient, - logger, - mlClient, - query, - }); - - return { context: obsAlertContext }; - }, -}); - const getApmTimeSeriesRoute = createApmServerRoute({ endpoint: 'POST /internal/apm/assistant/get_apm_timeseries', options: { @@ -120,6 +69,5 @@ const getDownstreamDependenciesRoute = createApmServerRoute({ export const assistantRouteRepository = { ...getApmTimeSeriesRoute, - ...getObservabilityAlertDetailsContextRoute, ...getDownstreamDependenciesRoute, }; diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details_contextual_insights.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details_contextual_insights.tsx index 1de4b4a136919e..7754badbf121d8 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details_contextual_insights.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details_contextual_insights.tsx @@ -10,6 +10,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import dedent from 'dedent'; +import { type AlertDetailsContextualInsight } from '../../../server/services'; import { useKibana } from '../../utils/kibana_react'; import { AlertData } from '../../hooks/use_fetch_alert_detail'; @@ -21,16 +22,16 @@ export function AlertDetailContextualInsights({ alert }: { alert: AlertData | nu const ObservabilityAIAssistantContextualInsight = observabilityAIAssistant?.ObservabilityAIAssistantContextualInsight; - const getPromptMessages = useCallback(async () => { + const getAlertContextMessages = useCallback(async () => { const fields = alert?.formatted.fields as Record | undefined; if (!observabilityAIAssistant || !fields || !alert) { return []; } try { - const { context } = await http.get<{ - context: Array<{ description: string; data: unknown }>; - }>('/internal/apm/assistant/alert_details_contextual_insights', { + const { alertContext } = await http.get<{ + alertContext: AlertDetailsContextualInsight[]; + }>('/internal/observability/assistant/alert_details_contextual_insights', { query: { alert_started_at: new Date(alert.formatted.start).toISOString(), @@ -47,26 +48,28 @@ export function AlertDetailContextualInsights({ alert }: { alert: AlertData | nu }, }); - const obsAlertContext = context + const obsAlertContext = alertContext .map(({ description, data }) => `${description}:\n${JSON.stringify(data, null, 2)}`) .join('\n\n'); return observabilityAIAssistant.getContextualInsightMessages({ message: `I'm looking at an alert and trying to understand why it was triggered`, instructions: dedent( - `I'm an SRE. I am looking at an alert that was triggered. I want to understand why it was triggered, what it means, and what I should do next. + `I'm an SRE. I am looking at an alert that was triggered. I want to understand why it was triggered, what it means, and what I should do next. - The following contextual information is available to help me understand the alert: + The following contextual information is available to help you understand the alert: ${obsAlertContext} Be brief and to the point. Do not list the alert details as bullet points. Refer to the contextual information provided above when relevant. - Pay specific attention to why the alert happened and what may have contributed to it. + Pay special attention to regressions in downstream dependencies like big increases or decreases in throughput, latency or failure rate + Suggest reasons why the alert happened and what may have contributed to it. ` ), }); } catch (e) { + console.error('An error occurred while fetching alert context', e); return observabilityAIAssistant.getContextualInsightMessages({ message: `I'm looking at an alert and trying to understand why it was triggered`, instructions: dedent( @@ -88,7 +91,7 @@ export function AlertDetailContextualInsights({ alert }: { alert: AlertData | nu 'xpack.observability.alertDetailContextualInsights.InsightButtonLabel', { defaultMessage: 'Help me understand this alert' } )} - messages={getPromptMessages} + messages={getAlertContextMessages} /> diff --git a/x-pack/plugins/observability_solution/observability/server/routes/assistant/route.ts b/x-pack/plugins/observability_solution/observability/server/routes/assistant/route.ts new file mode 100644 index 00000000000000..e6e04704971d22 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/server/routes/assistant/route.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; +import { alertDetailsContextRt } from '../../services'; +import { createObservabilityServerRoute } from '../create_observability_server_route'; + +const getObservabilityAlertDetailsContextRoute = createObservabilityServerRoute({ + endpoint: 'GET /internal/observability/assistant/alert_details_contextual_insights', + options: { + tags: [], + }, + params: t.type({ + query: alertDetailsContextRt, + }), + handler: async ({ dependencies, params, context, request }) => { + const alertContext = + await dependencies.assistant.alertDetailsContextualInsightsService.getAlertDetailsContext( + { + core: context.core, + licensing: context.licensing, + request, + }, + params.query + ); + + return { alertContext }; + }, +}); + +export const aiAssistantRouteRepository = { + ...getObservabilityAlertDetailsContextRoute, +}; diff --git a/x-pack/plugins/observability_solution/observability/server/routes/get_global_observability_server_route_repository.ts b/x-pack/plugins/observability_solution/observability/server/routes/get_global_observability_server_route_repository.ts index 78c4a2614b5289..1516c42f86fd10 100644 --- a/x-pack/plugins/observability_solution/observability/server/routes/get_global_observability_server_route_repository.ts +++ b/x-pack/plugins/observability_solution/observability/server/routes/get_global_observability_server_route_repository.ts @@ -5,11 +5,14 @@ * 2.0. */ +import { EndpointOf } from '@kbn/server-route-repository'; import { ObservabilityConfig } from '..'; +import { aiAssistantRouteRepository } from './assistant/route'; import { rulesRouteRepository } from './rules/route'; export function getObservabilityServerRouteRepository(config: ObservabilityConfig) { const repository = { + ...aiAssistantRouteRepository, ...rulesRouteRepository, }; return repository; @@ -18,3 +21,5 @@ export function getObservabilityServerRouteRepository(config: ObservabilityConfi export type ObservabilityServerRouteRepository = ReturnType< typeof getObservabilityServerRouteRepository >; + +export type APIEndpoint = EndpointOf; diff --git a/x-pack/plugins/observability_solution/observability/server/routes/types.ts b/x-pack/plugins/observability_solution/observability/server/routes/types.ts index a0ef6ee6c0c74b..39402531376406 100644 --- a/x-pack/plugins/observability_solution/observability/server/routes/types.ts +++ b/x-pack/plugins/observability_solution/observability/server/routes/types.ts @@ -7,12 +7,15 @@ import type { EndpointOf, ReturnOf, ServerRouteRepository } from '@kbn/server-route-repository'; import { KibanaRequest, Logger } from '@kbn/core/server'; -import { ObservabilityServerRouteRepository } from './get_global_observability_server_route_repository'; +import { + ObservabilityServerRouteRepository, + APIEndpoint, +} from './get_global_observability_server_route_repository'; import { ObservabilityRequestHandlerContext } from '../types'; import { RegisterRoutesDependencies } from './register_routes'; import { ObservabilityConfig } from '..'; -export type { ObservabilityServerRouteRepository }; +export type { ObservabilityServerRouteRepository, APIEndpoint }; export interface ObservabilityRouteHandlerResources { context: ObservabilityRequestHandlerContext; diff --git a/x-pack/plugins/observability_solution/observability/server/services/index.ts b/x-pack/plugins/observability_solution/observability/server/services/index.ts index 7c20d191440d67..840bac95ee48bb 100644 --- a/x-pack/plugins/observability_solution/observability/server/services/index.ts +++ b/x-pack/plugins/observability_solution/observability/server/services/index.ts @@ -13,9 +13,9 @@ import { SavedObjectsClientContract, } from '@kbn/core/server'; import { LicensingApiRequestHandlerContext } from '@kbn/licensing-plugin/server'; -import { concat } from 'lodash'; +import { flatten } from 'lodash'; -export const observabilityAlertDetailsContextRt = t.intersection([ +export const alertDetailsContextRt = t.intersection([ t.type({ alert_started_at: t.string, }), @@ -33,9 +33,7 @@ export const observabilityAlertDetailsContextRt = t.intersection([ }), ]); -export type AlertDetailsContextualInsightsHandlerQuery = t.TypeOf< - typeof observabilityAlertDetailsContextRt ->; +export type AlertDetailsContextualInsightsHandlerQuery = t.TypeOf; export interface AlertDetailsContextualInsight { key: string; @@ -77,11 +75,21 @@ export class AlertDetailsContextualInsightsService { context: AlertDetailsContextualInsightsRequestContext, query: AlertDetailsContextualInsightsHandlerQuery ): Promise { - if (this.handlers.length === 0) return []; + if (this.handlers.length === 0) { + return []; + } - return Promise.all(this.handlers.map((handler) => handler(context, query))).then((results) => { - const [head, ...rest] = results; - return concat(head, ...rest); - }); + const results = await Promise.all( + this.handlers.map(async (handler) => { + try { + return await handler(context, query); + } catch (error) { + console.error(`Error: Could not get alert context from handler`, error); + return []; + } + }) + ); + + return flatten(results); } } diff --git a/x-pack/plugins/observability_solution/observability/server/types.ts b/x-pack/plugins/observability_solution/observability/server/types.ts index 76a209f3180780..8c298d239a53d5 100644 --- a/x-pack/plugins/observability_solution/observability/server/types.ts +++ b/x-pack/plugins/observability_solution/observability/server/types.ts @@ -19,6 +19,7 @@ export type { ObservabilityRouteHandlerResources, AbstractObservabilityServerRouteRepository, ObservabilityServerRouteRepository, + APIEndpoint, ObservabilityAPIReturnType, } from './routes/types'; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts index f5e9ca339e9e8c..84d604e54cc6e9 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts @@ -12,6 +12,7 @@ import type { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plu import { KibanaRequest } from '@kbn/core/server'; import { aiAssistantSimulatedFunctionCalling } from '../..'; import { flushBuffer } from '../../service/util/flush_buffer'; +import { observableIntoOpenAIStream } from '../../service/util/observable_into_openai_stream'; import { observableIntoStream } from '../../service/util/observable_into_stream'; import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route'; import { screenContextRt, messageRt, functionRt } from '../runtime_types'; @@ -53,10 +54,13 @@ const chatCompleteInternalRt = t.intersection([ const chatCompletePublicRt = t.intersection([ chatCompleteBaseRt, - t.type({ + t.partial({ body: t.partial({ actions: t.array(functionRt), }), + query: t.partial({ + format: t.union([t.literal('default'), t.literal('openai')]), + }), }), ]); @@ -230,24 +234,32 @@ const publicChatCompleteRoute = createObservabilityAIAssistantServerRoute({ }, params: chatCompletePublicRt, handler: async (resources): Promise => { + const { params, logger } = resources; + const { body: { actions, ...restOfBody }, - } = resources.params; - return observableIntoStream( - await chatComplete({ - ...resources, - params: { - body: { - ...restOfBody, - screenContexts: [ - { - actions, - }, - ], - }, + query = {}, + } = params; + + const { format = 'default' } = query; + + const response$ = await chatComplete({ + ...resources, + params: { + body: { + ...restOfBody, + screenContexts: [ + { + actions, + }, + ], }, - }) - ); + }, + }); + + return format === 'openai' + ? observableIntoOpenAIStream(response$, logger) + : observableIntoStream(response$); }, }); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/observable_into_openai_stream.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/observable_into_openai_stream.ts new file mode 100644 index 00000000000000..a5e6ef2d17c918 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/observable_into_openai_stream.ts @@ -0,0 +1,91 @@ +/* + * 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 { Logger } from '@kbn/logging'; +import OpenAI from 'openai'; +import { + catchError, + concatMap, + endWith, + filter, + from, + ignoreElements, + map, + Observable, + of, +} from 'rxjs'; +import { PassThrough } from 'stream'; +import { + BufferFlushEvent, + ChatCompletionChunkEvent, + StreamingChatResponseEventType, + StreamingChatResponseEventWithoutError, + TokenCountEvent, +} from '../../../common/conversation_complete'; + +export function observableIntoOpenAIStream( + source: Observable, + logger: Logger +) { + const stream = new PassThrough(); + + source + .pipe( + filter( + (event): event is ChatCompletionChunkEvent => + event.type === StreamingChatResponseEventType.ChatCompletionChunk + ), + map((event) => { + const chunk: OpenAI.ChatCompletionChunk = { + model: 'unknown', + choices: [ + { + delta: { + content: event.message.content, + function_call: event.message.function_call, + }, + finish_reason: null, + index: 0, + }, + ], + created: new Date().getTime(), + id: event.id, + object: 'chat.completion.chunk', + }; + return JSON.stringify(chunk); + }), + catchError((error) => { + return of(JSON.stringify({ error: { message: error.message } })); + }), + endWith('[DONE]'), + concatMap((line) => { + return from( + new Promise((resolve, reject) => { + stream.write(`data: ${line}\n\n`, (err) => { + if (err) { + return reject(err); + } + resolve(); + }); + }) + ); + }), + ignoreElements() + ) + .subscribe({ + error: (error) => { + logger.error('Error writing stream'); + logger.error(JSON.stringify(error)); + stream.end(error); + }, + complete: () => { + stream.end(); + }, + }); + + return stream; +} diff --git a/x-pack/plugins/observability_solution/slo/public/components/slo/reset_confirmation_modal/slo_reset_confirmation_modal.tsx b/x-pack/plugins/observability_solution/slo/public/components/slo/reset_confirmation_modal/slo_reset_confirmation_modal.tsx index 14a925d5502e2d..fb295d709a455d 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/slo/reset_confirmation_modal/slo_reset_confirmation_modal.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/slo/reset_confirmation_modal/slo_reset_confirmation_modal.tsx @@ -14,12 +14,14 @@ export interface SloResetConfirmationModalProps { slo: SLOWithSummaryResponse | SLODefinitionResponse; onCancel: () => void; onConfirm: () => void; + isLoading?: boolean; } export function SloResetConfirmationModal({ slo, onCancel, onConfirm, + isLoading, }: SloResetConfirmationModalProps) { const { name } = slo; return ( @@ -38,6 +40,7 @@ export function SloResetConfirmationModal({ })} onCancel={onCancel} onConfirm={onConfirm} + isLoading={isLoading} > {i18n.translate('xpack.slo.resetConfirmationModal.descriptionText', { defaultMessage: 'Resetting this SLO will also regenerate the historical data.', diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_instances.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_instances.ts deleted file mode 100644 index 26367dcad3f6bc..00000000000000 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_slo_instances.ts +++ /dev/null @@ -1,55 +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 { GetSLOInstancesResponse } from '@kbn/slo-schema'; -import { useQuery } from '@tanstack/react-query'; -import { useKibana } from '../utils/kibana_react'; -import { sloKeys } from './query_key_factory'; - -export interface UseFetchSloInstancesResponse { - isInitialLoading: boolean; - isLoading: boolean; - isRefetching: boolean; - isSuccess: boolean; - isError: boolean; - data: GetSLOInstancesResponse | undefined; -} - -export function useFetchSloInstances({ sloId }: { sloId?: string }): UseFetchSloInstancesResponse { - const { http } = useKibana().services; - - const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({ - queryKey: sloKeys.detail(sloId), - queryFn: async ({ signal }) => { - try { - const response = await http.get( - `/internal/observability/slos/${sloId}/_instances`, - { - query: {}, - signal, - } - ); - - return response; - } catch (error) { - // ignore error for retrieving slos - } - }, - keepPreviousData: true, - enabled: Boolean(sloId), - refetchOnWindowFocus: false, - }); - - return { - data, - isLoading, - isInitialLoading, - isRefetching, - isSuccess, - isError, - }; -} diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_reset_slo.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_reset_slo.ts index e1a3a5a1dac1b8..18d09d58591455 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_reset_slo.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_reset_slo.ts @@ -42,8 +42,10 @@ export function useResetSlo() { }), }); }, - onSuccess: (_data, { name }) => { + onSuccess: (_data, { name, id }) => { queryClient.invalidateQueries({ queryKey: sloKeys.lists(), exact: false }); + queryClient.invalidateQueries({ queryKey: sloKeys.historicalSummaries(), exact: false }); + queryClient.invalidateQueries({ queryKey: sloKeys.details(), exact: false }); toasts.addSuccess( i18n.translate('xpack.slo.slo.reset.successNotification', { defaultMessage: '{name} reset successfully', diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/header_control.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/header_control.tsx index ddf350c0df4781..61c681fcfa8532 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/header_control.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/header_control.tsx @@ -19,10 +19,12 @@ import { SLOWithSummaryResponse } from '@kbn/slo-schema'; import React, { useCallback, useEffect, useState } from 'react'; import { paths } from '../../../../common/locators/paths'; import { SloDeleteConfirmationModal } from '../../../components/slo/delete_confirmation_modal/slo_delete_confirmation_modal'; +import { SloResetConfirmationModal } from '../../../components/slo/reset_confirmation_modal/slo_reset_confirmation_modal'; import { useCapabilities } from '../../../hooks/use_capabilities'; import { useCloneSlo } from '../../../hooks/use_clone_slo'; import { useDeleteSlo } from '../../../hooks/use_delete_slo'; import { useFetchRulesForSlo } from '../../../hooks/use_fetch_rules_for_slo'; +import { useResetSlo } from '../../../hooks/use_reset_slo'; import { useKibana } from '../../../utils/kibana_react'; import { convertSliApmParamsToApmAppDeeplinkUrl } from '../../../utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url'; import { isApmIndicatorType } from '../../../utils/slo/indicator'; @@ -45,14 +47,17 @@ export function HeaderControl({ isLoading, slo }: Props) { const hasApmReadCapabilities = capabilities.apm.show; const { hasWriteCapabilities } = useCapabilities(); - const { isDeletingSlo, removeDeleteQueryParam } = useGetQueryParams(); + const { isDeletingSlo, isResettingSlo, removeDeleteQueryParam, removeResetQueryParam } = + useGetQueryParams(); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isRuleFlyoutVisible, setRuleFlyoutVisibility] = useState(false); const [isEditRuleFlyoutOpen, setIsEditRuleFlyoutOpen] = useState(false); const [isDeleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = useState(false); + const [isResetConfirmationModalOpen, setResetConfirmationModalOpen] = useState(false); const { mutate: deleteSlo } = useDeleteSlo(); + const { mutateAsync: resetSlo, isLoading: isResetLoading } = useResetSlo(); const { data: rulesBySlo, refetchRules } = useFetchRulesForSlo({ sloIds: slo ? [slo.id] : undefined, @@ -67,7 +72,10 @@ export function HeaderControl({ isLoading, slo }: Props) { if (isDeletingSlo) { setDeleteConfirmationModalOpen(true); } - }, [isDeletingSlo]); + if (isResettingSlo) { + setResetConfirmationModalOpen(true); + } + }, [isDeletingSlo, isResettingSlo]); const onCloseRuleFlyout = () => { setRuleFlyoutVisibility(false); @@ -78,7 +86,7 @@ export function HeaderControl({ isLoading, slo }: Props) { setRuleFlyoutVisibility(true); }; - const { handleNavigateToRules, sloEditUrl, remoteDeleteUrl } = useSloActions({ + const { handleNavigateToRules, sloEditUrl, remoteDeleteUrl, remoteResetUrl } = useSloActions({ slo, rules, setIsEditRuleFlyoutOpen, @@ -119,13 +127,34 @@ export function HeaderControl({ isLoading, slo }: Props) { setDeleteConfirmationModalOpen(false); }; - const handleDeleteConfirm = async () => { + const handleDeleteConfirm = () => { if (slo) { deleteSlo({ id: slo.id, name: slo.name }); navigate(basePath.prepend(paths.slos)); } }; + const handleReset = () => { + if (!!remoteResetUrl) { + window.open(remoteResetUrl, '_blank'); + } else { + setResetConfirmationModalOpen(true); + } + }; + + const handleResetConfirm = async () => { + if (slo) { + await resetSlo({ id: slo.id, name: slo.name }); + removeResetQueryParam(); + setResetConfirmationModalOpen(false); + } + }; + + const handleResetCancel = () => { + removeResetQueryParam(); + setResetConfirmationModalOpen(false); + }; + const navigate = useCallback( (url: string) => setTimeout(() => navigateToUrl(url)), [navigateToUrl] @@ -263,6 +292,21 @@ export function HeaderControl({ isLoading, slo }: Props) { defaultMessage: 'Delete', })} {showRemoteLinkIcon} + , + + {i18n.translate('xpack.slo.slo.item.actions.reset', { + defaultMessage: 'Reset', + })} + {showRemoteLinkIcon} )} /> @@ -292,6 +336,15 @@ export function HeaderControl({ isLoading, slo }: Props) { onConfirm={handleDeleteConfirm} /> ) : null} + + {slo && isResetConfirmationModalOpen ? ( + + ) : null} ); } diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_get_query_params.ts b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_get_query_params.ts index 8e169e894c03f4..bea5080d91884b 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_get_query_params.ts +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_get_query_params.ts @@ -6,12 +6,13 @@ */ import { ALL_VALUE } from '@kbn/slo-schema'; -import { useHistory, useLocation } from 'react-router-dom'; import { useCallback } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; export const INSTANCE_SEARCH_PARAM = 'instanceId'; export const REMOTE_NAME_PARAM = 'remoteName'; export const DELETE_SLO = 'delete'; +export const RESET_SLO = 'reset'; export function useGetQueryParams() { const { search, pathname } = useLocation(); @@ -21,6 +22,7 @@ export function useGetQueryParams() { const instanceId = searchParams.get(INSTANCE_SEARCH_PARAM); const remoteName = searchParams.get(REMOTE_NAME_PARAM); const deleteSlo = searchParams.get(DELETE_SLO); + const resetSlo = searchParams.get(RESET_SLO); const removeDeleteQueryParam = useCallback(() => { const qParams = new URLSearchParams(search); @@ -35,10 +37,25 @@ export function useGetQueryParams() { } }, [deleteSlo, history, pathname, search]); + const removeResetQueryParam = useCallback(() => { + const qParams = new URLSearchParams(search); + + // remote reset param from url after initial load + if (resetSlo === 'true') { + qParams.delete(RESET_SLO); + history.replace({ + pathname, + search: qParams.toString(), + }); + } + }, [resetSlo, history, pathname, search]); + return { instanceId: !!instanceId && instanceId !== ALL_VALUE ? instanceId : undefined, remoteName, isDeletingSlo: deleteSlo === 'true', removeDeleteQueryParam, + isResettingSlo: resetSlo === 'true', + removeResetQueryParam, }; } diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_actions.ts b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_actions.ts index 2129f0947361a8..44a6b8979ecd63 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_actions.ts +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_actions.ts @@ -16,6 +16,7 @@ import { useKibana } from '../../../utils/kibana_react'; import { createRemoteSloDeleteUrl, createRemoteSloEditUrl, + createRemoteSloResetUrl, } from '../../../utils/slo/remote_slo_urls'; export const useSloActions = ({ @@ -42,6 +43,7 @@ export const useSloActions = ({ sloEditUrl: '', handleNavigateToRules: () => {}, remoteDeleteUrl: undefined, + remoteResetUrl: undefined, sloDetailsUrl: '', }; } @@ -76,6 +78,7 @@ export const useSloActions = ({ ); const remoteDeleteUrl = createRemoteSloDeleteUrl(slo, spaceId); + const remoteResetUrl = createRemoteSloResetUrl(slo, spaceId); const sloEditUrl = slo.remote ? createRemoteSloEditUrl(slo, spaceId) @@ -85,6 +88,7 @@ export const useSloActions = ({ sloEditUrl, handleNavigateToRules, remoteDeleteUrl, + remoteResetUrl, sloDetailsUrl: http.basePath.prepend(detailsUrl), }; }; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_outdated_definitions/outdated_slo.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_outdated_definitions/outdated_slo.tsx index 30e5c06cfbc07a..78fbe2bc8b5b0a 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_outdated_definitions/outdated_slo.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_outdated_definitions/outdated_slo.tsx @@ -54,6 +54,7 @@ export function OutdatedSlo({ slo, onReset, onDelete }: OutdatedSloProps) { const handleResetCancel = () => { setResetConfirmationModalOpen(false); }; + return ( diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/card_view/slo_card_item.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/card_view/slo_card_item.tsx index cf645bce217d07..ee5d75b6b651e6 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/card_view/slo_card_item.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/card_view/slo_card_item.tsx @@ -24,14 +24,16 @@ import { import { ALL_VALUE, HistoricalSummaryResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { Rule } from '@kbn/triggers-actions-ui-plugin/public'; import React, { useState } from 'react'; -import { EditBurnRateRuleFlyout } from '../common/edit_burn_rate_rule_flyout'; import { SloDeleteConfirmationModal } from '../../../../components/slo/delete_confirmation_modal/slo_delete_confirmation_modal'; +import { SloResetConfirmationModal } from '../../../../components/slo/reset_confirmation_modal/slo_reset_confirmation_modal'; +import { useResetSlo } from '../../../../hooks/use_reset_slo'; import { BurnRateRuleParams } from '../../../../typings'; import { useKibana } from '../../../../utils/kibana_react'; import { formatHistoricalData } from '../../../../utils/slo/chart_data_formatter'; import { useSloListActions } from '../../hooks/use_slo_list_actions'; import { useSloFormattedSummary } from '../../hooks/use_slo_summary'; import { BurnRateRuleFlyout } from '../common/burn_rate_rule_flyout'; +import { EditBurnRateRuleFlyout } from '../common/edit_burn_rate_rule_flyout'; import { SloCardItemActions } from './slo_card_item_actions'; import { SloCardItemBadges } from './slo_card_item_badges'; @@ -76,7 +78,9 @@ export function SloCardItem({ slo, rules, activeAlerts, historicalSummary, refet const [isAddRuleFlyoutOpen, setIsAddRuleFlyoutOpen] = useState(false); const [isEditRuleFlyoutOpen, setIsEditRuleFlyoutOpen] = useState(false); const [isDeleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = useState(false); + const [isResetConfirmationModalOpen, setResetConfirmationModalOpen] = useState(false); const [isDashboardAttachmentReady, setDashboardAttachmentReady] = useState(false); + const historicalSliData = formatHistoricalData(historicalSummary, 'sli_value'); const { handleCreateRule, handleDeleteCancel, handleDeleteConfirm, handleAttachToDashboardSave } = @@ -87,6 +91,17 @@ export function SloCardItem({ slo, rules, activeAlerts, historicalSummary, refet setIsAddRuleFlyoutOpen, }); + const { mutateAsync: resetSlo, isLoading: isResetLoading } = useResetSlo(); + + const handleResetConfirm = async () => { + await resetSlo({ id: slo.id, name: slo.name }); + setResetConfirmationModalOpen(false); + }; + + const handleResetCancel = () => { + setResetConfirmationModalOpen(false); + }; + return ( <> )} @@ -156,6 +172,16 @@ export function SloCardItem({ slo, rules, activeAlerts, historicalSummary, refet onConfirm={handleDeleteConfirm} /> ) : null} + + {isResetConfirmationModalOpen ? ( + + ) : null} + {isDashboardAttachmentReady ? ( void; setDeleteConfirmationModalOpen: (value: boolean) => void; + setResetConfirmationModalOpen: (value: boolean) => void; setIsAddRuleFlyoutOpen: (value: boolean) => void; setIsEditRuleFlyoutOpen: (value: boolean) => void; setDashboardAttachmentReady: (value: boolean) => void; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/compact_view/slo_list_compact_view.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/compact_view/slo_list_compact_view.tsx index 6ad7e054ed97e1..1361005be1be84 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/compact_view/slo_list_compact_view.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/compact_view/slo_list_compact_view.tsx @@ -24,6 +24,7 @@ import React, { useState } from 'react'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { paths } from '../../../../../common/locators/paths'; import { SloDeleteConfirmationModal } from '../../../../components/slo/delete_confirmation_modal/slo_delete_confirmation_modal'; +import { SloResetConfirmationModal } from '../../../../components/slo/reset_confirmation_modal/slo_reset_confirmation_modal'; import { SloStatusBadge } from '../../../../components/slo/slo_status_badge'; import { SloActiveAlertsBadge } from '../../../../components/slo/slo_status_badge/slo_active_alerts_badge'; import { sloKeys } from '../../../../hooks/query_key_factory'; @@ -34,12 +35,14 @@ import { useFetchActiveAlerts } from '../../../../hooks/use_fetch_active_alerts' import { useFetchHistoricalSummary } from '../../../../hooks/use_fetch_historical_summary'; import { useFetchRulesForSlo } from '../../../../hooks/use_fetch_rules_for_slo'; import { useGetFilteredRuleTypes } from '../../../../hooks/use_get_filtered_rule_types'; +import { useResetSlo } from '../../../../hooks/use_reset_slo'; import { useSpace } from '../../../../hooks/use_space'; import { useKibana } from '../../../../utils/kibana_react'; import { formatHistoricalData } from '../../../../utils/slo/chart_data_formatter'; import { createRemoteSloDeleteUrl, createRemoteSloEditUrl, + createRemoteSloResetUrl, } from '../../../../utils/slo/remote_slo_urls'; import { SloRemoteBadge } from '../badges/slo_remote_badge'; import { SloRulesBadge } from '../badges/slo_rules_badge'; @@ -75,9 +78,13 @@ export function SloListCompactView({ sloList, loading, error }: Props) { const { hasWriteCapabilities } = useCapabilities(); const filteredRuleTypes = useGetFilteredRuleTypes(); const queryClient = useQueryClient(); + const { mutate: deleteSlo } = useDeleteSlo(); + const { mutateAsync: resetSlo, isLoading: isResetLoading } = useResetSlo(); + const [sloToAddRule, setSloToAddRule] = useState(undefined); const [sloToDelete, setSloToDelete] = useState(undefined); + const [sloToReset, setSloToReset] = useState(undefined); const handleDeleteConfirm = () => { if (sloToDelete) { @@ -90,6 +97,17 @@ export function SloListCompactView({ sloList, loading, error }: Props) { setSloToDelete(undefined); }; + const handleResetConfirm = async () => { + if (sloToReset) { + await resetSlo({ id: sloToReset.id, name: sloToReset.name }); + setSloToReset(undefined); + } + }; + + const handleResetCancel = () => { + setSloToReset(undefined); + }; + const handleSavedRule = async () => { queryClient.invalidateQueries({ queryKey: sloKeys.rules(), exact: false }); }; @@ -242,6 +260,29 @@ export function SloListCompactView({ sloList, loading, error }: Props) { } }, }, + { + type: 'icon', + icon: 'refresh', + name: buildActionName( + i18n.translate('xpack.slo.item.actions.reset', { + defaultMessage: 'Reset', + }) + ), + description: i18n.translate('xpack.slo.item.actions.reset', { + defaultMessage: 'Reset', + }), + 'data-test-subj': 'sloActionsReset', + enabled: (slo: SLOWithSummaryResponse) => + (hasWriteCapabilities && !isRemote(slo)) || hasRemoteKibanaUrl(slo), + onClick: (slo: SLOWithSummaryResponse) => { + const remoteResetUrl = createRemoteSloResetUrl(slo, spaceId); + if (!!remoteResetUrl) { + window.open(remoteResetUrl, '_blank'); + } else { + setSloToReset(slo); + } + }, + }, ]; const columns: Array> = [ @@ -441,6 +482,15 @@ export function SloListCompactView({ sloList, loading, error }: Props) { onConfirm={handleDeleteConfirm} /> ) : null} + + {sloToReset ? ( + + ) : null} ); } diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_item_actions.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_item_actions.tsx index 2f6ce7da224e8c..a5de471bae883b 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_item_actions.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_item_actions.tsx @@ -30,6 +30,7 @@ interface Props { isActionsPopoverOpen: boolean; setIsActionsPopoverOpen: (value: boolean) => void; setDeleteConfirmationModalOpen: (value: boolean) => void; + setResetConfirmationModalOpen: (value: boolean) => void; setIsAddRuleFlyoutOpen: (value: boolean) => void; setIsEditRuleFlyoutOpen: (value: boolean) => void; setDashboardAttachmentReady?: (value: boolean) => void; @@ -65,6 +66,7 @@ export function SloItemActions({ setIsAddRuleFlyoutOpen, setIsEditRuleFlyoutOpen, setDeleteConfirmationModalOpen, + setResetConfirmationModalOpen, setDashboardAttachmentReady, btnProps, }: Props) { @@ -77,12 +79,13 @@ export function SloItemActions({ const { hasWriteCapabilities } = useCapabilities(); const navigateToClone = useCloneSlo(); - const { handleNavigateToRules, sloEditUrl, remoteDeleteUrl, sloDetailsUrl } = useSloActions({ - slo, - rules, - setIsEditRuleFlyoutOpen, - setIsActionsPopoverOpen, - }); + const { handleNavigateToRules, sloEditUrl, remoteDeleteUrl, remoteResetUrl, sloDetailsUrl } = + useSloActions({ + slo, + rules, + setIsEditRuleFlyoutOpen, + setIsActionsPopoverOpen, + }); const handleClickActions = () => { setIsActionsPopoverOpen(!isActionsPopoverOpen); @@ -105,6 +108,15 @@ export function SloItemActions({ } }; + const handleReset = () => { + if (!!remoteResetUrl) { + window.open(remoteResetUrl, '_blank'); + } else { + setResetConfirmationModalOpen(true); + setIsActionsPopoverOpen(false); + } + }; + const handleCreateRule = () => { setIsActionsPopoverOpen(false); setIsAddRuleFlyoutOpen(true); @@ -237,6 +249,20 @@ export function SloItemActions({ {i18n.translate('xpack.slo.item.actions.delete', { defaultMessage: 'Delete' })} {showRemoteLinkIcon} , + , + + {i18n.translate('xpack.slo.item.actions.reset', { defaultMessage: 'Reset' })} + {showRemoteLinkIcon} + , ].concat( !isDashboardContext ? ( { + await resetSlo({ id: slo.id, name: slo.name }); + setResetConfirmationModalOpen(false); + }; + + const handleResetCancel = () => { + setResetConfirmationModalOpen(false); + }; return ( @@ -99,6 +111,7 @@ export function SloListItem({ setIsEditRuleFlyoutOpen={setIsEditRuleFlyoutOpen} setIsActionsPopoverOpen={setIsActionsPopoverOpen} setDeleteConfirmationModalOpen={setDeleteConfirmationModalOpen} + setResetConfirmationModalOpen={setResetConfirmationModalOpen} /> @@ -122,6 +135,15 @@ export function SloListItem({ onConfirm={handleDeleteConfirm} /> ) : null} + + {isResetConfirmationModalOpen ? ( + + ) : null} ); } diff --git a/x-pack/plugins/observability_solution/slo/public/utils/slo/remote_slo_urls.ts b/x-pack/plugins/observability_solution/slo/public/utils/slo/remote_slo_urls.ts index 2e6c8bed91ba48..737a97fcfe73b7 100644 --- a/x-pack/plugins/observability_solution/slo/public/utils/slo/remote_slo_urls.ts +++ b/x-pack/plugins/observability_solution/slo/public/utils/slo/remote_slo_urls.ts @@ -45,6 +45,23 @@ export function createRemoteSloDeleteUrl(slo: SLOWithSummaryResponse, spaceId: s return remoteUrl.toString(); } +export function createRemoteSloResetUrl(slo: SLOWithSummaryResponse, spaceId: string = 'default') { + if (!slo.remote || slo.remote.kibanaUrl === '') { + return undefined; + } + + const spacePath = spaceId !== 'default' ? `/s/${spaceId}` : ''; + const detailsPath = paths.sloDetails( + slo.id, + ![slo.groupBy].flat().includes(ALL_VALUE) && slo.instanceId ? slo.instanceId : undefined + ); + + const remoteUrl = new URL(path.join(spacePath, detailsPath), slo.remote.kibanaUrl); + remoteUrl.searchParams.append('reset', 'true'); + + return remoteUrl.toString(); +} + export function createRemoteSloEditUrl(slo: SLOWithSummaryResponse, spaceId: string = 'default') { if (!slo.remote || slo.remote.kibanaUrl === '') { return undefined; diff --git a/x-pack/plugins/search_playground/common/types.ts b/x-pack/plugins/search_playground/common/types.ts index d2279633423adb..3ac8fe6e504366 100644 --- a/x-pack/plugins/search_playground/common/types.ts +++ b/x-pack/plugins/search_playground/common/types.ts @@ -19,6 +19,7 @@ export interface QuerySourceFields { dense_vector_query_fields: ModelFields[]; bm25_query_fields: string[]; source_fields: string[]; + skipped_fields: number; } export enum APIRoutes { diff --git a/x-pack/plugins/search_playground/public/components/edit_context/edit_context_flyout.test.tsx b/x-pack/plugins/search_playground/public/components/edit_context/edit_context_flyout.test.tsx index f1a9b835ac7727..4e5bb7f807900f 100644 --- a/x-pack/plugins/search_playground/public/components/edit_context/edit_context_flyout.test.tsx +++ b/x-pack/plugins/search_playground/public/components/edit_context/edit_context_flyout.test.tsx @@ -20,6 +20,12 @@ jest.mock('../../hooks/use_indices_fields', () => ({ bm25_query_fields: ['field1', 'field2'], source_fields: ['context_field1', 'context_field2'], }, + index2: { + elser_query_fields: [], + dense_vector_query_fields: [], + bm25_query_fields: ['field1', 'field2'], + source_fields: ['context_field1', 'context_field2'], + }, }, }), })); @@ -36,6 +42,11 @@ const MockFormProvider = ({ children }: { children: React.ReactElement }) => { const methods = useForm({ values: { indices: ['index1'], + docSize: 1, + sourceFields: { + index1: ['context_field1'], + index2: ['context_field2'], + }, }, }); return {children}; @@ -55,13 +66,14 @@ describe('EditContextFlyout component tests', () => { }); it('calls onClose when the close button is clicked', () => { - fireEvent.click(screen.getByTestId('euiFlyoutCloseButton')); + fireEvent.click(screen.getByRole('button', { name: 'Close' })); expect(onCloseMock).toHaveBeenCalledTimes(1); }); it('should see the context fields', async () => { - expect(screen.getByTestId('contextFieldsSelectable')).toBeInTheDocument(); - expect(screen.getByTestId('contextFieldsSelectable')).toHaveTextContent(`context_field2`); - expect(screen.getByTestId('contextFieldsSelectable')).toHaveTextContent(`context_field1`); + expect(screen.getByTestId('contextFieldsSelectable_index1')).toBeInTheDocument(); + fireEvent.click(screen.getByTestId('contextFieldsSelectable_index1')); + expect(screen.getByRole('option', { name: 'context_field1' })).toBeInTheDocument(); + expect(screen.getByRole('option', { name: 'context_field2' })).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/search_playground/public/components/edit_context/edit_context_flyout.tsx b/x-pack/plugins/search_playground/public/components/edit_context/edit_context_flyout.tsx index d960217c3eb9ba..38ee313dc1d878 100644 --- a/x-pack/plugins/search_playground/public/components/edit_context/edit_context_flyout.tsx +++ b/x-pack/plugins/search_playground/public/components/edit_context/edit_context_flyout.tsx @@ -18,11 +18,9 @@ import { EuiLink, EuiSpacer, EuiText, - EuiPanel, - EuiAccordion, - EuiSelectable, EuiSelect, - EuiSelectableOption, + EuiSuperSelect, + EuiFormRow, } from '@elastic/eui'; import { useController, useFormContext } from 'react-hook-form'; import { i18n } from '@kbn/i18n'; @@ -62,10 +60,10 @@ export const EditContextFlyout: React.FC = ({ onClose }) const [tempSourceFields, setTempSourceFields] = useState(sourceFields); - const toggleSourceField = (index: string, f: EuiSelectableOption[]) => { + const updateSourceField = (index: string, field: string) => { setTempSourceFields({ ...tempSourceFields, - [index]: f.filter(({ checked }) => checked === 'on').map(({ label }) => label), + [index]: [field], }); usageTracker.click(AnalyticsEvents.editContextFieldToggled); }; @@ -76,6 +74,7 @@ export const EditContextFlyout: React.FC = ({ onClose }) onChangeSize(docSize); onClose(); }; + const handleDocSizeChange = (e: React.ChangeEvent) => { usageTracker.click(AnalyticsEvents.editContextDocSizeChanged); setDocSize(Number(e.target.value)); @@ -119,71 +118,69 @@ export const EditContextFlyout: React.FC = ({ onClose }) - + - - - -
- + -
-
- {Object.entries(fields).map(([index, group], i) => ( - - - -
{index}
- - } - > - - ({ - label: field, - checked: tempSourceFields[index]?.includes(field) ? 'on' : undefined, - }))} - onChange={(newOptions) => toggleSourceField(index, newOptions)} - listProps={{ bordered: false }} - singleSelection="always" - > - {(list) => list} - -
-
-
- ))} + +
+ + + + +
+ +
+
+
+ {Object.entries(fields).map(([index, group]) => ( + + + ({ + value: field, + inputDisplay: field, + }))} + valueOfSelected={tempSourceFields[index]?.[0]} + onChange={(value) => updateSourceField(index, value)} + /> + + + ))} +
+
diff --git a/x-pack/plugins/search_playground/public/components/view_query/view_query_flyout.test.tsx b/x-pack/plugins/search_playground/public/components/view_query/view_query_flyout.test.tsx index 4af61de670ab75..410989eaf52ad4 100644 --- a/x-pack/plugins/search_playground/public/components/view_query/view_query_flyout.test.tsx +++ b/x-pack/plugins/search_playground/public/components/view_query/view_query_flyout.test.tsx @@ -18,6 +18,13 @@ jest.mock('../../hooks/use_indices_fields', () => ({ elser_query_fields: [], dense_vector_query_fields: [], bm25_query_fields: ['field1', 'field2'], + skipped_fields: 1, + }, + index2: { + elser_query_fields: [], + dense_vector_query_fields: [], + bm25_query_fields: ['field1', 'field2'], + skipped_fields: 0, }, }, }), @@ -34,7 +41,7 @@ jest.mock('../../hooks/use_usage_tracker', () => ({ const MockFormProvider = ({ children }: { children: React.ReactElement }) => { const methods = useForm({ values: { - indices: ['index1'], + indices: ['index1', 'index2'], }, }); return {children}; @@ -61,7 +68,19 @@ describe('ViewQueryFlyout component tests', () => { it('should see the view elasticsearch query', async () => { expect(screen.getByTestId('ViewElasticsearchQueryResult')).toBeInTheDocument(); expect(screen.getByTestId('ViewElasticsearchQueryResult')).toHaveTextContent( - `{ "retriever": { "standard": { "query": { "multi_match": { "query": "{query}", "fields": [ "field1" ] } } } } }` + `{ "retriever": { "rrf": { "retrievers": [ { "standard": { "query": { "multi_match": { "query": "{query}", "fields": [ "field1" ] } } } }, { "standard": { "query": { "multi_match": { "query": "{query}", "fields": [ "field1" ] } } } } ] } } }` ); }); + + it('displays query fields and indicates hidden fields', () => { + expect(screen.getByTestId('queryFieldsSelectable_index1')).toBeInTheDocument(); + expect(screen.getByTestId('queryFieldsSelectable_index2')).toBeInTheDocument(); + + // Check if hidden fields indicator is shown + expect(screen.getByTestId('skipped_fields_index1')).toBeInTheDocument(); + expect(screen.getByTestId('skipped_fields_index1')).toHaveTextContent('1 fields are hidden.'); + + // Check if hidden fields indicator is shown + expect(screen.queryByTestId('skipped_fields_index2')).not.toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/search_playground/public/components/view_query/view_query_flyout.tsx b/x-pack/plugins/search_playground/public/components/view_query/view_query_flyout.tsx index 8b5c01df5103b8..621fecfaf72d48 100644 --- a/x-pack/plugins/search_playground/public/components/view_query/view_query_flyout.tsx +++ b/x-pack/plugins/search_playground/public/components/view_query/view_query_flyout.tsx @@ -22,12 +22,16 @@ import { EuiSelectableOption, EuiText, EuiTitle, + EuiCheckbox, + EuiLink, + EuiIcon, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import React, { useEffect, useState } from 'react'; import { useFormContext } from 'react-hook-form'; import { useController } from 'react-hook-form'; import { AnalyticsEvents } from '../../analytics/constants'; +import { docLinks } from '../../../common/doc_links'; import { useIndicesFields } from '../../hooks/use_indices_fields'; import { useUsageTracker } from '../../hooks/use_usage_tracker'; import { ChatForm, ChatFormFields, IndicesQuerySourceFields } from '../../types'; @@ -157,7 +161,7 @@ export const ViewQueryFlyout: React.FC = ({ onClose }) => - +
= ({ onClose }) => />
- {Object.entries(fields).map(([index, group], i) => ( + {Object.entries(fields).map(([index, group]) => (
{index}
@@ -181,24 +185,70 @@ export const ViewQueryFlyout: React.FC = ({ onClose }) => ({ - label: typeof field === 'string' ? field : field.field, - checked: isQueryFieldSelected( + ].map((field, idx) => { + const checked = isQueryFieldSelected( index, typeof field === 'string' ? field : field.field - ) - ? 'on' - : undefined, - }))} + ); + return { + label: typeof field === 'string' ? field : field.field, + prepend: ( + {}} + /> + ), + checked: checked ? 'on' : undefined, + }; + })} + listProps={{ + bordered: false, + showIcons: false, + }} onChange={(newOptions) => updateFields(index, newOptions)} - listProps={{ bordered: false }} > {(list) => list} + {group.skipped_fields > 0 && ( + <> + + + + + + {` `} + + + + + + + + + + + )}
diff --git a/x-pack/plugins/search_playground/public/hooks/use_source_indices_fields.test.tsx b/x-pack/plugins/search_playground/public/hooks/use_source_indices_fields.test.tsx index 20e7551511eb27..f3b19e8d4360ec 100644 --- a/x-pack/plugins/search_playground/public/hooks/use_source_indices_fields.test.tsx +++ b/x-pack/plugins/search_playground/public/hooks/use_source_indices_fields.test.tsx @@ -43,6 +43,7 @@ describe.skip('useSourceIndicesFields Hook', () => { dense_vector_query_fields: [], bm25_query_fields: [], source_fields: ['field1'], + skipped_fields: 0, }, }; @@ -144,6 +145,7 @@ describe.skip('useSourceIndicesFields Hook', () => { dense_vector_query_fields: [], bm25_query_fields: [], source_fields: [], + skipped_fields: 0, }, }; @@ -196,6 +198,7 @@ describe.skip('useSourceIndicesFields Hook', () => { dense_vector_query_fields: [], bm25_query_fields: [], source_fields: [], + skipped_fields: 0, }, }; diff --git a/x-pack/plugins/search_playground/public/providers/playground_provider.tsx b/x-pack/plugins/search_playground/public/providers/playground_provider.tsx index e9e6e7d2a80008..b03ebaffb7da0d 100644 --- a/x-pack/plugins/search_playground/public/providers/playground_provider.tsx +++ b/x-pack/plugins/search_playground/public/providers/playground_provider.tsx @@ -24,7 +24,7 @@ export const PlaygroundProvider: FC> const form = useForm({ defaultValues: { prompt: 'You are an assistant for question-answering tasks.', - doc_size: 5, + doc_size: 3, source_fields: [], indices: defaultValues?.indices || [], }, diff --git a/x-pack/plugins/search_playground/public/utils/create_query.test.ts b/x-pack/plugins/search_playground/public/utils/create_query.test.ts index 58b289f2caf035..282326f0991d2b 100644 --- a/x-pack/plugins/search_playground/public/utils/create_query.test.ts +++ b/x-pack/plugins/search_playground/public/utils/create_query.test.ts @@ -23,6 +23,7 @@ describe('create_query', () => { dense_vector_query_fields: [], bm25_query_fields: [], source_fields: [], + skipped_fields: 0, }, }; @@ -55,6 +56,7 @@ describe('create_query', () => { ], bm25_query_fields: [], source_fields: [], + skipped_fields: 0, }, }; @@ -92,6 +94,7 @@ describe('create_query', () => { dense_vector_query_fields: [], bm25_query_fields: [], source_fields: [], + skipped_fields: 0, }, index2: { elser_query_fields: [ @@ -100,6 +103,7 @@ describe('create_query', () => { dense_vector_query_fields: [], bm25_query_fields: [], source_fields: [], + skipped_fields: 0, }, }; @@ -133,6 +137,7 @@ describe('create_query', () => { dense_vector_query_fields: [], bm25_query_fields: [], source_fields: [], + skipped_fields: 0, }, index2: { elser_query_fields: [ @@ -141,6 +146,7 @@ describe('create_query', () => { dense_vector_query_fields: [], bm25_query_fields: [], source_fields: [], + skipped_fields: 0, }, }; @@ -196,6 +202,7 @@ describe('create_query', () => { ], bm25_query_fields: [], source_fields: [], + skipped_fields: 0, }, }; @@ -228,6 +235,7 @@ describe('create_query', () => { dense_vector_query_fields: [], bm25_query_fields: [], source_fields: [], + skipped_fields: 0, }, }; @@ -257,6 +265,7 @@ describe('create_query', () => { dense_vector_query_fields: [], bm25_query_fields: ['content', 'title'], source_fields: [], + skipped_fields: 0, }, index2: { elser_query_fields: [ @@ -265,6 +274,7 @@ describe('create_query', () => { dense_vector_query_fields: [], bm25_query_fields: [], source_fields: [], + skipped_fields: 0, }, }; @@ -321,6 +331,7 @@ describe('create_query', () => { dense_vector_query_fields: [], bm25_query_fields: ['content', 'title'], source_fields: [], + skipped_fields: 0, }, index2: { elser_query_fields: [ @@ -329,6 +340,7 @@ describe('create_query', () => { dense_vector_query_fields: [], bm25_query_fields: [], source_fields: [], + skipped_fields: 0, }, }; @@ -390,6 +402,7 @@ describe('create_query', () => { ], bm25_query_fields: ['content', 'title'], source_fields: [], + skipped_fields: 0, }, index2: { elser_query_fields: [ @@ -398,6 +411,7 @@ describe('create_query', () => { dense_vector_query_fields: [], bm25_query_fields: [], source_fields: [], + skipped_fields: 0, }, }; @@ -435,6 +449,7 @@ describe('create_query', () => { ], bm25_query_fields: ['content', 'title'], source_fields: [], + skipped_fields: 0, }, }; @@ -487,6 +502,7 @@ describe('create_query', () => { ], bm25_query_fields: [], source_fields: [], + skipped_fields: 0, }, }; @@ -509,6 +525,7 @@ describe('create_query', () => { ], bm25_query_fields: [], source_fields: [], + skipped_fields: 0, }, index2: { elser_query_fields: [ @@ -524,6 +541,7 @@ describe('create_query', () => { ], bm25_query_fields: [], source_fields: [], + skipped_fields: 0, }, }; @@ -549,6 +567,7 @@ describe('create_query', () => { ], bm25_query_fields: [], source_fields: [], + skipped_fields: 0, }, index2: { elser_query_fields: [ @@ -564,6 +583,7 @@ describe('create_query', () => { ], bm25_query_fields: [], source_fields: [], + skipped_fields: 0, }, }; @@ -582,6 +602,7 @@ describe('create_query', () => { ], bm25_query_fields: [], source_fields: [], + skipped_fields: 0, }, }; @@ -595,6 +616,7 @@ describe('create_query', () => { dense_vector_query_fields: [], bm25_query_fields: ['title', 'text', 'content'], source_fields: [], + skipped_fields: 0, }, }; @@ -610,6 +632,7 @@ describe('create_query', () => { dense_vector_query_fields: [], bm25_query_fields: ['unknown1', 'unknown2'], source_fields: [], + skipped_fields: 0, }, }; @@ -659,6 +682,7 @@ describe('create_query', () => { 'url_path_dir2', 'url_path_dir1', ], + skipped_fields: 0, }, }; @@ -674,6 +698,7 @@ describe('create_query', () => { dense_vector_query_fields: [], bm25_query_fields: [], source_fields: [], + skipped_fields: 0, }, }; @@ -691,6 +716,7 @@ describe('create_query', () => { dense_vector_query_fields: [], bm25_query_fields: [], source_fields: ['non_suggested_field'], + skipped_fields: 0, }, }; diff --git a/x-pack/plugins/search_playground/server/lib/fetch_query_source_fields.test.ts b/x-pack/plugins/search_playground/server/lib/fetch_query_source_fields.test.ts index 6896b0f6025e53..048c0a8fd83e1a 100644 --- a/x-pack/plugins/search_playground/server/lib/fetch_query_source_fields.test.ts +++ b/x-pack/plugins/search_playground/server/lib/fetch_query_source_fields.test.ts @@ -48,6 +48,7 @@ describe('fetch_query_source_fields', () => { indices: ['workplace_index'], }, ], + skipped_fields: 8, source_fields: ['metadata.summary', 'metadata.rolePermissions', 'text', 'metadata.name'], }, workplace_index2: { @@ -58,6 +59,7 @@ describe('fetch_query_source_fields', () => { 'metadata.name', ], dense_vector_query_fields: [], + skipped_fields: 8, elser_query_fields: [ { field: 'content_vector.tokens', @@ -110,6 +112,7 @@ describe('fetch_query_source_fields', () => { }, ], elser_query_fields: [], + skipped_fields: 30, source_fields: [ 'page_content_key', 'title', @@ -150,11 +153,13 @@ describe('fetch_query_source_fields', () => { }, ], source_fields: ['body_content', 'headings', 'title'], + skipped_fields: 4, }, }); }); it('should return the correct fields for a document first index', () => { + // Skips the nested dense vector field. expect( parseFieldsCapabilities(DENSE_VECTOR_DOCUMENT_FIRST_FIELD_CAPS, [ { @@ -174,14 +179,7 @@ describe('fetch_query_source_fields', () => { 'metadata.summary', 'metadata.content', ], - dense_vector_query_fields: [ - { - field: 'passages.vector.predicted_value', - model_id: '.multilingual-e5-small', - nested: true, - indices: ['workplace_index_nested'], - }, - ], + dense_vector_query_fields: [], elser_query_fields: [], source_fields: [ 'metadata.category', @@ -193,6 +191,7 @@ describe('fetch_query_source_fields', () => { 'metadata.summary', 'metadata.content', ], + skipped_fields: 18, }, }); }); diff --git a/x-pack/plugins/search_playground/server/lib/fetch_query_source_fields.ts b/x-pack/plugins/search_playground/server/lib/fetch_query_source_fields.ts index 84cd8db481cefb..fb2eba4835cb28 100644 --- a/x-pack/plugins/search_playground/server/lib/fetch_query_source_fields.ts +++ b/x-pack/plugins/search_playground/server/lib/fetch_query_source_fields.ts @@ -7,7 +7,7 @@ import { FieldCapsResponse, SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { IScopedClusterClient } from '@kbn/core-elasticsearch-server'; -import { get } from 'lodash'; +import { get, has } from 'lodash'; import { IndicesQuerySourceFields } from '../types'; export const fetchFields = async ( @@ -45,10 +45,23 @@ export const fetchFields = async ( return parseFieldsCapabilities(fieldCapabilities, indexDocs); }; +const INFERENCE_MODEL_FIELD_REGEXP = /\.predicted_value|\.tokens/; + +const hasModelField = (field: string, indexDoc: any, nestedField: string | false) => { + if (field.match(INFERENCE_MODEL_FIELD_REGEXP)) { + const path = nestedField ? field.replace(`${nestedField}.`, `${nestedField}[0].`) : field; + return has( + indexDoc.doc, + `_source.${[path.replace(INFERENCE_MODEL_FIELD_REGEXP, '.model_id')]}` + ); + } + return false; +}; + const getModelField = (field: string, indexDoc: any, nestedField: string | false) => { // If the field is nested, we need to get the first occurrence as its an array const path = nestedField ? field.replace(`${nestedField}.`, `${nestedField}[0].`) : field; - return get(indexDoc.doc, `_source.${[path.replace(/\.predicted_value|\.tokens/, '.model_id')]}`); + return get(indexDoc.doc, `_source.${[path.replace(INFERENCE_MODEL_FIELD_REGEXP, '.model_id')]}`); }; const isFieldNested = (field: string, fieldCapsResponse: FieldCapsResponse) => { @@ -83,6 +96,7 @@ export const parseFieldsCapabilities = ( dense_vector_query_fields: [], bm25_query_fields: [], source_fields: [], + skipped_fields: 0, }; return acc; }, {}); @@ -106,25 +120,41 @@ export const parseFieldsCapabilities = ( if ('rank_features' in field || 'sparse_vector' in field) { const nestedField = isFieldNested(fieldKey, fieldCapsResponse); - const elserModelField = { - field: fieldKey, - model_id: getModelField(fieldKey, indexDoc, nestedField), - nested: !!isFieldNested(fieldKey, fieldCapsResponse), - indices: indicesPresentIn, - }; - acc[index].elser_query_fields.push(elserModelField); + if (!nestedField) { + const elserModelField = { + field: fieldKey, + model_id: getModelField(fieldKey, indexDoc, nestedField), + nested: !!isFieldNested(fieldKey, fieldCapsResponse), + indices: indicesPresentIn, + }; + acc[index].elser_query_fields.push(elserModelField); + } else { + acc[index].skipped_fields++; + } } else if ('dense_vector' in field) { const nestedField = isFieldNested(fieldKey, fieldCapsResponse); - const denseVectorField = { - field: fieldKey, - model_id: getModelField(fieldKey, indexDoc, nestedField), - nested: !!nestedField, - indices: indicesPresentIn, - }; - acc[index].dense_vector_query_fields.push(denseVectorField); + + // Check if the dense vector field has a model_id associated with it + // skip this field if has no model associated with it + // and the vectors were embedded outside of stack + if (hasModelField(fieldKey, indexDoc, nestedField) && !nestedField) { + const denseVectorField = { + field: fieldKey, + model_id: getModelField(fieldKey, indexDoc, nestedField), + nested: !!nestedField, + indices: indicesPresentIn, + }; + acc[index].dense_vector_query_fields.push(denseVectorField); + } else { + acc[index].skipped_fields++; + } } else if ('text' in field && field.text.searchable && shouldIgnoreField(fieldKey)) { acc[index].bm25_query_fields.push(fieldKey); acc[index].source_fields.push(fieldKey); + } else { + if (fieldKey !== '_id' && fieldKey !== '_index' && fieldKey !== '_type') { + acc[index].skipped_fields++; + } } } diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/fleet_integrations/get_all_integrations/get_all_integrations_route.ts b/x-pack/plugins/security_solution/common/api/detection_engine/fleet_integrations/get_all_integrations/get_all_integrations_route.ts new file mode 100644 index 00000000000000..0798808365e12b --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/detection_engine/fleet_integrations/get_all_integrations/get_all_integrations_route.ts @@ -0,0 +1,12 @@ +/* + * 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 { Integration } from '../model/integrations'; + +export interface GetAllIntegrationsResponse { + integrations: Integration[]; +} diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/fleet_integrations/index.ts b/x-pack/plugins/security_solution/common/api/detection_engine/fleet_integrations/index.ts index 63a824a430c6ec..1c503733669552 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/fleet_integrations/index.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/fleet_integrations/index.ts @@ -5,7 +5,10 @@ * 2.0. */ +export * from './get_all_integrations/get_all_integrations_route'; + export * from './get_installed_integrations/get_installed_integrations_route'; export * from './urls'; +export * from './model/integrations'; export * from './model/installed_integrations'; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/fleet_integrations/model/integrations.ts b/x-pack/plugins/security_solution/common/api/detection_engine/fleet_integrations/model/integrations.ts new file mode 100644 index 00000000000000..d1ddeb0ffa0572 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/detection_engine/fleet_integrations/model/integrations.ts @@ -0,0 +1,101 @@ +/* + * 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. + */ + +// ------------------------------------------------------------------------------------------------- +// Fleet Package Integration + +/** + * Information about a Fleet integration including info about its package. + * + * @example + * { + * package_name: 'aws', + * package_title: 'AWS', + * integration_name: 'cloudtrail', + * integration_title: 'AWS CloudTrail', + * latest_package_version: '1.2.3', + * is_installed: false + * is_enabled: false + * } + * + * @example + * { + * package_name: 'aws', + * package_title: 'AWS', + * integration_name: 'cloudtrail', + * integration_title: 'AWS CloudTrail', + * latest_package_version: '1.16.1', + * installed_package_version: '1.16.1', + * is_installed: true + * is_enabled: false + * } + * + * @example + * { + * package_name: 'system', + * package_title: 'System', + * latest_package_version: '2.0.1', + * installed_package_version: '1.13.0', + * is_installed: true + * is_enabled: true + * } + * + */ +export interface Integration { + /** + * Name is a unique package id within a given cluster. + * There can't be 2 or more different packages with the same name. + * @example 'aws' + */ + package_name: string; + + /** + * Title is a user-friendly name of the package that we show in the UI. + * @example 'AWS' + */ + package_title: string; + + /** + * Whether the package is installed + */ + is_installed: boolean; + + /** + * Whether this integration is enabled + */ + is_enabled: boolean; + + /** + * Version of the latest available package. Semver-compatible. + * @example '1.2.3' + */ + latest_package_version: string; + + /** + * Version of the installed package. Semver-compatible. + * @example '1.2.3' + */ + installed_package_version?: string; + + /** + * Name identifies an integration within its package. + * Undefined when package name === integration name. This indicates that it's the only integration + * within this package. + * @example 'cloudtrail' + * @example undefined + */ + integration_name?: string; + + /** + * Title is a user-friendly name of the integration that we show in the UI. + * Undefined when package name === integration name. This indicates that it's the only integration + * within this package. + * @example 'AWS CloudTrail' + * @example undefined + */ + integration_title?: string; +} diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/fleet_integrations/urls.ts b/x-pack/plugins/security_solution/common/api/detection_engine/fleet_integrations/urls.ts index b1216d855284e6..04cf30bc509fe1 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/fleet_integrations/urls.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/fleet_integrations/urls.ts @@ -7,5 +7,7 @@ import { INTERNAL_DETECTION_ENGINE_URL as INTERNAL_URL } from '../../../constants'; +export const GET_ALL_INTEGRATIONS_URL = `${INTERNAL_URL}/fleet/integrations/all` as const; + export const GET_INSTALLED_INTEGRATIONS_URL = `${INTERNAL_URL}/fleet/integrations/installed` as const; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts index fc29093779c753..d05a2723375342 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts @@ -53,11 +53,11 @@ import { MaxSignals, ThreatArray, SetupGuide, + RelatedIntegrationArray, RuleObjectId, RuleSignatureId, IsRuleImmutable, RuleSource, - RelatedIntegrationArray, RequiredFieldArray, RuleQuery, IndexPatternArray, @@ -136,6 +136,7 @@ export const BaseDefaultableFields = z.object({ max_signals: MaxSignals.optional(), threat: ThreatArray.optional(), setup: SetupGuide.optional(), + related_integrations: RelatedIntegrationArray.optional(), }); export type BaseCreateProps = z.infer; @@ -163,7 +164,6 @@ export const ResponseFields = z.object({ created_at: z.string().datetime(), created_by: z.string(), revision: z.number().int().min(0), - related_integrations: RelatedIntegrationArray, required_fields: RequiredFieldArray, execution_summary: RuleExecutionSummary.optional(), }); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml index 4a2279f7a7c8d1..dfb3bfb738a5c2 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml @@ -131,6 +131,9 @@ components: setup: $ref: './common_attributes.schema.yaml#/components/schemas/SetupGuide' + related_integrations: + $ref: './common_attributes.schema.yaml#/components/schemas/RelatedIntegrationArray' + BaseCreateProps: x-inline: true allOf: @@ -178,13 +181,11 @@ components: revision: type: integer minimum: 0 - # NOTE: For now, Related Integrations and Required Fields are + # NOTE: For now, Required Fields are # supported for prebuilt rules only. We don't want to allow users to edit these 3 # fields via the API. If we added them to baseParams.defaultable, they would # become a part of the request schema as optional fields. This is why we add them # here, in order to add them only to the response schema. - related_integrations: - $ref: './common_attributes.schema.yaml#/components/schemas/RelatedIntegrationArray' required_fields: $ref: './common_attributes.schema.yaml#/components/schemas/RequiredFieldArray' execution_summary: diff --git a/x-pack/plugins/security_solution/public/common/mock/create_react_query_wrapper.tsx b/x-pack/plugins/security_solution/public/common/mock/create_react_query_wrapper.tsx new file mode 100644 index 00000000000000..42377ecca87c8e --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/mock/create_react_query_wrapper.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +export function createReactQueryWrapper(): React.FC { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // Turn retries off, otherwise we won't be able to test errors + retry: false, + }, + }, + }); + + // eslint-disable-next-line react/display-name + return ({ children }) => ( + {children} + ); +} diff --git a/x-pack/plugins/security_solution/public/common/mock/index.ts b/x-pack/plugins/security_solution/public/common/mock/index.ts index d4cc1185846bb1..3be928b9dcc1fd 100644 --- a/x-pack/plugins/security_solution/public/common/mock/index.ts +++ b/x-pack/plugins/security_solution/public/common/mock/index.ts @@ -19,3 +19,4 @@ export * from './test_providers'; export * from './timeline_results'; export * from './utils'; export * from './create_store'; +export * from './create_react_query_wrapper'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/fleet_integrations/api/__mocks__/api_client.ts b/x-pack/plugins/security_solution/public/detection_engine/fleet_integrations/api/__mocks__/api_client.ts index f0dbf5dd6899e3..eadc0a1cb2d41c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/fleet_integrations/api/__mocks__/api_client.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/fleet_integrations/api/__mocks__/api_client.ts @@ -5,13 +5,49 @@ * 2.0. */ -import type { GetInstalledIntegrationsResponse } from '../../../../../common/api/detection_engine/fleet_integrations'; import type { + GetAllIntegrationsResponse, + GetInstalledIntegrationsResponse, +} from '../../../../../common/api/detection_engine/fleet_integrations'; +import type { + FetchAllIntegrationsArgs, FetchInstalledIntegrationsArgs, IFleetIntegrationsApiClient, } from '../api_client_interface'; export const fleetIntegrationsApi: jest.Mocked = { + fetchAllIntegrations: jest + .fn, [FetchAllIntegrationsArgs]>() + .mockResolvedValue({ + integrations: [ + { + package_name: 'o365', + package_title: 'Microsoft 365', + latest_package_version: '1.2.0', + installed_package_version: '1.0.0', + is_installed: false, + is_enabled: false, + }, + { + package_name: 'atlassian_bitbucket', + package_title: 'Atlassian Bitbucket', + latest_package_version: '1.0.1', + installed_package_version: '1.0.1', + integration_name: 'audit', + integration_title: 'Audit Logs', + is_installed: true, + is_enabled: true, + }, + { + package_name: 'system', + package_title: 'System', + latest_package_version: '1.6.4', + installed_package_version: '1.6.4', + is_installed: true, + is_enabled: true, + }, + ], + }), fetchInstalledIntegrations: jest .fn, [FetchInstalledIntegrationsArgs]>() .mockResolvedValue({ diff --git a/x-pack/plugins/security_solution/public/detection_engine/fleet_integrations/api/api_client.ts b/x-pack/plugins/security_solution/public/detection_engine/fleet_integrations/api/api_client.ts index 192683f84fc468..812daf5cf21046 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/fleet_integrations/api/api_client.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/fleet_integrations/api/api_client.ts @@ -5,16 +5,31 @@ * 2.0. */ -import type { GetInstalledIntegrationsResponse } from '../../../../common/api/detection_engine/fleet_integrations'; -import { GET_INSTALLED_INTEGRATIONS_URL } from '../../../../common/api/detection_engine/fleet_integrations'; +import type { + GetAllIntegrationsResponse, + GetInstalledIntegrationsResponse, +} from '../../../../common/api/detection_engine/fleet_integrations'; +import { + GET_ALL_INTEGRATIONS_URL, + GET_INSTALLED_INTEGRATIONS_URL, +} from '../../../../common/api/detection_engine/fleet_integrations'; import { KibanaServices } from '../../../common/lib/kibana'; import type { + FetchAllIntegrationsArgs, FetchInstalledIntegrationsArgs, IFleetIntegrationsApiClient, } from './api_client_interface'; export const fleetIntegrationsApi: IFleetIntegrationsApiClient = { + fetchAllIntegrations: (args: FetchAllIntegrationsArgs): Promise => { + return http().fetch(GET_ALL_INTEGRATIONS_URL, { + method: 'GET', + version: '1', + signal: args.signal, + }); + }, + fetchInstalledIntegrations: ( args: FetchInstalledIntegrationsArgs ): Promise => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/fleet_integrations/api/api_client_interface.ts b/x-pack/plugins/security_solution/public/detection_engine/fleet_integrations/api/api_client_interface.ts index ba847ae39b97ba..8b2610a098efcb 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/fleet_integrations/api/api_client_interface.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/fleet_integrations/api/api_client_interface.ts @@ -5,9 +5,19 @@ * 2.0. */ -import type { GetInstalledIntegrationsResponse } from '../../../../common/api/detection_engine/fleet_integrations'; +import type { + GetInstalledIntegrationsResponse, + GetAllIntegrationsResponse, +} from '../../../../common/api/detection_engine/fleet_integrations'; export interface IFleetIntegrationsApiClient { + /** + * Fetch all integrations with installed and enabled statuses + * + * @throws An error if response is not OK + */ + fetchAllIntegrations(args: FetchAllIntegrationsArgs): Promise; + /** * Fetch all installed integrations. * @throws An error if response is not OK @@ -17,6 +27,13 @@ export interface IFleetIntegrationsApiClient { ): Promise; } +export interface FetchAllIntegrationsArgs { + /** + * Optional signal for cancelling the request. + */ + signal?: AbortSignal; +} + export interface FetchInstalledIntegrationsArgs { /** * Array of Fleet packages to filter for. diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/default_related_integration.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/default_related_integration.ts new file mode 100644 index 00000000000000..bc8063c3db9ca4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/default_related_integration.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const DEFAULT_RELATED_INTEGRATION = { package: '', version: '' }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/index.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/index.ts new file mode 100644 index 00000000000000..0b169487aa490c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/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 { RelatedIntegrations } from './related_integrations'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/integration_status_badge.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/integration_status_badge.tsx new file mode 100644 index 00000000000000..0aa77fa9d39fd3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/integration_status_badge.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiBadge } from '@elastic/eui'; +import * as i18n from './translations'; + +interface IntegrationStatusBadgeProps { + isInstalled: boolean; + isEnabled: boolean; +} + +export function IntegrationStatusBadge({ + isInstalled, + isEnabled, +}: IntegrationStatusBadgeProps): JSX.Element { + const color = isEnabled ? 'success' : isInstalled ? 'primary' : undefined; + const statusText = isEnabled + ? i18n.INTEGRATION_INSTALLED_AND_ENABLED + : isInstalled + ? i18n.INTEGRATION_INSTALLED_AND_DISABLED + : i18n.INTEGRATION_NOT_INSTALLED; + + return ( + + {statusText} + + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integration_field.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integration_field.tsx new file mode 100644 index 00000000000000..c24220923441bb --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integration_field.tsx @@ -0,0 +1,241 @@ +/* + * 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 { ChangeEvent } from 'react'; +import React, { useCallback, useMemo } from 'react'; +import { capitalize } from 'lodash'; +import semver from 'semver'; +import { css } from '@emotion/css'; +import type { EuiComboBoxOptionOption } from '@elastic/eui'; +import { + EuiTextTruncate, + EuiButtonIcon, + EuiComboBox, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, +} from '@elastic/eui'; +import type { FieldHook } from '../../../../shared_imports'; +import type { Integration, RelatedIntegration } from '../../../../../common/api/detection_engine'; +import { useIntegrations } from '../../../../detections/components/rules/related_integrations/use_integrations'; +import { IntegrationStatusBadge } from './integration_status_badge'; +import { DEFAULT_RELATED_INTEGRATION } from './default_related_integration'; +import * as i18n from './translations'; + +interface RelatedIntegrationItemFormProps { + field: FieldHook; + relatedIntegrations: RelatedIntegration[]; + onRemove: () => void; +} + +export function RelatedIntegrationField({ + field, + relatedIntegrations, + onRemove, +}: RelatedIntegrationItemFormProps): JSX.Element { + const { data: integrations, isInitialLoading } = useIntegrations(); + const [integrationOptions, selectedIntegrationOptions] = useMemo(() => { + const currentKey = getKey(field.value.package, field.value.integration); + const relatedIntegrationsButCurrent = relatedIntegrations.filter( + (ri) => getKey(ri.package, ri.integration) !== currentKey + ); + const unusedIntegrations = filterOutUsedIntegrations( + integrations ?? [], + relatedIntegrationsButCurrent + ); + + const options = unusedIntegrations.map(transformIntegrationToOption); + const fallbackSelectedOption = + field.value.package.length > 0 + ? { + key: currentKey, + label: `${capitalize(field.value.package)} ${field.value.integration ?? ''}`, + } + : undefined; + const selectedOption = + options.find((option) => option.key === currentKey) ?? fallbackSelectedOption; + + return [options, selectedOption ? [selectedOption] : []]; + }, [integrations, field.value, relatedIntegrations]); + + const [packageErrorMessage, versionErrorMessage] = useMemo(() => { + const packagePath = `${field.path}.package`; + const versionPath = `${field.path}.version`; + + return [ + field.errors.find((err) => 'path' in err && err.path === packagePath), + field.errors.find((err) => 'path' in err && err.path === versionPath), + ]; + }, [field.path, field.errors]); + + const handleIntegrationChange = useCallback( + ([changedSelectedOption]: Array>) => + field.setValue({ + package: changedSelectedOption?.value?.package_name ?? '', + integration: changedSelectedOption?.value?.integration_name, + version: changedSelectedOption?.value + ? calculateRelevantSemver(changedSelectedOption.value) + : '', + }), + [field] + ); + + const handleVersionChange = useCallback( + (e: ChangeEvent) => + field.setValue((oldValue) => ({ + ...oldValue, + version: e.target.value, + })), + [field] + ); + + const hasError = Boolean(packageErrorMessage) || Boolean(versionErrorMessage); + const isLastField = relatedIntegrations.length === 1; + const isLastEmptyField = isLastField && field.value.package === ''; + const handleRemove = useCallback(() => { + if (isLastField) { + field.setValue(DEFAULT_RELATED_INTEGRATION); + return; + } + + onRemove(); + }, [onRemove, field, isLastField]); + + return ( + + + + + options={integrationOptions} + renderOption={renderIntegrationOption} + selectedOptions={selectedIntegrationOptions} + singleSelection + isLoading={isInitialLoading} + isDisabled={!integrations} + onChange={handleIntegrationChange} + fullWidth + aria-label={i18n.RELATED_INTEGRATION_ARIA_LABEL} + isInvalid={Boolean(packageErrorMessage)} + data-test-subj="relatedIntegrationComboBox" + /> + + + + + + + + + + ); +} + +const ROW_OVERFLOW_FIX_STYLE = css` + overflow: hidden; +`; + +/** + * Minimum width has been determined empirically like that + * semver value like `^1.2.3` doesn't overflow + */ +const MIN_WIDTH_VERSION_CONSTRAIN_STYLE = css` + min-width: 150px; +`; + +function filterOutUsedIntegrations( + integrations: Integration[], + relatedIntegrations: RelatedIntegration[] +): Integration[] { + const usedIntegrationsSet = new Set( + relatedIntegrations.map((ri) => getKey(ri.package, ri.integration)) + ); + + return integrations?.filter( + (i) => !usedIntegrationsSet.has(getKey(i.package_name, i.integration_name)) + ); +} + +function transformIntegrationToOption( + integration: Integration +): EuiComboBoxOptionOption { + const integrationTitle = integration.integration_title ?? integration.package_title; + const label = integration.is_enabled + ? i18n.INTEGRATION_ENABLED(integrationTitle) + : integration.is_installed + ? i18n.INTEGRATION_DISABLED(integrationTitle) + : integrationTitle; + + return { + key: getKey(integration.package_name, integration.integration_name), + label, + value: integration, + color: integration.is_enabled ? 'success' : integration.is_installed ? 'primary' : undefined, + }; +} + +function getKey(packageName: string | undefined, integrationName: string | undefined): string { + return `${packageName ?? ''}${integrationName ?? ''}`; +} + +function renderIntegrationOption( + option: EuiComboBoxOptionOption +): JSX.Element | string { + const { label, value } = option; + + if (!value) { + return label; + } + + return ( + + + + + + + + + ); +} + +function calculateRelevantSemver(integration: Integration): string { + if (!integration.installed_package_version) { + return `^${integration.latest_package_version}`; + } + + // In some rare cases users may install a prerelease integration version. + // We need to build constraint on the latest stable version and + // it's supposed `latest_package_version` is the latest stable version. + if (semver.gt(integration.installed_package_version, integration.latest_package_version)) { + return `^${integration.latest_package_version}`; + } + + return `^${integration.installed_package_version}`; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integration_field_row.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integration_field_row.tsx new file mode 100644 index 00000000000000..de549be3fae027 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integration_field_row.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import type { RelatedIntegration } from '../../../../../common/api/detection_engine'; +import type { ArrayItem, FieldConfig } from '../../../../shared_imports'; +import { FIELD_TYPES, UseField } from '../../../../shared_imports'; +import { DEFAULT_RELATED_INTEGRATION } from './default_related_integration'; +import { RelatedIntegrationField } from './related_integration_field'; +import { validateRelatedIntegration } from './validate_related_integration'; + +interface RelatedIntegrationFieldRowProps { + item: ArrayItem; + relatedIntegrations: RelatedIntegration[]; + removeItem: (id: number) => void; +} + +export function RelatedIntegrationFieldRow({ + item, + relatedIntegrations, + removeItem, +}: RelatedIntegrationFieldRowProps): JSX.Element { + const handleRemove = useCallback(() => removeItem(item.id), [removeItem, item.id]); + + return ( + + ); +} + +const RELATED_INTEGRATION_FIELD_CONFIG: FieldConfig = { + type: FIELD_TYPES.JSON, + validations: [{ validator: validateRelatedIntegration }], + defaultValue: DEFAULT_RELATED_INTEGRATION, +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.test.tsx new file mode 100644 index 00000000000000..21fa15c3587193 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.test.tsx @@ -0,0 +1,856 @@ +/* + * 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 { + screen, + render, + act, + fireEvent, + waitFor, + waitForElementToBeRemoved, +} from '@testing-library/react'; +import type { RelatedIntegration } from '../../../../../common/api/detection_engine'; +import { FIELD_TYPES, Form, useForm } from '../../../../shared_imports'; +import { createReactQueryWrapper } from '../../../../common/mock'; +import { fleetIntegrationsApi } from '../../../fleet_integrations/api/__mocks__'; +import { RelatedIntegrations } from './related_integrations'; + +// must match to the import in rules/related_integrations/use_integrations.tsx +jest.mock('../../../fleet_integrations/api'); +jest.mock('../../../../common/lib/kibana', () => ({ + useKibana: jest.fn().mockReturnValue({ + services: { + docLinks: { + links: { + securitySolution: { + ruleUiAdvancedParams: 'http://link-to-docs', + }, + }, + }, + }, + }), +})); + +const RELATED_INTEGRATION_ROW = 'relatedIntegrationRow'; +const COMBO_BOX_TOGGLE_BUTTON_TEST_ID = 'comboBoxToggleListButton'; +const COMBO_BOX_SELECTION_TEST_ID = 'euiComboBoxPill'; +const COMBO_BOX_CLEAR_BUTTON_TEST_ID = 'comboBoxClearButton'; +const VERSION_INPUT_TEST_ID = 'relatedIntegrationVersionDependency'; +const REMOVE_INTEGRATION_ROW_BUTTON_TEST_ID = 'relatedIntegrationRemove'; + +describe('RelatedIntegrations form part', () => { + beforeEach(() => { + fleetIntegrationsApi.fetchAllIntegrations.mockResolvedValue({ + integrations: [ + { + package_name: 'package-a', + package_title: 'Package A', + latest_package_version: '1.0.0', + is_installed: false, + is_enabled: false, + }, + ], + }); + }); + + it('renders related integrations legend', () => { + render(); + + expect(screen.getByText('Related integrations')).toBeVisible(); + }); + + describe('visual representation', () => { + it('shows package title when integration title is not set', async () => { + fleetIntegrationsApi.fetchAllIntegrations.mockResolvedValue({ + integrations: [ + { + package_name: 'package-a', + package_title: 'Package A', + latest_package_version: '1.2.0', + is_installed: false, + is_enabled: false, + }, + ], + }); + + render(, { wrapper: createReactQueryWrapper() }); + + await addRelatedIntegrationRow(); + await selectFirstEuiComboBoxOption({ + comboBoxToggleButton: screen.getByTestId(COMBO_BOX_TOGGLE_BUTTON_TEST_ID), + }); + + expect(screen.getByTestId(COMBO_BOX_SELECTION_TEST_ID)).toHaveTextContent('Package A'); + }); + + it('shows integration title when package and integration titles are set', async () => { + fleetIntegrationsApi.fetchAllIntegrations.mockResolvedValue({ + integrations: [ + { + package_name: 'package-a', + package_title: 'Package A', + integration_name: 'integration-a', + integration_title: 'Integration A', + latest_package_version: '1.2.0', + is_installed: false, + is_enabled: false, + }, + ], + }); + + render(, { wrapper: createReactQueryWrapper() }); + + await addRelatedIntegrationRow(); + await selectFirstEuiComboBoxOption({ + comboBoxToggleButton: screen.getByTestId(COMBO_BOX_TOGGLE_BUTTON_TEST_ID), + }); + + expect(screen.getByTestId(COMBO_BOX_SELECTION_TEST_ID)).toHaveTextContent('Integration A'); + }); + + it.each([ + [ + 'Not installed', + { + package_name: 'package-a', + package_title: 'Package A', + latest_package_version: '1.2.0', + is_installed: false, + is_enabled: false, + }, + ], + [ + 'Installed: Disabled', + { + package_name: 'package-a', + package_title: 'Package A', + installed_package_version: '1.2.0', + latest_package_version: '1.2.0', + is_installed: true, + is_enabled: false, + }, + ], + [ + 'Installed: Enabled', + { + package_name: 'package-a', + package_title: 'Package A', + installed_package_version: '1.2.0', + latest_package_version: '1.2.0', + is_installed: true, + is_enabled: true, + }, + ], + ])('shows integration status "%s" in combo box popover', async (status, integrationData) => { + fleetIntegrationsApi.fetchAllIntegrations.mockResolvedValue({ + integrations: [integrationData], + }); + + render(, { wrapper: createReactQueryWrapper() }); + + await addRelatedIntegrationRow(); + await showEuiComboBoxOptions(screen.getByTestId(COMBO_BOX_TOGGLE_BUTTON_TEST_ID)); + + expect(screen.getByRole('option')).toHaveTextContent(status); + }); + + it.each([ + [ + 'Package A', + { + package_name: 'package-a', + package_title: 'Package A', + latest_package_version: '1.2.0', + is_installed: false, + is_enabled: false, + }, + ], + [ + 'Package A: Disabled', + { + package_name: 'package-a', + package_title: 'Package A', + installed_package_version: '1.2.0', + latest_package_version: '1.2.0', + is_installed: true, + is_enabled: false, + }, + ], + [ + 'Package A: Enabled', + { + package_name: 'package-a', + package_title: 'Package A', + installed_package_version: '1.2.0', + latest_package_version: '1.2.0', + is_installed: true, + is_enabled: true, + }, + ], + ])( + 'shows integration name with its status "%s" when selected in combo box', + async (nameWithStatus, integrationData) => { + fleetIntegrationsApi.fetchAllIntegrations.mockResolvedValue({ + integrations: [integrationData], + }); + + render(, { wrapper: createReactQueryWrapper() }); + + await addRelatedIntegrationRow(); + await selectFirstEuiComboBoxOption({ + comboBoxToggleButton: screen.getByTestId(COMBO_BOX_TOGGLE_BUTTON_TEST_ID), + }); + + expect(screen.getByTestId(COMBO_BOX_SELECTION_TEST_ID)).toHaveTextContent( + new RegExp(`^${nameWithStatus}$`) + ); + } + ); + + it('shows integration version constraint corresponding to the latest package version when integration is NOT installed', async () => { + fleetIntegrationsApi.fetchAllIntegrations.mockResolvedValue({ + integrations: [ + { + package_name: 'package-a', + package_title: 'Package A', + latest_package_version: '1.2.0', + is_installed: false, + is_enabled: false, + }, + ], + }); + + render(, { wrapper: createReactQueryWrapper() }); + + await addRelatedIntegrationRow(); + await selectFirstEuiComboBoxOption({ + comboBoxToggleButton: screen.getByTestId(COMBO_BOX_TOGGLE_BUTTON_TEST_ID), + }); + + expect(screen.getByTestId(VERSION_INPUT_TEST_ID)).toHaveValue('^1.2.0'); + }); + + it('shows integration version constraint corresponding to the installed package version when integration is installed', async () => { + fleetIntegrationsApi.fetchAllIntegrations.mockResolvedValue({ + integrations: [ + { + package_name: 'package-a', + package_title: 'Package A', + installed_package_version: '1.1.0', + latest_package_version: '1.2.0', + is_installed: false, + is_enabled: false, + }, + ], + }); + + render(, { wrapper: createReactQueryWrapper() }); + + await addRelatedIntegrationRow(); + await selectFirstEuiComboBoxOption({ + comboBoxToggleButton: screen.getByTestId(COMBO_BOX_TOGGLE_BUTTON_TEST_ID), + }); + + expect(screen.getByTestId(VERSION_INPUT_TEST_ID)).toHaveValue('^1.1.0'); + }); + + it('shows saved earlier related integrations', async () => { + fleetIntegrationsApi.fetchAllIntegrations.mockResolvedValue({ + integrations: [ + { + package_name: 'package-a', + package_title: 'Package A', + latest_package_version: '1.0.0', + is_installed: false, + is_enabled: false, + }, + { + package_name: 'package-b', + package_title: 'Package B', + integration_name: 'integration-a', + integration_title: 'Integration A', + latest_package_version: '1.0.0', + is_installed: false, + is_enabled: false, + }, + ], + }); + + const initialRelatedIntegrations: RelatedIntegration[] = [ + { package: 'package-a', version: '1.2.3' }, + { package: 'package-b', integration: 'integration-a', version: '3.2.1' }, + ]; + + render(, { + wrapper: createReactQueryWrapper(), + }); + + await waitForIntegrationsToBeLoaded(); + + const visibleIntegrations = screen.getAllByTestId(COMBO_BOX_SELECTION_TEST_ID); + const visibleVersionInputs = screen.getAllByTestId(VERSION_INPUT_TEST_ID); + + expect(visibleIntegrations[0]).toHaveTextContent('Package A'); + expect(visibleVersionInputs[0]).toHaveValue('1.2.3'); + + expect(visibleIntegrations[1]).toHaveTextContent('Integration A'); + expect(visibleVersionInputs[1]).toHaveValue('3.2.1'); + }); + + it('shows saved earlier related integrations when there is no matching package found', async () => { + fleetIntegrationsApi.fetchAllIntegrations.mockResolvedValue({ + integrations: [], // package-a and package-b don't exist + }); + + const initialRelatedIntegrations: RelatedIntegration[] = [ + { package: 'package-a', version: '1.2.3' }, + { package: 'package-b', integration: 'integration-a', version: '3.2.1' }, + ]; + + render(, { + wrapper: createReactQueryWrapper(), + }); + + await waitForIntegrationsToBeLoaded(); + + const visibleIntegrations = screen.getAllByTestId(COMBO_BOX_SELECTION_TEST_ID); + const visibleVersionInputs = screen.getAllByTestId(VERSION_INPUT_TEST_ID); + + expect(visibleIntegrations[0]).toHaveTextContent('Package-a'); + expect(visibleVersionInputs[0]).toHaveValue('1.2.3'); + + expect(visibleIntegrations[1]).toHaveTextContent('Package-b integration-a'); + expect(visibleVersionInputs[1]).toHaveValue('3.2.1'); + }); + + it('shows saved earlier related integrations when API failed', async () => { + // suppress expected API error messages + jest.spyOn(console, 'error').mockReturnValue(); + + fleetIntegrationsApi.fetchAllIntegrations.mockRejectedValue(new Error('some error')); + + const initialRelatedIntegrations: RelatedIntegration[] = [ + { package: 'package-a', version: '1.2.3' }, + { package: 'package-b', integration: 'integration-a', version: '3.2.1' }, + ]; + + render(, { + wrapper: createReactQueryWrapper(), + }); + + await waitForIntegrationsToBeLoaded(); + + const visibleIntegrations = screen.getAllByTestId(COMBO_BOX_SELECTION_TEST_ID); + const visibleVersionInputs = screen.getAllByTestId(VERSION_INPUT_TEST_ID); + + expect(visibleIntegrations[0]).toHaveTextContent('Package-a'); + expect(visibleVersionInputs[0]).toHaveValue('1.2.3'); + + expect(visibleIntegrations[1]).toHaveTextContent('Package-b integration-a'); + expect(visibleVersionInputs[1]).toHaveValue('3.2.1'); + }); + }); + + describe('valid form submitting', () => { + it('returns undefined when no integrations are selected', async () => { + const handleSubmit = jest.fn(); + + render(, { wrapper: createReactQueryWrapper() }); + + await submitForm(); + await waitFor(() => { + expect(handleSubmit).toHaveBeenCalled(); + }); + + expect(handleSubmit).toHaveBeenCalledWith({ + data: undefined, + isValid: true, + }); + }); + + it('returns empty integrations when submitting not filled form', async () => { + const handleSubmit = jest.fn(); + + render(, { wrapper: createReactQueryWrapper() }); + + await addRelatedIntegrationRow(); + await addRelatedIntegrationRow(); + await submitForm(); + await waitFor(() => { + expect(handleSubmit).toHaveBeenCalled(); + }); + + expect(handleSubmit).toHaveBeenCalledWith({ + data: [ + { package: '', version: '' }, + { package: '', version: '' }, + ], + isValid: true, + }); + }); + + it('returns a mix of filled and empty integrations', async () => { + const handleSubmit = jest.fn(); + + render(, { wrapper: createReactQueryWrapper() }); + + await addRelatedIntegrationRow(); + await addRelatedIntegrationRow(); + await addRelatedIntegrationRow(); + await selectFirstEuiComboBoxOption({ + comboBoxToggleButton: screen.getAllByTestId(COMBO_BOX_TOGGLE_BUTTON_TEST_ID)[1], + }); + await submitForm(); + await waitFor(() => { + expect(handleSubmit).toHaveBeenCalled(); + }); + + expect(handleSubmit).toHaveBeenCalledWith({ + data: [ + { package: '', version: '' }, + { package: 'package-a', version: '^1.0.0' }, + { package: '', version: '' }, + ], + isValid: true, + }); + }); + + it('returns an empty integration after clearing selected integration', async () => { + const handleSubmit = jest.fn(); + + render(, { wrapper: createReactQueryWrapper() }); + + await addRelatedIntegrationRow(); + await selectFirstEuiComboBoxOption({ + comboBoxToggleButton: screen.getByTestId(COMBO_BOX_TOGGLE_BUTTON_TEST_ID), + }); + await clearEuiComboBoxSelection({ + clearButton: screen.getByTestId(COMBO_BOX_CLEAR_BUTTON_TEST_ID), + }); + + await submitForm(); + await waitFor(() => { + expect(handleSubmit).toHaveBeenCalled(); + }); + + expect(handleSubmit).toHaveBeenCalledWith({ + data: [{ package: '', version: '' }], + isValid: true, + }); + }); + + it('returns a selected integration', async () => { + fleetIntegrationsApi.fetchAllIntegrations.mockResolvedValue({ + integrations: [ + { + package_name: 'package-a', + package_title: 'Package A', + latest_package_version: '1.2.0', + is_installed: false, + is_enabled: false, + }, + ], + }); + + const handleSubmit = jest.fn(); + + render(, { wrapper: createReactQueryWrapper() }); + + await addRelatedIntegrationRow(); + await selectEuiComboBoxOption({ + comboBoxToggleButton: screen.getByTestId(COMBO_BOX_TOGGLE_BUTTON_TEST_ID), + optionIndex: 0, + }); + await submitForm(); + await waitFor(() => { + expect(handleSubmit).toHaveBeenCalled(); + }); + + expect(handleSubmit).toHaveBeenCalledWith({ + data: [{ integration: undefined, package: 'package-a', version: '^1.2.0' }], + isValid: true, + }); + }); + + it('returns a selected integration with version constraint modified', async () => { + fleetIntegrationsApi.fetchAllIntegrations.mockResolvedValue({ + integrations: [ + { + package_name: 'package-a', + package_title: 'Package A', + latest_package_version: '1.2.0', + is_installed: false, + is_enabled: false, + }, + ], + }); + + const handleSubmit = jest.fn(); + + render(, { wrapper: createReactQueryWrapper() }); + + await addRelatedIntegrationRow(); + await selectEuiComboBoxOption({ + comboBoxToggleButton: screen.getByTestId(COMBO_BOX_TOGGLE_BUTTON_TEST_ID), + optionIndex: 0, + }); + await setVersion({ input: screen.getByTestId(VERSION_INPUT_TEST_ID), value: '1.0.0' }); + await submitForm(); + await waitFor(() => { + expect(handleSubmit).toHaveBeenCalled(); + }); + + expect(handleSubmit).toHaveBeenCalledWith({ + data: [{ integration: undefined, package: 'package-a', version: '1.0.0' }], + isValid: true, + }); + }); + + it('returns saved earlier integrations', async () => { + fleetIntegrationsApi.fetchAllIntegrations.mockResolvedValue({ + integrations: [ + { + package_name: 'package-a', + package_title: 'Package A', + latest_package_version: '1.0.0', + is_installed: false, + is_enabled: false, + }, + { + package_name: 'package-b', + package_title: 'Package B', + integration_name: 'integration-a', + integration_title: 'Integration A', + latest_package_version: '1.0.0', + is_installed: false, + is_enabled: false, + }, + ], + }); + + const initialRelatedIntegrations: RelatedIntegration[] = [ + { package: 'package-a', version: '1.2.3' }, + { package: 'package-b', integration: 'integration-a', version: '3.2.1' }, + ]; + const handleSubmit = jest.fn(); + + render(, { + wrapper: createReactQueryWrapper(), + }); + await submitForm(); + await waitFor(() => { + expect(handleSubmit).toHaveBeenCalled(); + }); + + expect(handleSubmit).toHaveBeenCalledWith({ + data: [ + { package: 'package-a', integration: undefined, version: '1.2.3' }, + { package: 'package-b', integration: 'integration-a', version: '3.2.1' }, + ], + isValid: true, + }); + }); + + it('returns a saved earlier integration when there is no matching package found', async () => { + fleetIntegrationsApi.fetchAllIntegrations.mockResolvedValue({ + integrations: [], + }); + + const initialRelatedIntegrations: RelatedIntegration[] = [ + { package: 'package-a', version: '1.2.3' }, + ]; + const handleSubmit = jest.fn(); + + render(, { + wrapper: createReactQueryWrapper(), + }); + await submitForm(); + await waitFor(() => { + expect(handleSubmit).toHaveBeenCalled(); + }); + + expect(handleSubmit).toHaveBeenCalledWith({ + data: [{ integration: undefined, package: 'package-a', version: '1.2.3' }], + isValid: true, + }); + }); + + it('returns a saved earlier integration when API failed', async () => { + // suppress expected API error messages + jest.spyOn(console, 'error').mockReturnValue(); + + fleetIntegrationsApi.fetchAllIntegrations.mockRejectedValue(new Error('some error')); + + const initialRelatedIntegrations: RelatedIntegration[] = [ + { package: 'package-a', version: '^1.2.3' }, + ]; + const handleSubmit = jest.fn(); + + render(, { + wrapper: createReactQueryWrapper(), + }); + await submitForm(); + await waitFor(() => { + expect(handleSubmit).toHaveBeenCalled(); + }); + + expect(handleSubmit).toHaveBeenCalledWith({ + data: [{ integration: undefined, package: 'package-a', version: '^1.2.3' }], + isValid: true, + }); + }); + }); + + describe('validation errors', () => { + it('shows an error when version constraint is invalid', async () => { + render(, { wrapper: createReactQueryWrapper() }); + + await addRelatedIntegrationRow(); + await selectFirstEuiComboBoxOption({ + comboBoxToggleButton: screen.getByTestId(COMBO_BOX_TOGGLE_BUTTON_TEST_ID), + }); + await setVersion({ input: screen.getByTestId(VERSION_INPUT_TEST_ID), value: '100' }); + + expect(screen.getByTestId(RELATED_INTEGRATION_ROW)).toHaveTextContent( + 'Version constraint is invalid' + ); + }); + }); + + describe('removing an item', () => { + describe('when there is more than one item', () => { + it('removes just added item', async () => { + render(, { wrapper: createReactQueryWrapper() }); + + await addRelatedIntegrationRow(); + await addRelatedIntegrationRow(); + await removeLastRelatedIntegrationRow(); + + expect(screen.getAllByTestId(RELATED_INTEGRATION_ROW)).toHaveLength(1); + }); + + it('removes just added item after integration has been selected', async () => { + render(, { wrapper: createReactQueryWrapper() }); + + await addRelatedIntegrationRow(); + await addRelatedIntegrationRow(); + await selectFirstEuiComboBoxOption({ + comboBoxToggleButton: screen.getAllByTestId(COMBO_BOX_TOGGLE_BUTTON_TEST_ID).at(-1)!, + }); + await removeLastRelatedIntegrationRow(); + + expect(screen.getAllByTestId(RELATED_INTEGRATION_ROW)).toHaveLength(1); + }); + + it('submits an empty integration when just added integrations removed', async () => { + const handleSubmit = jest.fn(); + + render(, { wrapper: createReactQueryWrapper() }); + + await addRelatedIntegrationRow(); + await addRelatedIntegrationRow(); + await selectFirstEuiComboBoxOption({ + comboBoxToggleButton: screen.getAllByTestId(COMBO_BOX_TOGGLE_BUTTON_TEST_ID).at(-1)!, + }); + await removeLastRelatedIntegrationRow(); + await submitForm(); + await waitFor(() => { + expect(handleSubmit).toHaveBeenCalled(); + }); + + expect(handleSubmit).toHaveBeenCalledWith({ + data: [{ package: '', version: '' }], + isValid: true, + }); + }); + }); + + describe('sticky last form row', () => { + it('does not remove the last item', async () => { + render(, { wrapper: createReactQueryWrapper() }); + + await addRelatedIntegrationRow(); + await removeLastRelatedIntegrationRow(); + + expect(screen.getAllByTestId(RELATED_INTEGRATION_ROW)).toHaveLength(1); + }); + + it('disables remove button after clicking remove button on the last item', async () => { + render(, { wrapper: createReactQueryWrapper() }); + + await addRelatedIntegrationRow(); + await removeLastRelatedIntegrationRow(); + + expect(screen.getByTestId(REMOVE_INTEGRATION_ROW_BUTTON_TEST_ID)).toBeDisabled(); + }); + + it('clears selected integration when clicking remove the last form row button', async () => { + render(, { wrapper: createReactQueryWrapper() }); + + await addRelatedIntegrationRow(); + await selectFirstEuiComboBoxOption({ + comboBoxToggleButton: getLastByTestId(COMBO_BOX_TOGGLE_BUTTON_TEST_ID), + }); + await removeLastRelatedIntegrationRow(); + + expect(screen.queryByTestId(COMBO_BOX_SELECTION_TEST_ID)).not.toBeInTheDocument(); + }); + + it('submits an empty integration after clicking remove the last form row button', async () => { + const handleSubmit = jest.fn(); + + render(, { wrapper: createReactQueryWrapper() }); + + await addRelatedIntegrationRow(); + await selectFirstEuiComboBoxOption({ + comboBoxToggleButton: getLastByTestId(COMBO_BOX_TOGGLE_BUTTON_TEST_ID), + }); + await removeLastRelatedIntegrationRow(); + await submitForm(); + await waitFor(() => { + expect(handleSubmit).toHaveBeenCalled(); + }); + + expect(handleSubmit).toHaveBeenCalledWith({ + data: [{ package: '', version: '' }], + isValid: true, + }); + }); + + it('submits an empty integration after previously saved integrations were removed', async () => { + const initialRelatedIntegrations: RelatedIntegration[] = [ + { package: 'package-a', version: '^1.2.3' }, + ]; + const handleSubmit = jest.fn(); + + render(, { + wrapper: createReactQueryWrapper(), + }); + + await waitForIntegrationsToBeLoaded(); + await removeLastRelatedIntegrationRow(); + await submitForm(); + await waitFor(() => { + expect(handleSubmit).toHaveBeenCalled(); + }); + + expect(handleSubmit).toHaveBeenCalledWith({ + data: [{ package: '', version: '' }], + isValid: true, + }); + }); + }); + }); +}); + +interface TestFormProps { + initialState?: RelatedIntegration[]; + onSubmit?: (args: { data: RelatedIntegration[]; isValid: boolean }) => void; +} + +function TestForm({ initialState, onSubmit }: TestFormProps): JSX.Element { + const { form } = useForm({ + options: { stripEmptyFields: false }, + schema: { + relatedIntegrationsField: { + type: FIELD_TYPES.JSON, + }, + }, + defaultValue: { + relatedIntegrationsField: initialState, + }, + onSubmit: async (formData, isValid) => + onSubmit?.({ data: formData.relatedIntegrationsField, isValid }), + }); + + return ( +
+ + + + ); +} + +function getLastByTestId(testId: string): HTMLElement { + // getAllByTestId throws an error when there are no `testId` elements found + return screen.getAllByTestId(testId).at(-1)!; +} + +function waitForIntegrationsToBeLoaded(): Promise { + return waitForElementToBeRemoved(screen.queryAllByRole('progressbar')); +} + +function addRelatedIntegrationRow(): Promise { + return act(async () => { + fireEvent.click(screen.getByText('Add integration')); + }); +} + +function removeLastRelatedIntegrationRow(): Promise { + return act(async () => { + const lastRemoveButton = screen.getAllByTestId(REMOVE_INTEGRATION_ROW_BUTTON_TEST_ID).at(-1); + + if (!lastRemoveButton) { + throw new Error(`There are no "${REMOVE_INTEGRATION_ROW_BUTTON_TEST_ID}" found`); + } + + fireEvent.click(lastRemoveButton); + }); +} + +function showEuiComboBoxOptions(comboBoxToggleButton: HTMLElement): Promise { + fireEvent.click(comboBoxToggleButton); + + return waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); +} + +function selectEuiComboBoxOption({ + comboBoxToggleButton, + optionIndex, +}: { + comboBoxToggleButton: HTMLElement; + optionIndex: number; +}): Promise { + return act(async () => { + await showEuiComboBoxOptions(comboBoxToggleButton); + + fireEvent.click(screen.getAllByRole('option')[optionIndex]); + }); +} + +function clearEuiComboBoxSelection({ clearButton }: { clearButton: HTMLElement }): Promise { + return act(async () => { + fireEvent.click(clearButton); + }); +} + +function selectFirstEuiComboBoxOption({ + comboBoxToggleButton, +}: { + comboBoxToggleButton: HTMLElement; +}): Promise { + return selectEuiComboBoxOption({ comboBoxToggleButton, optionIndex: 0 }); +} + +function setVersion({ input, value }: { input: HTMLInputElement; value: string }): Promise { + return act(async () => { + fireEvent.input(input, { + target: { value }, + }); + }); +} + +function submitForm(): Promise { + return act(async () => { + fireEvent.click(screen.getByText('Submit')); + }); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.tsx new file mode 100644 index 00000000000000..a57ce5fe8cd7b0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { UseArray, useFormData } from '../../../../shared_imports'; +import { RelatedIntegrationsHelpInfo } from './related_integrations_help_info'; +import { RelatedIntegrationFieldRow } from './related_integration_field_row'; +import * as i18n from './translations'; + +interface RelatedIntegrationsProps { + path: string; + dataTestSubj?: string; +} + +export function RelatedIntegrations({ path, dataTestSubj }: RelatedIntegrationsProps): JSX.Element { + const label = ( + <> + {i18n.RELATED_INTEGRATIONS_LABEL} + + + ); + const [formData] = useFormData(); + + return ( + + {({ items, addItem, removeItem }) => ( + + {i18n.OPTIONAL} + + } + labelType="legend" + fullWidth + data-test-subj={dataTestSubj} + hasChildLabel={false} + > + <> + + {items.map((item) => ( + + + + ))} + + {items.length > 0 && } + + {i18n.ADD_INTEGRATION} + + + + )} + + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations_help_info.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations_help_info.tsx new file mode 100644 index 00000000000000..b694d17a804355 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations_help_info.tsx @@ -0,0 +1,63 @@ +/* + * 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 { useToggle } from 'react-use'; +import { EuiLink, EuiPopover, EuiText, EuiButtonIcon } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '../../../../common/lib/kibana'; + +/** + * Theme doesn't expose width variables. Using provided size variables will require + * multiplying it by another magic constant. + * + * 320px width looks + * like a [commonly used width in EUI](https://github.com/search?q=repo%3Aelastic%2Feui%20320&type=code). + */ +const POPOVER_WIDTH = 320; + +export function RelatedIntegrationsHelpInfo(): JSX.Element { + const [isPopoverOpen, togglePopover] = useToggle(false); + const { docLinks } = useKibana().services; + + const button = ( + + ); + + return ( + + + + + + ), + semverLink: ( + + + + ), + }} + /> + + + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/translations.ts new file mode 100644 index 00000000000000..2645298783f5cc --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/translations.ts @@ -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. + */ + +import { i18n } from '@kbn/i18n'; + +export const RELATED_INTEGRATIONS_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.fieldRelatedIntegrationsLabel', + { + defaultMessage: 'Related integrations', + } +); + +export const OPTIONAL = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.optionalText', + { + defaultMessage: 'Optional', + } +); + +export const RELATED_INTEGRATION_FIELDS_HELP_TEXT = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.helpText', + { + defaultMessage: 'Select an integration and correct a version constraint if necessary.', + } +); + +export const RELATED_INTEGRATION_ARIA_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.relatedIntegrationAriaLabel', + { + defaultMessage: 'Integrations selector', + } +); + +export const RELATED_INTEGRATION_VERSION_DEPENDENCY_ARIA_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.relatedIntegrationVersionDependencyAriaLabel', + { + defaultMessage: 'Related integration version constraint', + } +); + +export const RELATED_INTEGRATION_VERSION_DEPENDENCY_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.relatedIntegrationVersionDependencyPlaceholder', + { + defaultMessage: 'Semver', + } +); + +export const REMOVE_RELATED_INTEGRATION_BUTTON_ARIA_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.removeRelatedIntegrationButtonAriaLabel', + { + defaultMessage: 'Remove related integration', + } +); + +export const ADD_INTEGRATION = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.addIntegration', + { + defaultMessage: 'Add integration', + } +); + +export const INTEGRATION_VERSION = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.integrationVersion', + { + defaultMessage: 'Version', + } +); + +export const INTEGRATION_REQUIRED = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.validation.integrationRequired', + { + defaultMessage: 'Integration must be selected', + } +); + +export const VERSION_DEPENDENCY_REQUIRED = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.validation.versionRequired', + { + defaultMessage: 'Version constraint must be specified', + } +); + +export const VERSION_DEPENDENCY_INVALID = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.validation.versionInvalid', + { + defaultMessage: + 'Version constraint is invalid. Only tilde, caret or plain version supported e.g. ~1.2.3, ^1.2.3 or 1.2.3.', + } +); + +export const INTEGRATION_NOT_INSTALLED = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.notInstalledText', + { + defaultMessage: 'Not installed', + } +); + +export const INTEGRATION_INSTALLED_AND_DISABLED = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.installedDisabledText', + { + defaultMessage: 'Installed: Disabled', + } +); + +export const INTEGRATION_INSTALLED_AND_ENABLED = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.installedEnabledText', + { + defaultMessage: 'Installed: Enabled', + } +); + +export const INTEGRATION_DISABLED = (integrationTitle: string) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.integrationDisabledText', + { + defaultMessage: '{integrationTitle}: Disabled', + values: { + integrationTitle, + }, + } + ); + +export const INTEGRATION_ENABLED = (integrationTitle: string) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrations.integrationEnabledText', + { + defaultMessage: '{integrationTitle}: Enabled', + values: { + integrationTitle, + }, + } + ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/validate_related_integration.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/validate_related_integration.test.ts new file mode 100644 index 00000000000000..d991101cfce4b2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/validate_related_integration.test.ts @@ -0,0 +1,101 @@ +/* + * 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 { RelatedIntegration } from '../../../../../common/api/detection_engine'; +import type { ValidationFuncArg } from '../../../../shared_imports'; +import { validateRelatedIntegration } from './validate_related_integration'; + +describe('validateRelatedIntegration', () => { + describe('with successful outcome', () => { + it.each([ + ['simple package version dependency', { package: 'some-package', version: '1.2.3' }], + ['caret package version dependency', { package: 'some-package', version: '^1.2.3' }], + ['tilde package version dependency', { package: 'some-package', version: '~1.2.3' }], + ])(`validates %s`, (_, relatedIntegration) => { + const arg = { + value: relatedIntegration, + } as ValidationFuncArg; + + const result = validateRelatedIntegration(arg); + + expect(result).toBeUndefined(); + }); + + it('validates empty package as a valid related integration', () => { + const relatedIntegration = { package: '', version: '1.2.3' }; + const arg = { + value: relatedIntegration, + path: 'form.path.to.field', + } as ValidationFuncArg; + + const result = validateRelatedIntegration(arg); + + expect(result).toBeUndefined(); + }); + + it('ignores version when package is empty', () => { + const relatedIntegration = { package: '', version: 'invalid' }; + const arg = { + value: relatedIntegration, + path: 'form.path.to.field', + } as ValidationFuncArg; + + const result = validateRelatedIntegration(arg); + + expect(result).toBeUndefined(); + }); + }); + + describe('with unsuccessful outcome', () => { + it('validates empty version', () => { + const relatedIntegration = { package: 'some-package', version: '' }; + const arg = { + value: relatedIntegration, + path: 'form.path.to.field', + } as ValidationFuncArg; + + const result = validateRelatedIntegration(arg); + + expect(result).toMatchObject({ + code: 'ERR_FIELD_MISSING', + path: 'form.path.to.field.version', + }); + }); + + it('validates version with white spaces', () => { + const relatedIntegration = { package: 'some-package', version: ' ' }; + const arg = { + value: relatedIntegration, + path: 'form.path.to.field', + } as ValidationFuncArg; + + const result = validateRelatedIntegration(arg); + + expect(result).toMatchObject({ + code: 'ERR_FIELD_MISSING', + path: 'form.path.to.field.version', + }); + }); + + it.each([ + ['invalid format version', { package: 'some-package', version: '^1.2.' }], + ['unexpected version spaces', { package: 'some-package', version: ' ~ 1.2.3' }], + ])(`validates %s`, (_, relatedIntegration) => { + const arg = { + value: relatedIntegration, + path: 'form.path.to.field', + } as ValidationFuncArg; + + const result = validateRelatedIntegration(arg); + + expect(result).toMatchObject({ + code: 'ERR_FIELD_FORMAT', + path: 'form.path.to.field.version', + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/validate_related_integration.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/validate_related_integration.ts new file mode 100644 index 00000000000000..3cbf2eff33b3dc --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/validate_related_integration.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RelatedIntegration } from '../../../../../common/api/detection_engine'; +import type { FormData, ERROR_CODE, ValidationFunc } from '../../../../shared_imports'; +import * as i18n from './translations'; + +export function validateRelatedIntegration( + ...args: Parameters> +): ReturnType> | undefined { + const [{ value, path }] = args; + + // It allows to submit empty fields for better UX + // When integration isn't selected version shouldn't be validated + if (value.package.trim().length === 0) { + return; + } + + if (value.version.trim().length === 0) { + return { + code: 'ERR_FIELD_MISSING', + path: `${path}.version`, + message: i18n.VERSION_DEPENDENCY_REQUIRED, + }; + } + + if (!SEMVER_PATTERN.test(value.version)) { + return { + code: 'ERR_FIELD_FORMAT', + path: `${path}.version`, + message: i18n.VERSION_DEPENDENCY_INVALID, + }; + } +} + +const SEMVER_PATTERN = /^(\~|\^)?\d+\.\d+\.\d+$/; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx index 22e43dc31acb87..de34718ef050f6 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx @@ -5,26 +5,22 @@ * 2.0. */ -import React from 'react'; -import { mount } from 'enzyme'; - -import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; - +import React, { useEffect, useState } from 'react'; +import { screen, fireEvent, render, within, act, waitFor } from '@testing-library/react'; +import type { Type as RuleType } from '@kbn/securitysolution-io-ts-alerting-types'; import { StepDefineRule, aggregatableFields } from '.'; import { mockBrowserFields } from '../../../../common/containers/source/mock'; import { useRuleFromTimeline } from '../../../../detections/containers/detection_engine/rules/use_rule_from_timeline'; -import { fireEvent, render, within } from '@testing-library/react'; import { TestProviders } from '../../../../common/mock'; -import { useRuleForms } from '../../pages/form'; -import { stepActionsDefaultValue } from '../../../rule_creation/components/step_rule_actions'; -import { - defaultSchedule, - stepAboutDefaultValue, - stepDefineDefaultValue, -} from '../../../../detections/pages/detection_engine/rules/utils'; -import type { FormHook } from '../../../../shared_imports'; +import { schema as defineRuleSchema } from './schema'; +import { stepDefineDefaultValue } from '../../../../detections/pages/detection_engine/rules/utils'; +import type { FormSubmitHandler } from '../../../../shared_imports'; +import { useForm } from '../../../../shared_imports'; import type { DefineStepRule } from '../../../../detections/pages/detection_engine/rules/types'; +import { fleetIntegrationsApi } from '../../../fleet_integrations/api/__mocks__'; +// Mocks integrations +jest.mock('../../../fleet_integrations/api'); jest.mock('../../../../common/components/query_bar', () => { return { QueryBar: jest.fn(({ filterQuery }) => { @@ -279,37 +275,261 @@ test('aggregatableFields with aggregatable: true', function () { const mockUseRuleFromTimeline = useRuleFromTimeline as jest.Mock; const onOpenTimeline = jest.fn(); + +const COMBO_BOX_TOGGLE_BUTTON_TEST_ID = 'comboBoxToggleListButton'; +const VERSION_INPUT_TEST_ID = 'relatedIntegrationVersionDependency'; + describe('StepDefineRule', () => { - const TestComp = ({ - setFormRef, - ruleType = stepDefineDefaultValue.ruleType, - }: { - setFormRef: (form: FormHook) => void; - ruleType?: Type; - }) => { - const { defineStepForm, eqlOptionsSelected, setEqlOptionsSelected } = useRuleForms({ - defineStepDefault: { ...stepDefineDefaultValue, ruleType }, - aboutStepDefault: stepAboutDefaultValue, - scheduleStepDefault: defaultSchedule, - actionsStepDefault: stepActionsDefaultValue, + beforeEach(() => { + jest.clearAllMocks(); + mockUseRuleFromTimeline.mockReturnValue({ onOpenTimeline, loading: false }); + }); + + it('renders correctly', () => { + render(, { + wrapper: TestProviders, + }); + + expect(screen.getByTestId('stepDefineRule')).toBeDefined(); + }); + + describe('related integrations', () => { + beforeEach(() => { + fleetIntegrationsApi.fetchAllIntegrations.mockResolvedValue({ + integrations: [ + { + package_name: 'package-a', + package_title: 'Package A', + latest_package_version: '1.0.0', + is_installed: false, + is_enabled: false, + }, + ], + }); + }); + + it('submits form without selected related integrations', async () => { + const initialState = { + index: ['test-index'], + queryBar: { + query: { query: '*:*', language: 'kuery' }, + filters: [], + saved_id: null, + }, + }; + const handleSubmit = jest.fn(); + + render(, { + wrapper: TestProviders, + }); + + await submitForm(); + await waitFor(() => { + expect(handleSubmit).toHaveBeenCalled(); + }); + + expect(handleSubmit).toHaveBeenCalledWith( + expect.not.objectContaining({ + relatedIntegrations: expect.anything(), + }), + true + ); + }); + + it('submits saved early related integrations', async () => { + const initialState = { + index: ['test-index'], + queryBar: { + query: { query: '*:*', language: 'kuery' }, + filters: [], + saved_id: null, + }, + relatedIntegrations: [ + { package: 'package-a', version: '1.2.3' }, + { package: 'package-b', integration: 'integration-a', version: '3.2.1' }, + ], + }; + const handleSubmit = jest.fn(); + + render(, { + wrapper: TestProviders, + }); + + await submitForm(); + await waitFor(() => { + expect(handleSubmit).toHaveBeenCalled(); + }); + + expect(handleSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + relatedIntegrations: [ + { package: 'package-a', version: '1.2.3' }, + { package: 'package-b', integration: 'integration-a', version: '3.2.1' }, + ], + }), + true + ); + }); + + it('submits a selected related integration', async () => { + const initialState = { + index: ['test-index'], + queryBar: { + query: { query: '*:*', language: 'kuery' }, + filters: [], + saved_id: null, + }, + relatedIntegrations: undefined, + }; + const handleSubmit = jest.fn(); + + render(, { + wrapper: TestProviders, + }); + + await addRelatedIntegrationRow(); + await selectEuiComboBoxOption({ + comboBoxToggleButton: within(screen.getByTestId('relatedIntegrations')).getByTestId( + COMBO_BOX_TOGGLE_BUTTON_TEST_ID + ), + optionIndex: 0, + }); + await setVersion({ input: screen.getByTestId(VERSION_INPUT_TEST_ID), value: '1.2.3' }); + + await submitForm(); + await waitFor(() => { + expect(handleSubmit).toHaveBeenCalled(); + }); + + expect(handleSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + relatedIntegrations: [{ package: 'package-a', version: '1.2.3' }], + }), + true + ); + }); + }); + + describe('handleSetRuleFromTimeline', () => { + it('updates KQL query correctly', () => { + const kqlQuery = { + index: ['.alerts-security.alerts-default', 'logs-*', 'packetbeat-*'], + queryBar: { + filters: [], + query: { + query: 'host.name:*', + language: 'kuery', + }, + saved_id: null, + }, + }; + + mockUseRuleFromTimeline.mockImplementation((handleSetRuleFromTimeline) => { + useEffect(() => { + handleSetRuleFromTimeline(kqlQuery); + }, [handleSetRuleFromTimeline]); + + return { onOpenTimeline, loading: false }; + }); + + render(, { + wrapper: TestProviders, + }); + + expect(screen.getAllByTestId('query-bar')[0].textContent).toBe( + `${kqlQuery.queryBar.query.query} ${kqlQuery.queryBar.query.language}` + ); }); - setFormRef(defineStepForm); + it('updates EQL query correctly', async () => { + const eqlQuery = { + index: ['.alerts-security.alerts-default', 'logs-*', 'packetbeat-*'], + queryBar: { + filters: [], + query: { + query: 'process where true', + language: 'eql', + }, + saved_id: null, + }, + eqlOptions: { + eventCategoryField: 'cool.field', + tiebreakerField: 'another.field', + timestampField: 'cool.@timestamp', + query: 'process where true', + size: 77, + }, + }; + + mockUseRuleFromTimeline.mockImplementation((handleSetRuleFromTimeline) => { + useEffect(() => { + handleSetRuleFromTimeline(eqlQuery); + }, [handleSetRuleFromTimeline]); + + return { onOpenTimeline, loading: false }; + }); - return ( + render(, { + wrapper: TestProviders, + }); + + expect(screen.getByTestId(`eqlQueryBarTextInput`).textContent).toEqual( + eqlQuery.queryBar.query.query + ); + + await act(async () => { + fireEvent.click(screen.getByTestId('eql-settings-trigger')); + }); + + expect( + within(screen.getByTestId('eql-event-category-field')).queryByRole('combobox') + ).toHaveValue(eqlQuery.eqlOptions.eventCategoryField); + + expect( + within(screen.getByTestId('eql-tiebreaker-field')).queryByRole('combobox') + ).toHaveValue(eqlQuery.eqlOptions.tiebreakerField); + + expect(within(screen.getByTestId('eql-timestamp-field')).queryByRole('combobox')).toHaveValue( + eqlQuery.eqlOptions.timestampField + ); + }); + }); +}); + +interface TestFormProps { + ruleType?: RuleType; + initialState?: Partial; + onSubmit?: FormSubmitHandler; +} + +function TestForm({ + ruleType = stepDefineDefaultValue.ruleType, + initialState, + onSubmit, +}: TestFormProps): JSX.Element { + const [selectedEqlOptions, setSelectedEqlOptions] = useState(stepDefineDefaultValue.eqlOptions); + const { form } = useForm({ + options: { stripEmptyFields: false }, + schema: defineRuleSchema, + defaultValue: { ...stepDefineDefaultValue, ...initialState }, + onSubmit, + }); + + return ( + <> {}} - setIsThreatQueryBarValid={() => {}} + setIsQueryBarValid={jest.fn()} + setIsThreatQueryBarValid={jest.fn()} ruleType={ruleType} index={stepDefineDefaultValue.index} threatIndex={stepDefineDefaultValue.threatIndex} @@ -321,87 +541,51 @@ describe('StepDefineRule', () => { thresholdFields={[]} enableThresholdSuppression={false} /> - ); - }; - beforeEach(() => { - jest.clearAllMocks(); - mockUseRuleFromTimeline.mockReturnValue({ onOpenTimeline, loading: false }); + + + ); +} + +function submitForm(): Promise { + return act(async () => { + fireEvent.click(screen.getByText('Submit')); }); - it('renders correctly', () => { - const wrapper = mount( {}} />, { - wrappingComponent: TestProviders, - }); +} - expect(wrapper.find('Form[data-test-subj="stepDefineRule"]')).toHaveLength(1); +function addRelatedIntegrationRow(): Promise { + return act(async () => { + fireEvent.click(screen.getByText('Add integration')); }); +} - const kqlQuery = { - index: ['.alerts-security.alerts-default', 'logs-*', 'packetbeat-*'], - queryBar: { - filters: [], - query: { - query: 'host.name:*', - language: 'kuery', - }, - saved_id: null, - }, - }; +function showEuiComboBoxOptions(comboBoxToggleButton: HTMLElement): Promise { + fireEvent.click(comboBoxToggleButton); - const eqlQuery = { - index: ['.alerts-security.alerts-default', 'logs-*', 'packetbeat-*'], - queryBar: { - filters: [], - query: { - query: 'process where true', - language: 'eql', - }, - saved_id: null, - }, - eqlOptions: { - eventCategoryField: 'cool.field', - tiebreakerField: 'another.field', - timestampField: 'cool.@timestamp', - query: 'process where true', - size: 77, - }, - }; - it('handleSetRuleFromTimeline correctly updates the query', () => { - mockUseRuleFromTimeline.mockImplementation((handleSetRuleFromTimeline) => { - handleSetRuleFromTimeline(kqlQuery); - return { onOpenTimeline, loading: false }; - }); - const { getAllByTestId } = render( - - {}} /> - - ); - expect(getAllByTestId('query-bar')[0].textContent).toEqual( - `${kqlQuery.queryBar.query.query} ${kqlQuery.queryBar.query.language}` - ); + return waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); }); - it('handleSetRuleFromTimeline correctly updates eql query', async () => { - mockUseRuleFromTimeline - .mockImplementationOnce(() => ({ onOpenTimeline, loading: false })) - .mockImplementationOnce((handleSetRuleFromTimeline) => { - handleSetRuleFromTimeline(eqlQuery); - return { onOpenTimeline, loading: false }; - }); - const { getByTestId } = render( - - {}} ruleType="eql" /> - - ); - expect(getByTestId(`eqlQueryBarTextInput`).textContent).toEqual(eqlQuery.queryBar.query.query); - fireEvent.click(getByTestId(`eql-settings-trigger`)); - - expect(within(getByTestId(`eql-event-category-field`)).queryByRole('combobox')).toHaveValue( - eqlQuery.eqlOptions.eventCategoryField - ); - expect(within(getByTestId(`eql-tiebreaker-field`)).queryByRole('combobox')).toHaveValue( - eqlQuery.eqlOptions.tiebreakerField - ); - expect(within(getByTestId(`eql-timestamp-field`)).queryByRole('combobox')).toHaveValue( - eqlQuery.eqlOptions.timestampField - ); +} + +function selectEuiComboBoxOption({ + comboBoxToggleButton, + optionIndex, +}: { + comboBoxToggleButton: HTMLElement; + optionIndex: number; +}): Promise { + return act(async () => { + await showEuiComboBoxOptions(comboBoxToggleButton); + + fireEvent.click(within(screen.getByRole('listbox')).getAllByRole('option')[optionIndex]); }); -}); +} + +function setVersion({ input, value }: { input: HTMLInputElement; value: string }): Promise { + return act(async () => { + fireEvent.input(input, { + target: { value }, + }); + }); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx index 987274ee5488ec..317deb94797380 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx @@ -99,6 +99,7 @@ import { DurationInput } from '../duration_input'; import { MINIMUM_LICENSE_FOR_SUPPRESSION } from '../../../../../common/detection_engine/constants'; import { useUpsellingMessage } from '../../../../common/hooks/use_upselling'; import { useAlertSuppression } from '../../../rule_management/logic/use_alert_suppression'; +import { RelatedIntegrations } from '../../../rule_creation/components/related_integrations'; const CommonUseField = getUseField({ component: Field }); @@ -1114,6 +1115,9 @@ const StepDefineRuleComponent: FC = ({ + + + = { ], }, relatedIntegrations: { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRelatedIntegrationsLabel', - { - defaultMessage: 'Related integrations', - } - ), - helpText: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRelatedIntegrationsHelpText', - { - defaultMessage: 'Integration related to this Rule.', - } - ), + type: FIELD_TYPES.JSON, }, requiredFields: { label: i18n.translate( diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts index 40da9e9a204a41..e00e18ed7f6ea4 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts @@ -129,11 +129,55 @@ describe('helpers', () => { type: 'query', timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', timeline_title: 'Titled timeline', + related_integrations: [ + { + package: 'aws', + integration: 'route53', + version: '~1.2.3', + }, + { + package: 'system', + version: '^1.2.3', + }, + ], }; expect(result).toEqual(expected); }); + test('filters out empty related integrations', () => { + const result = formatDefineStepData({ + ...mockData, + relatedIntegrations: [ + { package: '', version: '' }, + { + package: 'aws', + integration: 'route53', + version: '~1.2.3', + }, + { package: '', version: '' }, + { + package: 'system', + version: '^1.2.3', + }, + ], + }); + + expect(result).toMatchObject({ + related_integrations: [ + { + package: 'aws', + integration: 'route53', + version: '~1.2.3', + }, + { + package: 'system', + version: '^1.2.3', + }, + ], + }); + }); + describe('saved_query and query rule types', () => { test('returns query rule if savedId provided but shouldLoadQueryDynamically != true', () => { const mockStepData: DefineStepRule = { @@ -308,6 +352,17 @@ describe('helpers', () => { machine_learning_job_id: ['some_jobert_id'], timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', timeline_title: 'Titled timeline', + related_integrations: [ + { + package: 'aws', + integration: 'route53', + version: '~1.2.3', + }, + { + package: 'system', + version: '^1.2.3', + }, + ], }; expect(result).toEqual(expected); @@ -501,6 +556,17 @@ describe('helpers', () => { threat_index: mockStepData.threatIndex, index: mockStepData.index, threat_filters: threatFilters, + related_integrations: [ + { + package: 'aws', + integration: 'route53', + version: '~1.2.3', + }, + { + package: 'system', + version: '^1.2.3', + }, + ], }; expect(result).toEqual(expected); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts index 81be2839c00296..18f23824b77a78 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts @@ -403,6 +403,7 @@ export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStep const baseFields = { type: ruleType, + related_integrations: defineStepData.relatedIntegrations?.filter((ri) => !isEmpty(ri.package)), ...(timeline.id != null && timeline.title != null && { timeline_id: timeline.id, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx index ff96fd64f027f8..807dc4b04f1b3c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx @@ -397,6 +397,7 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { const aboutStepFormValid = await aboutStepForm.validate(); const scheduleStepFormValid = await scheduleStepForm.validate(); const actionsStepFormValid = await actionsStepForm.validate(); + if ( defineStepFormValid && aboutStepFormValid && diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts index 49bd1649c3471f..80b0d3eedc8b76 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts @@ -216,7 +216,17 @@ export const mockDefineStepRule = (): DefineStepRule => ({ queryBar: mockQueryBar, threatQueryBar: mockQueryBar, requiredFields: [], - relatedIntegrations: [], + relatedIntegrations: [ + { + package: 'aws', + integration: 'route53', + version: '~1.2.3', + }, + { + package: 'system', + version: '^1.2.3', + }, + ], threatMapping: [], timeline: { id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/use_execution_events.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/use_execution_events.test.tsx index 09caca91b30f3d..f0480cbef8f3be 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/use_execution_events.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/use_execution_events.test.tsx @@ -5,9 +5,6 @@ * 2.0. */ -import type { FC, PropsWithChildren } from 'react'; -import React from 'react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { renderHook, cleanup } from '@testing-library/react-hooks'; import { @@ -18,6 +15,7 @@ import { import { useExecutionEvents } from './use_execution_events'; import { useToasts } from '../../../../common/lib/kibana'; import { api } from '../../api'; +import { createReactQueryWrapper } from '../../../../common/mock'; jest.mock('../../../../common/lib/kibana'); jest.mock('../../api'); @@ -33,21 +31,6 @@ describe('useExecutionEvents', () => { cleanup(); }); - const createReactQueryWrapper = () => { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - // Turn retries off, otherwise we won't be able to test errors - retry: false, - }, - }, - }); - const wrapper: FC> = ({ children }) => ( - {children} - ); - return wrapper; - }; - const render = () => renderHook(() => useExecutionEvents({ ruleId: SOME_RULE_ID }), { wrapper: createReactQueryWrapper(), diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_results_table/use_execution_results.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_results_table/use_execution_results.test.tsx index 45c7eaca3599ea..65d7ea7c3cda7c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_results_table/use_execution_results.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_results_table/use_execution_results.test.tsx @@ -5,14 +5,12 @@ * 2.0. */ -import type { FC, PropsWithChildren } from 'react'; -import React from 'react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { renderHook, cleanup } from '@testing-library/react-hooks'; import { useExecutionResults } from './use_execution_results'; import { useToasts } from '../../../../common/lib/kibana'; import { api } from '../../api'; +import { createReactQueryWrapper } from '../../../../common/mock'; jest.mock('../../../../common/lib/kibana'); jest.mock('../../api'); @@ -28,21 +26,6 @@ describe('useExecutionResults', () => { cleanup(); }); - const createReactQueryWrapper = () => { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - // Turn retries off, otherwise we won't be able to test errors - retry: false, - }, - }, - }); - const wrapper: FC> = ({ children }) => ( - {children} - ); - return wrapper; - }; - const render = () => renderHook( () => diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/integration_details.test.ts b/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/integration_details.test.ts index 54f6f9e1d24f8a..30d656c3bf79c3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/integration_details.test.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/integration_details.test.ts @@ -77,15 +77,19 @@ describe('Integration Details', () => { { package_name: 'aws', package_title: 'AWS', - package_version: '1.3.0', + latest_package_version: '1.3.0', + installed_package_version: '1.3.0', integration_name: 'route53', integration_title: 'AWS Route 53', + is_installed: true, is_enabled: false, }, { package_name: 'system', package_title: 'System', - package_version: '1.2.5', + latest_package_version: '1.2.5', + installed_package_version: '1.2.5', + is_installed: true, is_enabled: true, }, ] @@ -121,15 +125,19 @@ describe('Integration Details', () => { { package_name: 'aws', package_title: 'AWS', - package_version: '1.2.0', + latest_package_version: '1.2.0', + installed_package_version: '1.2.0', integration_name: 'route53', integration_title: 'AWS Route 53', + is_installed: true, is_enabled: false, }, { package_name: 'system', package_title: 'System', - package_version: '1.2.2', + latest_package_version: '1.2.2', + installed_package_version: '1.2.2', + is_installed: true, is_enabled: true, }, ] @@ -156,15 +164,19 @@ describe('Integration Details', () => { { package_name: 'aws', package_title: 'AWS', - package_version: '2.0.1', + latest_package_version: '2.0.1', + installed_package_version: '2.0.1', integration_name: 'route53', integration_title: 'AWS Route 53', + is_installed: true, is_enabled: false, }, { package_name: 'system', package_title: 'System', - package_version: '1.3.0', + latest_package_version: '1.3.0', + installed_package_version: '1.3.0', + is_installed: true, is_enabled: true, }, ] diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/integration_details.ts b/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/integration_details.ts index 2dfde8348f2f77..e8537931bae524 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/integration_details.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/integration_details.ts @@ -8,10 +8,7 @@ import { capitalize } from 'lodash'; import semver from 'semver'; -import type { - InstalledIntegration, - InstalledIntegrationArray, -} from '../../../../../common/api/detection_engine/fleet_integrations'; +import type { Integration } from '../../../../../common/api/detection_engine/fleet_integrations'; import type { RelatedIntegration, RelatedIntegrationArray, @@ -42,57 +39,60 @@ export interface UnknownInstallationStatus { } /** - * Given an array of integrations and an array of installed integrations this will return an - * array of integrations augmented with install details like targetVersion, and `version_satisfied` + * Given an array of integrations and an array of all known integrations this will return an + * array of integrations augmented with details like targetVersion, and `version_satisfied` * has. */ export const calculateIntegrationDetails = ( relatedIntegrations: RelatedIntegrationArray, - installedIntegrations: InstalledIntegrationArray | undefined + knownIntegrations: Integration[] | undefined ): IntegrationDetails[] => { - const integrationMatches = findIntegrationMatches(relatedIntegrations, installedIntegrations); - const integrationDetails = integrationMatches.map((integration) => { - return createIntegrationDetails(integration); - }); + const integrationMatches = findIntegrationMatches(relatedIntegrations, knownIntegrations); + const integrationDetails = integrationMatches.map((integration) => + createIntegrationDetails(integration) + ); - return integrationDetails.sort((a, b) => { - return a.integrationTitle.localeCompare(b.integrationTitle); - }); + return integrationDetails.sort((a, b) => a.integrationTitle.localeCompare(b.integrationTitle)); }; interface IntegrationMatch { related: RelatedIntegration; - installed: InstalledIntegration | null; + found?: Integration; isLoaded: boolean; } const findIntegrationMatches = ( relatedIntegrations: RelatedIntegrationArray, - installedIntegrations: InstalledIntegrationArray | undefined + integrations: Integration[] | undefined ): IntegrationMatch[] => { + const integrationsMap = new Map( + (integrations ?? []).map((integration) => [ + `${integration.package_name}${integration.integration_name ?? ''}`, + integration, + ]) + ); + return relatedIntegrations.map((ri: RelatedIntegration) => { - if (installedIntegrations == null) { + const key = `${ri.package}${ri.integration ?? ''}`; + const matchIntegration = integrationsMap.get(key); + + if (!matchIntegration) { return { related: ri, - installed: null, isLoaded: false, }; - } else { - const match = installedIntegrations.find( - (ii: InstalledIntegration) => - ii.package_name === ri.package && ii?.integration_name === ri?.integration - ); - return { - related: ri, - installed: match ?? null, - isLoaded: true, - }; } + + return { + related: ri, + found: matchIntegration, + isLoaded: true, + }; }); }; const createIntegrationDetails = (integration: IntegrationMatch): IntegrationDetails => { - const { related, installed, isLoaded } = integration; + const { related, found, isLoaded } = integration; const packageName = related.package; const integrationName = related.integration ?? null; @@ -117,8 +117,7 @@ const createIntegrationDetails = (integration: IntegrationMatch): IntegrationDet }; } - // We know that the integration is not installed - if (installed == null) { + if (!found) { const integrationTitle = getCapitalizedTitle(packageName, integrationName); const targetVersion = getMinimumConcreteVersionMatchingSemver(requiredVersion); const targetUrl = buildTargetUrl(packageName, integrationName, targetVersion); @@ -140,35 +139,34 @@ const createIntegrationDetails = (integration: IntegrationMatch): IntegrationDet }; } - // We know that the integration is installed - { - const integrationTitle = installed.integration_title ?? installed.package_title; - - // Version check e.g. installed version `1.2.3` satisfies required version `~1.2.1` - const installedVersion = installed.package_version; - const isVersionSatisfied = semver.satisfies(installedVersion, requiredVersion); - const targetVersion = isVersionSatisfied + const integrationTitle = found.integration_title ?? found.package_title; + // Version check e.g. installed version `1.2.3` satisfies required version `~1.2.1` + const installedVersion = found.installed_package_version ?? ''; + const isVersionSatisfied = installedVersion + ? semver.satisfies(installedVersion, requiredVersion, { includePrerelease: true }) + : true; + const targetVersion = + installedVersion && isVersionSatisfied ? installedVersion : getMinimumConcreteVersionMatchingSemver(requiredVersion); - const targetUrl = buildTargetUrl(packageName, integrationName, targetVersion); - - return { - packageName, - integrationName, - integrationTitle, - requiredVersion, - targetVersion, - targetUrl, - installationStatus: { - isKnown: true, - isInstalled: true, - isEnabled: installed.is_enabled, - isVersionMismatch: !isVersionSatisfied, - installedVersion, - }, - }; - } + const targetUrl = buildTargetUrl(packageName, integrationName, targetVersion); + + return { + packageName, + integrationName, + integrationTitle, + requiredVersion, + targetVersion, + targetUrl, + installationStatus: { + isKnown: true, + isInstalled: found.is_installed, + isEnabled: found.is_enabled, + isVersionMismatch: !isVersionSatisfied, + installedVersion, + }, + }; }; const getCapitalizedTitle = (packageName: string, integrationName: string | null): string => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/integrations_description/integration_status_badge.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/integrations_description/integration_status_badge.tsx index 30463c744073e4..fecbb3e85df399 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/integrations_description/integration_status_badge.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/integrations_description/integration_status_badge.tsx @@ -31,26 +31,22 @@ const IntegrationStatusBadgeComponent: React.FC = ( const { isInstalled, isEnabled } = installationStatus; - const badgeInstalledColor = 'success'; - const badgeUninstalledColor = '#E0E5EE'; - const badgeColor = isInstalled ? badgeInstalledColor : badgeUninstalledColor; - - const badgeTooltip = isInstalled + const color = isEnabled ? 'success' : isInstalled ? 'primary' : undefined; + const tooltipText = isInstalled ? isEnabled ? i18n.INTEGRATIONS_ENABLED_TOOLTIP : i18n.INTEGRATIONS_INSTALLED_TOOLTIP : i18n.INTEGRATIONS_UNINSTALLED_TOOLTIP; - - const badgeText = isInstalled - ? isEnabled - ? i18n.INTEGRATIONS_ENABLED - : i18n.INTEGRATIONS_INSTALLED + const statusText = isEnabled + ? i18n.INTEGRATIONS_ENABLED + : isInstalled + ? i18n.INTEGRATIONS_DISABLED : i18n.INTEGRATIONS_UNINSTALLED; return ( - - - {badgeText} + + + {statusText} ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/translations.ts index 1037993a246d26..dbd928315cf5c3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/translations.ts @@ -7,10 +7,10 @@ import { i18n } from '@kbn/i18n'; -export const INTEGRATIONS_INSTALLED = i18n.translate( - 'xpack.securitySolution.detectionEngine.relatedIntegrations.installedTitle', +export const INTEGRATIONS_DISABLED = i18n.translate( + 'xpack.securitySolution.detectionEngine.relatedIntegrations.disabledTitle', { - defaultMessage: 'Installed', + defaultMessage: 'Disabled', } ); @@ -40,7 +40,7 @@ export const INTEGRATIONS_UNINSTALLED_TOOLTIP = i18n.translate( export const INTEGRATIONS_ENABLED = i18n.translate( 'xpack.securitySolution.detectionEngine.relatedIntegrations.enabledTitle', { - defaultMessage: 'Installed: enabled', + defaultMessage: 'Enabled', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/use_installed_integrations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/use_installed_integrations.tsx deleted file mode 100644 index 01b7d5fe6e6133..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/use_installed_integrations.tsx +++ /dev/null @@ -1,52 +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 { useQuery } from '@tanstack/react-query'; - -import type { InstalledIntegrationArray } from '../../../../../common/api/detection_engine/fleet_integrations'; -import { fleetIntegrationsApi } from '../../../../detection_engine/fleet_integrations'; -// import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; -// import * as i18n from './translations'; - -const ONE_MINUTE = 60000; - -export interface UseInstalledIntegrationsArgs { - packages?: string[]; - skip?: boolean; -} - -export const useInstalledIntegrations = ({ - packages, - skip = false, -}: UseInstalledIntegrationsArgs) => { - // const { addError } = useAppToasts(); - - return useQuery( - [ - 'installedIntegrations', - { - packages, - }, - ], - async ({ signal }) => { - const integrations = await fleetIntegrationsApi.fetchInstalledIntegrations({ - packages, - signal, - }); - return integrations.installed_integrations ?? []; - }, - { - keepPreviousData: true, - staleTime: ONE_MINUTE * 5, - enabled: !skip, - onError: (e) => { - // Suppressing for now to prevent excessive errors when fleet isn't configured - // addError(e, { title: i18n.INTEGRATIONS_FETCH_FAILURE }); - }, - } - ); -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/use_installed_integrations.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/use_integrations.test.tsx similarity index 65% rename from x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/use_installed_integrations.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/use_integrations.test.tsx index 74791dfff20325..eba2248f82a4d1 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/use_installed_integrations.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/use_integrations.test.tsx @@ -5,20 +5,18 @@ * 2.0. */ -import type { FC, PropsWithChildren } from 'react'; -import React from 'react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { renderHook, cleanup } from '@testing-library/react-hooks'; -import { useInstalledIntegrations } from './use_installed_integrations'; +import { useIntegrations } from './use_integrations'; import { fleetIntegrationsApi } from '../../../../detection_engine/fleet_integrations/api'; import { useToasts } from '../../../../common/lib/kibana'; +import { createReactQueryWrapper } from '../../../../common/mock'; jest.mock('../../../../detection_engine/fleet_integrations/api'); jest.mock('../../../../common/lib/kibana'); -describe('useInstalledIntegrations', () => { +describe('useIntegrations', () => { beforeEach(() => { jest.clearAllMocks(); }); @@ -27,26 +25,10 @@ describe('useInstalledIntegrations', () => { cleanup(); }); - const createReactQueryWrapper = () => { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - // Turn retries off, otherwise we won't be able to test errors - retry: false, - }, - }, - }); - const wrapper: FC> = ({ children }) => ( - {children} - ); - return wrapper; - }; - const render = ({ skip } = { skip: false }) => renderHook( () => - useInstalledIntegrations({ - packages: [], + useIntegrations({ skip, }), { @@ -54,31 +36,22 @@ describe('useInstalledIntegrations', () => { } ); - it('calls the API via fetchInstalledIntegrations', async () => { - const fetchInstalledIntegrations = jest.spyOn( - fleetIntegrationsApi, - 'fetchInstalledIntegrations' - ); + it('calls the API via fetchAllIntegrations', async () => { + const fetchAllIntegrations = jest.spyOn(fleetIntegrationsApi, 'fetchAllIntegrations'); const { waitForNextUpdate } = render(); await waitForNextUpdate(); - expect(fetchInstalledIntegrations).toHaveBeenCalledTimes(1); - expect(fetchInstalledIntegrations).toHaveBeenLastCalledWith( - expect.objectContaining({ packages: [] }) - ); + expect(fetchAllIntegrations).toHaveBeenCalledTimes(1); }); it('does not call the API when skip is true', async () => { - const fetchInstalledIntegrations = jest.spyOn( - fleetIntegrationsApi, - 'fetchInstalledIntegrations' - ); + const fetchAllIntegrations = jest.spyOn(fleetIntegrationsApi, 'fetchAllIntegrations'); render({ skip: true }); - expect(fetchInstalledIntegrations).toHaveBeenCalledTimes(0); + expect(fetchAllIntegrations).toHaveBeenCalledTimes(0); }); it('fetches data from the API', async () => { @@ -97,19 +70,32 @@ describe('useInstalledIntegrations', () => { expect(result.current.isSuccess).toEqual(true); expect(result.current.isError).toEqual(false); expect(result.current.data).toEqual([ + { + package_name: 'o365', + package_title: 'Microsoft 365', + latest_package_version: '1.2.0', + installed_package_version: '1.0.0', + is_installed: false, + is_enabled: false, + }, { integration_name: 'audit', integration_title: 'Audit Logs', - is_enabled: true, + package_name: 'atlassian_bitbucket', package_title: 'Atlassian Bitbucket', - package_version: '1.0.1', + latest_package_version: '1.0.1', + installed_package_version: '1.0.1', + is_installed: true, + is_enabled: true, }, { - is_enabled: true, package_name: 'system', package_title: 'System', - package_version: '1.6.4', + latest_package_version: '1.6.4', + installed_package_version: '1.6.4', + is_installed: true, + is_enabled: true, }, ]); }); @@ -117,7 +103,7 @@ describe('useInstalledIntegrations', () => { // Skipping until we re-enable errors it.skip('handles exceptions from the API', async () => { const exception = new Error('Boom!'); - jest.spyOn(fleetIntegrationsApi, 'fetchInstalledIntegrations').mockRejectedValue(exception); + jest.spyOn(fleetIntegrationsApi, 'fetchAllIntegrations').mockRejectedValue(exception); const { result, waitForNextUpdate } = render(); @@ -138,7 +124,7 @@ describe('useInstalledIntegrations', () => { // And shows a toast with the caught exception expect(useToasts().addError).toHaveBeenCalledTimes(1); expect(useToasts().addError).toHaveBeenCalledWith(exception, { - title: 'Failed to fetch installed integrations', + title: 'Failed to fetch integrations', }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/use_integrations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/use_integrations.tsx new file mode 100644 index 00000000000000..3a03fcb39fce6a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/use_integrations.tsx @@ -0,0 +1,35 @@ +/* + * 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 { useQuery } from '@tanstack/react-query'; + +import type { Integration } from '../../../../../common/api/detection_engine/fleet_integrations'; +import { fleetIntegrationsApi } from '../../../../detection_engine/fleet_integrations'; + +const ONE_MINUTE = 60000; + +export interface UseIntegrationsArgs { + skip?: boolean; +} + +export const useIntegrations = ({ skip = false }: UseIntegrationsArgs = {}) => { + return useQuery( + ['integrations'], + async ({ signal }) => { + const response = await fleetIntegrationsApi.fetchAllIntegrations({ + signal, + }); + + return response.integrations ?? []; + }, + { + keepPreviousData: true, + staleTime: ONE_MINUTE * 5, + enabled: !skip, + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/use_related_integrations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/use_related_integrations.ts index dc16d365fff939..92ba42873e1069 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/use_related_integrations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/use_related_integrations.ts @@ -10,7 +10,7 @@ import { useMemo } from 'react'; import type { RelatedIntegrationArray } from '../../../../../common/api/detection_engine/model/rule_schema'; import type { IntegrationDetails } from './integration_details'; import { calculateIntegrationDetails } from './integration_details'; -import { useInstalledIntegrations } from './use_installed_integrations'; +import { useIntegrations } from './use_integrations'; export interface UseRelatedIntegrationsResult { integrations: IntegrationDetails[]; @@ -20,17 +20,14 @@ export interface UseRelatedIntegrationsResult { export const useRelatedIntegrations = ( relatedIntegrations: RelatedIntegrationArray ): UseRelatedIntegrationsResult => { - const { data: installedIntegrations } = useInstalledIntegrations({ packages: [] }); + const { data: integrations } = useIntegrations(); return useMemo(() => { - const integrationDetails = calculateIntegrationDetails( - relatedIntegrations, - installedIntegrations - ); + const integrationDetails = calculateIntegrationDetails(relatedIntegrations, integrations); return { integrations: integrationDetails, - isLoaded: installedIntegrations != null, + isLoaded: integrations != null, }; - }, [relatedIntegrations, installedIntegrations]); + }, [relatedIntegrations, integrations]); }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index fa0168c7d2e982..fa2ae6af7876e8 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -37,6 +37,7 @@ import type { RuleAction, AlertSuppression, ThresholdAlertSuppression, + RelatedIntegration, } from '../../../../../common/api/detection_engine/model/rule_schema'; import type { SortOrder } from '../../../../../common/api/detection_engine'; import type { EqlOptionsSelected } from '../../../../../common/search_strategy'; @@ -144,7 +145,7 @@ export interface DefineStepRule { queryBar: FieldValueQueryBar; dataViewId?: string; dataViewTitle?: string; - relatedIntegrations: RelatedIntegrationArray; + relatedIntegrations?: RelatedIntegrationArray; requiredFields: RequiredFieldArray; ruleType: Type; timeline: FieldValueTimeline; @@ -223,6 +224,7 @@ export interface DefineStepRuleJson { event_category_override?: string; tiebreaker_field?: string; alert_suppression?: AlertSuppression | ThresholdAlertSuppression; + related_integrations?: RelatedIntegration[]; } export interface AboutStepRuleJson { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_fetch_alerts.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_fetch_alerts.test.ts similarity index 79% rename from x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_fetch_alerts.test.tsx rename to x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_fetch_alerts.test.ts index b0c2e1c3a2ef51..c4910f5daa42a1 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_fetch_alerts.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_fetch_alerts.test.ts @@ -5,12 +5,11 @@ * 2.0. */ -import React from 'react'; import { renderHook } from '@testing-library/react-hooks'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useKibana } from '../../../../common/lib/kibana'; import { createFindAlerts } from '../services/find_alerts'; import { useFetchAlerts, type UseAlertsQueryParams } from './use_fetch_alerts'; +import { createReactQueryWrapper } from '../../../../common/mock'; jest.mock('../../../../common/lib/kibana'); jest.mock('../services/find_alerts'); @@ -29,11 +28,6 @@ describe('useFetchAlerts', () => { }); it('fetches alerts and handles loading state', async () => { - const queryClient = new QueryClient(); - const wrapper = ({ children }: { children: React.ReactChild }) => ( - {children} - ); - jest .mocked(createFindAlerts) .mockReturnValue( @@ -47,7 +41,9 @@ describe('useFetchAlerts', () => { sort: [{ '@timestamp': 'desc' }], }; - const { result, waitFor } = renderHook(() => useFetchAlerts(params), { wrapper }); + const { result, waitFor } = renderHook(() => useFetchAlerts(params), { + wrapper: createReactQueryWrapper(), + }); expect(result.current.loading).toBe(true); @@ -60,11 +56,6 @@ describe('useFetchAlerts', () => { }); it('handles error state', async () => { - const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); - const wrapper = ({ children }: { children: React.ReactChild }) => ( - {children} - ); - // hide console error due to the line after jest.spyOn(console, 'error').mockImplementation(() => {}); @@ -79,7 +70,9 @@ describe('useFetchAlerts', () => { sort: [{ '@timestamp': 'desc' }], }; - const { result, waitFor } = renderHook(() => useFetchAlerts(params), { wrapper }); + const { result, waitFor } = renderHook(() => useFetchAlerts(params), { + wrapper: createReactQueryWrapper(), + }); expect(result.current.loading).toBe(true); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_cases.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_cases.test.ts similarity index 66% rename from x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_cases.test.tsx rename to x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_cases.test.ts index c9d63d4432d6f8..6ebdc2bc4b7c74 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_cases.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_cases.test.ts @@ -6,26 +6,27 @@ */ import { renderHook } from '@testing-library/react-hooks'; - +import { createReactQueryWrapper } from '../../../../common/mock'; import { useFetchRelatedCases } from './use_fetch_related_cases'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import type { ReactNode } from 'react'; -import React from 'react'; -const eventId = 'eventId'; +jest.mock('../../../../common/lib/kibana', () => ({ + useKibana: jest.fn().mockReturnValue({ + services: { + cases: { + api: { + getRelatedCases: jest.fn().mockResolvedValue([]), + }, + }, + }, + }), +})); -const createWrapper = () => { - const queryClient = new QueryClient(); - // eslint-disable-next-line react/display-name - return ({ children }: { children: ReactNode }) => ( - {children} - ); -}; +const eventId = 'eventId'; describe('useFetchRelatedCases', () => { it(`should return loading true while data is loading`, () => { const hookResult = renderHook(() => useFetchRelatedCases({ eventId }), { - wrapper: createWrapper(), + wrapper: createReactQueryWrapper(), }); expect(hookResult.result.current.loading).toEqual(true); diff --git a/x-pack/plugins/security_solution/public/shared_imports.ts b/x-pack/plugins/security_solution/public/shared_imports.ts index 8686aecb3b99f1..80d345d7102d92 100644 --- a/x-pack/plugins/security_solution/public/shared_imports.ts +++ b/x-pack/plugins/security_solution/public/shared_imports.ts @@ -11,9 +11,12 @@ export type { FormData, FormHook, FormSchema, + FormSubmitHandler, ValidationError, + ValidationFuncArg, ValidationFunc, ArrayItem, + FieldConfig, } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; export { getUseField, @@ -23,6 +26,7 @@ export { FormDataProvider, UseField, UseMultiFields, + UseArray, useForm, useFormContext, useFormData, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx index c5ab49e8f4f72a..9d9de2c9004091 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx @@ -9,12 +9,11 @@ import { cloneDeep } from 'lodash/fp'; import moment from 'moment'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { fireEvent, screen, render, waitFor } from '@testing-library/react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import React from 'react'; import '../../../../common/mock/formatted_relative'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { mockTimelineResults } from '../../../../common/mock/timeline_results'; -import { TestProviders } from '../../../../common/mock'; +import { createReactQueryWrapper, TestProviders } from '../../../../common/mock'; import type { OpenTimelineResult, TimelineResultNote } from '../types'; import { NotePreviews } from '.'; import { useDeleteNote } from './hooks/use_delete_note'; @@ -39,7 +38,6 @@ describe('NotePreviews', () => { let note1updated: number; let note2updated: number; let note3updated: number; - let queryClient: QueryClient; beforeEach(() => { mockResults = cloneDeep(mockTimelineResults); @@ -47,14 +45,6 @@ describe('NotePreviews', () => { note2updated = moment(note1updated).add(1, 'minute').valueOf(); note3updated = moment(note2updated).add(1, 'minute').valueOf(); (useDeepEqualSelector as jest.Mock).mockReset(); - queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, - }); - (useDeleteNote as jest.Mock).mockReturnValue({ mutate: deleteMutateMock, onSuccess: jest.fn(), @@ -66,11 +56,9 @@ describe('NotePreviews', () => { test('it renders a note preview for each note when isModal is false', () => { const hasNotes: OpenTimelineResult[] = [{ ...mockResults[0] }]; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(, { + wrappingComponent: createReactQueryWrapper(), + }); hasNotes[0].notes?.forEach(({ savedObjectId }) => { expect(wrapper.find(`[data-test-subj="note-preview-${savedObjectId}"]`).exists()).toBe(true); @@ -80,11 +68,9 @@ describe('NotePreviews', () => { test('it renders a note preview for each note when isModal is true', () => { const hasNotes: OpenTimelineResult[] = [{ ...mockResults[0] }]; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(, { + wrappingComponent: createReactQueryWrapper(), + }); hasNotes[0].notes?.forEach(({ savedObjectId }) => { expect(wrapper.find(`[data-test-subj="note-preview-${savedObjectId}"]`).exists()).toBe(true); @@ -113,11 +99,9 @@ describe('NotePreviews', () => { }, ]; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(, { + wrappingComponent: createReactQueryWrapper(), + }); expect(wrapper.find('div.euiCommentEvent__headerUsername').at(1).text()).toEqual('bob'); }); @@ -144,11 +128,9 @@ describe('NotePreviews', () => { }, ]; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(, { + wrappingComponent: createReactQueryWrapper(), + }); expect(wrapper.find('div.euiCommentEvent__headerUsername').at(2).text()).toEqual('bob'); }); @@ -174,11 +156,9 @@ describe('NotePreviews', () => { }, ]; - const wrapper = mountWithIntl( - - {' '} - - ); + const wrapper = mountWithIntl(, { + wrappingComponent: createReactQueryWrapper(), + }); expect(wrapper.find('div.euiCommentEvent__headerUsername').at(2).text()).toEqual('bob'); }); @@ -188,9 +168,10 @@ describe('NotePreviews', () => { (useDeepEqualSelector as jest.Mock).mockReturnValue(timeline); const wrapper = mountWithIntl( - - - + , + { + wrappingComponent: createReactQueryWrapper(), + } ); expect(wrapper.find('[data-test-subj="note-preview-description"]').first().text()).toContain( @@ -202,11 +183,9 @@ describe('NotePreviews', () => { const timeline = mockTimelineResults[0]; (useDeepEqualSelector as jest.Mock).mockReturnValue({ ...timeline, description: undefined }); - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(, { + wrappingComponent: createReactQueryWrapper(), + }); expect(wrapper.find('[data-test-subj="note-preview-description"]').exists()).toBe(false); }); @@ -216,19 +195,20 @@ describe('NotePreviews', () => { (useDeepEqualSelector as jest.Mock).mockReturnValue(timeline); const wrapper = mountWithIntl( - - - + , + { + wrappingComponent: createReactQueryWrapper(), + } ); expect(wrapper.find('[data-test-subj="delete-note"] button').prop('disabled')).toBeTruthy(); @@ -239,20 +219,21 @@ describe('NotePreviews', () => { (useDeepEqualSelector as jest.Mock).mockReturnValue(timeline); const wrapper = mountWithIntl( - - - + , + { + wrappingComponent: createReactQueryWrapper(), + } ); expect(wrapper.find('[data-test-subj="delete-note"] button').prop('disabled')).toBeFalsy(); @@ -268,30 +249,31 @@ describe('NotePreviews', () => { render( - - - - + + , + { + wrapper: createReactQueryWrapper(), + } ); fireEvent.click(screen.queryAllByTestId('delete-note')[0]); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx index b6ed788faae0c9..a46fd3e70616fd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx @@ -9,7 +9,6 @@ import type { EuiButtonIconProps } from '@elastic/eui'; import { cloneDeep, omit } from 'lodash/fp'; import React from 'react'; import { ThemeProvider } from 'styled-components'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import '../../../../common/mock/formatted_relative'; @@ -24,6 +23,7 @@ import { TimelinesTable } from '.'; import * as i18n from '../translations'; import { getMockTimelinesTableProps } from './mocks'; import { getMockTheme } from '../../../../common/lib/kibana/kibana_react.mock'; +import { createReactQueryWrapper } from '../../../../common/mock'; const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } }); @@ -40,17 +40,9 @@ jest.mock('react-redux', () => { describe('#getCommonColumns', () => { let mockResults: OpenTimelineResult[]; - let queryClient: QueryClient; beforeEach(() => { mockResults = cloneDeep(mockTimelineResults); - queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, - }); }); describe('Expand column', () => { @@ -60,11 +52,9 @@ describe('#getCommonColumns', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(hasNotes), }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(, { + wrappingComponent: createReactQueryWrapper(), + }); expect(wrapper.find('[data-test-subj="expand-notes"]').exists()).toBe(true); }); @@ -75,11 +65,9 @@ describe('#getCommonColumns', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(missingNotes), }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(, { + wrappingComponent: createReactQueryWrapper(), + }); expect(wrapper.find('[data-test-subj="expand-notes"]').exists()).toBe(false); }); @@ -89,11 +77,9 @@ describe('#getCommonColumns', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(nullNotes), }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(, { + wrappingComponent: createReactQueryWrapper(), + }); expect(wrapper.find('[data-test-subj="expand-notes"]').exists()).toBe(false); }); @@ -103,11 +89,9 @@ describe('#getCommonColumns', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(emptylNotes), }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(, { + wrappingComponent: createReactQueryWrapper(), + }); expect(wrapper.find('[data-test-subj="expand-notes"]').exists()).toBe(false); }); @@ -118,11 +102,9 @@ describe('#getCommonColumns', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(missingSavedObjectId), }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(, { + wrappingComponent: createReactQueryWrapper(), + }); expect(wrapper.find('[data-test-subj="expand-notes"]').exists()).toBe(false); }); @@ -132,11 +114,9 @@ describe('#getCommonColumns', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(nullSavedObjectId), }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(, { + wrappingComponent: createReactQueryWrapper(), + }); expect(wrapper.find('[data-test-subj="expand-notes"]').exists()).toBe(false); }); @@ -146,11 +126,9 @@ describe('#getCommonColumns', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(hasNotes), }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(, { + wrappingComponent: createReactQueryWrapper(), + }); const props = wrapper .find('[data-test-subj="expand-notes"]') .first() @@ -170,11 +148,9 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(hasNotes), itemIdToExpandedNotesRowMap, }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(, { + wrappingComponent: createReactQueryWrapper(), + }); const props = wrapper .find('[data-test-subj="expand-notes"]') .first() @@ -197,11 +173,9 @@ describe('#getCommonColumns', () => { itemIdToExpandedNotesRowMap, onToggleShowNotes, }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(, { + wrappingComponent: createReactQueryWrapper(), + }); wrapper.find('[data-test-subj="expand-notes"]').first().simulate('click'); expect(onToggleShowNotes).toBeCalledWith({ @@ -229,11 +203,12 @@ describe('#getCommonColumns', () => { }; const wrapper = mountWithIntl( - - - - - + + + , + { + wrappingComponent: createReactQueryWrapper(), + } ); wrapper.find('[data-test-subj="expand-notes"]').first().simulate('click'); @@ -250,11 +225,12 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(mockResults), }; const wrapper = mountWithIntl( - - - - - + + + , + { + wrappingComponent: createReactQueryWrapper(), + } ); expect(wrapper.find('thead tr th').at(1).text()).toContain(i18n.TIMELINE_NAME); @@ -265,11 +241,12 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(mockResults), }; const wrapper = mountWithIntl( - - - - - + + + , + { + wrappingComponent: createReactQueryWrapper(), + } ); expect( @@ -289,11 +266,12 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(missingSavedObjectId), }; const wrapper = mountWithIntl( - - - - - + + + , + { + wrappingComponent: createReactQueryWrapper(), + } ); expect( @@ -311,11 +289,12 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(missingTitle), }; const wrapper = mountWithIntl( - - - - - + + + , + { + wrappingComponent: createReactQueryWrapper(), + } ); expect( @@ -335,11 +314,12 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(withMissingSavedObjectIdAndTitle), }; const wrapper = mountWithIntl( - - - - - + + + , + { + wrappingComponent: createReactQueryWrapper(), + } ); expect( @@ -356,11 +336,12 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(withJustWhitespaceTitle), }; const wrapper = mountWithIntl( - - - - - + + + , + { + wrappingComponent: createReactQueryWrapper(), + } ); expect( @@ -380,11 +361,12 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(withMissingSavedObjectId), }; const wrapper = mountWithIntl( - - - - - + + + , + { + wrappingComponent: createReactQueryWrapper(), + } ); expect( @@ -397,11 +379,12 @@ describe('#getCommonColumns', () => { test('it renders a hyperlink when the timeline has a saved object id', () => { const wrapper = mountWithIntl( - - - - - + + + , + { + wrappingComponent: createReactQueryWrapper(), + } ); expect( @@ -421,11 +404,12 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(missingSavedObjectId), }; const wrapper = mountWithIntl( - - - - - + + + , + { + wrappingComponent: createReactQueryWrapper(), + } ); expect( @@ -444,11 +428,12 @@ describe('#getCommonColumns', () => { onOpenTimeline, }; const wrapper = mountWithIntl( - - - - - + + + , + { + wrappingComponent: createReactQueryWrapper(), + } ); wrapper @@ -466,11 +451,12 @@ describe('#getCommonColumns', () => { describe('Description column', () => { test('it renders the expected column name', () => { const wrapper = mountWithIntl( - - - - - + + + , + { + wrappingComponent: createReactQueryWrapper(), + } ); expect(wrapper.find('thead tr th').at(2).text()).toContain(i18n.DESCRIPTION); @@ -478,11 +464,12 @@ describe('#getCommonColumns', () => { test('it renders the description when the timeline has a description', () => { const wrapper = mountWithIntl( - - - - - + + + , + { + wrappingComponent: createReactQueryWrapper(), + } ); expect(wrapper.find('[data-test-subj="description"]').first().text()).toEqual( @@ -494,11 +481,12 @@ describe('#getCommonColumns', () => { const missingDescription: OpenTimelineResult[] = [omit('description', { ...mockResults[0] })]; const wrapper = mountWithIntl( - - - - - + + + , + { + wrappingComponent: createReactQueryWrapper(), + } ); expect(wrapper.find('[data-test-subj="description"]').first().text()).toEqual( getEmptyValue() @@ -514,11 +502,12 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(justWhitespaceDescription), }; const wrapper = mountWithIntl( - - - - - + + + , + { + wrappingComponent: createReactQueryWrapper(), + } ); expect(wrapper.find('[data-test-subj="description"]').first().text()).toEqual( getEmptyValue() @@ -529,11 +518,12 @@ describe('#getCommonColumns', () => { describe('Last Modified column', () => { test('it renders the expected column name', () => { const wrapper = mountWithIntl( - - - - - + + + , + { + wrappingComponent: createReactQueryWrapper(), + } ); expect(wrapper.find('thead tr th').at(3).text()).toContain(i18n.LAST_MODIFIED); @@ -541,11 +531,12 @@ describe('#getCommonColumns', () => { test('it renders the last modified (updated) date when the timeline has an updated property', () => { const wrapper = mountWithIntl( - - - - - + + + , + { + wrappingComponent: createReactQueryWrapper(), + } ); expect(wrapper.find('[data-test-subj="updated"]').first().text().length).toBeGreaterThan( @@ -558,11 +549,12 @@ describe('#getCommonColumns', () => { const missingUpdated: OpenTimelineResult[] = [omit('updated', { ...mockResults[0] })]; const wrapper = mountWithIntl( - - - - - + + + , + { + wrappingComponent: createReactQueryWrapper(), + } ); expect(wrapper.find('[data-test-subj="updated"]').first().text()).toEqual(getEmptyValue()); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/hooks/use_managed_user.test.ts b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/hooks/use_managed_user.test.ts index 5a26600948267e..94b01fbc0d57ac 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/hooks/use_managed_user.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/hooks/use_managed_user.test.ts @@ -6,31 +6,30 @@ */ import { renderHook } from '@testing-library/react-hooks'; -import type { InstalledIntegration } from '../../../../../../common/api/detection_engine/fleet_integrations'; +import type { Integration } from '../../../../../../common/api/detection_engine/fleet_integrations'; import { TestProviders } from '../../../../../common/mock'; import { ENTRA_ID_PACKAGE_NAME } from '../constants'; import { useManagedUser } from './use_managed_user'; -const makeInstalledIntegration = ( - pkgName = 'testPkg', - isEnabled = false -): InstalledIntegration => ({ +const makeIntegration = (pkgName = 'testPkg', isEnabled = false): Integration => ({ package_name: pkgName, package_title: '', - package_version: '', + latest_package_version: '', + installed_package_version: '', integration_name: '', integration_title: '', + is_installed: true, is_enabled: isEnabled, }); -const mockUseInstalledIntegrations = jest.fn().mockReturnValue({ +const mockUseIntegrations = jest.fn().mockReturnValue({ data: [], }); jest.mock( - '../../../../../detections/components/rules/related_integrations/use_installed_integrations', + '../../../../../detections/components/rules/related_integrations/use_integrations', () => ({ - useInstalledIntegrations: () => mockUseInstalledIntegrations(), + useIntegrations: () => mockUseIntegrations(), }) ); @@ -67,8 +66,8 @@ describe('useManagedUser', () => { mockSearch.mockClear(); }); it('returns isIntegrationEnabled:true when it finds an enabled integration with the given name', () => { - mockUseInstalledIntegrations.mockReturnValue({ - data: [makeInstalledIntegration(ENTRA_ID_PACKAGE_NAME, true)], + mockUseIntegrations.mockReturnValue({ + data: [makeIntegration(ENTRA_ID_PACKAGE_NAME, true)], }); const { result } = renderHook(() => useManagedUser('test-userName', undefined, false), { @@ -79,8 +78,8 @@ describe('useManagedUser', () => { }); it('returns isIntegrationEnabled:false when it does not find an enabled integration with the given name', () => { - mockUseInstalledIntegrations.mockReturnValue({ - data: [makeInstalledIntegration('fake-name', true)], + mockUseIntegrations.mockReturnValue({ + data: [makeIntegration('fake-name', true)], }); const { result } = renderHook(() => useManagedUser('test-userName', undefined, false), { @@ -130,7 +129,7 @@ describe('useManagedUser', () => { it('should return loading false when the feature is disabled', () => { mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); - mockUseInstalledIntegrations.mockReturnValue({ + mockUseIntegrations.mockReturnValue({ data: [], isLoading: true, }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/hooks/use_managed_user.ts b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/hooks/use_managed_user.ts index 2b985291638952..46191c8a8fb35e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/hooks/use_managed_user.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/hooks/use_managed_user.ts @@ -9,7 +9,7 @@ import { useEffect, useMemo } from 'react'; import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import type { ManagedUserHits } from '../../../../../../common/search_strategy/security_solution/users/managed_details'; -import { useInstalledIntegrations } from '../../../../../detections/components/rules/related_integrations/use_installed_integrations'; +import { useIntegrations } from '../../../../../detections/components/rules/related_integrations/use_integrations'; import { UsersQueries } from '../../../../../../common/search_strategy'; import { useSpaceId } from '../../../../../common/hooks/use_space_id'; import { useSearchStrategy } from '../../../../../common/containers/use_search_strategy'; @@ -69,8 +69,7 @@ export const useManagedUser = ( } }, [from, search, to, isInitializing, defaultIndex, userName, isLoading, email, skip]); - const { data: installedIntegrations, isLoading: loadingIntegrations } = useInstalledIntegrations({ - packages, + const { data: integrations, isLoading: loadingIntegrations } = useIntegrations({ skip, }); @@ -85,11 +84,11 @@ export const useManagedUser = ( const isIntegrationEnabled = useMemo( () => - !!installedIntegrations?.some( + !!integrations?.some( ({ package_name: packageName, is_enabled: isEnabled }) => isEnabled && packages.includes(packageName) ), - [installedIntegrations] + [integrations] ); return useMemo( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/extract_integrations.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/extract_integrations.test.ts new file mode 100644 index 00000000000000..e5837c8184a5f0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/extract_integrations.test.ts @@ -0,0 +1,715 @@ +/* + * 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 { PackageList, PackagePolicy, PackagePolicyInput } from '@kbn/fleet-plugin/common'; +import { extractIntegrations } from './extract_integrations'; + +describe('extractIntegrations', () => { + describe('for packages with multiple policy templates', () => { + it('extracts package title', () => { + const packages = [ + { + name: 'package-a', + title: 'Package A', + version: '1.1.1', + policy_templates: [ + { + name: 'integration-a', + title: 'Integration A', + }, + { + name: 'integration-b', + title: 'Integration B', + }, + ], + }, + ] as PackageList; + + const result = extractIntegrations(packages, []); + + expect(result).toEqual([ + expect.objectContaining({ + package_name: 'package-a', + integration_name: 'integration-a', + package_title: 'Package A', + }), + expect.objectContaining({ + package_name: 'package-a', + integration_name: 'integration-b', + package_title: 'Package A', + }), + ]); + }); + + it('extracts integration title by concatenating package and capitalized integration titles', () => { + const packages = [ + { + name: 'package-a', + title: 'Package A', + version: '1.1.1', + policy_templates: [ + { + name: 'integration-a', + title: 'Integration A', + }, + { + name: 'integration-b', + title: 'Integration B', + }, + ], + }, + ] as PackageList; + + const result = extractIntegrations(packages, []); + + expect(result).toEqual([ + expect.objectContaining({ + integration_name: 'integration-a', + integration_title: 'Package A Integration a', + }), + expect.objectContaining({ + integration_name: 'integration-b', + integration_title: 'Package A Integration b', + }), + ]); + }); + + it('extracts latest available version', () => { + const packages = [ + { + name: 'package-a', + title: 'Package A', + version: '1.1.1', + policy_templates: [ + { + name: 'integration-a', + title: 'Integration A', + }, + { + name: 'integration-b', + title: 'Integration B', + }, + ], + }, + ] as PackageList; + + const result = extractIntegrations(packages, []); + + expect(result).toEqual([ + expect.objectContaining({ + integration_name: 'integration-a', + latest_package_version: '1.1.1', + }), + expect.objectContaining({ + integration_name: 'integration-b', + latest_package_version: '1.1.1', + }), + ]); + }); + + it('extracts not installed integrations', () => { + const packages = [ + { + name: 'package-a', + title: 'Package A', + version: '1.1.1', + policy_templates: [ + { + name: 'integration-a', + title: 'Integration A', + }, + { + name: 'integration-b', + title: 'Integration B', + }, + ], + }, + ] as PackageList; + + const result = extractIntegrations(packages, []); + + expect(result).toEqual([ + expect.objectContaining({ + integration_name: 'integration-a', + is_installed: false, + is_enabled: false, + }), + expect.objectContaining({ + integration_name: 'integration-b', + is_installed: false, + is_enabled: false, + }), + ]); + }); + + it('extracts installed integrations', () => { + const packages = [ + { + name: 'package-a', + title: 'Package A', + version: '1.1.1', + status: 'installed', + policy_templates: [ + { + name: 'integration-a', + title: 'Integration A', + }, + { + name: 'integration-b', + title: 'Integration B', + }, + ], + savedObject: { + attributes: { + install_version: '1.0.0', + }, + }, + }, + ] as PackageList; + + const result = extractIntegrations(packages, []); + + expect(result).toEqual([ + expect.objectContaining({ + integration_name: 'integration-a', + is_installed: true, + is_enabled: false, + }), + expect.objectContaining({ + integration_name: 'integration-b', + is_installed: true, + is_enabled: false, + }), + ]); + }); + + it('extracts enabled integrations', () => { + const packages = [ + { + name: 'package-a', + title: 'Package A', + version: '1.1.1', + status: 'installed', + policy_templates: [ + { + name: 'integration-a', + title: 'Integration A', + }, + { + name: 'integration-b', + title: 'Integration B', + }, + ], + savedObject: { + attributes: { + install_version: '1.0.0', + }, + }, + }, + ] as PackageList; + const policies = [ + { + inputs: [ + { + enabled: true, + policy_template: 'integration-a', + }, + { + enabled: true, + type: 'integration-b', + }, + ], + package: { + name: 'package-a', + }, + }, + ] as PackagePolicy[]; + + const result = extractIntegrations(packages, policies); + + expect(result).toEqual([ + expect.objectContaining({ + integration_name: 'integration-a', + is_installed: true, + is_enabled: true, + }), + expect.objectContaining({ + integration_name: 'integration-b', + is_installed: true, + is_enabled: true, + }), + ]); + }); + + it('extracts installed package version', () => { + const packages = [ + { + name: 'package-a', + title: 'Package A', + version: '1.1.1', + status: 'installed', + policy_templates: [ + { + name: 'integration-a', + title: 'Integration A', + }, + { + name: 'integration-b', + title: 'Integration B', + }, + ], + savedObject: { + attributes: { + install_version: '1.0.0', + }, + }, + }, + ] as PackageList; + const policies = [ + { + inputs: [ + { + enabled: true, + policy_template: 'integration-a', + }, + { + enabled: true, + type: 'integration-b', + }, + ], + package: { + name: 'package-a', + }, + }, + ] as PackagePolicy[]; + + const result = extractIntegrations(packages, policies); + + expect(result).toEqual([ + expect.objectContaining({ + integration_name: 'integration-a', + installed_package_version: '1.0.0', + }), + expect.objectContaining({ + integration_name: 'integration-b', + installed_package_version: '1.0.0', + }), + ]); + }); + }); + + describe('for packages with only one policy template', () => { + it('extracts package title', () => { + const packages = [ + { + name: 'package-a', + title: 'Package A', + version: '1.1.1', + policy_templates: [ + { + name: 'integration-a', + title: 'Integration A', + }, + ], + }, + ] as PackageList; + + const result = extractIntegrations(packages, []); + + expect(result).toEqual([ + expect.objectContaining({ + package_name: 'package-a', + package_title: 'Package A', + }), + ]); + }); + + it('extracts integration title by concatenating package and capitalized integration titles', () => { + const packages = [ + { + name: 'package-a', + title: 'Package A', + version: '1.1.1', + policy_templates: [ + { + name: 'integration-a', + title: 'Integration A', + }, + ], + }, + ] as PackageList; + + const result = extractIntegrations(packages, []); + + expect(result).toEqual([ + expect.objectContaining({ + integration_name: 'integration-a', + integration_title: 'Package A Integration a', + }), + ]); + }); + + it('omits integration_name and integration_title are omitted when package and integration names match', () => { + const packages = [ + { + name: 'integration-a', + title: 'Integration A', + version: '1.1.1', + policy_templates: [ + { + name: 'integration-a', + title: 'Integration A', + }, + ], + }, + ] as PackageList; + + const result = extractIntegrations(packages, []); + + expect(result).toEqual([ + expect.not.objectContaining({ + integration_name: expect.anything(), + integration_title: expect.anything(), + }), + ]); + }); + + it('extracts latest available version', () => { + const packages = [ + { + name: 'package-a', + title: 'Package A', + version: '1.1.1', + policy_templates: [ + { + name: 'integration-a', + title: 'Integration A', + }, + ], + }, + ] as PackageList; + + const result = extractIntegrations(packages, []); + + expect(result).toEqual([ + expect.objectContaining({ + latest_package_version: '1.1.1', + }), + ]); + }); + + it('extracts not installed integrations', () => { + const packages = [ + { + name: 'package-a', + title: 'Package A', + version: '1.1.1', + policy_templates: [ + { + name: 'integration-a', + title: 'Integration A', + }, + ], + }, + ] as PackageList; + + const result = extractIntegrations(packages, []); + + expect(result).toEqual([ + expect.objectContaining({ + is_installed: false, + is_enabled: false, + }), + ]); + }); + + it('extracts installed integrations', () => { + const packages = [ + { + name: 'package-a', + title: 'Package A', + version: '1.1.1', + status: 'installed', + policy_templates: [ + { + name: 'integration-a', + title: 'Integration A', + }, + ], + savedObject: { + attributes: { + install_version: '1.0.0', + }, + }, + }, + ] as PackageList; + + const result = extractIntegrations(packages, []); + + expect(result).toEqual([ + expect.objectContaining({ + is_installed: true, + is_enabled: false, + }), + ]); + }); + + it('extracts enabled integrations', () => { + const packages = [ + { + name: 'package-a', + title: 'Package A', + version: '1.1.1', + status: 'installed', + policy_templates: [ + { + name: 'integration-a', + title: 'Integration A', + }, + ], + savedObject: { + attributes: { + install_version: '1.0.0', + }, + }, + }, + ] as PackageList; + const policies = [ + { + inputs: [ + { + enabled: true, + policy_template: 'integration-a', + }, + ], + package: { + name: 'package-a', + }, + }, + ] as PackagePolicy[]; + + const result = extractIntegrations(packages, policies); + + expect(result).toEqual([ + expect.objectContaining({ + is_installed: true, + is_enabled: true, + }), + ]); + }); + + it('extracts installed package version', () => { + const packages = [ + { + name: 'package-a', + title: 'Package A', + version: '1.1.1', + status: 'installed', + policy_templates: [ + { + name: 'integration-a', + title: 'Integration A', + }, + ], + savedObject: { + attributes: { + install_version: '1.0.0', + }, + }, + }, + ] as PackageList; + const policies = [ + { + inputs: [ + { + enabled: true, + policy_template: 'integration-a', + }, + ], + package: { + name: 'package-a', + }, + }, + ] as PackagePolicy[]; + + const result = extractIntegrations(packages, policies); + + expect(result).toEqual([ + expect.objectContaining({ + installed_package_version: '1.0.0', + }), + ]); + }); + }); + + describe('for packages without policy templates', () => { + it('extracts package title', () => { + const packages = [ + { + name: 'package-a', + title: 'Package A', + version: '1.1.1', + }, + ] as PackageList; + + const result = extractIntegrations(packages, []); + + expect(result).toEqual([ + expect.objectContaining({ + package_name: 'package-a', + package_title: 'Package A', + }), + ]); + }); + + it('omits integration_name and integration_title', () => { + const packages = [ + { + name: 'integration-a', + title: 'Integration A', + version: '1.1.1', + }, + ] as PackageList; + + const result = extractIntegrations(packages, []); + + expect(result).toEqual([ + expect.not.objectContaining({ + integration_name: expect.anything(), + integration_title: expect.anything(), + }), + ]); + }); + + it('extracts latest available version', () => { + const packages = [ + { + name: 'package-a', + title: 'Package A', + version: '1.1.1', + }, + ] as PackageList; + + const result = extractIntegrations(packages, []); + + expect(result).toEqual([ + expect.objectContaining({ + latest_package_version: '1.1.1', + }), + ]); + }); + + it('extracts not installed integrations', () => { + const packages = [ + { + name: 'package-a', + title: 'Package A', + version: '1.1.1', + }, + ] as PackageList; + + const result = extractIntegrations(packages, []); + + expect(result).toEqual([ + expect.objectContaining({ + is_installed: false, + is_enabled: false, + }), + ]); + }); + + it('extracts installed integrations', () => { + const packages = [ + { + name: 'package-a', + title: 'Package A', + version: '1.1.1', + status: 'installed', + savedObject: { + attributes: { + install_version: '1.0.0', + }, + }, + }, + ] as PackageList; + + const result = extractIntegrations(packages, []); + + expect(result).toEqual([ + expect.objectContaining({ + is_installed: true, + is_enabled: false, + }), + ]); + }); + + it('extracts enabled integrations', () => { + const packages = [ + { + name: 'package-a', + title: 'Package A', + version: '1.1.1', + status: 'installed', + savedObject: { + attributes: { + install_version: '1.0.0', + }, + }, + }, + ] as PackageList; + const policies = [ + { + package: { + name: 'package-a', + }, + inputs: [] as PackagePolicyInput[], + }, + ] as PackagePolicy[]; + + const result = extractIntegrations(packages, policies); + + expect(result).toEqual([ + expect.objectContaining({ + is_installed: true, + is_enabled: true, + }), + ]); + }); + + it('extracts installed package version', () => { + const packages = [ + { + name: 'package-a', + title: 'Package A', + version: '1.1.1', + status: 'installed', + savedObject: { + attributes: { + install_version: '1.0.0', + }, + }, + }, + ] as PackageList; + const policies = [ + { + package: { + name: 'package-a', + }, + inputs: [] as PackagePolicyInput[], + }, + ] as PackagePolicy[]; + + const result = extractIntegrations(packages, policies); + + expect(result).toEqual([ + expect.objectContaining({ + installed_package_version: '1.0.0', + }), + ]); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/extract_integrations.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/extract_integrations.ts new file mode 100644 index 00000000000000..23cd03cedbe364 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/extract_integrations.ts @@ -0,0 +1,93 @@ +/* + * 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 { capitalize } from 'lodash'; +import type { PackageList, PackagePolicy } from '@kbn/fleet-plugin/common'; +import type { Integration } from '../../../../../../common/api/detection_engine/fleet_integrations/model/integrations'; + +export function extractIntegrations( + packages: PackageList, + packagePolicies: PackagePolicy[] +): Integration[] { + const result: Integration[] = []; + const enabledIntegrationsSet = extractEnabledIntegrations(packagePolicies); + + for (const fleetPackage of packages) { + const packageName = fleetPackage.name; + const packageTitle = fleetPackage.title; + const isPackageInstalled = fleetPackage.status === 'installed'; + // Actual `installed_version` is buried in SO, root `version` is latest package version available + const installedPackageVersion = fleetPackage.savedObject?.attributes.install_version; + // Policy templates correspond to package's integrations. + const packagePolicyTemplates = fleetPackage.policy_templates ?? []; + + for (const policyTemplate of packagePolicyTemplates) { + const integrationId = getIntegrationId(packageName, policyTemplate.name); + const integrationName = policyTemplate.name; + const integrationTitle = + packagePolicyTemplates.length === 1 && policyTemplate.name === fleetPackage.name + ? packageTitle + : `${packageTitle} ${capitalize(policyTemplate.title)}`; + + const integration: Integration = { + package_name: packageName, + package_title: packageTitle, + latest_package_version: fleetPackage.version, + installed_package_version: installedPackageVersion, + integration_name: packageName !== integrationName ? integrationName : undefined, + integration_title: packageName !== integrationName ? integrationTitle : undefined, + is_installed: isPackageInstalled, // All integrations installed as a part of the package + is_enabled: enabledIntegrationsSet.has(integrationId), + }; + + result.push(integration); + } + + // some packages don't have policy templates at al, e.g. Lateral Movement Detection + if (packagePolicyTemplates.length === 0) { + result.push({ + package_name: packageName, + package_title: packageTitle, + latest_package_version: fleetPackage.version, + installed_package_version: installedPackageVersion, + is_installed: isPackageInstalled, + is_enabled: enabledIntegrationsSet.has(getIntegrationId(packageName, '')), + }); + } + } + + return result; +} + +function extractEnabledIntegrations(packagePolicies: PackagePolicy[]): Set { + const enabledIntegrations = new Set(); + + for (const packagePolicy of packagePolicies) { + for (const input of packagePolicy.inputs) { + if (input.enabled) { + const packageName = packagePolicy.package?.name.trim() ?? ''; // e.g. 'cloudtrail' + const integrationName = (input.policy_template ?? input.type ?? '').trim(); // e.g. 'cloudtrail' + const enabledIntegrationKey = `${packageName}${integrationName}`; + + enabledIntegrations.add(enabledIntegrationKey); + } + } + + // Base package may not have policy template, so pull directly from `policy.package` if so + if (packagePolicy.package) { + const packageName = packagePolicy.package.name.trim(); + + enabledIntegrations.add(packageName); + } + } + + return enabledIntegrations; +} + +function getIntegrationId(packageName: string, integrationName: string): string { + return `${packageName}${integrationName}`; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/route.ts new file mode 100644 index 00000000000000..e5bae990528031 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/route.ts @@ -0,0 +1,74 @@ +/* + * 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 { transformError } from '@kbn/securitysolution-es-utils'; +import { PREBUILT_RULES_PACKAGE_NAME } from '../../../../../../common/detection_engine/constants'; +import { buildSiemResponse } from '../../../routes/utils'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; +import type { GetAllIntegrationsResponse } from '../../../../../../common/api/detection_engine/fleet_integrations'; +import { GET_ALL_INTEGRATIONS_URL } from '../../../../../../common/api/detection_engine/fleet_integrations'; +import { extractIntegrations } from './extract_integrations'; +import { sortPackagesBySecurityCategory } from './sort_packages_by_security_category'; +import { sortIntegrationsByStatus } from './sort_integrations_by_status'; + +/** + * Returns an array of Fleet integrations and their packages + */ +export const getAllIntegrationsRoute = (router: SecuritySolutionPluginRouter) => { + router.versioned + .get({ + access: 'internal', + path: GET_ALL_INTEGRATIONS_URL, + options: { + tags: ['access:securitySolution'], + }, + }) + .addVersion( + { + version: '1', + validate: false, + }, + async (context, _, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const ctx = await context.resolve(['core', 'securitySolution']); + const fleet = ctx.securitySolution.getInternalFleetServices(); + + const [packages, packagePolicies] = await Promise.all([ + fleet.packages.getPackages(), + fleet.packagePolicy.list(fleet.internalReadonlySoClient, {}), + ]); + // Elastic prebuilt rules is a special package and should be skipped + const packagesWithoutPrebuiltSecurityRules = packages.filter( + (x) => x.name !== PREBUILT_RULES_PACKAGE_NAME + ); + + sortPackagesBySecurityCategory(packagesWithoutPrebuiltSecurityRules); + + const integrations = extractIntegrations( + packagesWithoutPrebuiltSecurityRules, + packagePolicies.items + ); + + sortIntegrationsByStatus(integrations); + + const body: GetAllIntegrationsResponse = { + integrations, + }; + + return response.ok({ body }); + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/sort_integrations_by_status.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/sort_integrations_by_status.ts new file mode 100644 index 00000000000000..fa626f369621b9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/sort_integrations_by_status.ts @@ -0,0 +1,29 @@ +/* + * 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 { Integration } from '../../../../../../common/api/detection_engine/fleet_integrations/model/integrations'; + +/** + * Sorts integrations in place + */ +export function sortIntegrationsByStatus(integration: Integration[]): void { + integration.sort((a, b) => { + if (a.is_enabled && !b.is_enabled) { + return -1; + } else if (!a.is_enabled && b.is_enabled) { + return 1; + } + + if (a.is_installed && !b.is_installed) { + return -1; + } else if (!a.is_installed && b.is_installed) { + return 1; + } + + return 0; + }); +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/sort_packages_by_security_category.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/sort_packages_by_security_category.ts new file mode 100644 index 00000000000000..25faae31dbd938 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/sort_packages_by_security_category.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PackageList } from '@kbn/fleet-plugin/common'; + +/** + * Sorts packages in place + */ +export function sortPackagesBySecurityCategory(packages: PackageList): void { + packages.sort((a, b) => { + if (a.categories?.includes('security') && !b.categories?.includes('security')) { + return -1; + } + + if (!a.categories?.includes('security') && b.categories?.includes('security')) { + return 1; + } + + return 0; + }); +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_installed_integrations/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_installed_integrations/route.ts index bf13e1f49134e4..407c7d54adc526 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_installed_integrations/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_installed_integrations/route.ts @@ -5,7 +5,6 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; import { transformError } from '@kbn/securitysolution-es-utils'; import { buildSiemResponse } from '../../../routes/utils'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; @@ -17,10 +16,7 @@ import { createInstalledIntegrationSet } from './installed_integration_set'; /** * Returns an array of installed Fleet integrations and their packages. */ -export const getInstalledIntegrationsRoute = ( - router: SecuritySolutionPluginRouter, - logger: Logger -) => { +export const getInstalledIntegrationsRoute = (router: SecuritySolutionPluginRouter) => { router.versioned .get({ access: 'internal', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/register_routes.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/register_routes.ts index 2c6c2c2ae21f8a..cd60509ca5f51b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/register_routes.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/register_routes.ts @@ -5,14 +5,11 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; import type { SecuritySolutionPluginRouter } from '../../../../types'; - +import { getAllIntegrationsRoute } from './get_all_integrations/route'; import { getInstalledIntegrationsRoute } from './get_installed_integrations/route'; -export const registerFleetIntegrationsRoutes = ( - router: SecuritySolutionPluginRouter, - logger: Logger -) => { - getInstalledIntegrationsRoute(router, logger); +export const registerFleetIntegrationsRoutes = (router: SecuritySolutionPluginRouter) => { + getAllIntegrationsRoute(router); + getInstalledIntegrationsRoute(router); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts index 07fb5640eb482a..48f097ac7a8609 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts @@ -7,7 +7,6 @@ import * as z from 'zod'; import { - RelatedIntegrationArray, RequiredFieldArray, SetupGuide, RuleSignatureId, @@ -35,7 +34,6 @@ export const PrebuiltRuleAsset = BaseCreateProps.and(TypeSpecificCreateProps).an z.object({ rule_id: RuleSignatureId, version: RuleVersion, - related_integrations: RelatedIntegrationArray.optional(), required_fields: RequiredFieldArray.optional(), setup: SetupGuide.optional(), }) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts index a4fcd4797b75b1..d36f0ab4ad66e9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts @@ -57,7 +57,7 @@ export const updateRules = async ({ timelineTitle: ruleUpdate.timeline_title, meta: ruleUpdate.meta, maxSignals: ruleUpdate.max_signals ?? DEFAULT_MAX_SIGNALS, - relatedIntegrations: existingRule.params.relatedIntegrations, + relatedIntegrations: ruleUpdate.related_integrations ?? [], requiredFields: existingRule.params.requiredFields, riskScore: ruleUpdate.risk_score, riskScoreMapping: ruleUpdate.risk_score_mapping ?? [], diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index 4365eb4bcc3fa3..bc1a26534cd6c7 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -78,7 +78,7 @@ export const initRoutes = ( previewRuleDataClient: IRuleDataClient, previewTelemetryReceiver: ITelemetryReceiver ) => { - registerFleetIntegrationsRoutes(router, logger); + registerFleetIntegrationsRoutes(router); registerLegacyRuleActionsRoutes(router, logger); registerPrebuiltRulesRoutes(router, security); registerRuleExceptionsRoutes(router); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 7e7d4218db8fe8..9d357b045bb5f0 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -34320,8 +34320,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldNameLabel": "Nom", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldNewTermsFieldHelpText": "Sélectionnez un champ pour vérifier les nouveaux termes.", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldReferenceUrlsLabel": "URL de référence", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRelatedIntegrationsHelpText": "Intégration liée à cette règle.", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRelatedIntegrationsLabel": "Intégrations liées", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRequiredFieldsHelpText": "Champs requis pour le fonctionnement de cette règle.", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRequiredFieldsLabel": "Champ requis", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRuleNameOverrideHelpText": "Choisissez un champ de l'événement source pour remplir le nom de règle dans la liste d'alertes.", @@ -35154,7 +35152,6 @@ "xpack.securitySolution.detectionEngine.relatedIntegrations.badgeTitle": "intégrations", "xpack.securitySolution.detectionEngine.relatedIntegrations.enabledTitle": "Installé : activé", "xpack.securitySolution.detectionEngine.relatedIntegrations.enabledTooltip": "L'intégration est installée et une politique d'intégration avec la configuration requise existe. Assurez-vous que des agents Elastic sont affectés à cette politique pour ingérer des événements compatibles.", - "xpack.securitySolution.detectionEngine.relatedIntegrations.installedTitle": "Installé", "xpack.securitySolution.detectionEngine.relatedIntegrations.installedTooltip": "L’intégration est installée. Configurez une politique d’intégration et assurez-vous que des agents Elastic sont affectés à cette politique pour ingérer des événements compatibles.", "xpack.securitySolution.detectionEngine.relatedIntegrations.uninstalledTitle": "Non installé", "xpack.securitySolution.detectionEngine.relatedIntegrations.uninstalledTooltip": "L’intégration n’est pas installée. Suivez le lien d'intégration pour installer et configurer l'intégration.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 60e15dd80a172f..9900efe948cae4 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -34289,8 +34289,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldNameLabel": "名前", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldNewTermsFieldHelpText": "新しい用語を確認するフィールドを選択します。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldReferenceUrlsLabel": "参照URL", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRelatedIntegrationsHelpText": "統合はこのルールに関連しています。", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRelatedIntegrationsLabel": "関連する統合", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRequiredFieldsHelpText": "このルールの機能に必要なフィールド。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRequiredFieldsLabel": "必須フィールド", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRuleNameOverrideHelpText": "ソースイベントからフィールドを選択し、アラートリストのルール名を入力します。", @@ -35123,7 +35121,6 @@ "xpack.securitySolution.detectionEngine.relatedIntegrations.badgeTitle": "統合", "xpack.securitySolution.detectionEngine.relatedIntegrations.enabledTitle": "インストール済み:有効", "xpack.securitySolution.detectionEngine.relatedIntegrations.enabledTooltip": "統合はインストールされ、必要な構成が行われている統合ポリシーが存在します。Elasticエージェントにこのポリシーが割り当てられていることを確認し、互換性があるイベントを取り込みます。", - "xpack.securitySolution.detectionEngine.relatedIntegrations.installedTitle": "インストール済み", "xpack.securitySolution.detectionEngine.relatedIntegrations.installedTooltip": "統合がインストールされています。統合ポリシーを構成し、Elasticエージェントにこのポリシーが割り当てられていることを確認して、対応するイベントを取り込みます。", "xpack.securitySolution.detectionEngine.relatedIntegrations.uninstalledTitle": "未インストール", "xpack.securitySolution.detectionEngine.relatedIntegrations.uninstalledTooltip": "統合はインストールされていません。統合リンクに従って、インストールし、統合を構成してください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 430a3b8d469163..44d958e7b4527f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -34332,8 +34332,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldNameLabel": "名称", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldNewTermsFieldHelpText": "选择字段以检查新字词。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldReferenceUrlsLabel": "引用 URL", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRelatedIntegrationsHelpText": "与此规则相关的集成。", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRelatedIntegrationsLabel": "相关集成", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRequiredFieldsHelpText": "此规则正常运行所需的字段。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRequiredFieldsLabel": "必填字段", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRuleNameOverrideHelpText": "从源事件中选择字段来填充告警列表中的规则名称。", @@ -35166,7 +35164,6 @@ "xpack.securitySolution.detectionEngine.relatedIntegrations.badgeTitle": "集成", "xpack.securitySolution.detectionEngine.relatedIntegrations.enabledTitle": "已安装:已启用", "xpack.securitySolution.detectionEngine.relatedIntegrations.enabledTooltip": "集成已安装,并且存在具有所需配置的集成策略。确保 Elastic 代理已分配此策略以采集兼容的事件。", - "xpack.securitySolution.detectionEngine.relatedIntegrations.installedTitle": "已安装", "xpack.securitySolution.detectionEngine.relatedIntegrations.installedTooltip": "已安装集成。配置集成策略,并确保 Elastic 代理已分配此策略以采集兼容的事件。", "xpack.securitySolution.detectionEngine.relatedIntegrations.uninstalledTitle": "未安装", "xpack.securitySolution.detectionEngine.relatedIntegrations.uninstalledTooltip": "未安装集成。访问集成链接以安装和配置集成。", diff --git a/x-pack/test/api_integration/apis/ml/jobs/all_jobs_and_group_ids.ts b/x-pack/test/api_integration/apis/ml/jobs/all_jobs_and_group_ids.ts new file mode 100644 index 00000000000000..b4efa23eedf2c6 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/jobs/all_jobs_and_group_ids.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { getCommonRequestHeader } from '../../../../functional/services/ml/common_api'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { MULTI_METRIC_JOB_CONFIG, SINGLE_METRIC_JOB_CONFIG } from './common_jobs'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testSetupJobConfigs = [SINGLE_METRIC_JOB_CONFIG, MULTI_METRIC_JOB_CONFIG]; + + const testCalendarsConfigs = [ + { + calendar_id: `test_get_cal_1`, + job_ids: ['multi-metric'], + description: `Test calendar 1`, + }, + { + calendar_id: `test_get_cal_2`, + job_ids: [MULTI_METRIC_JOB_CONFIG.job_id, 'multi-metric'], + description: `Test calendar 2`, + }, + { + calendar_id: `test_get_cal_3`, + job_ids: ['brand-new-group'], + description: `Test calendar 3`, + }, + ]; + + async function runRequest(user: USER, expectedResponseCode: number) { + const { body, status } = await supertest + .get('/internal/ml/jobs/all_jobs_and_group_ids') + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(getCommonRequestHeader('1')); + ml.api.assertResponseStatusCode(expectedResponseCode, status, body); + + return body; + } + + const expectedIds = { + jobIds: [MULTI_METRIC_JOB_CONFIG.job_id, SINGLE_METRIC_JOB_CONFIG.job_id], + groupIds: ['automated', 'brand-new-group', 'farequote', 'multi-metric', 'single-metric'], + }; + + describe('get all job and group IDs', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + for (const job of testSetupJobConfigs) { + await ml.api.createAnomalyDetectionJob(job); + } + for (const cal of testCalendarsConfigs) { + await ml.api.createCalendar(cal.calendar_id, cal); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('returns expected list of job and group Ids', async () => { + const ids = await runRequest(USER.ML_VIEWER, 200); + + expect(ids).to.eql( + expectedIds, + `response job and group IDs list should equal ${JSON.stringify(expectedIds)})` + ); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/jobs/index.ts b/x-pack/test/api_integration/apis/ml/jobs/index.ts index 561199b4e743da..96ed8131facd2a 100644 --- a/x-pack/test/api_integration/apis/ml/jobs/index.ts +++ b/x-pack/test/api_integration/apis/ml/jobs/index.ts @@ -28,5 +28,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./update_groups')); loadTestFile(require.resolve('./category_results')); loadTestFile(require.resolve('./jobs_with_time_range')); + loadTestFile(require.resolve('./all_jobs_and_group_ids')); }); } diff --git a/x-pack/test/apm_api_integration/common/config.ts b/x-pack/test/apm_api_integration/common/config.ts index a4f02524b98cf1..c22fb0325288b3 100644 --- a/x-pack/test/apm_api_integration/common/config.ts +++ b/x-pack/test/apm_api_integration/common/config.ts @@ -75,7 +75,7 @@ export interface CreateTest { logSynthtraceEsClient: ( context: InheritedFtrProviderContext ) => Promise; - synthtraceEsClient: (context: InheritedFtrProviderContext) => Promise; + apmSynthtraceEsClient: (context: InheritedFtrProviderContext) => Promise; synthtraceKibanaClient: ( context: InheritedFtrProviderContext ) => Promise; @@ -112,7 +112,7 @@ export function createTestConfig( ...services, apmFtrConfig: () => config, registry: RegistryProvider, - synthtraceEsClient: (context: InheritedFtrProviderContext) => { + apmSynthtraceEsClient: (context: InheritedFtrProviderContext) => { return bootstrapApmSynthtrace(context, synthtraceKibanaClient); }, logSynthtraceEsClient: (context: InheritedFtrProviderContext) => diff --git a/x-pack/test/apm_api_integration/tests/agent_explorer/agent_explorer.spec.ts b/x-pack/test/apm_api_integration/tests/agent_explorer/agent_explorer.spec.ts index 7fea0939d41ee9..95e71167aaab48 100644 --- a/x-pack/test/apm_api_integration/tests/agent_explorer/agent_explorer.spec.ts +++ b/x-pack/test/apm_api_integration/tests/agent_explorer/agent_explorer.spec.ts @@ -14,7 +14,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -103,7 +103,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { 'service.language.name': 'javascript', }); - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('5m') .rate(1) @@ -143,7 +143,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('labels.telemetry_auto_version takes precedence over agent.version for otelAgets', async () => { const { status, body } = await callApi(); diff --git a/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts index f98a82a6cd30ca..033d64e8f12e8e 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/anomaly_alert.spec.ts @@ -26,7 +26,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const es = getService('es'); const logger = getService('log'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); registry.when( 'fetching service anomalies with a trial license', { config: 'trial', archives: [] }, @@ -62,7 +62,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]; }); - await synthtraceEsClient.index(events); + await apmSynthtraceEsClient.index(events); await createAndRunApmMlJobs({ es, ml, environments: ['production'], logger }); }); @@ -72,7 +72,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); async function cleanup() { - await synthtraceEsClient.clean(); + await apmSynthtraceEsClient.clean(); await cleanupRuleAndAlertState({ es, supertest, logger }); await ml.cleanMlIndices(); } diff --git a/x-pack/test/apm_api_integration/tests/alerts/error_count_threshold.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/error_count_threshold.spec.ts index 545706af43107e..46d62449de475f 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/error_count_threshold.spec.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/error_count_threshold.spec.ts @@ -31,7 +31,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const es = getService('es'); const logger = getService('log'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); registry.when('error count threshold alert', { config: 'basic', archives: [] }, () => { const javaErrorMessage = 'a java error'; @@ -96,10 +96,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]; }); - return Promise.all([synthtraceEsClient.index(events), synthtraceEsClient.index(phpEvents)]); + return Promise.all([ + apmSynthtraceEsClient.index(events), + apmSynthtraceEsClient.index(phpEvents), + ]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); // FLAKY: https://github.com/elastic/kibana/issues/176948 describe('create rule without kql filter', () => { diff --git a/x-pack/test/apm_api_integration/tests/alerts/generate_data.ts b/x-pack/test/apm_api_integration/tests/alerts/generate_data.ts index 2765c0a1e265b2..79fd804aea1cd8 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/generate_data.ts @@ -21,12 +21,12 @@ export const config = { }; export async function generateLatencyData({ - synthtraceEsClient, + apmSynthtraceEsClient, serviceName, start, end, }: { - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; serviceName: string; start: number; end: number; @@ -58,16 +58,16 @@ export async function generateLatencyData({ ), ]; - await synthtraceEsClient.index(documents); + await apmSynthtraceEsClient.index(documents); } export async function generateErrorData({ - synthtraceEsClient, + apmSynthtraceEsClient, serviceName, start, end, }: { - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; serviceName: string; start: number; end: number; @@ -110,5 +110,5 @@ export async function generateErrorData({ ]; }); - await synthtraceEsClient.index(documents); + await apmSynthtraceEsClient.index(documents); } diff --git a/x-pack/test/apm_api_integration/tests/alerts/preview_chart_error_count.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/preview_chart_error_count.spec.ts index c1b2243c1373d1..897f4467344442 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/preview_chart_error_count.spec.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/preview_chart_error_count.spec.ts @@ -19,7 +19,7 @@ import { generateErrorData } from './generate_data'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -72,11 +72,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { // FLAKY: https://github.com/elastic/kibana/issues/172769 describe('error_count', () => { beforeEach(async () => { - await generateErrorData({ serviceName: 'synth-go', start, end, synthtraceEsClient }); - await generateErrorData({ serviceName: 'synth-java', start, end, synthtraceEsClient }); + await generateErrorData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }); + await generateErrorData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }); }); - afterEach(() => synthtraceEsClient.clean()); + afterEach(() => apmSynthtraceEsClient.clean()); it('with data', async () => { const options = getOptions(); @@ -308,11 +308,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { // FLAKY: https://github.com/elastic/kibana/issues/176975 describe('error_count', () => { before(async () => { - await generateErrorData({ serviceName: 'synth-go', start, end, synthtraceEsClient }); - await generateErrorData({ serviceName: 'synth-java', start, end, synthtraceEsClient }); + await generateErrorData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }); + await generateErrorData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('with data', async () => { const options = getOptionsWithFilterQuery(); diff --git a/x-pack/test/apm_api_integration/tests/alerts/preview_chart_error_rate.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/preview_chart_error_rate.spec.ts index 2a3479bcffea07..bc11d1cb68fd93 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/preview_chart_error_rate.spec.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/preview_chart_error_rate.spec.ts @@ -19,7 +19,7 @@ import { generateErrorData } from './generate_data'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -73,11 +73,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { // FLAKY: https://github.com/elastic/kibana/issues/176977 describe('transaction_error_rate', () => { before(async () => { - await generateErrorData({ serviceName: 'synth-go', start, end, synthtraceEsClient }); - await generateErrorData({ serviceName: 'synth-java', start, end, synthtraceEsClient }); + await generateErrorData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }); + await generateErrorData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('with data', async () => { const options = getOptions(); @@ -332,11 +332,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { // FLAKY: https://github.com/elastic/kibana/issues/176983 describe('transaction_error_rate', () => { before(async () => { - await generateErrorData({ serviceName: 'synth-go', start, end, synthtraceEsClient }); - await generateErrorData({ serviceName: 'synth-java', start, end, synthtraceEsClient }); + await generateErrorData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }); + await generateErrorData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('with data', async () => { const options = getOptionsWithFilterQuery(); diff --git a/x-pack/test/apm_api_integration/tests/alerts/preview_chart_transaction_duration.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/preview_chart_transaction_duration.spec.ts index 3c149458ce2224..4bae66ed9e66b6 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/preview_chart_transaction_duration.spec.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/preview_chart_transaction_duration.spec.ts @@ -19,7 +19,7 @@ import { generateLatencyData } from './generate_data'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -76,11 +76,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { // Failing: See https://github.com/elastic/kibana/issues/176989 describe('transaction_duration', () => { before(async () => { - await generateLatencyData({ serviceName: 'synth-go', start, end, synthtraceEsClient }); - await generateLatencyData({ serviceName: 'synth-java', start, end, synthtraceEsClient }); + await generateLatencyData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }); + await generateLatencyData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('with data', async () => { const options = getOptions(); @@ -304,11 +304,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when(`with data loaded and using KQL filter`, { config: 'basic', archives: [] }, () => { describe('transaction_duration', () => { before(async () => { - await generateLatencyData({ serviceName: 'synth-go', start, end, synthtraceEsClient }); - await generateLatencyData({ serviceName: 'synth-java', start, end, synthtraceEsClient }); + await generateLatencyData({ serviceName: 'synth-go', start, end, apmSynthtraceEsClient }); + await generateLatencyData({ serviceName: 'synth-java', start, end, apmSynthtraceEsClient }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('with data', async () => { const options = getOptionsWithFilterQuery(); diff --git a/x-pack/test/apm_api_integration/tests/alerts/transaction_duration.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/transaction_duration.spec.ts index 5eab7e772a4cda..7d4ec54fa52fbe 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/transaction_duration.spec.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/transaction_duration.spec.ts @@ -31,7 +31,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const es = getService('es'); const logger = getService('log'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const ruleParams = { threshold: 3000, @@ -68,11 +68,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { .success(), ]; }); - return synthtraceEsClient.index(events); + return apmSynthtraceEsClient.index(events); }); after(async () => { - await synthtraceEsClient.clean(); + await apmSynthtraceEsClient.clean(); }); // FLAKY: https://github.com/elastic/kibana/issues/176996 diff --git a/x-pack/test/apm_api_integration/tests/alerts/transaction_error_rate.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/transaction_error_rate.spec.ts index ec66b7aaf67ec9..6416337c3bfc9e 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/transaction_error_rate.spec.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/transaction_error_rate.spec.ts @@ -30,7 +30,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const es = getService('es'); const logger = getService('log'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); registry.when('transaction error rate alert', { config: 'basic', archives: [] }, () => { before(() => { @@ -66,11 +66,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { .success(), ]; }); - return synthtraceEsClient.index(events); + return apmSynthtraceEsClient.index(events); }); after(async () => { - await synthtraceEsClient.clean(); + await apmSynthtraceEsClient.clean(); }); // FLAKY: https://github.com/elastic/kibana/issues/177104 diff --git a/x-pack/test/apm_api_integration/tests/anomalies/anomaly_charts.spec.ts b/x-pack/test/apm_api_integration/tests/anomalies/anomaly_charts.spec.ts index 93effd3984daf2..1ba02adbade97f 100644 --- a/x-pack/test/apm_api_integration/tests/anomalies/anomaly_charts.spec.ts +++ b/x-pack/test/apm_api_integration/tests/anomalies/anomaly_charts.spec.ts @@ -22,7 +22,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const ml = getService('ml'); const es = getService('es'); const logger = getService('log'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = moment().subtract(2, 'days'); const end = moment(); @@ -128,7 +128,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]; }); - await synthtraceEsClient.index(events); + await apmSynthtraceEsClient.index(events); }); afterEach(async () => { @@ -136,7 +136,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); async function cleanup() { - await synthtraceEsClient.clean(); + await apmSynthtraceEsClient.clean(); await ml.cleanMlIndices(); } diff --git a/x-pack/test/apm_api_integration/tests/assistant/obs_alert_details_context.spec.ts b/x-pack/test/apm_api_integration/tests/assistant/obs_alert_details_context.spec.ts deleted file mode 100644 index 5a98ec708bcf37..00000000000000 --- a/x-pack/test/apm_api_integration/tests/assistant/obs_alert_details_context.spec.ts +++ /dev/null @@ -1,507 +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 moment from 'moment'; -import { log, apm, generateShortId, timerange } from '@kbn/apm-synthtrace-client'; -import expect from '@kbn/expect'; -import { LogCategories } from '@kbn/apm-plugin/server/routes/assistant_functions/get_log_categories'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { SupertestReturnType } from '../../common/apm_api_supertest'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceClient = getService('synthtraceEsClient'); - const logSynthtraceClient = getService('logSynthtraceEsClient'); - - registry.when( - 'fetching observability alerts details context for AI assistant contextual insights', - { config: 'trial', archives: [] }, - () => { - const start = moment().subtract(10, 'minutes').valueOf(); - const end = moment().valueOf(); - const range = timerange(start, end); - - describe('when no traces or logs are available', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; - before(async () => { - response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', - params: { - query: { - alert_started_at: new Date(end).toISOString(), - }, - }, - }); - }); - - it('returns nothing', () => { - expect(response.body.context).to.eql([]); - }); - }); - - describe('when traces and logs are ingested and logs are not annotated with service.name', async () => { - before(async () => { - await ingestTraces({ 'service.name': 'Backend', 'container.id': 'my-container-a' }); - await ingestLogs({ - 'container.id': 'my-container-a', - 'kubernetes.pod.name': 'pod-a', - }); - }); - - after(async () => { - await cleanup(); - }); - - describe('when no params are specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; - before(async () => { - response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', - params: { - query: { - alert_started_at: new Date(end).toISOString(), - }, - }, - }); - }); - - it('returns only 1 log category', async () => { - expect(response.body.context).to.have.length(1); - expect( - (response.body.context[0]?.data as LogCategories)?.map( - ({ errorCategory }: { errorCategory: string }) => errorCategory - ) - ).to.eql(['Error message from container my-container-a']); - }); - }); - - describe('when service name is specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; - before(async () => { - response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', - params: { - query: { - alert_started_at: new Date(end).toISOString(), - 'service.name': 'Backend', - }, - }, - }); - }); - - it('returns service summary', () => { - const serviceSummary = response.body.context.find( - ({ key }) => key === 'serviceSummary' - ); - expect(serviceSummary?.data).to.eql({ - 'service.name': 'Backend', - 'service.environment': ['production'], - 'agent.name': 'java', - 'service.version': ['1.0.0'], - 'language.name': 'java', - instances: 1, - anomalies: [], - alerts: [], - deployments: [], - }); - }); - - it('returns downstream dependencies', async () => { - const downstreamDependencies = response.body.context.find( - ({ key }) => key === 'downstreamDependencies' - ); - expect(downstreamDependencies?.data).to.eql([ - { - 'span.destination.service.resource': 'elasticsearch', - 'span.type': 'db', - 'span.subtype': 'elasticsearch', - }, - ]); - }); - - it('returns log categories', () => { - const logCategories = response.body.context.find(({ key }) => key === 'logCategories'); - expect(logCategories?.data).to.have.length(1); - - const logCategory = (logCategories?.data as LogCategories)?.[0]; - expect(logCategory?.sampleMessage).to.match( - /Error message #\d{16} from container my-container-a/ - ); - expect(logCategory?.docCount).to.be.greaterThan(0); - expect(logCategory?.errorCategory).to.be('Error message from container my-container-a'); - }); - }); - - describe('when container id is specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; - before(async () => { - response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', - params: { - query: { - alert_started_at: new Date(end).toISOString(), - 'container.id': 'my-container-a', - }, - }, - }); - }); - - it('returns service summary', () => { - const serviceSummary = response.body.context.find( - ({ key }) => key === 'serviceSummary' - ); - expect(serviceSummary?.data).to.eql({ - 'service.name': 'Backend', - 'service.environment': ['production'], - 'agent.name': 'java', - 'service.version': ['1.0.0'], - 'language.name': 'java', - instances: 1, - anomalies: [], - alerts: [], - deployments: [], - }); - }); - - it('returns downstream dependencies', async () => { - const downstreamDependencies = response.body.context.find( - ({ key }) => key === 'downstreamDependencies' - ); - expect(downstreamDependencies?.data).to.eql([ - { - 'span.destination.service.resource': 'elasticsearch', - 'span.type': 'db', - 'span.subtype': 'elasticsearch', - }, - ]); - }); - - it('returns log categories', () => { - const logCategories = response.body.context.find(({ key }) => key === 'logCategories'); - expect(logCategories?.data).to.have.length(1); - - const logCategory = (logCategories?.data as LogCategories)?.[0]; - expect(logCategory?.sampleMessage).to.match( - /Error message #\d{16} from container my-container-a/ - ); - expect(logCategory?.docCount).to.be.greaterThan(0); - expect(logCategory?.errorCategory).to.be('Error message from container my-container-a'); - }); - }); - - describe('when non-existing container id is specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; - before(async () => { - response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', - params: { - query: { - alert_started_at: new Date(end).toISOString(), - 'container.id': 'non-existing-container', - }, - }, - }); - }); - - it('returns nothing', () => { - expect(response.body.context).to.eql([]); - }); - }); - - describe('when non-existing service.name is specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; - before(async () => { - response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', - params: { - query: { - alert_started_at: new Date(end).toISOString(), - 'service.name': 'non-existing-service', - }, - }, - }); - }); - - it('returns empty service summary', () => { - const serviceSummary = response.body.context.find( - ({ key }) => key === 'serviceSummary' - ); - expect(serviceSummary?.data).to.eql({ - 'service.name': 'non-existing-service', - 'service.environment': [], - instances: 1, - anomalies: [], - alerts: [], - deployments: [], - }); - }); - - it('returns no downstream dependencies', async () => { - const downstreamDependencies = response.body.context.find( - ({ key }) => key === 'downstreamDependencies' - ); - expect(downstreamDependencies).to.eql(undefined); - }); - - it('returns log categories', () => { - const logCategories = response.body.context.find(({ key }) => key === 'logCategories'); - expect(logCategories?.data).to.have.length(1); - }); - }); - }); - - describe('when traces and logs are ingested and logs are annotated with service.name', async () => { - before(async () => { - await ingestTraces({ 'service.name': 'Backend', 'container.id': 'my-container-a' }); - await ingestLogs({ - 'service.name': 'Backend', - 'container.id': 'my-container-a', - 'kubernetes.pod.name': 'pod-a', - }); - - // also ingest unrelated Frontend traces and logs that should not show up in the response when fetching "Backend"-related things - await ingestTraces({ 'service.name': 'Frontend', 'container.id': 'my-container-b' }); - await ingestLogs({ - 'service.name': 'Frontend', - 'container.id': 'my-container-b', - 'kubernetes.pod.name': 'pod-b', - }); - - // also ingest logs that are not annotated with service.name - await ingestLogs({ - 'container.id': 'my-container-c', - 'kubernetes.pod.name': 'pod-c', - }); - }); - - after(async () => { - await cleanup(); - }); - - describe('when no params are specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; - before(async () => { - response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', - params: { - query: { - alert_started_at: new Date(end).toISOString(), - }, - }, - }); - }); - - it('returns no service summary', async () => { - const serviceSummary = response.body.context.find( - ({ key }) => key === 'serviceSummary' - ); - expect(serviceSummary).to.be(undefined); - }); - - it('returns 1 log category', async () => { - const logCategories = response.body.context.find(({ key }) => key === 'logCategories'); - expect( - (logCategories?.data as LogCategories)?.map( - ({ errorCategory }: { errorCategory: string }) => errorCategory - ) - ).to.eql(['Error message from service', 'Error message from container my-container-c']); - }); - }); - - describe('when service name is specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; - before(async () => { - response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', - params: { - query: { - alert_started_at: new Date(end).toISOString(), - 'service.name': 'Backend', - }, - }, - }); - }); - - it('returns log categories', () => { - const logCategories = response.body.context.find(({ key }) => key === 'logCategories'); - expect(logCategories?.data).to.have.length(1); - - const logCategory = (logCategories?.data as LogCategories)?.[0]; - expect(logCategory?.sampleMessage).to.match( - /Error message #\d{16} from service Backend/ - ); - expect(logCategory?.docCount).to.be.greaterThan(0); - expect(logCategory?.errorCategory).to.be('Error message from service Backend'); - }); - }); - - describe('when container id is specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; - before(async () => { - response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', - params: { - query: { - alert_started_at: new Date(end).toISOString(), - 'container.id': 'my-container-a', - }, - }, - }); - }); - - it('returns log categories', () => { - const logCategories = response.body.context.find(({ key }) => key === 'logCategories'); - expect(logCategories?.data).to.have.length(1); - - const logCategory = (logCategories?.data as LogCategories)?.[0]; - expect(logCategory?.sampleMessage).to.match( - /Error message #\d{16} from service Backend/ - ); - expect(logCategory?.docCount).to.be.greaterThan(0); - expect(logCategory?.errorCategory).to.be('Error message from service Backend'); - }); - }); - - describe('when non-existing service.name is specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; - before(async () => { - response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', - params: { - query: { - alert_started_at: new Date(end).toISOString(), - 'service.name': 'non-existing-service', - }, - }, - }); - }); - - it('returns empty service summary', () => { - const serviceSummary = response.body.context.find( - ({ key }) => key === 'serviceSummary' - ); - expect(serviceSummary?.data).to.eql({ - 'service.name': 'non-existing-service', - 'service.environment': [], - instances: 1, - anomalies: [], - alerts: [], - deployments: [], - }); - }); - - it('does not return log categories', () => { - const logCategories = response.body.context.find(({ key }) => key === 'logCategories'); - expect(logCategories?.data).to.have.length(1); - - expect( - (logCategories?.data as LogCategories)?.map( - ({ errorCategory }: { errorCategory: string }) => errorCategory - ) - ).to.eql(['Error message from container my-container-c']); - }); - }); - }); - - async function ingestTraces(eventMetadata: { - 'service.name': string; - 'container.id'?: string; - 'host.name'?: string; - 'kubernetes.pod.name'?: string; - }) { - const serviceInstance = apm - .service({ - name: eventMetadata['service.name'], - environment: 'production', - agentName: 'java', - }) - .instance('my-instance'); - - const events = range - .interval('1m') - .rate(1) - .generator((timestamp) => { - return serviceInstance - .transaction({ transactionName: 'tx' }) - .timestamp(timestamp) - .duration(10000) - .defaults({ 'service.version': '1.0.0', ...eventMetadata }) - .outcome('success') - .children( - serviceInstance - .span({ - spanName: 'GET apm-*/_search', - spanType: 'db', - spanSubtype: 'elasticsearch', - }) - .duration(1000) - .success() - .destination('elasticsearch') - .timestamp(timestamp) - ); - }); - - await apmSynthtraceClient.index(events); - } - - function ingestLogs(eventMetadata: { - 'service.name'?: string; - 'container.id'?: string; - 'kubernetes.pod.name'?: string; - 'host.name'?: string; - }) { - const getMessage = () => { - const msgPrefix = `Error message #${generateShortId()}`; - - if (eventMetadata['service.name']) { - return `${msgPrefix} from service ${eventMetadata['service.name']}`; - } - - if (eventMetadata['container.id']) { - return `${msgPrefix} from container ${eventMetadata['container.id']}`; - } - - if (eventMetadata['kubernetes.pod.name']) { - return `${msgPrefix} from pod ${eventMetadata['kubernetes.pod.name']}`; - } - - if (eventMetadata['host.name']) { - return `${msgPrefix} from host ${eventMetadata['host.name']}`; - } - - return msgPrefix; - }; - - const events = range - .interval('1m') - .rate(1) - .generator((timestamp) => { - return [ - log - .create() - .message(getMessage()) - .logLevel('error') - .defaults({ - 'trace.id': generateShortId(), - 'agent.name': 'synth-agent', - ...eventMetadata, - }) - .timestamp(timestamp), - ]; - }); - - return logSynthtraceClient.index(events); - } - - async function cleanup() { - await apmSynthtraceClient.clean(); - await logSynthtraceClient.clean(); - } - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/cold_start/cold_start.spec.ts b/x-pack/test/apm_api_integration/tests/cold_start/cold_start.spec.ts index 5d4eb4e1585c08..7d0b2f6d8a6252 100644 --- a/x-pack/test/apm_api_integration/tests/cold_start/cold_start.spec.ts +++ b/x-pack/test/apm_api_integration/tests/cold_start/cold_start.spec.ts @@ -22,7 +22,7 @@ type ColdStartRate = export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const { serviceName } = dataConfig; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -74,7 +74,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { before(async () => { await generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, coldStartRate: 10, @@ -85,7 +85,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { status = response.status; }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns correct HTTP status', () => { expect(status).to.be(200); @@ -121,14 +121,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { const comparisonEndDate = moment(start).add(3, 'minutes'); await generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start: startDate.valueOf(), end: endDate.valueOf(), coldStartRate: 10, warmStartRate: 30, }); await generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start: comparisonStartDate.getTime(), end: comparisonEndDate.valueOf(), coldStartRate: 20, @@ -146,7 +146,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { status = response.status; }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns correct HTTP status', () => { expect(status).to.be(200); diff --git a/x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/cold_start_by_transaction_name.spec.ts b/x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/cold_start_by_transaction_name.spec.ts index 4078aaa08e793d..7019735a05498f 100644 --- a/x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/cold_start_by_transaction_name.spec.ts +++ b/x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/cold_start_by_transaction_name.spec.ts @@ -22,7 +22,7 @@ type ColdStartRate = export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const { serviceName, transactionName } = dataConfig; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -79,7 +79,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { before(async () => { await generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, coldStartRate: 10, @@ -90,7 +90,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { status = response.status; }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns correct HTTP status', () => { expect(status).to.be(200); @@ -126,14 +126,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { const comparisonEndDate = moment(start).add(3, 'minutes'); await generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start: startDate.valueOf(), end: endDate.valueOf(), coldStartRate: 10, warmStartRate: 30, }); await generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start: comparisonStartDate.getTime(), end: comparisonEndDate.valueOf(), coldStartRate: 20, @@ -151,7 +151,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { status = response.status; }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns correct HTTP status', () => { expect(status).to.be(200); diff --git a/x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/generate_data.ts b/x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/generate_data.ts index 407e115d473b7e..ff4d725a3d0177 100644 --- a/x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/generate_data.ts @@ -14,13 +14,13 @@ export const dataConfig = { }; export async function generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, coldStartRate, warmStartRate, }: { - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; start: number; end: number; coldStartRate: number; @@ -60,5 +60,5 @@ export async function generateData({ ), ]; - await synthtraceEsClient.index(traceEvents); + await apmSynthtraceEsClient.index(traceEvents); } diff --git a/x-pack/test/apm_api_integration/tests/cold_start/generate_data.ts b/x-pack/test/apm_api_integration/tests/cold_start/generate_data.ts index ecbba07e3b498e..9c36e53e826320 100644 --- a/x-pack/test/apm_api_integration/tests/cold_start/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/cold_start/generate_data.ts @@ -20,13 +20,13 @@ export const dataConfig = { }; export async function generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, coldStartRate, warmStartRate, }: { - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; start: number; end: number; coldStartRate: number; @@ -66,5 +66,5 @@ export async function generateData({ ), ]; - await synthtraceEsClient.index(traceEvents); + await apmSynthtraceEsClient.index(traceEvents); } diff --git a/x-pack/test/apm_api_integration/tests/custom_dashboards/custom_dashboards.spec.ts b/x-pack/test/apm_api_integration/tests/custom_dashboards/custom_dashboards.spec.ts index 3dcd39df10bb86..46903bf1b5e6b4 100644 --- a/x-pack/test/apm_api_integration/tests/custom_dashboards/custom_dashboards.spec.ts +++ b/x-pack/test/apm_api_integration/tests/custom_dashboards/custom_dashboards.spec.ts @@ -17,7 +17,7 @@ import { export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtrace = getService('synthtraceEsClient'); + const synthtrace = getService('apmSynthtraceEsClient'); const start = '2023-08-22T00:00:00.000Z'; const end = '2023-08-22T00:15:00.000Z'; diff --git a/x-pack/test/apm_api_integration/tests/data_view/static.spec.ts b/x-pack/test/apm_api_integration/tests/data_view/static.spec.ts index dbe7db5830f933..56b310f8f2fe63 100644 --- a/x-pack/test/apm_api_integration/tests/data_view/static.spec.ts +++ b/x-pack/test/apm_api_integration/tests/data_view/static.spec.ts @@ -19,7 +19,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); const supertest = getService('supertest'); - const synthtrace = getService('synthtraceEsClient'); + const synthtrace = getService('apmSynthtraceEsClient'); const logger = getService('log'); const dataViewPattern = 'traces-apm*,apm-*,logs-apm*,apm-*,metrics-apm*,apm-*'; diff --git a/x-pack/test/apm_api_integration/tests/dependencies/dependency_metrics.spec.ts b/x-pack/test/apm_api_integration/tests/dependencies/dependency_metrics.spec.ts index 40b0120a3d96fc..c9cb6aea713063 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/dependency_metrics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/dependencies/dependency_metrics.spec.ts @@ -27,7 +27,7 @@ const { export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -98,7 +98,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('Dependency metrics when data is loaded', { config: 'basic', archives: [] }, () => { before(async () => { await generateOperationData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, }); @@ -299,6 +299,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); }); } diff --git a/x-pack/test/apm_api_integration/tests/dependencies/generate_data.ts b/x-pack/test/apm_api_integration/tests/dependencies/generate_data.ts index fad21e46f60701..58b708f0ab2538 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/dependencies/generate_data.ts @@ -22,11 +22,11 @@ export const dataConfig = { }; export async function generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, }: { - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; start: number; end: number; }) { @@ -35,7 +35,7 @@ export async function generateData({ .instance('instance-a'); const { rate, transaction, span } = dataConfig; - await synthtraceEsClient.index( + await apmSynthtraceEsClient.index( timerange(start, end) .interval('1m') .rate(rate) diff --git a/x-pack/test/apm_api_integration/tests/dependencies/generate_operation_data.ts b/x-pack/test/apm_api_integration/tests/dependencies/generate_operation_data.ts index d68183960ca5a6..97d7c35e23733c 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/generate_operation_data.ts +++ b/x-pack/test/apm_api_integration/tests/dependencies/generate_operation_data.ts @@ -21,11 +21,11 @@ export const generateOperationDataConfig = { export async function generateOperationData({ start, end, - synthtraceEsClient, + apmSynthtraceEsClient, }: { start: number; end: number; - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; }) { const synthGoInstance = apm .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) @@ -36,7 +36,7 @@ export async function generateOperationData({ const interval = timerange(start, end).interval('1m'); - return await synthtraceEsClient.index([ + return await apmSynthtraceEsClient.index([ interval .rate(generateOperationDataConfig.ES_SEARCH_UNKNOWN_RATE) .generator((timestamp) => diff --git a/x-pack/test/apm_api_integration/tests/dependencies/metadata.spec.ts b/x-pack/test/apm_api_integration/tests/dependencies/metadata.spec.ts index 62fb3c79588452..33b81c85991115 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/metadata.spec.ts +++ b/x-pack/test/apm_api_integration/tests/dependencies/metadata.spec.ts @@ -11,7 +11,7 @@ import { dataConfig, generateData } from './generate_data'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -47,10 +47,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { 'Dependency metadata when data is generated', { config: 'basic', archives: [] }, () => { - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns correct metadata for the dependency', async () => { - await generateData({ synthtraceEsClient, start, end }); + await generateData({ apmSynthtraceEsClient, start, end }); const { status, body } = await callApi(); const { span } = dataConfig; @@ -59,7 +59,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(body.metadata.spanType).to.equal(span.type); expect(body.metadata.spanSubtype).to.equal(span.subType); - await synthtraceEsClient.clean(); + await apmSynthtraceEsClient.clean(); }); } ); diff --git a/x-pack/test/apm_api_integration/tests/dependencies/service_dependencies.spec.ts b/x-pack/test/apm_api_integration/tests/dependencies/service_dependencies.spec.ts index 3ac094dc9e540d..ab9563169fe9bf 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/service_dependencies.spec.ts +++ b/x-pack/test/apm_api_integration/tests/dependencies/service_dependencies.spec.ts @@ -11,7 +11,7 @@ import { generateData } from './generate_data'; export default function ApiTest({ getService }: FtrProviderContext) { const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const registry = getService('registry'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -51,9 +51,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('Dependency for services', { config: 'basic', archives: [] }, () => { describe('when data is loaded', () => { before(async () => { - await generateData({ synthtraceEsClient, start, end }); + await generateData({ apmSynthtraceEsClient, start, end }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns a list of dependencies for a service', async () => { const { status, body } = await callApi(); @@ -89,9 +89,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('Dependency for services breakdown', { config: 'basic', archives: [] }, () => { describe('when data is loaded', () => { before(async () => { - await generateData({ synthtraceEsClient, start, end }); + await generateData({ apmSynthtraceEsClient, start, end }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns a list of dependencies for a service', async () => { const { status, body } = await callApi(); diff --git a/x-pack/test/apm_api_integration/tests/dependencies/top_dependencies.spec.ts b/x-pack/test/apm_api_integration/tests/dependencies/top_dependencies.spec.ts index d08b96ab2e57f8..acdea5a3d54de0 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/top_dependencies.spec.ts +++ b/x-pack/test/apm_api_integration/tests/dependencies/top_dependencies.spec.ts @@ -16,7 +16,7 @@ type TopDependencies = APIReturnType<'GET /internal/apm/dependencies/top_depende export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -55,12 +55,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { let topDependencies: TopDependencies; before(async () => { - await generateData({ synthtraceEsClient, start, end }); + await generateData({ apmSynthtraceEsClient, start, end }); const response = await callApi(); topDependencies = response.body; }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns an array of dependencies', () => { expect(topDependencies).to.have.property('dependencies'); diff --git a/x-pack/test/apm_api_integration/tests/dependencies/top_operations.spec.ts b/x-pack/test/apm_api_integration/tests/dependencies/top_operations.spec.ts index 353c32a263eb15..20a38369bcd5da 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/top_operations.spec.ts +++ b/x-pack/test/apm_api_integration/tests/dependencies/top_operations.spec.ts @@ -30,7 +30,7 @@ const { export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -74,13 +74,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('Top operations when data is generated', { config: 'basic', archives: [] }, () => { before(() => generateOperationData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, }) ); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('requested for elasticsearch', () => { let response: TopOperations; diff --git a/x-pack/test/apm_api_integration/tests/dependencies/top_spans.spec.ts b/x-pack/test/apm_api_integration/tests/dependencies/top_spans.spec.ts index 1f6cae4f8393b8..b07c7c323ed9c0 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/top_spans.spec.ts +++ b/x-pack/test/apm_api_integration/tests/dependencies/top_spans.spec.ts @@ -13,7 +13,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -80,7 +80,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .instance('instance-a'); before(async () => { - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('1m') .rate(1) @@ -239,7 +239,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); } ); } diff --git a/x-pack/test/apm_api_integration/tests/dependencies/upstream_services.spec.ts b/x-pack/test/apm_api_integration/tests/dependencies/upstream_services.spec.ts index 7a44acaf5f9e92..1a7e958881d96e 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/upstream_services.spec.ts +++ b/x-pack/test/apm_api_integration/tests/dependencies/upstream_services.spec.ts @@ -11,7 +11,7 @@ import { generateData } from './generate_data'; export default function ApiTest({ getService }: FtrProviderContext) { const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const registry = getService('registry'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -51,9 +51,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('Dependency upstream services', { config: 'basic', archives: [] }, () => { describe('when data is loaded', () => { before(async () => { - await generateData({ synthtraceEsClient, start, end }); + await generateData({ apmSynthtraceEsClient, start, end }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns a list of upstream services for the dependency', async () => { const { status, body } = await callApi(); diff --git a/x-pack/test/apm_api_integration/tests/diagnostics/apm_events.spec.ts b/x-pack/test/apm_api_integration/tests/diagnostics/apm_events.spec.ts index d30cc24e801796..870f339ddf6029 100644 --- a/x-pack/test/apm_api_integration/tests/diagnostics/apm_events.spec.ts +++ b/x-pack/test/apm_api_integration/tests/diagnostics/apm_events.spec.ts @@ -15,7 +15,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); const es = getService('es'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -43,7 +43,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) .instance('instance-a'); - await synthtraceEsClient.index( + await apmSynthtraceEsClient.index( timerange(start, end) .interval('1m') .rate(30) @@ -57,7 +57,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns zero doc_counts when no time range is specified', async () => { const { body } = await apmApiClient.readUser({ diff --git a/x-pack/test/apm_api_integration/tests/diagnostics/data_streams.spec.ts b/x-pack/test/apm_api_integration/tests/diagnostics/data_streams.spec.ts index cd94fe5d604182..969ce9fabd5a6f 100644 --- a/x-pack/test/apm_api_integration/tests/diagnostics/data_streams.spec.ts +++ b/x-pack/test/apm_api_integration/tests/diagnostics/data_streams.spec.ts @@ -13,7 +13,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); const es = getService('es'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const synthtraceKibanaClient = getService('synthtraceKibanaClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -53,7 +53,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) .instance('instance-a'); - await synthtraceEsClient.index( + await apmSynthtraceEsClient.index( timerange(start, end) .interval('1m') .rate(30) @@ -67,7 +67,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns 5 data streams', async () => { const { status, body } = await apmApiClient.adminUser({ diff --git a/x-pack/test/apm_api_integration/tests/diagnostics/index_pattern_settings.ts b/x-pack/test/apm_api_integration/tests/diagnostics/index_pattern_settings.ts index 675145ab8673b8..64749066a2e04d 100644 --- a/x-pack/test/apm_api_integration/tests/diagnostics/index_pattern_settings.ts +++ b/x-pack/test/apm_api_integration/tests/diagnostics/index_pattern_settings.ts @@ -14,7 +14,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); const es = getService('es'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const synthtraceKibanaClient = getService('synthtraceKibanaClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -49,7 +49,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const instance = apm .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) .instance('instance-a'); - await synthtraceEsClient.index( + await apmSynthtraceEsClient.index( timerange(start, end) .interval('1m') .rate(30) @@ -63,7 +63,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns APM index templates', async () => { const { status, body } = await apmApiClient.adminUser({ diff --git a/x-pack/test/apm_api_integration/tests/diagnostics/index_templates.spec.ts b/x-pack/test/apm_api_integration/tests/diagnostics/index_templates.spec.ts index fcde133347ee3a..2ece2835b59444 100644 --- a/x-pack/test/apm_api_integration/tests/diagnostics/index_templates.spec.ts +++ b/x-pack/test/apm_api_integration/tests/diagnostics/index_templates.spec.ts @@ -14,7 +14,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); const es = getService('es'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const synthtraceKibanaClient = getService('synthtraceKibanaClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -47,7 +47,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const instance = apm .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) .instance('instance-a'); - await synthtraceEsClient.index( + await apmSynthtraceEsClient.index( timerange(start, end) .interval('1m') .rate(30) @@ -61,7 +61,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('verifies that all the default APM index templates exist', async () => { const { status, body } = await apmApiClient.adminUser({ diff --git a/x-pack/test/apm_api_integration/tests/diagnostics/indices.spec.ts b/x-pack/test/apm_api_integration/tests/diagnostics/indices.spec.ts index 4f7507e74e2f3e..477824524b48c0 100644 --- a/x-pack/test/apm_api_integration/tests/diagnostics/indices.spec.ts +++ b/x-pack/test/apm_api_integration/tests/diagnostics/indices.spec.ts @@ -13,7 +13,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const es = getService('es'); const synthtraceKibanaClient = getService('synthtraceKibanaClient'); @@ -40,7 +40,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) .instance('instance-a'); - await synthtraceEsClient.index( + await apmSynthtraceEsClient.index( timerange(start, end) .interval('1m') .rate(30) @@ -54,7 +54,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns empty response', async () => { const { status, body } = await apmApiClient.adminUser({ @@ -76,7 +76,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) .instance('instance-a'); - await synthtraceEsClient.index( + await apmSynthtraceEsClient.index( timerange(start, end) .interval('1m') .rate(30) @@ -94,7 +94,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { await es.indices.delete({ index: 'traces-apm-default' }); const latestVersion = await synthtraceKibanaClient.fetchLatestApmPackageVersion(); await synthtraceKibanaClient.installApmPackage(latestVersion); - await synthtraceEsClient.clean(); + await apmSynthtraceEsClient.clean(); }); it('returns a list of items with mapping issues', async () => { @@ -121,7 +121,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) .instance('instance-a'); - await synthtraceEsClient.index( + await apmSynthtraceEsClient.index( timerange(start, end) .interval('1m') .rate(30) @@ -138,7 +138,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { after(async () => { const latestVersion = await synthtraceKibanaClient.fetchLatestApmPackageVersion(); await synthtraceKibanaClient.installApmPackage(latestVersion); - await synthtraceEsClient.clean(); + await apmSynthtraceEsClient.clean(); }); describe.skip('an ingest pipeline is removed', () => { diff --git a/x-pack/test/apm_api_integration/tests/environment/generate_data.ts b/x-pack/test/apm_api_integration/tests/environment/generate_data.ts index 6e9ae2831e6be9..af71533bf3e0d5 100644 --- a/x-pack/test/apm_api_integration/tests/environment/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/environment/generate_data.ts @@ -10,11 +10,11 @@ import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; // Generate synthetic data for the environment test suite export async function generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, }: { - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; start: number; end: number; }) { @@ -63,5 +63,5 @@ export async function generateData({ return [...loopGeneratedDocs, customDoc]; }); - await synthtraceEsClient.index(docs); + await apmSynthtraceEsClient.index(docs); } diff --git a/x-pack/test/apm_api_integration/tests/environment/get_environment.spec.ts b/x-pack/test/apm_api_integration/tests/environment/get_environment.spec.ts index bdb24342c8653e..8a717e735b0f23 100644 --- a/x-pack/test/apm_api_integration/tests/environment/get_environment.spec.ts +++ b/x-pack/test/apm_api_integration/tests/environment/get_environment.spec.ts @@ -17,19 +17,19 @@ const end = new Date(endNumber).toISOString(); export default function environmentsAPITests({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); // FLAKY: https://github.com/elastic/kibana/issues/177305 registry.when('environments when data is loaded', { config: 'basic', archives: [] }, async () => { before(async () => { await generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start: startNumber, end: endNumber, }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('get environments', () => { describe('when service name is not specified', () => { diff --git a/x-pack/test/apm_api_integration/tests/error_rate/service_apis.spec.ts b/x-pack/test/apm_api_integration/tests/error_rate/service_apis.spec.ts index 9a0ba400e13da6..2ab6b1bb97a5ed 100644 --- a/x-pack/test/apm_api_integration/tests/error_rate/service_apis.spec.ts +++ b/x-pack/test/apm_api_integration/tests/error_rate/service_apis.spec.ts @@ -17,7 +17,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -166,7 +166,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const transactionNameProductList = 'GET /api/product/list'; const transactionNameProductId = 'GET /api/product/:id'; - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('1m') .rate(GO_PROD_LIST_RATE) @@ -210,7 +210,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('compare error rate value between service inventory, error rate chart, service inventory and transactions apis', () => { before(async () => { diff --git a/x-pack/test/apm_api_integration/tests/error_rate/service_maps.spec.ts b/x-pack/test/apm_api_integration/tests/error_rate/service_maps.spec.ts index fa4b3f7074352b..aa7d635b977cc9 100644 --- a/x-pack/test/apm_api_integration/tests/error_rate/service_maps.spec.ts +++ b/x-pack/test/apm_api_integration/tests/error_rate/service_maps.spec.ts @@ -15,7 +15,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -88,7 +88,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const transactionNameProductList = 'GET /api/product/list'; const transactionNameProductId = 'GET /api/product/:id'; - return synthtraceEsClient.index([ + return apmSynthtraceEsClient.index([ timerange(start, end) .interval('1m') .rate(GO_PROD_LIST_RATE) @@ -138,7 +138,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); // FLAKY: https://github.com/elastic/kibana/issues/172772 describe('compare latency value between service inventory and service maps', () => { diff --git a/x-pack/test/apm_api_integration/tests/errors/distribution.spec.ts b/x-pack/test/apm_api_integration/tests/errors/distribution.spec.ts index 2b30dca5db80a1..544ca97817af04 100644 --- a/x-pack/test/apm_api_integration/tests/errors/distribution.spec.ts +++ b/x-pack/test/apm_api_integration/tests/errors/distribution.spec.ts @@ -21,7 +21,7 @@ type ErrorsDistribution = export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -65,10 +65,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { describe('errors distribution', () => { const { appleTransaction, bananaTransaction } = config; before(async () => { - await generateData({ serviceName, start, end, synthtraceEsClient }); + await generateData({ serviceName, start, end, apmSynthtraceEsClient }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('without comparison', () => { let errorsDistribution: ErrorsDistribution; diff --git a/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts b/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts index bac97c76a3f362..fd01833cb4f502 100644 --- a/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts +++ b/x-pack/test/apm_api_integration/tests/errors/error_group_list.spec.ts @@ -19,7 +19,7 @@ type ErrorGroups = export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -73,7 +73,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .service({ name: serviceName, environment: 'production', agentName: 'go' }) .instance('instance-a'); - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('1m') .rate(appleTransaction.successRate) @@ -123,7 +123,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('returns the correct data', () => { let errorGroups: ErrorGroups; diff --git a/x-pack/test/apm_api_integration/tests/errors/generate_data.ts b/x-pack/test/apm_api_integration/tests/errors/generate_data.ts index 1ecc0f02e3f39e..a7e627a048e053 100644 --- a/x-pack/test/apm_api_integration/tests/errors/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/errors/generate_data.ts @@ -21,12 +21,12 @@ export const config = { }; export async function generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, serviceName, start, end, }: { - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; serviceName: string; start: number; end: number; @@ -69,5 +69,5 @@ export async function generateData({ ]; }); - await synthtraceEsClient.index(documents); + await apmSynthtraceEsClient.index(documents); } diff --git a/x-pack/test/apm_api_integration/tests/errors/group_id_samples.spec.ts b/x-pack/test/apm_api_integration/tests/errors/group_id_samples.spec.ts index 165fb81e3c643d..004f853b6c56af 100644 --- a/x-pack/test/apm_api_integration/tests/errors/group_id_samples.spec.ts +++ b/x-pack/test/apm_api_integration/tests/errors/group_id_samples.spec.ts @@ -22,7 +22,7 @@ type ErrorSampleDetails = export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -80,10 +80,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { const { bananaTransaction } = config; describe('error group id', () => { before(async () => { - await generateData({ serviceName, start, end, synthtraceEsClient }); + await generateData({ serviceName, start, end, apmSynthtraceEsClient }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('return correct data', () => { let errorsSamplesResponse: ErrorGroupSamples; @@ -108,10 +108,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('when error sample data is loaded', { config: 'basic', archives: [] }, () => { describe('error sample id', () => { before(async () => { - await generateData({ serviceName, start, end, synthtraceEsClient }); + await generateData({ serviceName, start, end, apmSynthtraceEsClient }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('return correct data', () => { let errorSampleDetailsResponse: ErrorSampleDetails; @@ -146,7 +146,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const errorMessage = 'Error 1'; const groupId = getErrorGroupingKey(errorMessage); - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('15m') .rate(1) @@ -174,7 +174,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { errorGroupSamplesResponse = (await callErrorGroupSamplesApi({ groupId })).body; }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns the errors in the correct order (sampled first, then unsampled)', () => { const idsOfErrors = errorGroupSamplesResponse.errorSampleIds.map((id) => parseInt(id, 10)); diff --git a/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/generate_data.ts b/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/generate_data.ts index 47c48693eaa0e4..7732d85efa58ff 100644 --- a/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/generate_data.ts @@ -21,12 +21,12 @@ export const config = { }; export async function generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, serviceName, start, end, }: { - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; serviceName: string; start: number; end: number; @@ -69,5 +69,5 @@ export async function generateData({ ]; }); - await synthtraceEsClient.index(documents); + await apmSynthtraceEsClient.index(documents); } diff --git a/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/top_erroneous_transactions.spec.ts b/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/top_erroneous_transactions.spec.ts index e22099af5cbbe5..ff985e0af388fa 100644 --- a/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/top_erroneous_transactions.spec.ts +++ b/x-pack/test/apm_api_integration/tests/errors/top_erroneous_transactions/top_erroneous_transactions.spec.ts @@ -21,7 +21,7 @@ type ErroneousTransactions = export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -73,10 +73,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { describe('returns the correct data', () => { before(async () => { - await generateData({ serviceName, start, end, synthtraceEsClient }); + await generateData({ serviceName, start, end, apmSynthtraceEsClient }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('without comparison', () => { const numberOfBuckets = 15; diff --git a/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/generate_data.ts b/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/generate_data.ts index 6026f762292c3d..9f983fbb8877be 100644 --- a/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/generate_data.ts @@ -21,12 +21,12 @@ export const config = { }; export async function generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, serviceName, start, end, }: { - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; serviceName: string; start: number; end: number; @@ -72,5 +72,5 @@ export async function generateData({ ]; }); - await synthtraceEsClient.index(documents); + await apmSynthtraceEsClient.index(documents); } diff --git a/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/top_errors_main_stats.spec.ts b/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/top_errors_main_stats.spec.ts index 9bb171ff64f308..8e946e081554fc 100644 --- a/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/top_errors_main_stats.spec.ts +++ b/x-pack/test/apm_api_integration/tests/errors/top_errors_for_transaction/top_errors_main_stats.spec.ts @@ -20,7 +20,7 @@ type ErrorGroups = export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -66,10 +66,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { } = config; before(async () => { - await generateData({ serviceName, start, end, synthtraceEsClient }); + await generateData({ serviceName, start, end, apmSynthtraceEsClient }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('returns the correct data', () => { const NUMBER_OF_BUCKETS = 15; diff --git a/x-pack/test/apm_api_integration/tests/fleet/input_only_package.spec.ts b/x-pack/test/apm_api_integration/tests/fleet/input_only_package.spec.ts index 5d02209ca0aa6e..b7d90ff9713cef 100644 --- a/x-pack/test/apm_api_integration/tests/fleet/input_only_package.spec.ts +++ b/x-pack/test/apm_api_integration/tests/fleet/input_only_package.spec.ts @@ -33,7 +33,7 @@ export default function ApiTest(ftrProviderContext: FtrProviderContext) { const bettertest = getBettertest(supertest); const config = getService('config'); const synthtraceKibanaClient = getService('synthtraceKibanaClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const API_KEY_NAME = 'apm_api_key_testing'; const APM_AGENT_POLICY_NAME = 'apm_agent_policy_testing'; @@ -104,7 +104,7 @@ export default function ApiTest(ftrProviderContext: FtrProviderContext) { async function cleanAll() { try { - await synthtraceEsClient.clean(); + await apmSynthtraceEsClient.clean(); await es.security.invalidateApiKey({ name: API_KEY_NAME }); await deleteAgentPolicyAndPackagePolicyByName({ bettertest, diff --git a/x-pack/test/apm_api_integration/tests/historical_data/has_data.spec.ts b/x-pack/test/apm_api_integration/tests/historical_data/has_data.spec.ts index 440fe7fd39b3b1..e0b5a0e076ffd4 100644 --- a/x-pack/test/apm_api_integration/tests/historical_data/has_data.spec.ts +++ b/x-pack/test/apm_api_integration/tests/historical_data/has_data.spec.ts @@ -13,7 +13,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); // FLAKY: https://github.com/elastic/kibana/issues/177385 registry.when('Historical data ', { config: 'basic', archives: [] }, () => { @@ -45,10 +45,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { ), ]; - await synthtraceEsClient.index(documents); + await apmSynthtraceEsClient.index(documents); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns hasData=true', async () => { const response = await apmApiClient.readUser({ endpoint: `GET /internal/apm/has_data` }); diff --git a/x-pack/test/apm_api_integration/tests/infrastructure/generate_data.ts b/x-pack/test/apm_api_integration/tests/infrastructure/generate_data.ts index eb9a34f6e1f631..437c4791e19723 100644 --- a/x-pack/test/apm_api_integration/tests/infrastructure/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/infrastructure/generate_data.ts @@ -8,11 +8,11 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; export async function generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, }: { - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; start: number; end: number; }) { @@ -24,7 +24,7 @@ export async function generateData({ .service({ name: 'synth-java', environment: 'production', agentName: 'java' }) .instance('instance-b'); - await synthtraceEsClient.index( + await apmSynthtraceEsClient.index( timerange(start, end) .interval('1m') .generator((timestamp) => { diff --git a/x-pack/test/apm_api_integration/tests/infrastructure/infrastructure_attributes.spec.ts b/x-pack/test/apm_api_integration/tests/infrastructure/infrastructure_attributes.spec.ts index e1b0d3c66cbaae..7a79e2f8be4b10 100644 --- a/x-pack/test/apm_api_integration/tests/infrastructure/infrastructure_attributes.spec.ts +++ b/x-pack/test/apm_api_integration/tests/infrastructure/infrastructure_attributes.spec.ts @@ -11,7 +11,7 @@ import { generateData } from './generate_data'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -52,10 +52,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('Infrastructure attributes', { config: 'basic', archives: [] }, () => { describe('when data is loaded', () => { beforeEach(async () => { - await generateData({ start, end, synthtraceEsClient }); + await generateData({ start, end, apmSynthtraceEsClient }); }); - afterEach(() => synthtraceEsClient.clean()); + afterEach(() => apmSynthtraceEsClient.clean()); describe('when service runs in container', () => { it('returns arrays of container ids and pod names', async () => { diff --git a/x-pack/test/apm_api_integration/tests/latency/service_apis.spec.ts b/x-pack/test/apm_api_integration/tests/latency/service_apis.spec.ts index 397c2f107ccd9d..35ef23c8a44303 100644 --- a/x-pack/test/apm_api_integration/tests/latency/service_apis.spec.ts +++ b/x-pack/test/apm_api_integration/tests/latency/service_apis.spec.ts @@ -17,7 +17,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -170,7 +170,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .service({ name: serviceName, environment: 'development', agentName: 'go' }) .instance('instance-b'); - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('1m') .rate(GO_PROD_RATE) @@ -192,7 +192,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('compare latency value between service inventory, latency chart, service inventory and transactions apis', () => { before(async () => { diff --git a/x-pack/test/apm_api_integration/tests/latency/service_maps.spec.ts b/x-pack/test/apm_api_integration/tests/latency/service_maps.spec.ts index 6796e448d216d2..298fde675bc4ac 100644 --- a/x-pack/test/apm_api_integration/tests/latency/service_maps.spec.ts +++ b/x-pack/test/apm_api_integration/tests/latency/service_maps.spec.ts @@ -15,7 +15,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -87,7 +87,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .service({ name: serviceName, environment: 'development', agentName: 'go' }) .instance('instance-b'); - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('1m') .rate(GO_PROD_RATE) @@ -112,7 +112,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); // FLAKY: https://github.com/elastic/kibana/issues/176976 describe('compare latency value between service inventory and service maps', () => { diff --git a/x-pack/test/apm_api_integration/tests/metrics/memory/generate_data.ts b/x-pack/test/apm_api_integration/tests/metrics/memory/generate_data.ts index 22abd719892064..c5bc09bc6f67b8 100644 --- a/x-pack/test/apm_api_integration/tests/metrics/memory/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/metrics/memory/generate_data.ts @@ -22,11 +22,11 @@ export const expectedValues = { }; export async function generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, }: { - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; start: number; end: number; }) { @@ -73,5 +73,5 @@ export async function generateData({ .timestamp(timestamp), ]); - await synthtraceEsClient.index(transactionsEvents); + await apmSynthtraceEsClient.index(transactionsEvents); } diff --git a/x-pack/test/apm_api_integration/tests/metrics/memory/memory_metrics.spec.ts b/x-pack/test/apm_api_integration/tests/metrics/memory/memory_metrics.spec.ts index 5af51dff5604cd..e0f8fc1cf28c28 100644 --- a/x-pack/test/apm_api_integration/tests/metrics/memory/memory_metrics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/metrics/memory/memory_metrics.spec.ts @@ -12,7 +12,7 @@ import { config, generateData } from './generate_data'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2023-01-01T00:00:00.000Z').getTime(); const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1; @@ -36,10 +36,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { // FLAKY: https://github.com/elastic/kibana/issues/176990 registry.when('Memory', { config: 'trial', archives: [] }, () => { before(async () => { - await generateData({ start, end, synthtraceEsClient }); + await generateData({ start, end, apmSynthtraceEsClient }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns system memory stats', async () => { const expectedFreeMemory = 1 - config.memoryFree / config.memoryTotal; diff --git a/x-pack/test/apm_api_integration/tests/metrics/serverless/generate_data.ts b/x-pack/test/apm_api_integration/tests/metrics/serverless/generate_data.ts index e6a7cc57f06985..b835201d51564f 100644 --- a/x-pack/test/apm_api_integration/tests/metrics/serverless/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/metrics/serverless/generate_data.ts @@ -26,11 +26,11 @@ export const expectedValues = { }; export async function generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, }: { - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; start: number; end: number; }) { @@ -116,5 +116,5 @@ export async function generateData({ .success(), ]); - await synthtraceEsClient.index(transactionsEvents); + await apmSynthtraceEsClient.index(transactionsEvents); } diff --git a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_active_instances.spec.ts b/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_active_instances.spec.ts index cbb6317c3bd756..1b15e03c919872 100644 --- a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_active_instances.spec.ts +++ b/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_active_instances.spec.ts @@ -14,7 +14,7 @@ import { config, expectedValues, generateData } from './generate_data'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -49,10 +49,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { const { expectedMemoryUsed } = expectedValues; before(async () => { - await generateData({ start, end, synthtraceEsClient }); + await generateData({ start, end, apmSynthtraceEsClient }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('Python service', () => { let activeInstances: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/active_instances'>; diff --git a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_functions_overview.spec.ts b/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_functions_overview.spec.ts index 133953dd9772fa..94792228a2859b 100644 --- a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_functions_overview.spec.ts +++ b/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_functions_overview.spec.ts @@ -13,7 +13,7 @@ import { config, expectedValues, generateData } from './generate_data'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -46,10 +46,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { const { expectedMemoryUsed } = expectedValues; before(async () => { - await generateData({ start, end, synthtraceEsClient }); + await generateData({ start, end, apmSynthtraceEsClient }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('Python service', () => { let functionsOverview: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/functions_overview'>; diff --git a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_metrics_charts.spec.ts b/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_metrics_charts.spec.ts index b94d63e491ffd6..192fb3c87c48b8 100644 --- a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_metrics_charts.spec.ts +++ b/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_metrics_charts.spec.ts @@ -21,7 +21,7 @@ function isNotNullOrZeroCoordinate(coordinate: Coordinate) { export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -77,10 +77,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { } = config; before(async () => { - await generateData({ start, end, synthtraceEsClient }); + await generateData({ start, end, apmSynthtraceEsClient }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('Python service', () => { let serverlessMetrics: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/charts'>; diff --git a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_summary.spec.ts b/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_summary.spec.ts index 0f013ca69776bb..727c7eee1e3cdd 100644 --- a/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_summary.spec.ts +++ b/x-pack/test/apm_api_integration/tests/metrics/serverless/serverless_summary.spec.ts @@ -13,7 +13,7 @@ import { config, expectedValues, generateData } from './generate_data'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -55,10 +55,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { const { expectedMemoryUsedRate } = expectedValues; before(async () => { - await generateData({ start, end, synthtraceEsClient }); + await generateData({ start, end, apmSynthtraceEsClient }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('Python service', () => { let serverlessSummary: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/summary'>; diff --git a/x-pack/test/apm_api_integration/tests/mobile/crashes/crash_group_list.spec.ts b/x-pack/test/apm_api_integration/tests/mobile/crashes/crash_group_list.spec.ts index 05897e7c22cd3b..274199437f188c 100644 --- a/x-pack/test/apm_api_integration/tests/mobile/crashes/crash_group_list.spec.ts +++ b/x-pack/test/apm_api_integration/tests/mobile/crashes/crash_group_list.spec.ts @@ -19,7 +19,7 @@ type ErrorGroups = export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const serviceName = 'synth-swift'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -73,7 +73,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .service({ name: serviceName, environment: 'production', agentName: 'swift' }) .instance('instance-a'); - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('1m') .rate(appleTransaction.successRate) @@ -131,7 +131,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('returns the correct data', () => { let errorGroups: ErrorGroups; diff --git a/x-pack/test/apm_api_integration/tests/mobile/crashes/distribution.spec.ts b/x-pack/test/apm_api_integration/tests/mobile/crashes/distribution.spec.ts index d098d83f7a19ee..aad32f3490d2aa 100644 --- a/x-pack/test/apm_api_integration/tests/mobile/crashes/distribution.spec.ts +++ b/x-pack/test/apm_api_integration/tests/mobile/crashes/distribution.spec.ts @@ -21,7 +21,7 @@ type ErrorsDistribution = export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const serviceName = 'synth-swift'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -65,10 +65,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { describe('errors distribution', () => { const { appleTransaction, bananaTransaction } = config; before(async () => { - await generateData({ serviceName, start, end, synthtraceEsClient }); + await generateData({ serviceName, start, end, apmSynthtraceEsClient }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('without comparison', () => { let errorsDistribution: ErrorsDistribution; diff --git a/x-pack/test/apm_api_integration/tests/mobile/crashes/generate_data.ts b/x-pack/test/apm_api_integration/tests/mobile/crashes/generate_data.ts index 606d97fb9ce04d..1e25cef4d3fd39 100644 --- a/x-pack/test/apm_api_integration/tests/mobile/crashes/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/mobile/crashes/generate_data.ts @@ -21,12 +21,12 @@ export const config = { }; export async function generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, serviceName, start, end, }: { - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; serviceName: string; start: number; end: number; @@ -72,5 +72,5 @@ export async function generateData({ ]; }); - await synthtraceEsClient.index(documents); + await apmSynthtraceEsClient.index(documents); } diff --git a/x-pack/test/apm_api_integration/tests/mobile/errors/generate_data.ts b/x-pack/test/apm_api_integration/tests/mobile/errors/generate_data.ts index 663849f274adbd..c1ae3723358246 100644 --- a/x-pack/test/apm_api_integration/tests/mobile/errors/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/mobile/errors/generate_data.ts @@ -21,12 +21,12 @@ export const config = { }; export async function generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, serviceName, start, end, }: { - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; serviceName: string; start: number; end: number; @@ -69,5 +69,5 @@ export async function generateData({ ]; }); - await synthtraceEsClient.index(documents); + await apmSynthtraceEsClient.index(documents); } diff --git a/x-pack/test/apm_api_integration/tests/mobile/errors/group_id_samples.spec.ts b/x-pack/test/apm_api_integration/tests/mobile/errors/group_id_samples.spec.ts index 29413e79ffd74e..e3e69a540881c3 100644 --- a/x-pack/test/apm_api_integration/tests/mobile/errors/group_id_samples.spec.ts +++ b/x-pack/test/apm_api_integration/tests/mobile/errors/group_id_samples.spec.ts @@ -22,7 +22,7 @@ type ErrorSampleDetails = export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -80,10 +80,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { const { bananaTransaction } = config; describe('error group id', () => { before(async () => { - await generateData({ serviceName, start, end, synthtraceEsClient }); + await generateData({ serviceName, start, end, apmSynthtraceEsClient }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('return correct data', () => { let errorsSamplesResponse: ErrorGroupSamples; @@ -108,10 +108,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('when error sample data is loaded', { config: 'basic', archives: [] }, () => { describe('error sample id', () => { before(async () => { - await generateData({ serviceName, start, end, synthtraceEsClient }); + await generateData({ serviceName, start, end, apmSynthtraceEsClient }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('return correct data', () => { let errorSampleDetailsResponse: ErrorSampleDetails; @@ -146,7 +146,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const errorMessage = 'Error 1'; const groupId = getErrorGroupingKey(errorMessage); - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('15m') .rate(1) @@ -174,7 +174,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { errorGroupSamplesResponse = (await callErrorGroupSamplesApi({ groupId })).body; }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns the errors in the correct order (sampled first, then unsampled)', () => { const idsOfErrors = errorGroupSamplesResponse.errorSampleIds.map((id) => parseInt(id, 10)); diff --git a/x-pack/test/apm_api_integration/tests/mobile/generate_mobile_data.ts b/x-pack/test/apm_api_integration/tests/mobile/generate_mobile_data.ts index 91a8aac9bc3d36..a4420b3f53c7c4 100644 --- a/x-pack/test/apm_api_integration/tests/mobile/generate_mobile_data.ts +++ b/x-pack/test/apm_api_integration/tests/mobile/generate_mobile_data.ts @@ -12,11 +12,11 @@ export const SERVICE_VERSIONS = ['2.3', '1.2', '1.1']; export async function generateMobileData({ start, end, - synthtraceEsClient, + apmSynthtraceEsClient, }: { start: number; end: number; - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; }) { const galaxy10 = apm .mobileApp({ @@ -210,7 +210,7 @@ export async function generateMobileData({ }) .setNetworkConnection({ type: 'wifi' }); - return await synthtraceEsClient.index([ + return await apmSynthtraceEsClient.index([ timerange(start, end) .interval('5m') .rate(1) diff --git a/x-pack/test/apm_api_integration/tests/mobile/mobile_detailed_statistics_by_field.spec.ts b/x-pack/test/apm_api_integration/tests/mobile/mobile_detailed_statistics_by_field.spec.ts index 0e128ba6b102f7..40bf9729bee6ed 100644 --- a/x-pack/test/apm_api_integration/tests/mobile/mobile_detailed_statistics_by_field.spec.ts +++ b/x-pack/test/apm_api_integration/tests/mobile/mobile_detailed_statistics_by_field.spec.ts @@ -19,7 +19,7 @@ type MobileDetailedStatisticsResponse = export default function ApiTest({ getService }: FtrProviderContext) { const apmApiClient = getService('apmApiClient'); const registry = getService('registry'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2023-01-01T00:00:00.000Z').getTime(); const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1; @@ -79,13 +79,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { () => { before(async () => { await generateMobileData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('when comparison is disable', () => { it('returns current period data only', async () => { diff --git a/x-pack/test/apm_api_integration/tests/mobile/mobile_filters.spec.ts b/x-pack/test/apm_api_integration/tests/mobile/mobile_filters.spec.ts index 2dbe88baf179ec..42862668016098 100644 --- a/x-pack/test/apm_api_integration/tests/mobile/mobile_filters.spec.ts +++ b/x-pack/test/apm_api_integration/tests/mobile/mobile_filters.spec.ts @@ -17,11 +17,11 @@ type MobileFilters = APIReturnType<'GET /internal/apm/services/{serviceName}/mob async function generateData({ start, end, - synthtraceEsClient, + apmSynthtraceEsClient, }: { start: number; end: number; - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; }) { const galaxy10 = apm .mobileApp({ @@ -88,7 +88,7 @@ async function generateData({ carrierMCC: '440', }); - return await synthtraceEsClient.index([ + return await apmSynthtraceEsClient.index([ timerange(start, end) .interval('5m') .rate(1) @@ -136,7 +136,7 @@ async function generateData({ export default function ApiTest({ getService }: FtrProviderContext) { const apmApiClient = getService('apmApiClient'); const registry = getService('registry'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2023-01-01T00:00:00.000Z').getTime(); const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1; @@ -181,13 +181,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('Mobile filters', { config: 'basic', archives: [] }, () => { before(async () => { await generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('when data is loaded', () => { let response: MobileFilters; diff --git a/x-pack/test/apm_api_integration/tests/mobile/mobile_http_requests_timeseries.spec.ts b/x-pack/test/apm_api_integration/tests/mobile/mobile_http_requests_timeseries.spec.ts index b924af87d3e207..4c661c9ae14f65 100644 --- a/x-pack/test/apm_api_integration/tests/mobile/mobile_http_requests_timeseries.spec.ts +++ b/x-pack/test/apm_api_integration/tests/mobile/mobile_http_requests_timeseries.spec.ts @@ -13,7 +13,7 @@ import { generateMobileData } from './generate_mobile_data'; export default function ApiTest({ getService }: FtrProviderContext) { const apmApiClient = getService('apmApiClient'); const registry = getService('registry'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2023-01-01T00:00:00.000Z').getTime(); const end = new Date('2023-01-01T02:00:00.000Z').getTime(); @@ -66,13 +66,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('Mobile HTTP requests with data loaded', { config: 'basic', archives: [] }, () => { before(async () => { await generateMobileData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('when data is loaded', () => { it('returns timeseries for http requests chart', async () => { diff --git a/x-pack/test/apm_api_integration/tests/mobile/mobile_location_stats.spec.ts b/x-pack/test/apm_api_integration/tests/mobile/mobile_location_stats.spec.ts index 8e6e7d7cfd52eb..0acf17308b0d83 100644 --- a/x-pack/test/apm_api_integration/tests/mobile/mobile_location_stats.spec.ts +++ b/x-pack/test/apm_api_integration/tests/mobile/mobile_location_stats.spec.ts @@ -22,11 +22,11 @@ type MobileLocationStats = async function generateData({ start, end, - synthtraceEsClient, + apmSynthtraceEsClient, }: { start: number; end: number; - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; }) { const galaxy10 = apm .mobileApp({ @@ -130,7 +130,7 @@ async function generateData({ carrierMCC: '440', }); - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('5m') .rate(1) @@ -179,7 +179,7 @@ async function generateData({ export default function ApiTest({ getService }: FtrProviderContext) { const apmApiClient = getService('apmApiClient'); const registry = getService('registry'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2023-01-01T00:00:00.000Z').getTime(); const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1; @@ -236,13 +236,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('Location stats', { config: 'basic', archives: [] }, () => { before(async () => { await generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('when data is loaded', () => { let response: MobileLocationStats; diff --git a/x-pack/test/apm_api_integration/tests/mobile/mobile_main_statistics_by_field.spec.ts b/x-pack/test/apm_api_integration/tests/mobile/mobile_main_statistics_by_field.spec.ts index 57832797b37984..a3f95eaeb495d8 100644 --- a/x-pack/test/apm_api_integration/tests/mobile/mobile_main_statistics_by_field.spec.ts +++ b/x-pack/test/apm_api_integration/tests/mobile/mobile_main_statistics_by_field.spec.ts @@ -30,11 +30,11 @@ function calculateThroughput({ start, end }: { start: number; end: number }) { async function generateData({ start, end, - synthtraceEsClient, + apmSynthtraceEsClient, }: { start: number; end: number; - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; }) { const galaxy10 = apm .mobileApp({ @@ -101,7 +101,7 @@ async function generateData({ carrierMCC: '440', }); - return await synthtraceEsClient.index([ + return await apmSynthtraceEsClient.index([ timerange(start, end) .interval('5m') .rate(1) @@ -129,7 +129,7 @@ async function generateData({ export default function ApiTest({ getService }: FtrProviderContext) { const apmApiClient = getService('apmApiClient'); const registry = getService('registry'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2023-01-01T00:00:00.000Z').getTime(); const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1; @@ -182,13 +182,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('Mobile main statistics', { config: 'basic', archives: [] }, () => { before(async () => { await generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('when data is loaded', () => { const huaweiLatency = calculateLatency(HUAWEI_DURATION); diff --git a/x-pack/test/apm_api_integration/tests/mobile/mobile_most_used_chart.spec.ts b/x-pack/test/apm_api_integration/tests/mobile/mobile_most_used_chart.spec.ts index b4d595a7283561..497e6987a3e2fb 100644 --- a/x-pack/test/apm_api_integration/tests/mobile/mobile_most_used_chart.spec.ts +++ b/x-pack/test/apm_api_integration/tests/mobile/mobile_most_used_chart.spec.ts @@ -17,7 +17,7 @@ type MostUsedCharts = export default function ApiTest({ getService }: FtrProviderContext) { const apmApiClient = getService('apmApiClient'); const registry = getService('registry'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2023-01-01T00:00:00.000Z').getTime(); const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1; @@ -68,13 +68,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('Mobile stats', { config: 'basic', archives: [] }, () => { before(async () => { await generateMobileData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('when data is loaded', () => { let response: MostUsedCharts; diff --git a/x-pack/test/apm_api_integration/tests/mobile/mobile_sessions_timeseries.spec.ts b/x-pack/test/apm_api_integration/tests/mobile/mobile_sessions_timeseries.spec.ts index d9d70e3ede65f4..99f0f245c8c4cc 100644 --- a/x-pack/test/apm_api_integration/tests/mobile/mobile_sessions_timeseries.spec.ts +++ b/x-pack/test/apm_api_integration/tests/mobile/mobile_sessions_timeseries.spec.ts @@ -13,7 +13,7 @@ import { generateMobileData } from './generate_mobile_data'; export default function ApiTest({ getService }: FtrProviderContext) { const apmApiClient = getService('apmApiClient'); const registry = getService('registry'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2023-01-01T00:00:00.000Z').getTime(); const end = new Date('2023-01-01T02:00:00.000Z').getTime(); @@ -62,13 +62,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('with data loaded', { config: 'basic', archives: [] }, () => { before(async () => { await generateMobileData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('when data is loaded', () => { it('returns timeseries for sessions chart', async () => { diff --git a/x-pack/test/apm_api_integration/tests/mobile/mobile_stats.spec.ts b/x-pack/test/apm_api_integration/tests/mobile/mobile_stats.spec.ts index 8de8fea4a55366..22b7d0c8b8f65b 100644 --- a/x-pack/test/apm_api_integration/tests/mobile/mobile_stats.spec.ts +++ b/x-pack/test/apm_api_integration/tests/mobile/mobile_stats.spec.ts @@ -22,11 +22,11 @@ type MobileStats = APIReturnType<'GET /internal/apm/mobile-services/{serviceName async function generateData({ start, end, - synthtraceEsClient, + apmSynthtraceEsClient, }: { start: number; end: number; - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; }) { const galaxy10 = apm .mobileApp({ @@ -93,7 +93,7 @@ async function generateData({ carrierMCC: '440', }); - return await synthtraceEsClient.index([ + return await apmSynthtraceEsClient.index([ timerange(start, end) .interval('5m') .rate(1) @@ -137,7 +137,7 @@ async function generateData({ export default function ApiTest({ getService }: FtrProviderContext) { const apmApiClient = getService('apmApiClient'); const registry = getService('registry'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2023-01-01T00:00:00.000Z').getTime(); const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1; @@ -188,13 +188,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('Mobile stats', { config: 'basic', archives: [] }, () => { before(async () => { await generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('when data is loaded', () => { let response: MobileStats; diff --git a/x-pack/test/apm_api_integration/tests/mobile/mobile_terms_by_field.spec.ts b/x-pack/test/apm_api_integration/tests/mobile/mobile_terms_by_field.spec.ts index 80f8ed366dd522..d50371423c1668 100644 --- a/x-pack/test/apm_api_integration/tests/mobile/mobile_terms_by_field.spec.ts +++ b/x-pack/test/apm_api_integration/tests/mobile/mobile_terms_by_field.spec.ts @@ -17,11 +17,11 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; async function generateData({ start, end, - synthtraceEsClient, + apmSynthtraceEsClient, }: { start: number; end: number; - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; }) { const galaxy10 = apm .mobileApp({ @@ -88,7 +88,7 @@ async function generateData({ carrierMCC: '440', }); - return await synthtraceEsClient.index([ + return await apmSynthtraceEsClient.index([ timerange(start, end) .interval('5m') .rate(1) @@ -127,7 +127,7 @@ async function generateData({ export default function ApiTest({ getService }: FtrProviderContext) { const apmApiClient = getService('apmApiClient'); const registry = getService('registry'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2023-01-01T00:00:00.000Z').getTime(); const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1; @@ -189,13 +189,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('Mobile terms', { config: 'basic', archives: [] }, () => { before(async () => { await generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('when data is loaded', () => { it('returns mobile devices', async () => { diff --git a/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.spec.ts b/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.spec.ts index a72ca0539c7af1..763d8eee929d28 100644 --- a/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.spec.ts +++ b/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.spec.ts @@ -16,7 +16,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -106,7 +106,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .service({ name: 'synth-java', environment: 'production', agentName: 'java' }) .instance('instance-c'); - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('1m') .rate(GO_PROD_RATE) @@ -137,7 +137,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('compare throughput values', () => { let throughputValues: Awaited>; diff --git a/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/generate_data.ts b/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/generate_data.ts index 292307d7cc1e3a..7f9b1487bb8ef7 100644 --- a/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/generate_data.ts @@ -8,11 +8,11 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; export async function generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, }: { - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; start: number; end: number; }) { @@ -28,7 +28,7 @@ export async function generateData({ .instance('instance-3'), ]; - await synthtraceEsClient.index( + await apmSynthtraceEsClient.index( synthServices.map((service) => timerange(start, end) .interval('5m') diff --git a/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts b/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts index 427edcf0b16142..044006e2733485 100644 --- a/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts @@ -22,7 +22,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); const supertest = getService('supertest'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const es = getService('es'); const log = getService('log'); const start = Date.now() - 24 * 60 * 60 * 1000; @@ -52,7 +52,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { before(async () => { const [, { body: synthbeansServiceGroup }, { body: opbeansServiceGroup }] = await Promise.all( [ - generateData({ start, end, synthtraceEsClient }), + generateData({ start, end, apmSynthtraceEsClient }), createServiceGroupApi({ apmApiClient, groupName: 'synthbeans', @@ -71,7 +71,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { after(async () => { await deleteAllServiceGroups(apmApiClient); - await synthtraceEsClient.clean(); + await apmSynthtraceEsClient.clean(); }); it('returns the correct number of services', async () => { diff --git a/x-pack/test/apm_api_integration/tests/service_groups/service_group_with_overflow/generate_data.ts b/x-pack/test/apm_api_integration/tests/service_groups/service_group_with_overflow/generate_data.ts index e688e6ac6836a4..b9fe538d580dd1 100644 --- a/x-pack/test/apm_api_integration/tests/service_groups/service_group_with_overflow/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/service_groups/service_group_with_overflow/generate_data.ts @@ -8,11 +8,11 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; export async function generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, }: { - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; start: string; end: string; }) { @@ -25,7 +25,7 @@ export async function generateData({ .instance('instance-2'), ]; - await synthtraceEsClient.index( + await apmSynthtraceEsClient.index( synthServices.map((service) => timerange(start, end) .interval('5m') diff --git a/x-pack/test/apm_api_integration/tests/service_groups/service_group_with_overflow/service_group_with_overflow.spec.ts b/x-pack/test/apm_api_integration/tests/service_groups/service_group_with_overflow/service_group_with_overflow.spec.ts index 26d6b54025cca0..93b873143599a7 100644 --- a/x-pack/test/apm_api_integration/tests/service_groups/service_group_with_overflow/service_group_with_overflow.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_groups/service_group_with_overflow/service_group_with_overflow.spec.ts @@ -18,7 +18,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); const es = getService('es'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); registry.when( 'Display overflow bucket in Service Groups', @@ -33,11 +33,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { after(async () => { await deleteAllServiceGroups(apmApiClient); - synthtraceEsClient.clean(); + apmSynthtraceEsClient.clean(); }); before(async () => { - await generateData({ start, end, synthtraceEsClient }); + await generateData({ start, end, apmSynthtraceEsClient }); const docs = [ createServiceTransactionMetricsDocs({ diff --git a/x-pack/test/apm_api_integration/tests/service_maps/service_maps_kuery_filter.spec.ts b/x-pack/test/apm_api_integration/tests/service_maps/service_maps_kuery_filter.spec.ts index aacee3d7393be2..4be91cbe7bab61 100644 --- a/x-pack/test/apm_api_integration/tests/service_maps/service_maps_kuery_filter.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_maps/service_maps_kuery_filter.spec.ts @@ -16,7 +16,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2023-01-01T00:00:00.000Z').getTime(); const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1; @@ -68,10 +68,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, }) ); - await synthtraceEsClient.index(events); + await apmSynthtraceEsClient.index(events); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns full service map when no kuery is defined', async () => { const { status, body } = await callApi(); diff --git a/x-pack/test/apm_api_integration/tests/service_nodes/get_service_nodes.spec.ts b/x-pack/test/apm_api_integration/tests/service_nodes/get_service_nodes.spec.ts index 4806f344219bbb..6b24587b6bc138 100644 --- a/x-pack/test/apm_api_integration/tests/service_nodes/get_service_nodes.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_nodes/get_service_nodes.spec.ts @@ -12,7 +12,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const apmApiClient = getService('apmApiClient'); const registry = getService('registry'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -54,7 +54,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const instance = apm .service({ name: serviceName, environment: 'production', agentName: 'go' }) .instance(instanceName); - await synthtraceEsClient.index( + await apmSynthtraceEsClient.index( timerange(start, end) .interval('1m') .rate(1) @@ -70,7 +70,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ) ); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns service nodes', async () => { const response = await callApi(); diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instance_details.spec.ts b/x-pack/test/apm_api_integration/tests/service_overview/instance_details.spec.ts index cdc613ddcbeb8f..013237934904f4 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/instance_details.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/instance_details.spec.ts @@ -17,7 +17,7 @@ type ServiceOverviewInstanceDetails = export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtrace = getService('synthtraceEsClient'); + const synthtrace = getService('apmSynthtraceEsClient'); const start = '2023-08-22T00:00:00.000Z'; const end = '2023-08-22T01:00:00.000Z'; diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.spec.ts index 1f53d20a978897..b0a6bf51d1ccfa 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.spec.ts @@ -25,7 +25,7 @@ type ServiceOverviewInstancesMainStatistics = export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtrace = getService('synthtraceEsClient'); + const synthtrace = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:10:00.000Z').getTime(); diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_detailed_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_detailed_statistics.spec.ts index 9ec17ff8374ada..620d705f4463ff 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_detailed_statistics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_detailed_statistics.spec.ts @@ -24,7 +24,7 @@ type ErrorGroupsDetailedStatistics = export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -69,10 +69,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { describe('when data is loaded', () => { const { PROD_LIST_ERROR_RATE, PROD_ID_ERROR_RATE } = config; before(async () => { - await generateData({ serviceName, start, end, synthtraceEsClient }); + await generateData({ serviceName, start, end, apmSynthtraceEsClient }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('without data comparison', () => { let errorGroupsDetailedStatistics: ErrorGroupsDetailedStatistics; diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.spec.ts index 7096b65b1dbfae..3377cdabb38474 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.spec.ts @@ -20,7 +20,7 @@ type ErrorGroupsMainStatistics = export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -64,10 +64,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { const { PROD_LIST_ERROR_RATE, PROD_ID_ERROR_RATE, ERROR_NAME_1, ERROR_NAME_2 } = config; before(async () => { - await generateData({ serviceName, start, end, synthtraceEsClient }); + await generateData({ serviceName, start, end, apmSynthtraceEsClient }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('returns the correct data', () => { let errorGroupMainStatistics: ErrorGroupsMainStatistics; diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups/generate_data.ts b/x-pack/test/apm_api_integration/tests/services/error_groups/generate_data.ts index 01421a73b8d272..33d1ad4fc566f3 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/services/error_groups/generate_data.ts @@ -17,12 +17,12 @@ export const config = { }; export async function generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, serviceName, start, end, }: { - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; serviceName: string; start: number; end: number; @@ -43,7 +43,7 @@ export async function generateData({ ERROR_NAME_2, } = config; - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('1m') .rate(PROD_LIST_RATE) diff --git a/x-pack/test/apm_api_integration/tests/services/get_service_node_metadata.spec.ts b/x-pack/test/apm_api_integration/tests/services/get_service_node_metadata.spec.ts index 5203c2c4634018..6644ed8bc7d1cf 100644 --- a/x-pack/test/apm_api_integration/tests/services/get_service_node_metadata.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/get_service_node_metadata.spec.ts @@ -14,7 +14,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const apmApiClient = getService('apmApiClient'); const registry = getService('registry'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -66,7 +66,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const instance = apm .service({ name: serviceName, environment: 'production', agentName: 'go' }) .instance(instanceName); - await synthtraceEsClient.index( + await apmSynthtraceEsClient.index( timerange(start, end) .interval('1m') .rate(1) @@ -80,7 +80,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ) ); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns service node metadata', async () => { const response = await callApi(); diff --git a/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts b/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts index 2d32e0a85ffde4..958c2ed88f4604 100644 --- a/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts @@ -18,7 +18,7 @@ export default function ServiceAlerts({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); const supertest = getService('supertest'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const es = getService('es'); const dayInMs = 24 * 60 * 60 * 1000; const start = Date.now() - dayInMs; @@ -73,7 +73,7 @@ export default function ServiceAlerts({ getService }: FtrProviderContext) { .instance('instance-1'), ]; - await synthtraceEsClient.index( + await apmSynthtraceEsClient.index( synthServices.map((service) => timerange(start, end) .interval('5m') @@ -114,7 +114,7 @@ export default function ServiceAlerts({ getService }: FtrProviderContext) { }); after(async () => { - await synthtraceEsClient.clean(); + await apmSynthtraceEsClient.clean(); }); describe('with alerts', () => { diff --git a/x-pack/test/apm_api_integration/tests/services/service_details/generate_data.ts b/x-pack/test/apm_api_integration/tests/services/service_details/generate_data.ts index 811e0c2b9ede19..653f0113ba2851 100644 --- a/x-pack/test/apm_api_integration/tests/services/service_details/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/services/service_details/generate_data.ts @@ -43,11 +43,11 @@ export const dataConfig = { }; export async function generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, }: { - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; start: number; end: number; }) { @@ -127,5 +127,5 @@ export async function generateData({ ), ]; - await synthtraceEsClient.index(traceEvents); + await apmSynthtraceEsClient.index(traceEvents); } diff --git a/x-pack/test/apm_api_integration/tests/services/service_details/service_details.spec.ts b/x-pack/test/apm_api_integration/tests/services/service_details/service_details.spec.ts index 9ba86c29943ae1..4d8b250ec623c3 100644 --- a/x-pack/test/apm_api_integration/tests/services/service_details/service_details.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/service_details/service_details.spec.ts @@ -16,7 +16,7 @@ type ServiceDetails = APIReturnType<'GET /internal/apm/services/{serviceName}/me export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const { service: { name: serviceName }, @@ -57,13 +57,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { let status: number; before(async () => { - await generateData({ synthtraceEsClient, start, end }); + await generateData({ apmSynthtraceEsClient, start, end }); const response = await callApi(); body = response.body; status = response.status; }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns correct HTTP status', () => { expect(status).to.be(200); diff --git a/x-pack/test/apm_api_integration/tests/services/service_icons/generate_data.ts b/x-pack/test/apm_api_integration/tests/services/service_icons/generate_data.ts index ddfe8b57286566..bc842d2342c58f 100644 --- a/x-pack/test/apm_api_integration/tests/services/service_icons/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/services/service_icons/generate_data.ts @@ -22,11 +22,11 @@ export const dataConfig = { }; export async function generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, }: { - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; start: number; end: number; }) { @@ -53,5 +53,5 @@ export async function generateData({ .success() ); - await synthtraceEsClient.index(traceEvents); + await apmSynthtraceEsClient.index(traceEvents); } diff --git a/x-pack/test/apm_api_integration/tests/services/service_icons/service_icons.spec.ts b/x-pack/test/apm_api_integration/tests/services/service_icons/service_icons.spec.ts index ee7e31f7c81dd7..3516edd1800cbd 100644 --- a/x-pack/test/apm_api_integration/tests/services/service_icons/service_icons.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/service_icons/service_icons.spec.ts @@ -16,7 +16,7 @@ type ServiceIconMetadata = APIReturnType<'GET /internal/apm/services/{serviceNam export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const { serviceName } = dataConfig; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -50,13 +50,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { let status: number; before(async () => { - await generateData({ synthtraceEsClient, start, end }); + await generateData({ apmSynthtraceEsClient, start, end }); const response = await callApi(); body = response.body; status = response.status; }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns correct HTTP status', () => { expect(status).to.be(200); diff --git a/x-pack/test/apm_api_integration/tests/services/services_detailed_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/services/services_detailed_statistics.spec.ts index 44ac364bdf7d4c..0a33450e7f9804 100644 --- a/x-pack/test/apm_api_integration/tests/services/services_detailed_statistics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/services_detailed_statistics.spec.ts @@ -23,7 +23,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const apmApiClient = getService('apmApiClient'); - const synthtrace = getService('synthtraceEsClient'); + const synthtrace = getService('apmSynthtraceEsClient'); const start = '2021-01-01T00:00:00.000Z'; const end = '2021-01-01T00:59:59.999Z'; diff --git a/x-pack/test/apm_api_integration/tests/services/throughput.spec.ts b/x-pack/test/apm_api_integration/tests/services/throughput.spec.ts index 624706d30115ae..d8b3227890f1da 100644 --- a/x-pack/test/apm_api_integration/tests/services/throughput.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/throughput.spec.ts @@ -26,7 +26,7 @@ type ThroughputReturn = APIReturnType<'GET /internal/apm/services/{serviceName}/ export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -97,7 +97,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .service({ name: 'synth-java', environment: 'development', agentName: 'java' }) .instance('instance-c'); - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('1m') .rate(GO_PROD_RATE) @@ -128,7 +128,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('compare transactions and metrics based throughput', () => { let throughputMetrics: ThroughputReturn; diff --git a/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts b/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts index d35e28d30aec7c..48b394c0926387 100644 --- a/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/top_services.spec.ts @@ -20,7 +20,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtrace = getService('synthtraceEsClient'); + const synthtrace = getService('apmSynthtraceEsClient'); const archiveName = 'apm_8.0.0'; diff --git a/x-pack/test/apm_api_integration/tests/services/transaction_types.spec.ts b/x-pack/test/apm_api_integration/tests/services/transaction_types.spec.ts index b8e5586d2efad8..57dbbb1c6ee49f 100644 --- a/x-pack/test/apm_api_integration/tests/services/transaction_types.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/transaction_types.spec.ts @@ -14,7 +14,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtrace = getService('synthtraceEsClient'); + const synthtrace = getService('apmSynthtraceEsClient'); const start = '2023-10-28T00:00:00.000Z'; const end = '2023-10-28T00:14:59.999Z'; diff --git a/x-pack/test/apm_api_integration/tests/settings/agent_configuration/add_agent_config_metrics.ts b/x-pack/test/apm_api_integration/tests/settings/agent_configuration/add_agent_config_metrics.ts index b2fe4fd6099962..dc915f3accb791 100644 --- a/x-pack/test/apm_api_integration/tests/settings/agent_configuration/add_agent_config_metrics.ts +++ b/x-pack/test/apm_api_integration/tests/settings/agent_configuration/add_agent_config_metrics.ts @@ -9,14 +9,14 @@ import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { Readable } from 'stream'; export function addAgentConfigEtagMetric({ - synthtraceEsClient, + apmSynthtraceEsClient, timestamp, etag, }: { - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; timestamp: number; etag: string; }) { const agentConfigMetric = observer().agentConfig().etag(etag).timestamp(timestamp); - return synthtraceEsClient.index(Readable.from([agentConfigMetric])); + return apmSynthtraceEsClient.index(Readable.from([agentConfigMetric])); } diff --git a/x-pack/test/apm_api_integration/tests/settings/agent_configuration/agent_configuration.spec.ts b/x-pack/test/apm_api_integration/tests/settings/agent_configuration/agent_configuration.spec.ts index 431eca227d6058..98a971610a1495 100644 --- a/x-pack/test/apm_api_integration/tests/settings/agent_configuration/agent_configuration.spec.ts +++ b/x-pack/test/apm_api_integration/tests/settings/agent_configuration/agent_configuration.spec.ts @@ -20,7 +20,7 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte const apmApiClient = getService('apmApiClient'); const log = getService('log'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const archiveName = 'apm_8.0.0'; @@ -416,13 +416,13 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte describe('when there are agent config metrics for this etag', () => { before(async () => { await addAgentConfigEtagMetric({ - synthtraceEsClient, + apmSynthtraceEsClient, timestamp: Date.now(), etag: agentConfiguration.etag, }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it(`should have 'applied_by_agent=true' when getting a config from all configurations`, async () => { const { diff --git a/x-pack/test/apm_api_integration/tests/span_links/span_links.spec.ts b/x-pack/test/apm_api_integration/tests/span_links/span_links.spec.ts index d9488107d9d5f7..871f44da4cdc1a 100644 --- a/x-pack/test/apm_api_integration/tests/span_links/span_links.spec.ts +++ b/x-pack/test/apm_api_integration/tests/span_links/span_links.spec.ts @@ -13,7 +13,7 @@ import { generateSpanLinksData } from './data_generator'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2022-01-01T00:00:00.000Z').getTime(); const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1; @@ -27,7 +27,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ids = spanLinksData.ids; - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ Readable.from(spanLinksData.events.producerInternalOnly), Readable.from(spanLinksData.events.producerExternalOnly), Readable.from(spanLinksData.events.producerConsumer), @@ -35,7 +35,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('Span links count on traces', () => { async function fetchTraces({ diff --git a/x-pack/test/apm_api_integration/tests/storage_explorer/get_services.spec.ts b/x-pack/test/apm_api_integration/tests/storage_explorer/get_services.spec.ts index 70f9ab369dcf54..01d258a18717af 100644 --- a/x-pack/test/apm_api_integration/tests/storage_explorer/get_services.spec.ts +++ b/x-pack/test/apm_api_integration/tests/storage_explorer/get_services.spec.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const apmApiClient = getService('apmApiClient'); const start = '2021-01-01T12:00:00.000Z'; @@ -84,10 +84,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { serviceC.transaction({ transactionName: 'GET /api' }).duration(1000).timestamp(timestamp) ); - await synthtraceEsClient.index([eventsWithinTimerange, eventsOutsideOfTimerange]); + await apmSynthtraceEsClient.index([eventsWithinTimerange, eventsOutsideOfTimerange]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('with no kuery, environment or index lifecycle phase set it returns services based on the terms enum API', async () => { const items = await getServices(); diff --git a/x-pack/test/apm_api_integration/tests/storage_explorer/storage_details.spec.ts b/x-pack/test/apm_api_integration/tests/storage_explorer/storage_details.spec.ts index 8c9dc354356a95..84f9ca4e35e977 100644 --- a/x-pack/test/apm_api_integration/tests/storage_explorer/storage_details.spec.ts +++ b/x-pack/test/apm_api_integration/tests/storage_explorer/storage_details.spec.ts @@ -21,7 +21,7 @@ type StorageDetails = APIReturnType<'GET /internal/apm/services/{serviceName}/st export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -78,7 +78,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .service({ name: serviceName, environment: 'production', agentName: 'go' }) .instance('instance'); - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('5m') .rate(1) @@ -110,7 +110,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it.skip('returns correct stats for processor events', async () => { const { status, body } = await callApi(); diff --git a/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer.spec.ts b/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer.spec.ts index f6ade43b4e29fc..6f90d35c6ebff5 100644 --- a/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer.spec.ts +++ b/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer.spec.ts @@ -15,7 +15,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -72,7 +72,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .service({ name: nodeServiceName, environment: 'dev', agentName: 'node' }) .instance('instance-node-dev'); - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('5m') .rate(1) @@ -103,7 +103,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns correct stats', async () => { const { status, body } = await callApi(); diff --git a/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer_summary_stats.spec.ts b/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer_summary_stats.spec.ts index cab335bded51f9..97375f0b685e26 100644 --- a/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer_summary_stats.spec.ts +++ b/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer_summary_stats.spec.ts @@ -15,7 +15,7 @@ import { roundNumber } from '../../utils'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -73,7 +73,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .service({ name: nodeServiceName, environment: 'dev', agentName: 'node' }) .instance('instance-node'); - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('1m') .rate(1) @@ -95,7 +95,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns correct summary stats', async () => { const { status, body } = await callApi(); diff --git a/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer_timeseries_chart.spec.ts b/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer_timeseries_chart.spec.ts index 757d9be55ee716..64f94bf3900df5 100644 --- a/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer_timeseries_chart.spec.ts +++ b/x-pack/test/apm_api_integration/tests/storage_explorer/storage_explorer_timeseries_chart.spec.ts @@ -16,7 +16,7 @@ type StorageTimeSeries = APIReturnType<'GET /internal/apm/storage_chart'>; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -64,7 +64,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .service({ name: 'synth-go-2', environment: 'production', agentName: 'go' }) .instance('instance'); - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('5m') .rate(1) @@ -90,7 +90,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { status = response.status; }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns correct HTTP status', async () => { expect(status).to.be(200); diff --git a/x-pack/test/apm_api_integration/tests/suggestions/generate_data.ts b/x-pack/test/apm_api_integration/tests/suggestions/generate_data.ts index 283c907782cdfa..0311861436ec1e 100644 --- a/x-pack/test/apm_api_integration/tests/suggestions/generate_data.ts +++ b/x-pack/test/apm_api_integration/tests/suggestions/generate_data.ts @@ -9,11 +9,11 @@ import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { times } from 'lodash'; export async function generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start, end, }: { - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; start: number; end: number; }) { @@ -73,5 +73,5 @@ export async function generateData({ return [...autoGeneratedDocs, customDoc]; }); - return await synthtraceEsClient.index(docs); + return await apmSynthtraceEsClient.index(docs); } diff --git a/x-pack/test/apm_api_integration/tests/suggestions/suggestions.spec.ts b/x-pack/test/apm_api_integration/tests/suggestions/suggestions.spec.ts index 932f6f56360164..d4d1c3b1417002 100644 --- a/x-pack/test/apm_api_integration/tests/suggestions/suggestions.spec.ts +++ b/x-pack/test/apm_api_integration/tests/suggestions/suggestions.spec.ts @@ -23,19 +23,19 @@ const end = new Date(endNumber).toISOString(); export default function suggestionsTests({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); // FLAKY: https://github.com/elastic/kibana/issues/177538 registry.when('suggestions when data is loaded', { config: 'basic', archives: [] }, async () => { before(async () => { await generateData({ - synthtraceEsClient, + apmSynthtraceEsClient, start: startNumber, end: endNumber, }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe(`field: ${SERVICE_ENVIRONMENT}`, () => { describe('when fieldValue is empty', () => { diff --git a/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts b/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts index 3edf6946df9a6c..fe591631fafe74 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts +++ b/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts @@ -14,7 +14,7 @@ import { roundNumber } from '../../utils'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -106,7 +106,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .service({ name: 'synth-java', environment: 'development', agentName: 'java' }) .instance('instance-c'); - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('1m') .rate(GO_PROD_RATE) @@ -174,7 +174,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('verify top dependencies', () => { before(async () => { diff --git a/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts b/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts index eb8019d3b358f1..9d69ce74bf0ead 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts +++ b/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts @@ -17,7 +17,7 @@ import { roundNumber } from '../../utils'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -154,7 +154,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .service({ name: serviceName, environment: 'development', agentName: 'go' }) .instance('instance-b'); - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('1m') .rate(GO_PROD_RATE) @@ -176,7 +176,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('compare throughput value between service inventory, throughput chart, service inventory and transactions apis', () => { before(async () => { diff --git a/x-pack/test/apm_api_integration/tests/throughput/service_maps.spec.ts b/x-pack/test/apm_api_integration/tests/throughput/service_maps.spec.ts index e502e4ea66066e..5ee475344e2867 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/service_maps.spec.ts +++ b/x-pack/test/apm_api_integration/tests/throughput/service_maps.spec.ts @@ -15,7 +15,7 @@ import { roundNumber } from '../../utils'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -95,7 +95,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .service({ name: serviceName, environment: 'development', agentName: 'go' }) .instance('instance-b'); - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('1m') .rate(GO_PROD_RATE) @@ -117,7 +117,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); // FLAKY: https://github.com/elastic/kibana/issues/176984 describe('compare throughput value between service inventory and service maps', () => { diff --git a/x-pack/test/apm_api_integration/tests/time_range_metadata/many_apm_server_versions.spec.ts b/x-pack/test/apm_api_integration/tests/time_range_metadata/many_apm_server_versions.spec.ts index 8c3ffbd6528153..01b4b355308368 100644 --- a/x-pack/test/apm_api_integration/tests/time_range_metadata/many_apm_server_versions.spec.ts +++ b/x-pack/test/apm_api_integration/tests/time_range_metadata/many_apm_server_versions.spec.ts @@ -26,7 +26,7 @@ import { ApmApiClient } from '../../common/config'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtrace = getService('synthtraceEsClient'); + const synthtrace = getService('apmSynthtraceEsClient'); const es = getService('es'); const baseTime = new Date('2023-10-01T00:00:00.000Z').getTime(); diff --git a/x-pack/test/apm_api_integration/tests/time_range_metadata/time_range_metadata.spec.ts b/x-pack/test/apm_api_integration/tests/time_range_metadata/time_range_metadata.spec.ts index b46b6c98fb1dc6..43aee07583c180 100644 --- a/x-pack/test/apm_api_integration/tests/time_range_metadata/time_range_metadata.spec.ts +++ b/x-pack/test/apm_api_integration/tests/time_range_metadata/time_range_metadata.spec.ts @@ -23,7 +23,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const es = getService('es'); const log = getService('log'); @@ -95,7 +95,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { start: withoutSummaryFieldStart, end: withoutSummaryFieldEnd, isLegacy: true, - synthtrace: synthtraceEsClient, + synthtrace: apmSynthtraceEsClient, logger: log, }); @@ -103,13 +103,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { start: withSummaryFieldStart, end: withSummaryFieldEnd, isLegacy: false, - synthtrace: synthtraceEsClient, + synthtrace: apmSynthtraceEsClient, logger: log, }); }); after(() => { - return synthtraceEsClient.clean(); + return apmSynthtraceEsClient.clean(); }); describe('Values for hasDurationSummaryField for transaction metrics', () => { @@ -155,7 +155,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { before(() => { const instance = apm.service('my-service', 'production', 'java').instance('instance'); - return synthtraceEsClient.index( + return apmSynthtraceEsClient.index( timerange(moment(start).subtract(1, 'day'), end) .interval('1m') .rate(1) @@ -166,7 +166,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); after(() => { - return synthtraceEsClient.clean(); + return apmSynthtraceEsClient.clean(); }); describe('with default settings', () => { @@ -508,7 +508,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); after(() => { - return synthtraceEsClient.clean(); + return apmSynthtraceEsClient.clean(); }); } ); diff --git a/x-pack/test/apm_api_integration/tests/traces/critical_path.spec.ts b/x-pack/test/apm_api_integration/tests/traces/critical_path.spec.ts index baa194a7ab56af..6d55c55ba6dbf2 100644 --- a/x-pack/test/apm_api_integration/tests/traces/critical_path.spec.ts +++ b/x-pack/test/apm_api_integration/tests/traces/critical_path.spec.ts @@ -16,7 +16,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2022-01-01T00:00:00.000Z').getTime(); const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1; @@ -63,7 +63,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const traceIds = compact(uniq(serialized.map((event) => event['trace.id']))); - await synthtraceEsClient.index(Readable.from(unserialized)); + await apmSynthtraceEsClient.index(Readable.from(unserialized)); return apmApiClient .readUser({ @@ -271,7 +271,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { fn: () => generateTrace(), }); - await synthtraceEsClient.clean(); + await apmSynthtraceEsClient.clean(); const { rootNodes: filteredRootNodes } = await fetchAndBuildCriticalPathTree({ fn: () => generateTrace(), @@ -411,7 +411,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, ]); - await synthtraceEsClient.clean(); + await apmSynthtraceEsClient.clean(); const { rootNodes: filteredRootNodes } = await fetchAndBuildCriticalPathTree({ fn: () => generateTrace(), @@ -428,6 +428,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); }); } diff --git a/x-pack/test/apm_api_integration/tests/traces/find_traces.spec.ts b/x-pack/test/apm_api_integration/tests/traces/find_traces.spec.ts index 653672879c2813..369490ae06d446 100644 --- a/x-pack/test/apm_api_integration/tests/traces/find_traces.spec.ts +++ b/x-pack/test/apm_api_integration/tests/traces/find_traces.spec.ts @@ -17,7 +17,7 @@ import { generateTrace } from './generate_trace'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2022-01-01T00:00:00.000Z').getTime(); const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1; @@ -103,7 +103,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .service({ name: 'python', environment: 'production', agentName: 'python' }) .instance('python'); - return synthtraceEsClient.index( + return apmSynthtraceEsClient.index( timerange(start, end) .interval('15m') .rate(1) @@ -220,6 +220,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); }); } diff --git a/x-pack/test/apm_api_integration/tests/traces/large_trace/generate_large_trace.ts b/x-pack/test/apm_api_integration/tests/traces/large_trace/generate_large_trace.ts index 3cd580a6e7a968..a6d55c7d02bce7 100644 --- a/x-pack/test/apm_api_integration/tests/traces/large_trace/generate_large_trace.ts +++ b/x-pack/test/apm_api_integration/tests/traces/large_trace/generate_large_trace.ts @@ -16,14 +16,14 @@ export function generateLargeTrace({ start, end, rootTransactionName, - synthtraceEsClient, + apmSynthtraceEsClient, repeaterFactor, environment, }: { start: number; end: number; rootTransactionName: string; - synthtraceEsClient: ApmSynthtraceEsClient; + apmSynthtraceEsClient: ApmSynthtraceEsClient; repeaterFactor: number; environment: string; }) { @@ -137,5 +137,5 @@ export function generateLargeTrace({ }).getTransaction(); }); - return synthtraceEsClient.index(traces); + return apmSynthtraceEsClient.index(traces); } diff --git a/x-pack/test/apm_api_integration/tests/traces/large_trace/large_trace.spec.ts b/x-pack/test/apm_api_integration/tests/traces/large_trace/large_trace.spec.ts index d16a9efd082ed8..023db5c0d2ba0f 100644 --- a/x-pack/test/apm_api_integration/tests/traces/large_trace/large_trace.spec.ts +++ b/x-pack/test/apm_api_integration/tests/traces/large_trace/large_trace.spec.ts @@ -26,7 +26,7 @@ const environment = 'long_trace_scenario'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const es = getService('es'); // FLAKY: https://github.com/elastic/kibana/issues/177660 @@ -37,14 +37,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { start, end, rootTransactionName, - synthtraceEsClient, + apmSynthtraceEsClient, repeaterFactor: 10, environment, }); }); after(async () => { - await synthtraceEsClient.clean(); + await apmSynthtraceEsClient.clean(); }); describe('when maxTraceItems is 5000 (default)', () => { diff --git a/x-pack/test/apm_api_integration/tests/traces/span_details.spec.ts b/x-pack/test/apm_api_integration/tests/traces/span_details.spec.ts index 2598a6cdf97abf..a428ea9cb2e508 100644 --- a/x-pack/test/apm_api_integration/tests/traces/span_details.spec.ts +++ b/x-pack/test/apm_api_integration/tests/traces/span_details.spec.ts @@ -12,7 +12,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2022-01-01T00:00:00.000Z').getTime(); const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1; @@ -100,10 +100,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { parentTransactionId = span?.['parent.id']!; traceId = span?.['trace.id']!; - await synthtraceEsClient.index(Readable.from(unserialized)); + await apmSynthtraceEsClient.index(Readable.from(unserialized)); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('span details', () => { let spanDetails: Awaited>['body']; diff --git a/x-pack/test/apm_api_integration/tests/traces/trace_by_id.spec.ts b/x-pack/test/apm_api_integration/tests/traces/trace_by_id.spec.ts index 8bcf83a503022e..de07f3664104cf 100644 --- a/x-pack/test/apm_api_integration/tests/traces/trace_by_id.spec.ts +++ b/x-pack/test/apm_api_integration/tests/traces/trace_by_id.spec.ts @@ -13,7 +13,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2022-01-01T00:00:00.000Z').getTime(); const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1; @@ -90,10 +90,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { entryTransactionId = serialized[0]['transaction.id']!; serviceATraceId = serialized[0]['trace.id']!; - await synthtraceEsClient.index(Readable.from(unserialized)); + await apmSynthtraceEsClient.index(Readable.from(unserialized)); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('return trace', () => { let traces: APIReturnType<'GET /internal/apm/traces/{traceId}'>; diff --git a/x-pack/test/apm_api_integration/tests/traces/transaction_details.spec.ts b/x-pack/test/apm_api_integration/tests/traces/transaction_details.spec.ts index 674f56c79a6079..3665bfd8e8ea60 100644 --- a/x-pack/test/apm_api_integration/tests/traces/transaction_details.spec.ts +++ b/x-pack/test/apm_api_integration/tests/traces/transaction_details.spec.ts @@ -12,7 +12,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const start = new Date('2022-01-01T00:00:00.000Z').getTime(); const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1; @@ -96,10 +96,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { transactionId = transaction?.['transaction.id']!; traceId = transaction?.['trace.id']!; - await synthtraceEsClient.index(Readable.from(unserialized)); + await apmSynthtraceEsClient.index(Readable.from(unserialized)); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('transaction details', () => { let transactionDetails: Awaited>['body']; diff --git a/x-pack/test/apm_api_integration/tests/transactions/error_rate.spec.ts b/x-pack/test/apm_api_integration/tests/transactions/error_rate.spec.ts index 123bb0d6d594d8..724390fdfa61fc 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/error_rate.spec.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/error_rate.spec.ts @@ -24,7 +24,7 @@ type ErrorRate = export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); // url parameters const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -142,10 +142,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { .failure() ), ]; - await synthtraceEsClient.index(documents); + await apmSynthtraceEsClient.index(documents); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('returns the transaction error rate', () => { let errorRateResponse: ErrorRate; diff --git a/x-pack/test/apm_api_integration/tests/transactions/latency.spec.ts b/x-pack/test/apm_api_integration/tests/transactions/latency.spec.ts index eb876e6e312b76..eefe5cfb0d0fe6 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/latency.spec.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/latency.spec.ts @@ -25,7 +25,7 @@ type LatencyChartReturnType = export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -90,7 +90,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .service({ name: serviceName, environment: 'development', agentName: 'go' }) .instance('instance-b'); - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .ratePerMinute(GO_PROD_RATE) .generator((timestamp) => @@ -110,7 +110,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); const expectedLatencyAvgValueMs = ((GO_PROD_RATE * GO_PROD_DURATION + GO_DEV_RATE * GO_DEV_DURATION) / diff --git a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_alerts.spec.ts b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_alerts.spec.ts index d8868f59aa7eb7..7468437d8bc725 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_alerts.spec.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_alerts.spec.ts @@ -25,7 +25,7 @@ type TransactionsGroupsMainStatistics = export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const supertest = getService('supertest'); const es = getService('es'); const serviceName = 'synth-go'; @@ -107,7 +107,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .service({ name: serviceName, environment: 'production', agentName: 'go' }) .instance('instance-a'); - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('1m') .rate(1) @@ -135,7 +135,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('Transaction groups with avg transaction duration alerts', () => { let ruleId: string; diff --git a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.spec.ts index e195e035d50a27..77a4b67b4bc4e1 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.spec.ts @@ -21,7 +21,7 @@ type TransactionsGroupsDetailedStatistics = export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -94,7 +94,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const transactionName = 'GET /api/product/list'; - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('1m') .rate(GO_PROD_RATE) @@ -118,7 +118,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); describe('without comparisons', () => { let transactionsStatistics: TransactionsGroupsDetailedStatistics; diff --git a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_main_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_main_statistics.spec.ts index 646e1b790add03..d7c5e78fdcd124 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_main_statistics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_main_statistics.spec.ts @@ -16,7 +16,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const synthtraceEsClient = getService('synthtraceEsClient'); + const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -97,7 +97,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .service({ name: serviceName, environment: 'production', agentName: 'go' }) .instance('instance-a'); - await synthtraceEsClient.index([ + await apmSynthtraceEsClient.index([ timerange(start, end) .interval('1m') .rate(GO_PROD_RATE) @@ -124,7 +124,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }), ]); }); - after(() => synthtraceEsClient.clean()); + after(() => apmSynthtraceEsClient.clean()); it('returns the correct data', async () => { const transactionsGroupsPrimaryStatistics = await callApi(); diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/public_complete/public_complete.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/public_complete/public_complete.spec.ts index 4430d0405764db..ac2fa36f6b0fd1 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/public_complete/public_complete.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/public_complete/public_complete.spec.ts @@ -44,12 +44,19 @@ export default function ApiTest({ getService }: FtrProviderContext) { let proxy: LlmProxy; let connectorId: string; - async function getEvents( - params: { - actions?: Array>; - instructions?: string[]; - }, - cb: (conversationSimulator: LlmResponseSimulator) => Promise + interface RequestOptions { + actions?: Array>; + instructions?: string[]; + format?: 'openai'; + } + + type ConversationSimulatorCallback = ( + conversationSimulator: LlmResponseSimulator + ) => Promise; + + async function getResponseBody( + { actions, instructions, format }: RequestOptions, + conversationSimulatorCallback: ConversationSimulatorCallback ) { const titleInterceptor = proxy.intercept('title', (body) => isFunctionTitleRequest(body)); @@ -61,13 +68,16 @@ export default function ApiTest({ getService }: FtrProviderContext) { const responsePromise = new Promise((resolve, reject) => { supertest .post(PUBLIC_COMPLETE_API_URL) + .query({ + format, + }) .set('kbn-xsrf', 'foo') .send({ messages, connectorId, persist: true, - actions: params.actions, - instructions: params.instructions, + actions, + instructions, }) .end((err, response) => { if (err) { @@ -87,11 +97,22 @@ export default function ApiTest({ getService }: FtrProviderContext) { await titleSimulator.complete(); await conversationSimulator.status(200); - await cb(conversationSimulator); + if (conversationSimulatorCallback) { + await conversationSimulatorCallback(conversationSimulator); + } const response = await responsePromise; - return String(response.body) + return String(response.body); + } + + async function getEvents( + options: RequestOptions, + conversationSimulatorCallback: ConversationSimulatorCallback + ) { + const responseBody = await getResponseBody(options, conversationSimulatorCallback); + + return responseBody .split('\n') .map((line) => line.trim()) .filter(Boolean) @@ -99,6 +120,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { .slice(2); // ignore context request/response, we're testing this elsewhere } + async function getOpenAIResponse(conversationSimulatorCallback: ConversationSimulatorCallback) { + const responseBody = await getResponseBody( + { + format: 'openai', + }, + conversationSimulatorCallback + ); + + return responseBody; + } + before(async () => { proxy = await createLlmProxy(log); @@ -209,6 +241,72 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(request.messages[0].content).to.contain('This is a random instruction'); }); }); + + describe('with openai format', async () => { + let responseBody: string; + + before(async () => { + responseBody = await getOpenAIResponse(async (conversationSimulator) => { + await conversationSimulator.next('Hello'); + await conversationSimulator.complete(); + }); + }); + + function extractDataParts(lines: string[]) { + return lines.map((line) => { + // .replace is easier, but we want to verify here whether + // it matches the SSE syntax (`data: ...`) + const [, dataPart] = line.match(/^data: (.*)$/) || ['', '']; + return dataPart.trim(); + }); + } + + function getLines() { + return responseBody.split('\n\n').filter(Boolean); + } + + it('outputs each line an SSE-compatible format (data: ...)', () => { + const lines = getLines(); + + lines.forEach((line) => { + expect(line.match(/^data: /)); + }); + }); + + it('ouputs one chunk, and one [DONE] event', () => { + const dataParts = extractDataParts(getLines()); + + expect(dataParts[0]).not.to.be.empty(); + expect(dataParts[1]).to.be('[DONE]'); + }); + + it('outuputs an OpenAI-compatible chunk', () => { + const [dataLine] = extractDataParts(getLines()); + + expect(() => { + JSON.parse(dataLine); + }).not.to.throwException(); + + const parsedChunk = JSON.parse(dataLine); + + expect(parsedChunk).to.eql({ + model: 'unknown', + choices: [ + { + delta: { + content: 'Hello', + }, + finish_reason: null, + index: 0, + }, + ], + object: 'chat.completion.chunk', + // just test that these are a string and a number + id: String(parsedChunk.id), + created: Number(parsedChunk.created), + }); + }); + }); }); } diff --git a/x-pack/test/observability_api_integration/common/bootstrap_synthtrace.ts b/x-pack/test/observability_api_integration/common/bootstrap_synthtrace.ts new file mode 100644 index 00000000000000..fd3997e7630cf2 --- /dev/null +++ b/x-pack/test/observability_api_integration/common/bootstrap_synthtrace.ts @@ -0,0 +1,50 @@ +/* + * 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 { + ApmSynthtraceEsClient, + ApmSynthtraceKibanaClient, + createLogger, + LogLevel, +} from '@kbn/apm-synthtrace'; +import url from 'url'; +import { kbnTestConfig } from '@kbn/test'; +import { FtrProviderContext } from './ftr_provider_context'; + +export async function bootstrapApmSynthtraceEsClient( + context: FtrProviderContext, + kibanaClient: ApmSynthtraceKibanaClient +) { + const es = context.getService('es'); + + const kibanaVersion = await kibanaClient.fetchLatestApmPackageVersion(); + await kibanaClient.installApmPackage(kibanaVersion); + + const esClient = new ApmSynthtraceEsClient({ + client: es, + logger: createLogger(LogLevel.info), + version: kibanaVersion, + refreshAfterIndex: true, + }); + + return esClient; +} + +export function getSynthtraceKibanaClient(kibanaServerUrl: string) { + const kibanaServerUrlWithAuth = url + .format({ + ...url.parse(kibanaServerUrl), + auth: `elastic:${kbnTestConfig.getUrlParts().password}`, + }) + .slice(0, -1); + + const kibanaClient = new ApmSynthtraceKibanaClient({ + target: kibanaServerUrlWithAuth, + logger: createLogger(LogLevel.debug), + }); + + return kibanaClient; +} diff --git a/x-pack/test/observability_api_integration/common/config.ts b/x-pack/test/observability_api_integration/common/config.ts index 83249182084f3e..8baf4f5d116f09 100644 --- a/x-pack/test/observability_api_integration/common/config.ts +++ b/x-pack/test/observability_api_integration/common/config.ts @@ -5,7 +5,13 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; +import { Config, FtrConfigProviderContext, kbnTestConfig } from '@kbn/test'; +import { format, UrlObject } from 'url'; +import { LogsSynthtraceEsClient, createLogger, LogLevel } from '@kbn/apm-synthtrace'; +import supertest from 'supertest'; +import { bootstrapApmSynthtraceEsClient, getSynthtraceKibanaClient } from './bootstrap_synthtrace'; +import { FtrProviderContext } from './ftr_provider_context'; +import { createObsApiClient } from './obs_api_supertest'; interface Settings { license: 'basic' | 'trial'; @@ -13,6 +19,41 @@ interface Settings { name: string; } +export type CustomApiTestServices = ReturnType; +function getCustomApiTestServices(xPackAPITestsConfig: Config) { + const servers = xPackAPITestsConfig.get('servers'); + const kibanaServer = servers.kibana as UrlObject; + const kibanaServerUrl = format(kibanaServer); + const synthtraceKibanaClient = getSynthtraceKibanaClient(kibanaServerUrl); + + return { + apmSynthtraceEsClient: (context: FtrProviderContext) => { + return bootstrapApmSynthtraceEsClient(context, synthtraceKibanaClient); + }, + logSynthtraceEsClient: (context: FtrProviderContext) => + new LogsSynthtraceEsClient({ + client: context.getService('es'), + logger: createLogger(LogLevel.info), + refreshAfterIndex: true, + }), + synthtraceKibanaClient: () => synthtraceKibanaClient, + obsApiClient: async (context: FtrProviderContext) => { + const getApiClientForUsername = (username: string) => { + const url = format({ + ...kibanaServer, + auth: `${username}:${kbnTestConfig.getUrlParts().password}`, + }); + + return createObsApiClient(supertest(url)); + }; + + return { + adminUser: getApiClientForUsername('elastic'), + }; + }, + }; +} + export function createTestConfig(settings: Settings) { const { testFiles, license, name } = settings; @@ -21,10 +62,15 @@ export function createTestConfig(settings: Settings) { require.resolve('../../api_integration/config.ts') ); + const customTestServices = getCustomApiTestServices(xPackAPITestsConfig); + return { testFiles, servers: xPackAPITestsConfig.get('servers'), - services: xPackAPITestsConfig.get('services'), + services: { + ...xPackAPITestsConfig.get('services'), + ...customTestServices, + }, junit: { reportName: name, }, diff --git a/x-pack/test/observability_api_integration/common/ftr_provider_context.ts b/x-pack/test/observability_api_integration/common/ftr_provider_context.ts index 2ea45b854eb280..b1d69d89e287a5 100644 --- a/x-pack/test/observability_api_integration/common/ftr_provider_context.ts +++ b/x-pack/test/observability_api_integration/common/ftr_provider_context.ts @@ -5,4 +5,12 @@ * 2.0. */ +import { GenericFtrProviderContext } from '@kbn/test'; +import { services } from '../../api_integration/services'; +import { CustomApiTestServices } from './config'; + export type { FtrProviderContext } from '../../api_integration/ftr_provider_context'; +export type ObsFtrProviderContext = GenericFtrProviderContext< + typeof services & CustomApiTestServices, + {} +>; diff --git a/x-pack/test/observability_api_integration/common/obs_api_supertest.ts b/x-pack/test/observability_api_integration/common/obs_api_supertest.ts new file mode 100644 index 00000000000000..e0788dcd6785d7 --- /dev/null +++ b/x-pack/test/observability_api_integration/common/obs_api_supertest.ts @@ -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 { format } from 'url'; +import supertest from 'supertest'; +import request from 'superagent'; +import { formatRequest, ClientRequestParamsOf, ReturnOf } from '@kbn/server-route-repository'; +import type { + ObservabilityServerRouteRepository, + APIEndpoint, +} from '@kbn/observability-plugin/server'; + +export type APIReturnType = ReturnOf< + ObservabilityServerRouteRepository, + TEndpoint +>; + +export type APIClientRequestParamsOf = ClientRequestParamsOf< + ObservabilityServerRouteRepository, + TEndpoint +>; + +export function createObsApiClient(st: supertest.SuperTest) { + return async ( + options: { + type?: 'form-data'; + endpoint: TEndpoint; + spaceId?: string; + } & APIClientRequestParamsOf & { params?: { query?: { _inspect?: boolean } } } + ): Promise> => { + const { endpoint, type } = options; + + const params = 'params' in options ? (options.params as Record) : {}; + + const { method, pathname, version } = formatRequest(endpoint, params.path); + const pathnameWithSpaceId = options.spaceId ? `/s/${options.spaceId}${pathname}` : pathname; + const url = format({ pathname: pathnameWithSpaceId, query: params?.query }); + + // eslint-disable-next-line no-console + console.debug(`Calling Observability API: ${method.toUpperCase()} ${url}`); + + const headers: Record = { + 'kbn-xsrf': 'foo', + 'x-elastic-internal-origin': 'foo', + }; + + if (version) { + headers['Elastic-Api-Version'] = version; + } + + let res: request.Response; + if (type === 'form-data') { + const fields: Array<[string, any]> = Object.entries(params.body); + const formDataRequest = st[method](url) + .set(headers) + .set('Content-type', 'multipart/form-data'); + + for (const field of fields) { + formDataRequest.field(field[0], field[1]); + } + + res = await formDataRequest; + } else if (params.body) { + res = await st[method](url).send(params.body).set(headers); + } else { + res = await st[method](url).set(headers); + } + + // supertest doesn't throw on http errors + if (res?.status !== 200) { + throw new ObservabilityApiError(res, endpoint); + } + + return res; + }; +} + +type ApiErrorResponse = Omit & { + body: { + statusCode: number; + error: string; + message: string; + attributes: object; + }; +}; + +export type ObservabilityApiSupertest = ReturnType; + +export class ObservabilityApiError extends Error { + res: ApiErrorResponse; + + constructor(res: request.Response, endpoint: string) { + super( + `Unhandled ObservabilityApiError. +Status: "${res.status}" +Endpoint: "${endpoint}" +Body: ${JSON.stringify(res.body)}` + ); + + this.res = res; + } +} + +export interface SupertestReturnType { + status: number; + body: APIReturnType; +} diff --git a/x-pack/test/observability_api_integration/trial/tests/index.ts b/x-pack/test/observability_api_integration/trial/tests/index.ts index e426efd90188ce..3d7f31517121d9 100644 --- a/x-pack/test/observability_api_integration/trial/tests/index.ts +++ b/x-pack/test/observability_api_integration/trial/tests/index.ts @@ -11,5 +11,6 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('Observability specs (trial)', function () { loadTestFile(require.resolve('./annotations')); + loadTestFile(require.resolve('./obs_alert_details_context')); }); } diff --git a/x-pack/test/observability_api_integration/trial/tests/obs_alert_details_context.ts b/x-pack/test/observability_api_integration/trial/tests/obs_alert_details_context.ts new file mode 100644 index 00000000000000..bf75c8c5585d47 --- /dev/null +++ b/x-pack/test/observability_api_integration/trial/tests/obs_alert_details_context.ts @@ -0,0 +1,515 @@ +/* + * 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 moment from 'moment'; +import { log, apm, generateShortId, timerange } from '@kbn/apm-synthtrace-client'; +import expect from '@kbn/expect'; +import { LogCategory } from '@kbn/apm-plugin/server/routes/assistant_functions/get_log_categories'; +import { SupertestReturnType } from '../../common/obs_api_supertest'; +import { ObsFtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ApiTest({ getService }: ObsFtrProviderContext) { + const obsApiClient = getService('obsApiClient'); + const apmSynthtraceClient = getService('apmSynthtraceEsClient'); + const logSynthtraceClient = getService('logSynthtraceEsClient'); + + describe('fetching observability alerts details context for AI assistant contextual insights', () => { + const start = moment().subtract(10, 'minutes').valueOf(); + const end = moment().valueOf(); + const range = timerange(start, end); + + describe('when no traces or logs are available', async () => { + let response: SupertestReturnType<'GET /internal/observability/assistant/alert_details_contextual_insights'>; + before(async () => { + response = await obsApiClient.adminUser({ + endpoint: 'GET /internal/observability/assistant/alert_details_contextual_insights', + params: { + query: { + alert_started_at: new Date(end).toISOString(), + }, + }, + }); + }); + + it('returns nothing', () => { + expect(response.body.alertContext).to.eql([]); + }); + }); + + describe('when traces and logs are ingested and logs are not annotated with service.name', async () => { + before(async () => { + await Promise.all([ + ingestTraces({ 'service.name': 'Backend', 'container.id': 'my-container-a' }), + ingestLogs({ + 'container.id': 'my-container-a', + 'kubernetes.pod.name': 'pod-a', + }), + ]); + }); + + after(async () => { + await cleanup(); + }); + + describe('when no params are specified', async () => { + let response: SupertestReturnType<'GET /internal/observability/assistant/alert_details_contextual_insights'>; + before(async () => { + response = await obsApiClient.adminUser({ + endpoint: 'GET /internal/observability/assistant/alert_details_contextual_insights', + params: { + query: { + alert_started_at: new Date(end).toISOString(), + }, + }, + }); + }); + + it('returns only 1 log category', async () => { + expect(response.body.alertContext).to.have.length(1); + + const logCategories = response.body.alertContext.find( + ({ key }) => key === 'logCategories' + )?.data as LogCategory[]; + + expect( + logCategories.map(({ errorCategory }: { errorCategory: string }) => errorCategory) + ).to.eql(['Error message from container my-container-a']); + }); + }); + + describe('when service name is specified', async () => { + let response: SupertestReturnType<'GET /internal/observability/assistant/alert_details_contextual_insights'>; + before(async () => { + response = await obsApiClient.adminUser({ + endpoint: 'GET /internal/observability/assistant/alert_details_contextual_insights', + params: { + query: { + alert_started_at: new Date(end).toISOString(), + 'service.name': 'Backend', + }, + }, + }); + }); + + it('returns service summary', () => { + const serviceSummary = response.body.alertContext.find( + ({ key }) => key === 'serviceSummary' + ); + expect(serviceSummary?.data).to.eql({ + 'service.name': 'Backend', + 'service.environment': ['production'], + 'agent.name': 'java', + 'service.version': ['1.0.0'], + 'language.name': 'java', + instances: 1, + anomalies: [], + alerts: [], + deployments: [], + }); + }); + + it('returns downstream dependencies', async () => { + const downstreamDependencies = response.body.alertContext.find( + ({ key }) => key === 'downstreamDependencies' + ); + expect(downstreamDependencies?.data).to.eql([ + { + 'span.destination.service.resource': 'elasticsearch', + 'span.type': 'db', + 'span.subtype': 'elasticsearch', + }, + ]); + }); + + it('returns log categories', () => { + const logCategories = response.body.alertContext.find( + ({ key }) => key === 'logCategories' + )?.data as LogCategory[]; + + expect(logCategories).to.have.length(1); + + const logCategory = logCategories[0]; + expect(logCategory?.sampleMessage).to.match( + /Error message #\d{16} from container my-container-a/ + ); + expect(logCategory?.docCount).to.be.greaterThan(0); + expect(logCategory?.errorCategory).to.be('Error message from container my-container-a'); + }); + }); + + describe('when container id is specified', async () => { + let response: SupertestReturnType<'GET /internal/observability/assistant/alert_details_contextual_insights'>; + before(async () => { + response = await obsApiClient.adminUser({ + endpoint: 'GET /internal/observability/assistant/alert_details_contextual_insights', + params: { + query: { + alert_started_at: new Date(end).toISOString(), + 'container.id': 'my-container-a', + }, + }, + }); + }); + + it('returns service summary', () => { + const serviceSummary = response.body.alertContext.find( + ({ key }) => key === 'serviceSummary' + ); + expect(serviceSummary?.data).to.eql({ + 'service.name': 'Backend', + 'service.environment': ['production'], + 'agent.name': 'java', + 'service.version': ['1.0.0'], + 'language.name': 'java', + instances: 1, + anomalies: [], + alerts: [], + deployments: [], + }); + }); + + it('returns downstream dependencies', async () => { + const downstreamDependencies = response.body.alertContext.find( + ({ key }) => key === 'downstreamDependencies' + ); + expect(downstreamDependencies?.data).to.eql([ + { + 'span.destination.service.resource': 'elasticsearch', + 'span.type': 'db', + 'span.subtype': 'elasticsearch', + }, + ]); + }); + + it('returns log categories', () => { + const logCategories = response.body.alertContext.find( + ({ key }) => key === 'logCategories' + )?.data as LogCategory[]; + expect(logCategories).to.have.length(1); + + const logCategory = logCategories[0]; + expect(logCategory?.sampleMessage).to.match( + /Error message #\d{16} from container my-container-a/ + ); + expect(logCategory?.docCount).to.be.greaterThan(0); + expect(logCategory?.errorCategory).to.be('Error message from container my-container-a'); + }); + }); + + describe('when non-existing container id is specified', async () => { + let response: SupertestReturnType<'GET /internal/observability/assistant/alert_details_contextual_insights'>; + before(async () => { + response = await obsApiClient.adminUser({ + endpoint: 'GET /internal/observability/assistant/alert_details_contextual_insights', + params: { + query: { + alert_started_at: new Date(end).toISOString(), + 'container.id': 'non-existing-container', + }, + }, + }); + }); + + it('returns nothing', () => { + expect(response.body.alertContext).to.eql([]); + }); + }); + + describe('when non-existing service.name is specified', async () => { + let response: SupertestReturnType<'GET /internal/observability/assistant/alert_details_contextual_insights'>; + before(async () => { + response = await obsApiClient.adminUser({ + endpoint: 'GET /internal/observability/assistant/alert_details_contextual_insights', + params: { + query: { + alert_started_at: new Date(end).toISOString(), + 'service.name': 'non-existing-service', + }, + }, + }); + }); + + it('returns empty service summary', () => { + const serviceSummary = response.body.alertContext.find( + ({ key }) => key === 'serviceSummary' + ); + expect(serviceSummary?.data).to.eql({ + 'service.name': 'non-existing-service', + 'service.environment': [], + instances: 1, + anomalies: [], + alerts: [], + deployments: [], + }); + }); + + it('returns no downstream dependencies', async () => { + const downstreamDependencies = response.body.alertContext.find( + ({ key }) => key === 'downstreamDependencies' + ); + expect(downstreamDependencies).to.eql(undefined); + }); + + it('returns log categories', () => { + const logCategories = response.body.alertContext.find( + ({ key }) => key === 'logCategories' + )?.data as LogCategory[]; + expect(logCategories).to.have.length(1); + }); + }); + }); + + describe('when traces and logs are ingested and logs are annotated with service.name', async () => { + before(async () => { + await ingestTraces({ 'service.name': 'Backend', 'container.id': 'my-container-a' }); + await ingestLogs({ + 'service.name': 'Backend', + 'container.id': 'my-container-a', + 'kubernetes.pod.name': 'pod-a', + }); + + // also ingest unrelated Frontend traces and logs that should not show up in the response when fetching "Backend"-related things + await ingestTraces({ 'service.name': 'Frontend', 'container.id': 'my-container-b' }); + await ingestLogs({ + 'service.name': 'Frontend', + 'container.id': 'my-container-b', + 'kubernetes.pod.name': 'pod-b', + }); + + // also ingest logs that are not annotated with service.name + await ingestLogs({ + 'container.id': 'my-container-c', + 'kubernetes.pod.name': 'pod-c', + }); + }); + + after(async () => { + await cleanup(); + }); + + describe('when no params are specified', async () => { + let response: SupertestReturnType<'GET /internal/observability/assistant/alert_details_contextual_insights'>; + before(async () => { + response = await obsApiClient.adminUser({ + endpoint: 'GET /internal/observability/assistant/alert_details_contextual_insights', + params: { + query: { + alert_started_at: new Date(end).toISOString(), + }, + }, + }); + }); + + it('returns no service summary', async () => { + const serviceSummary = response.body.alertContext.find( + ({ key }) => key === 'serviceSummary' + ); + expect(serviceSummary).to.be(undefined); + }); + + it('returns 1 log category', async () => { + const logCategories = response.body.alertContext.find( + ({ key }) => key === 'logCategories' + )?.data as LogCategory[]; + expect( + logCategories.map(({ errorCategory }: { errorCategory: string }) => errorCategory) + ).to.eql(['Error message from service', 'Error message from container my-container-c']); + }); + }); + + describe('when service name is specified', async () => { + let response: SupertestReturnType<'GET /internal/observability/assistant/alert_details_contextual_insights'>; + before(async () => { + response = await obsApiClient.adminUser({ + endpoint: 'GET /internal/observability/assistant/alert_details_contextual_insights', + params: { + query: { + alert_started_at: new Date(end).toISOString(), + 'service.name': 'Backend', + }, + }, + }); + }); + + it('returns log categories', () => { + const logCategories = response.body.alertContext.find( + ({ key }) => key === 'logCategories' + )?.data as LogCategory[]; + expect(logCategories).to.have.length(1); + + const logCategory = logCategories[0]; + expect(logCategory?.sampleMessage).to.match(/Error message #\d{16} from service Backend/); + expect(logCategory?.docCount).to.be.greaterThan(0); + expect(logCategory?.errorCategory).to.be('Error message from service Backend'); + }); + }); + + describe('when container id is specified', async () => { + let response: SupertestReturnType<'GET /internal/observability/assistant/alert_details_contextual_insights'>; + before(async () => { + response = await obsApiClient.adminUser({ + endpoint: 'GET /internal/observability/assistant/alert_details_contextual_insights', + params: { + query: { + alert_started_at: new Date(end).toISOString(), + 'container.id': 'my-container-a', + }, + }, + }); + }); + + it('returns log categories', () => { + const logCategories = response.body.alertContext.find( + ({ key }) => key === 'logCategories' + )?.data as LogCategory[]; + expect(logCategories).to.have.length(1); + + const logCategory = logCategories[0]; + expect(logCategory?.sampleMessage).to.match(/Error message #\d{16} from service Backend/); + expect(logCategory?.docCount).to.be.greaterThan(0); + expect(logCategory?.errorCategory).to.be('Error message from service Backend'); + }); + }); + + describe('when non-existing service.name is specified', async () => { + let response: SupertestReturnType<'GET /internal/observability/assistant/alert_details_contextual_insights'>; + before(async () => { + response = await obsApiClient.adminUser({ + endpoint: 'GET /internal/observability/assistant/alert_details_contextual_insights', + params: { + query: { + alert_started_at: new Date(end).toISOString(), + 'service.name': 'non-existing-service', + }, + }, + }); + }); + + it('returns empty service summary', () => { + const serviceSummary = response.body.alertContext.find( + ({ key }) => key === 'serviceSummary' + ); + expect(serviceSummary?.data).to.eql({ + 'service.name': 'non-existing-service', + 'service.environment': [], + instances: 1, + anomalies: [], + alerts: [], + deployments: [], + }); + }); + + it('does not return log categories', () => { + const logCategories = response.body.alertContext.find( + ({ key }) => key === 'logCategories' + )?.data as LogCategory[]; + expect(logCategories).to.have.length(1); + + expect( + logCategories.map(({ errorCategory }: { errorCategory: string }) => errorCategory) + ).to.eql(['Error message from container my-container-c']); + }); + }); + }); + + function ingestTraces(eventMetadata: { + 'service.name': string; + 'container.id'?: string; + 'host.name'?: string; + 'kubernetes.pod.name'?: string; + }) { + const serviceInstance = apm + .service({ + name: eventMetadata['service.name'], + environment: 'production', + agentName: 'java', + }) + .instance('my-instance'); + + const events = range + .interval('1m') + .rate(1) + .generator((timestamp) => { + return serviceInstance + .transaction({ transactionName: 'tx' }) + .timestamp(timestamp) + .duration(10000) + .defaults({ 'service.version': '1.0.0', ...eventMetadata }) + .outcome('success') + .children( + serviceInstance + .span({ + spanName: 'GET apm-*/_search', + spanType: 'db', + spanSubtype: 'elasticsearch', + }) + .duration(1000) + .success() + .destination('elasticsearch') + .timestamp(timestamp) + ); + }); + + return apmSynthtraceClient.index(events); + } + + function ingestLogs(eventMetadata: { + 'service.name'?: string; + 'container.id'?: string; + 'kubernetes.pod.name'?: string; + 'host.name'?: string; + }) { + const getMessage = () => { + const msgPrefix = `Error message #${generateShortId()}`; + + if (eventMetadata['service.name']) { + return `${msgPrefix} from service ${eventMetadata['service.name']}`; + } + + if (eventMetadata['container.id']) { + return `${msgPrefix} from container ${eventMetadata['container.id']}`; + } + + if (eventMetadata['kubernetes.pod.name']) { + return `${msgPrefix} from pod ${eventMetadata['kubernetes.pod.name']}`; + } + + if (eventMetadata['host.name']) { + return `${msgPrefix} from host ${eventMetadata['host.name']}`; + } + + return msgPrefix; + }; + + const events = range + .interval('1m') + .rate(1) + .generator((timestamp) => { + return [ + log + .create() + .message(getMessage()) + .logLevel('error') + .defaults({ + 'trace.id': generateShortId(), + 'agent.name': 'synth-agent', + ...eventMetadata, + }) + .timestamp(timestamp), + ]; + }); + + return logSynthtraceClient.index(events); + } + + async function cleanup() { + await apmSynthtraceClient.clean(); + await logSynthtraceClient.clean(); + } + }); +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts index 8b59070202b087..fb7543b9fe700e 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts @@ -5,7 +5,7 @@ * 2.0. */ -import expect from '@kbn/expect'; +import expect from 'expect'; import { DETECTION_ENGINE_RULES_BULK_ACTION, DETECTION_ENGINE_RULES_URL, @@ -19,9 +19,11 @@ import { getCreateExceptionListDetectionSchemaMock } from '@kbn/lists-plugin/com import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; import { getCreateExceptionListItemMinimalSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_item_schema.mock'; import { WebhookAuthType } from '@kbn/stack-connectors-plugin/common/webhook/constants'; +import { BaseDefaultableFields } from '@kbn/security-solution-plugin/common/api/detection_engine'; import { binaryToString, getSimpleMlRule, + getCustomQueryRuleParams, getSimpleRule, getSimpleRuleOutput, getSlackAction, @@ -42,6 +44,7 @@ import { FtrProviderContext } from '../../../../../ftr_provider_context'; export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); + const securitySolutionApi = getService('securitySolutionApi'); const es = getService('es'); const log = getService('log'); const esArchiver = getService('esArchiver'); @@ -98,7 +101,9 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should export rules', async () => { - await createRule(supertest, log, getSimpleRule()); + const mockRule = getCustomQueryRuleParams(); + + await securitySolutionApi.createRule({ body: mockRule }); const { body } = await postBulkAction() .send({ query: '', action: BulkActionTypeEnum.export }) @@ -109,12 +114,8 @@ export default ({ getService }: FtrProviderContext): void => { const [ruleJson, exportDetailsJson] = body.toString().split(/\n/); - const rule = removeServerGeneratedProperties(JSON.parse(ruleJson)); - const expectedRule = updateUsername(getSimpleRuleOutput(), ELASTICSEARCH_USERNAME); - expect(rule).to.eql(expectedRule); - - const exportDetails = JSON.parse(exportDetailsJson); - expect(exportDetails).to.eql({ + expect(JSON.parse(ruleJson)).toMatchObject(mockRule); + expect(JSON.parse(exportDetailsJson)).toEqual({ exported_exception_list_count: 0, exported_exception_list_item_count: 0, exported_count: 1, @@ -132,6 +133,35 @@ export default ({ getService }: FtrProviderContext): void => { missing_action_connections: [], }); }); + + it('should export rules with defaultbale fields when values are set', async () => { + const defaultableFields: BaseDefaultableFields = { + related_integrations: [ + { package: 'package-a', version: '^1.2.3' }, + { package: 'package-b', integration: 'integration-b', version: '~1.1.1' }, + ], + }; + const mockRule = getCustomQueryRuleParams(defaultableFields); + + await securitySolutionApi.createRule({ body: mockRule }); + + const { body } = await securitySolutionApi + .performBulkAction({ + query: {}, + body: { + action: BulkActionTypeEnum.export, + }, + }) + .expect(200) + .expect('Content-Type', 'application/ndjson') + .expect('Content-Disposition', 'attachment; filename="rules_export.ndjson"') + .parse(binaryToString); + + const [ruleJson] = body.toString().split(/\n/); + + expect(JSON.parse(ruleJson)).toMatchObject(defaultableFields); + }); + it('should export rules with actions connectors', async () => { // create new actions const webHookAction = await createWebHookConnector(); @@ -184,7 +214,7 @@ export default ({ getService }: FtrProviderContext): void => { const rule = removeServerGeneratedProperties(JSON.parse(ruleJson)); const expectedRule = updateUsername(getSimpleRuleOutput(), ELASTICSEARCH_USERNAME); - expect(rule).to.eql({ + expect(rule).toEqual({ ...expectedRule, actions: [ { @@ -200,14 +230,14 @@ export default ({ getService }: FtrProviderContext): void => { ], }); const { attributes, id, type } = JSON.parse(connectorsJson); - expect(attributes.actionTypeId).to.eql(exportedConnectors.attributes.actionTypeId); - expect(id).to.eql(exportedConnectors.id); - expect(type).to.eql(exportedConnectors.type); - expect(attributes.name).to.eql(exportedConnectors.attributes.name); - expect(attributes.secrets).to.eql(exportedConnectors.attributes.secrets); - expect(attributes.isMissingSecrets).to.eql(exportedConnectors.attributes.isMissingSecrets); + expect(attributes.actionTypeId).toEqual(exportedConnectors.attributes.actionTypeId); + expect(id).toEqual(exportedConnectors.id); + expect(type).toEqual(exportedConnectors.type); + expect(attributes.name).toEqual(exportedConnectors.attributes.name); + expect(attributes.secrets).toEqual(exportedConnectors.attributes.secrets); + expect(attributes.isMissingSecrets).toEqual(exportedConnectors.attributes.isMissingSecrets); const exportDetails = JSON.parse(exportDetailsJson); - expect(exportDetails).to.eql({ + expect(exportDetails).toEqual({ exported_exception_list_count: 0, exported_exception_list_item_count: 0, exported_count: 2, @@ -235,10 +265,10 @@ export default ({ getService }: FtrProviderContext): void => { .send({ query: '', action: BulkActionTypeEnum.delete }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the deleted rule is returned with the response - expect(body.attributes.results.deleted[0].name).to.eql(testRule.name); + expect(body.attributes.results.deleted[0].name).toEqual(testRule.name); // Check that the updates have been persisted await fetchRule(ruleId).expect(404); @@ -252,14 +282,14 @@ export default ({ getService }: FtrProviderContext): void => { .send({ query: '', action: BulkActionTypeEnum.enable }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].enabled).to.eql(true); + expect(body.attributes.results.updated[0].enabled).toEqual(true); // Check that the updates have been persisted const { body: ruleBody } = await fetchRule(ruleId).expect(200); - expect(ruleBody.enabled).to.eql(true); + expect(ruleBody.enabled).toEqual(true); }); it('should disable rules', async () => { @@ -270,20 +300,27 @@ export default ({ getService }: FtrProviderContext): void => { .send({ query: '', action: BulkActionTypeEnum.disable }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].enabled).to.eql(false); + expect(body.attributes.results.updated[0].enabled).toEqual(false); // Check that the updates have been persisted const { body: ruleBody } = await fetchRule(ruleId).expect(200); - expect(ruleBody.enabled).to.eql(false); + expect(ruleBody.enabled).toEqual(false); }); it('should duplicate rules', async () => { const ruleId = 'ruleId'; - const ruleToDuplicate = getSimpleRule(ruleId); - await createRule(supertest, log, ruleToDuplicate); + const ruleToDuplicate = getCustomQueryRuleParams({ + rule_id: ruleId, + related_integrations: [ + { package: 'package-a', version: '^1.2.3' }, + { package: 'package-b', integration: 'integration-b', version: '~1.1.1' }, + ], + }); + + await securitySolutionApi.createRule({ body: ruleToDuplicate }); const { body } = await postBulkAction() .send({ @@ -293,19 +330,30 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the duplicated rule is returned with the response - expect(body.attributes.results.created[0].name).to.eql(`${ruleToDuplicate.name} [Duplicate]`); + expect(body.attributes.results.created[0].name).toEqual( + `${ruleToDuplicate.name} [Duplicate]` + ); // Check that the updates have been persisted - const { body: rulesResponse } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL}/_find`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') + const { body: rulesResponse } = await securitySolutionApi.findRules({ query: {} }); + + expect(rulesResponse.total).toEqual(2); + + const duplicatedRuleId = body.attributes.results.created[0].id; + const { body: duplicatedRule } = await securitySolutionApi + .readRule({ + query: { id: duplicatedRuleId }, + }) .expect(200); - expect(rulesResponse.total).to.eql(2); + expect(duplicatedRule).toMatchObject({ + ...ruleToDuplicate, + name: `${ruleToDuplicate.name} [Duplicate]`, + rule_id: expect.any(String), + }); }); it('should duplicate rules with exceptions - expired exceptions included', async () => { @@ -381,15 +429,17 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); // Item should have been duplicated, even if expired - expect(foundItems.total).to.eql(1); + expect(foundItems.total).toEqual(1); - expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the duplicated rule is returned with the response - expect(body.attributes.results.created[0].name).to.eql(`${ruleToDuplicate.name} [Duplicate]`); + expect(body.attributes.results.created[0].name).toEqual( + `${ruleToDuplicate.name} [Duplicate]` + ); // Check that the exceptions are duplicated - expect(body.attributes.results.created[0].exceptions_list).to.eql([ + expect(body.attributes.results.created[0].exceptions_list).toEqual([ { type: exceptionList.type, list_id: exceptionList.list_id, @@ -411,7 +461,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('elastic-api-version', '2023-10-31') .expect(200); - expect(rulesResponse.total).to.eql(2); + expect(rulesResponse.total).toEqual(2); }); it('should duplicate rules with exceptions - expired exceptions excluded', async () => { @@ -487,15 +537,17 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); // Item should NOT have been duplicated, since it is expired - expect(foundItems.total).to.eql(0); + expect(foundItems.total).toEqual(0); - expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the duplicated rule is returned with the response - expect(body.attributes.results.created[0].name).to.eql(`${ruleToDuplicate.name} [Duplicate]`); + expect(body.attributes.results.created[0].name).toEqual( + `${ruleToDuplicate.name} [Duplicate]` + ); // Check that the exceptions are duplicted - expect(body.attributes.results.created[0].exceptions_list).to.eql([ + expect(body.attributes.results.created[0].exceptions_list).toEqual([ { type: exceptionList.type, list_id: exceptionList.list_id, @@ -517,7 +569,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .expect(200); - expect(rulesResponse.total).to.eql(2); + expect(rulesResponse.total).toEqual(2); }); describe('edit action', () => { @@ -575,7 +627,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(bulkEditResponse.attributes.summary).to.eql({ + expect(bulkEditResponse.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, @@ -583,12 +635,12 @@ export default ({ getService }: FtrProviderContext): void => { }); // Check that the updated rule is returned with the response - expect(bulkEditResponse.attributes.results.updated[0].tags).to.eql(resultingTags); + expect(bulkEditResponse.attributes.results.updated[0].tags).toEqual(resultingTags); // Check that the updates have been persisted const { body: updatedRule } = await fetchRule(ruleId).expect(200); - expect(updatedRule.tags).to.eql(resultingTags); + expect(updatedRule.tags).toEqual(resultingTags); }); }); @@ -632,7 +684,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(bulkEditResponse.attributes.summary).to.eql({ + expect(bulkEditResponse.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, @@ -640,12 +692,12 @@ export default ({ getService }: FtrProviderContext): void => { }); // Check that the updated rule is returned with the response - expect(bulkEditResponse.attributes.results.updated[0].tags).to.eql(resultingTags); + expect(bulkEditResponse.attributes.results.updated[0].tags).toEqual(resultingTags); // Check that the updates have been persisted const { body: updatedRule } = await fetchRule(ruleId).expect(200); - expect(updatedRule.tags).to.eql(resultingTags); + expect(updatedRule.tags).toEqual(resultingTags); }); }); @@ -688,7 +740,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(bulkEditResponse.attributes.summary).to.eql({ + expect(bulkEditResponse.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, @@ -696,12 +748,12 @@ export default ({ getService }: FtrProviderContext): void => { }); // Check that the updated rule is returned with the response - expect(bulkEditResponse.attributes.results.updated[0].tags).to.eql(resultingTags); + expect(bulkEditResponse.attributes.results.updated[0].tags).toEqual(resultingTags); // Check that the updates have been persisted const { body: updatedRule } = await fetchRule(ruleId).expect(200); - expect(updatedRule.tags).to.eql(resultingTags); + expect(updatedRule.tags).toEqual(resultingTags); }); }); @@ -765,7 +817,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(bulkEditResponse.attributes.summary).to.eql({ + expect(bulkEditResponse.attributes.summary).toEqual({ failed: 0, skipped: 1, succeeded: 0, @@ -773,14 +825,14 @@ export default ({ getService }: FtrProviderContext): void => { }); // Check that the rules is returned as skipped with expected skip reason - expect(bulkEditResponse.attributes.results.skipped[0].skip_reason).to.eql( + expect(bulkEditResponse.attributes.results.skipped[0].skip_reason).toEqual( 'RULE_NOT_MODIFIED' ); // Check that the no changes have been persisted const { body: updatedRule } = await fetchRule(ruleId).expect(200); - expect(updatedRule.tags).to.eql(resultingTags); + expect(updatedRule.tags).toEqual(resultingTags); }); } ); @@ -804,7 +856,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(bulkEditResponse.attributes.summary).to.eql({ + expect(bulkEditResponse.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, @@ -812,12 +864,12 @@ export default ({ getService }: FtrProviderContext): void => { }); // Check that the updated rule is returned with the response - expect(bulkEditResponse.attributes.results.updated[0].index).to.eql(['initial-index-*']); + expect(bulkEditResponse.attributes.results.updated[0].index).toEqual(['initial-index-*']); // Check that the updates have been persisted const { body: updatedRule } = await fetchRule(ruleId).expect(200); - expect(updatedRule.index).to.eql(['initial-index-*']); + expect(updatedRule.index).toEqual(['initial-index-*']); }); it('should add index patterns to rules', async () => { @@ -839,7 +891,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(bulkEditResponse.attributes.summary).to.eql({ + expect(bulkEditResponse.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, @@ -847,14 +899,14 @@ export default ({ getService }: FtrProviderContext): void => { }); // Check that the updated rule is returned with the response - expect(bulkEditResponse.attributes.results.updated[0].index).to.eql( + expect(bulkEditResponse.attributes.results.updated[0].index).toEqual( resultingIndexPatterns ); // Check that the updates have been persisted const { body: updatedRule } = await fetchRule(ruleId).expect(200); - expect(updatedRule.index).to.eql(resultingIndexPatterns); + expect(updatedRule.index).toEqual(resultingIndexPatterns); }); it('should delete index patterns from rules', async () => { @@ -876,7 +928,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(bulkEditResponse.attributes.summary).to.eql({ + expect(bulkEditResponse.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, @@ -884,14 +936,14 @@ export default ({ getService }: FtrProviderContext): void => { }); // Check that the updated rule is returned with the response - expect(bulkEditResponse.attributes.results.updated[0].index).to.eql( + expect(bulkEditResponse.attributes.results.updated[0].index).toEqual( resultingIndexPatterns ); // Check that the updates have been persisted const { body: updatedRule } = await fetchRule(ruleId).expect(200); - expect(updatedRule.index).to.eql(resultingIndexPatterns); + expect(updatedRule.index).toEqual(resultingIndexPatterns); }); it('should return error if index patterns action is applied to machine learning rule', async () => { @@ -910,8 +962,13 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(500); - expect(body.attributes.summary).to.eql({ failed: 1, skipped: 0, succeeded: 0, total: 1 }); - expect(body.attributes.errors[0]).to.eql({ + expect(body.attributes.summary).toEqual({ + failed: 1, + skipped: 0, + succeeded: 0, + total: 1, + }); + expect(body.attributes.errors[0]).toEqual({ message: "Index patterns can't be added. Machine learning rule doesn't have index patterns property", status_code: 500, @@ -943,8 +1000,13 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(500); - expect(body.attributes.summary).to.eql({ failed: 1, skipped: 0, succeeded: 0, total: 1 }); - expect(body.attributes.errors[0]).to.eql({ + expect(body.attributes.summary).toEqual({ + failed: 1, + skipped: 0, + succeeded: 0, + total: 1, + }); + expect(body.attributes.errors[0]).toEqual({ message: "Mutated params invalid: Index patterns can't be empty", status_code: 500, rules: [ @@ -976,8 +1038,13 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(500); - expect(body.attributes.summary).to.eql({ failed: 1, skipped: 0, succeeded: 0, total: 1 }); - expect(body.attributes.errors[0]).to.eql({ + expect(body.attributes.summary).toEqual({ + failed: 1, + skipped: 0, + succeeded: 0, + total: 1, + }); + expect(body.attributes.errors[0]).toEqual({ message: "Mutated params invalid: Index patterns can't be empty", status_code: 500, rules: [ @@ -991,7 +1058,7 @@ export default ({ getService }: FtrProviderContext): void => { // Check that the rule hasn't been updated const { body: reFetchedRule } = await fetchRule(ruleId).expect(200); - expect(reFetchedRule.index).to.eql(['simple-index-*']); + expect(reFetchedRule.index).toEqual(['simple-index-*']); }); const skipIndexPatternsUpdateCases = [ @@ -1063,7 +1130,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(bulkEditResponse.attributes.summary).to.eql({ + expect(bulkEditResponse.attributes.summary).toEqual({ failed: 0, skipped: 1, succeeded: 0, @@ -1071,14 +1138,14 @@ export default ({ getService }: FtrProviderContext): void => { }); // Check that the rules is returned as skipped with expected skip reason - expect(bulkEditResponse.attributes.results.skipped[0].skip_reason).to.eql( + expect(bulkEditResponse.attributes.results.skipped[0].skip_reason).toEqual( 'RULE_NOT_MODIFIED' ); // Check that the no changes have been persisted const { body: updatedRule } = await fetchRule(ruleId).expect(200); - expect(updatedRule.index).to.eql(resultingIndexPatterns); + expect(updatedRule.index).toEqual(resultingIndexPatterns); }); } ); @@ -1106,17 +1173,17 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].timeline_id).to.eql(timelineId); - expect(body.attributes.results.updated[0].timeline_title).to.eql(timelineTitle); + expect(body.attributes.results.updated[0].timeline_id).toEqual(timelineId); + expect(body.attributes.results.updated[0].timeline_title).toEqual(timelineTitle); // Check that the updates have been persisted const { body: rule } = await fetchRule(ruleId).expect(200); - expect(rule.timeline_id).to.eql(timelineId); - expect(rule.timeline_title).to.eql(timelineTitle); + expect(rule.timeline_id).toEqual(timelineId); + expect(rule.timeline_title).toEqual(timelineTitle); }); it('should correctly remove timeline template', async () => { @@ -1130,8 +1197,8 @@ export default ({ getService }: FtrProviderContext): void => { }); // ensure rule has been created with timeline properties - expect(createdRule.timeline_id).to.be(timelineId); - expect(createdRule.timeline_title).to.be(timelineTitle); + expect(createdRule.timeline_id).toBe(timelineId); + expect(createdRule.timeline_title).toBe(timelineTitle); const { body } = await postBulkAction() .send({ @@ -1149,17 +1216,17 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].timeline_id).to.be(undefined); - expect(body.attributes.results.updated[0].timeline_title).to.be(undefined); + expect(body.attributes.results.updated[0].timeline_id).toBe(undefined); + expect(body.attributes.results.updated[0].timeline_title).toBe(undefined); // Check that the updates have been persisted const { body: rule } = await fetchRule(ruleId).expect(200); - expect(rule.timeline_id).to.be(undefined); - expect(rule.timeline_title).to.be(undefined); + expect(rule.timeline_id).toBe(undefined); + expect(rule.timeline_title).toBe(undefined); }); it('should return error if index patterns action is applied to machine learning rule', async () => { @@ -1178,8 +1245,8 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(500); - expect(body.attributes.summary).to.eql({ failed: 1, skipped: 0, succeeded: 0, total: 1 }); - expect(body.attributes.errors[0]).to.eql({ + expect(body.attributes.summary).toEqual({ failed: 1, skipped: 0, succeeded: 0, total: 1 }); + expect(body.attributes.errors[0]).toEqual({ message: "Index patterns can't be added. Machine learning rule doesn't have index patterns property", status_code: 500, @@ -1211,8 +1278,8 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(500); - expect(body.attributes.summary).to.eql({ failed: 1, skipped: 0, succeeded: 0, total: 1 }); - expect(body.attributes.errors[0]).to.eql({ + expect(body.attributes.summary).toEqual({ failed: 1, skipped: 0, succeeded: 0, total: 1 }); + expect(body.attributes.errors[0]).toEqual({ message: "Mutated params invalid: Index patterns can't be empty", status_code: 500, rules: [ @@ -1240,12 +1307,12 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.results.updated[0].version).to.be(rule.version + 1); + expect(body.attributes.results.updated[0].version).toBe(rule.version + 1); // Check that the updates have been persisted const { body: updatedRule } = await fetchRule(ruleId).expect(200); - expect(updatedRule.version).to.be(rule.version + 1); + expect(updatedRule.version).toBe(rule.version + 1); }); describe('prebuilt rules', () => { @@ -1301,13 +1368,13 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(500); - expect(body.attributes.summary).to.eql({ + expect(body.attributes.summary).toEqual({ failed: 1, skipped: 0, succeeded: 0, total: 1, }); - expect(body.attributes.errors[0]).to.eql({ + expect(body.attributes.errors[0]).toEqual({ message: "Elastic rule can't be edited", status_code: 500, rules: [ @@ -1369,12 +1436,12 @@ export default ({ getService }: FtrProviderContext): void => { ]; // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].actions).to.eql(expectedRuleActions); + expect(body.attributes.results.updated[0].actions).toEqual(expectedRuleActions); // Check that the updates have been persisted const { body: readRule } = await fetchRule(ruleId).expect(200); - expect(readRule.actions).to.eql(expectedRuleActions); + expect(readRule.actions).toEqual(expectedRuleActions); }); it('should set action correctly to existing non empty actions list', async () => { @@ -1427,12 +1494,12 @@ export default ({ getService }: FtrProviderContext): void => { ]; // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].actions).to.eql(expectedRuleActions); + expect(body.attributes.results.updated[0].actions).toEqual(expectedRuleActions); // Check that the updates have been persisted const { body: readRule } = await fetchRule(ruleId).expect(200); - expect(readRule.actions).to.eql(expectedRuleActions); + expect(readRule.actions).toEqual(expectedRuleActions); }); it('should set actions to empty list, actions payload is empty list', async () => { @@ -1472,12 +1539,12 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].actions).to.eql([]); + expect(body.attributes.results.updated[0].actions).toEqual([]); // Check that the updates have been persisted const { body: readRule } = await fetchRule(ruleId).expect(200); - expect(readRule.actions).to.eql([]); + expect(readRule.actions).toEqual([]); }); }); @@ -1521,12 +1588,12 @@ export default ({ getService }: FtrProviderContext): void => { ]; // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].actions).to.eql(expectedRuleActions); + expect(body.attributes.results.updated[0].actions).toEqual(expectedRuleActions); // Check that the updates have been persisted const { body: readRule } = await fetchRule(ruleId).expect(200); - expect(readRule.actions).to.eql(expectedRuleActions); + expect(readRule.actions).toEqual(expectedRuleActions); }); it('should add action correctly to non empty actions list of the same type', async () => { @@ -1586,12 +1653,12 @@ export default ({ getService }: FtrProviderContext): void => { ]; // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].actions).to.eql(expectedRuleActions); + expect(body.attributes.results.updated[0].actions).toEqual(expectedRuleActions); // Check that the updates have been persisted const { body: readRule } = await fetchRule(ruleId).expect(200); - expect(readRule.actions).to.eql(expectedRuleActions); + expect(readRule.actions).toEqual(expectedRuleActions); }); it('should add action correctly to non empty actions list of a different type', async () => { @@ -1659,12 +1726,12 @@ export default ({ getService }: FtrProviderContext): void => { ]; // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].actions).to.eql(expectedRuleActions); + expect(body.attributes.results.updated[0].actions).toEqual(expectedRuleActions); // Check that the updates have been persisted const { body: readRule } = await fetchRule(ruleId).expect(200); - expect(readRule.actions).to.eql(expectedRuleActions); + expect(readRule.actions).toEqual(expectedRuleActions); }); it('should not change actions of rule if empty list of actions added', async () => { @@ -1704,12 +1771,12 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); // Check that the rule is skipped and was not updated - expect(body.attributes.results.skipped[0].id).to.eql(createdRule.id); + expect(body.attributes.results.skipped[0].id).toEqual(createdRule.id); // Check that the updates have been persisted const { body: readRule } = await fetchRule(ruleId).expect(200); - expect(readRule.actions).to.eql([ + expect(readRule.actions).toEqual([ { ...defaultRuleAction, uuid: createdRule.actions[0].uuid, @@ -1755,13 +1822,13 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); // Check that the rule is skipped and was not updated - expect(body.attributes.results.skipped[0].id).to.eql(createdRule.id); + expect(body.attributes.results.skipped[0].id).toEqual(createdRule.id); // Check that the updates have been persisted const { body: readRule } = await fetchRule(ruleId).expect(200); - expect(readRule.throttle).to.eql(undefined); - expect(readRule.actions).to.eql(createdRule.actions); + expect(readRule.throttle).toEqual(undefined); + expect(readRule.actions).toEqual(createdRule.actions); }); }); @@ -1803,7 +1870,7 @@ export default ({ getService }: FtrProviderContext): void => { const editedRule = body.attributes.results.updated[0]; // Check that the updated rule is returned with the response - expect(editedRule.actions).to.eql([ + expect(editedRule.actions).toEqual([ { ...webHookActionMock, id: webHookConnector.id, @@ -1813,12 +1880,12 @@ export default ({ getService }: FtrProviderContext): void => { }, ]); // version of prebuilt rule should not change - expect(editedRule.version).to.be(prebuiltRule.version); + expect(editedRule.version).toBe(prebuiltRule.version); // Check that the updates have been persisted const { body: readRule } = await fetchRule(prebuiltRule.rule_id).expect(200); - expect(readRule.actions).to.eql([ + expect(readRule.actions).toEqual([ { ...webHookActionMock, id: webHookConnector.id, @@ -1827,7 +1894,7 @@ export default ({ getService }: FtrProviderContext): void => { frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); - expect(prebuiltRule.version).to.be(readRule.version); + expect(prebuiltRule.version).toBe(readRule.version); }); }); @@ -1863,13 +1930,13 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(500); - expect(body.attributes.summary).to.eql({ + expect(body.attributes.summary).toEqual({ failed: 1, skipped: 0, succeeded: 0, total: 1, }); - expect(body.attributes.errors[0]).to.eql({ + expect(body.attributes.errors[0]).toEqual({ message: "Elastic rule can't be edited", status_code: 500, rules: [ @@ -1883,9 +1950,9 @@ export default ({ getService }: FtrProviderContext): void => { // Check that the updates were not made const { body: readRule } = await fetchRule(prebuiltRule.rule_id).expect(200); - expect(readRule.actions).to.eql(prebuiltRule.actions); - expect(readRule.tags).to.eql(prebuiltRule.tags); - expect(readRule.version).to.be(prebuiltRule.version); + expect(readRule.actions).toEqual(prebuiltRule.actions); + expect(readRule.tags).toEqual(prebuiltRule.tags); + expect(readRule.version).toBe(prebuiltRule.version); }); }); @@ -1925,12 +1992,12 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); // Check that the rule is skipped and was not updated - expect(body.attributes.results.skipped[0].id).to.eql(createdRule.id); + expect(body.attributes.results.skipped[0].id).toEqual(createdRule.id); // Check that the updates have been persisted const { body: rule } = await fetchRule(ruleId).expect(200); - expect(rule.throttle).to.eql(undefined); + expect(rule.throttle).toEqual(undefined); }); }); @@ -1987,7 +2054,7 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].throttle).to.eql(expectedThrottle); + expect(body.attributes.results.updated[0].throttle).toEqual(expectedThrottle); const expectedActions = body.attributes.results.updated[0].actions.map( (action: any) => ({ @@ -2004,8 +2071,8 @@ export default ({ getService }: FtrProviderContext): void => { // Check that the updates have been persisted const { body: rule } = await fetchRule(ruleId).expect(200); - expect(rule.throttle).to.eql(expectedThrottle); - expect(rule.actions).to.eql(expectedActions); + expect(rule.throttle).toEqual(expectedThrottle); + expect(rule.actions).toEqual(expectedActions); }); }); }); @@ -2047,7 +2114,7 @@ export default ({ getService }: FtrProviderContext): void => { // Check whether notifyWhen set correctly const { body: rule } = await fetchRuleByAlertApi(createdRule.id).expect(200); - expect(rule.notify_when).to.eql(expected.notifyWhen); + expect(rule.notify_when).toEqual(expected.notifyWhen); }); }); }); @@ -2078,10 +2145,10 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(400); - expect(body.statusCode).to.eql(400); - expect(body.error).to.eql('Bad Request'); - expect(body.message).to.contain('edit.0.value.interval: Invalid'); - expect(body.message).to.contain('edit.0.value.lookback: Invalid'); + expect(body.statusCode).toEqual(400); + expect(body.error).toEqual('Bad Request'); + expect(body.message).toContain('edit.0.value.interval: Invalid'); + expect(body.message).toContain('edit.0.value.lookback: Invalid'); }); it('should update schedule values in rules with a valid payload', async () => { @@ -2108,11 +2175,16 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).toEqual({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); - expect(body.attributes.results.updated[0].interval).to.eql(interval); - expect(body.attributes.results.updated[0].meta).to.eql({ from: `${lookbackMinutes}m` }); - expect(body.attributes.results.updated[0].from).to.eql( + expect(body.attributes.results.updated[0].interval).toEqual(interval); + expect(body.attributes.results.updated[0].meta).toEqual({ from: `${lookbackMinutes}m` }); + expect(body.attributes.results.updated[0].from).toEqual( `now-${(intervalMinutes + lookbackMinutes) * 60}s` ); }); @@ -2144,7 +2216,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(setIndexBody.attributes.summary).to.eql({ + expect(setIndexBody.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, @@ -2152,13 +2224,13 @@ export default ({ getService }: FtrProviderContext): void => { }); // Check that the updated rule is returned with the response - expect(setIndexBody.attributes.results.updated[0].index).to.eql(['initial-index-*']); - expect(setIndexBody.attributes.results.updated[0].data_view_id).to.eql(undefined); + expect(setIndexBody.attributes.results.updated[0].index).toEqual(['initial-index-*']); + expect(setIndexBody.attributes.results.updated[0].data_view_id).toEqual(undefined); // Check that the updates have been persisted const { body: setIndexRule } = await fetchRule(ruleId).expect(200); - expect(setIndexRule.index).to.eql(['initial-index-*']); + expect(setIndexRule.index).toEqual(['initial-index-*']); }); it('should return skipped rule and NOT add an index pattern to a rule or overwrite the data view when overwrite_data_views is false', async () => { @@ -2185,25 +2257,25 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(setIndexBody.attributes.summary).to.eql({ + expect(setIndexBody.attributes.summary).toEqual({ failed: 0, skipped: 1, succeeded: 0, total: 1, }); - expect(setIndexBody.attributes.errors).to.be(undefined); + expect(setIndexBody.attributes.errors).toBe(undefined); // Check that the skipped rule is returned with the response - expect(setIndexBody.attributes.results.skipped[0].id).to.eql(simpleRule.id); - expect(setIndexBody.attributes.results.skipped[0].name).to.eql(simpleRule.name); - expect(setIndexBody.attributes.results.skipped[0].skip_reason).to.eql('RULE_NOT_MODIFIED'); + expect(setIndexBody.attributes.results.skipped[0].id).toEqual(simpleRule.id); + expect(setIndexBody.attributes.results.skipped[0].name).toEqual(simpleRule.name); + expect(setIndexBody.attributes.results.skipped[0].skip_reason).toEqual('RULE_NOT_MODIFIED'); // Check that the rule has not been updated const { body: setIndexRule } = await fetchRule(ruleId).expect(200); - expect(setIndexRule.index).to.eql(undefined); - expect(setIndexRule.data_view_id).to.eql(dataViewId); + expect(setIndexRule.index).toEqual(undefined); + expect(setIndexRule.data_view_id).toEqual(dataViewId); }); it('should set an index pattern to a rule and overwrite the data view when overwrite_data_views is true', async () => { @@ -2230,7 +2302,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(setIndexBody.attributes.summary).to.eql({ + expect(setIndexBody.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, @@ -2238,14 +2310,14 @@ export default ({ getService }: FtrProviderContext): void => { }); // Check that the updated rule is returned with the response - expect(setIndexBody.attributes.results.updated[0].index).to.eql(['initial-index-*']); - expect(setIndexBody.attributes.results.updated[0].data_view_id).to.eql(undefined); + expect(setIndexBody.attributes.results.updated[0].index).toEqual(['initial-index-*']); + expect(setIndexBody.attributes.results.updated[0].data_view_id).toEqual(undefined); // Check that the updates have been persisted const { body: setIndexRule } = await fetchRule(ruleId).expect(200); - expect(setIndexRule.index).to.eql(['initial-index-*']); - expect(setIndexRule.data_view_id).to.eql(undefined); + expect(setIndexRule.index).toEqual(['initial-index-*']); + expect(setIndexRule.data_view_id).toEqual(undefined); }); it('should return error when set an empty index pattern to a rule and overwrite the data view when overwrite_data_views is true', async () => { @@ -2271,8 +2343,8 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(500); - expect(body.attributes.summary).to.eql({ failed: 1, skipped: 0, succeeded: 0, total: 1 }); - expect(body.attributes.errors[0]).to.eql({ + expect(body.attributes.summary).toEqual({ failed: 1, skipped: 0, succeeded: 0, total: 1 }); + expect(body.attributes.errors[0]).toEqual({ message: "Mutated params invalid: Index patterns can't be empty", status_code: 500, rules: [ @@ -2307,25 +2379,25 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(setIndexBody.attributes.summary).to.eql({ + expect(setIndexBody.attributes.summary).toEqual({ failed: 0, skipped: 1, succeeded: 0, total: 1, }); - expect(setIndexBody.attributes.errors).to.be(undefined); + expect(setIndexBody.attributes.errors).toBe(undefined); // Check that the skipped rule is returned with the response - expect(setIndexBody.attributes.results.skipped[0].id).to.eql(simpleRule.id); - expect(setIndexBody.attributes.results.skipped[0].name).to.eql(simpleRule.name); - expect(setIndexBody.attributes.results.skipped[0].skip_reason).to.eql('RULE_NOT_MODIFIED'); + expect(setIndexBody.attributes.results.skipped[0].id).toEqual(simpleRule.id); + expect(setIndexBody.attributes.results.skipped[0].name).toEqual(simpleRule.name); + expect(setIndexBody.attributes.results.skipped[0].skip_reason).toEqual('RULE_NOT_MODIFIED'); // Check that the rule has not been updated const { body: setIndexRule } = await fetchRule(ruleId).expect(200); - expect(setIndexRule.index).to.eql(undefined); - expect(setIndexRule.data_view_id).to.eql(dataViewId); + expect(setIndexRule.index).toEqual(undefined); + expect(setIndexRule.data_view_id).toEqual(dataViewId); }); // This rule will now not have a source defined - as has been the behavior of rules since the beginning @@ -2353,7 +2425,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, @@ -2361,14 +2433,14 @@ export default ({ getService }: FtrProviderContext): void => { }); // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].index).to.eql(undefined); - expect(body.attributes.results.updated[0].data_view_id).to.eql(undefined); + expect(body.attributes.results.updated[0].index).toEqual(undefined); + expect(body.attributes.results.updated[0].data_view_id).toEqual(undefined); // Check that the updates have been persisted const { body: setIndexRule } = await fetchRule(ruleId).expect(200); - expect(setIndexRule.index).to.eql(undefined); - expect(setIndexRule.data_view_id).to.eql(undefined); + expect(setIndexRule.index).toEqual(undefined); + expect(setIndexRule.data_view_id).toEqual(undefined); }); it('should return error if all index patterns removed from a rule with data views and overwrite_data_views is true', async () => { @@ -2394,8 +2466,8 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(500); - expect(body.attributes.summary).to.eql({ failed: 1, skipped: 0, succeeded: 0, total: 1 }); - expect(body.attributes.errors[0]).to.eql({ + expect(body.attributes.summary).toEqual({ failed: 1, skipped: 0, succeeded: 0, total: 1 }); + expect(body.attributes.errors[0]).toEqual({ message: "Mutated params invalid: Index patterns can't be empty", status_code: 500, rules: [ @@ -2430,13 +2502,13 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, skipped: 1, succeeded: 0, total: 1 }); - expect(body.attributes.errors).to.be(undefined); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 1, succeeded: 0, total: 1 }); + expect(body.attributes.errors).toBe(undefined); // Check that the skipped rule is returned with the response - expect(body.attributes.results.skipped[0].id).to.eql(rule.id); - expect(body.attributes.results.skipped[0].name).to.eql(rule.name); - expect(body.attributes.results.skipped[0].skip_reason).to.eql('RULE_NOT_MODIFIED'); + expect(body.attributes.results.skipped[0].id).toEqual(rule.id); + expect(body.attributes.results.skipped[0].name).toEqual(rule.name); + expect(body.attributes.results.skipped[0].skip_reason).toEqual('RULE_NOT_MODIFIED'); }); }); @@ -2466,17 +2538,17 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].tags).to.eql(['tag1', 'tag2', 'tag3']); - expect(body.attributes.results.updated[0].index).to.eql(['index1-*', 'initial-index-*']); + expect(body.attributes.results.updated[0].tags).toEqual(['tag1', 'tag2', 'tag3']); + expect(body.attributes.results.updated[0].index).toEqual(['index1-*', 'initial-index-*']); // Check that the rule has been persisted const { body: updatedRule } = await fetchRule(ruleId).expect(200); - expect(updatedRule.index).to.eql(['index1-*', 'initial-index-*']); - expect(updatedRule.tags).to.eql(['tag1', 'tag2', 'tag3']); + expect(updatedRule.index).toEqual(['index1-*', 'initial-index-*']); + expect(updatedRule.tags).toEqual(['tag1', 'tag2', 'tag3']); }); it('should return one updated rule when applying one valid operation and one operation to be skipped on a rule', async () => { @@ -2506,17 +2578,17 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].tags).to.eql(['tag1', 'tag2']); - expect(body.attributes.results.updated[0].index).to.eql(['index1-*', 'initial-index-*']); + expect(body.attributes.results.updated[0].tags).toEqual(['tag1', 'tag2']); + expect(body.attributes.results.updated[0].index).toEqual(['index1-*', 'initial-index-*']); // Check that the rule has been persisted const { body: updatedRule } = await fetchRule(ruleId).expect(200); - expect(updatedRule.index).to.eql(['index1-*', 'initial-index-*']); - expect(updatedRule.tags).to.eql(['tag1', 'tag2']); + expect(updatedRule.index).toEqual(['index1-*', 'initial-index-*']); + expect(updatedRule.tags).toEqual(['tag1', 'tag2']); }); it('should return one skipped rule when two (all) operations result in a no-op', async () => { @@ -2546,18 +2618,18 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, skipped: 1, succeeded: 0, total: 1 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 1, succeeded: 0, total: 1 }); // Check that the skipped rule is returned with the response - expect(body.attributes.results.skipped[0].name).to.eql(rule.name); - expect(body.attributes.results.skipped[0].id).to.eql(rule.id); - expect(body.attributes.results.skipped[0].skip_reason).to.eql('RULE_NOT_MODIFIED'); + expect(body.attributes.results.skipped[0].name).toEqual(rule.name); + expect(body.attributes.results.skipped[0].id).toEqual(rule.id); + expect(body.attributes.results.skipped[0].skip_reason).toEqual('RULE_NOT_MODIFIED'); // Check that no change to the rule have been persisted const { body: skippedRule } = await fetchRule(ruleId).expect(200); - expect(skippedRule.index).to.eql(['index1-*']); - expect(skippedRule.tags).to.eql(['tag1', 'tag2']); + expect(skippedRule.index).toEqual(['index1-*']); + expect(skippedRule.tags).toEqual(['tag1', 'tag2']); }); }); @@ -2585,7 +2657,7 @@ export default ({ getService }: FtrProviderContext): void => { ) ); - expect(responses.filter((r) => r.body.statusCode === 429).length).to.eql(5); + expect(responses.filter((r) => r.body.statusCode === 429).length).toEqual(5); }); it('should bulk update rule by id', async () => { @@ -2613,17 +2685,17 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].timeline_id).to.eql(timelineId); - expect(body.attributes.results.updated[0].timeline_title).to.eql(timelineTitle); + expect(body.attributes.results.updated[0].timeline_id).toEqual(timelineId); + expect(body.attributes.results.updated[0].timeline_title).toEqual(timelineTitle); // Check that the updates have been persisted const { body: rule } = await fetchRule(ruleId).expect(200); - expect(rule.timeline_id).to.eql(timelineId); - expect(rule.timeline_title).to.eql(timelineTitle); + expect(rule.timeline_id).toEqual(timelineId); + expect(rule.timeline_title).toEqual(timelineTitle); }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules.ts index 28a362213ae3b4..6ba0cc273c8c59 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules.ts @@ -5,11 +5,12 @@ * 2.0. */ -import expect from '@kbn/expect'; +import expect from 'expect'; import { RuleCreateProps } from '@kbn/security-solution-plugin/common/api/detection_engine'; import { getSimpleRule, + getCustomQueryRuleParams, getSimpleRuleOutputWithoutRuleId, getSimpleRuleWithoutRuleId, removeServerGeneratedProperties, @@ -65,7 +66,31 @@ export default ({ getService }: FtrProviderContext) => { const bodyToCompare = removeServerGeneratedProperties(body); const expectedRule = updateUsername(getSimpleRuleOutput(), ELASTICSEARCH_USERNAME); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); + }); + + it('should create a rule with defaultable fields', async () => { + const expectedRule = getCustomQueryRuleParams({ + rule_id: 'rule-1', + related_integrations: [ + { package: 'package-a', version: '^1.2.3' }, + { package: 'package-b', integration: 'integration-b', version: '~1.1.1' }, + ], + }); + + const { body: createdRuleResponse } = await securitySolutionApi + .createRule({ body: expectedRule }) + .expect(200); + + expect(createdRuleResponse).toMatchObject(expectedRule); + + const { body: createdRule } = await securitySolutionApi + .readRule({ + query: { rule_id: 'rule-1' }, + }) + .expect(200); + + expect(createdRule).toMatchObject(expectedRule); }); it('should create a single rule without an input index', async () => { @@ -120,7 +145,7 @@ export default ({ getService }: FtrProviderContext) => { ELASTICSEARCH_USERNAME ); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); }); it('should create a single rule without a rule_id', async () => { @@ -134,7 +159,7 @@ export default ({ getService }: FtrProviderContext) => { ELASTICSEARCH_USERNAME ); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); }); it('should cause a 409 conflict if we attempt to create the same rule_id twice', async () => { @@ -144,7 +169,7 @@ export default ({ getService }: FtrProviderContext) => { .createRule({ body: getSimpleRule() }) .expect(409); - expect(body).to.eql({ + expect(body).toEqual({ message: 'rule_id: "rule-1" already exists', status_code: 409, }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules_bulk.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules_bulk.ts index 3a1cdbbf373ed7..17b4ea3e3604e8 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules_bulk.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules_bulk.ts @@ -5,13 +5,14 @@ * 2.0. */ -import expect from '@kbn/expect'; +import expect from 'expect'; import { EsArchivePathBuilder } from '../../../../../es_archive_path_builder'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; import { getSimpleRule, getSimpleRuleOutput, + getCustomQueryRuleParams, getSimpleRuleOutputWithoutRuleId, getSimpleRuleWithoutRuleId, removeServerGeneratedProperties, @@ -64,7 +65,31 @@ export default ({ getService }: FtrProviderContext): void => { const bodyToCompare = removeServerGeneratedProperties(body[0]); const expectedRule = updateUsername(getSimpleRuleOutput(), ELASTICSEARCH_USERNAME); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); + }); + + it('should create a rule with defaultable fields', async () => { + const expectedRule = getCustomQueryRuleParams({ + rule_id: 'rule-1', + related_integrations: [ + { package: 'package-a', version: '^1.2.3' }, + { package: 'package-b', integration: 'integration-b', version: '~1.1.1' }, + ], + }); + + const { body: createdRulesBulkResponse } = await securitySolutionApi + .bulkCreateRules({ body: [expectedRule] }) + .expect(200); + + expect(createdRulesBulkResponse[0]).toMatchObject(expectedRule); + + const { body: createdRule } = await securitySolutionApi + .readRule({ + query: { rule_id: 'rule-1' }, + }) + .expect(200); + + expect(createdRule).toMatchObject(expectedRule); }); it('should create a single rule without a rule_id', async () => { @@ -78,7 +103,7 @@ export default ({ getService }: FtrProviderContext): void => { ELASTICSEARCH_USERNAME ); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); }); it('should return a 200 ok but have a 409 conflict if we attempt to create the same rule_id twice', async () => { @@ -86,7 +111,7 @@ export default ({ getService }: FtrProviderContext): void => { .bulkCreateRules({ body: [getSimpleRule(), getSimpleRule()] }) .expect(200); - expect(body).to.eql([ + expect(body).toEqual([ { error: { message: 'rule_id: "rule-1" already exists', @@ -104,7 +129,7 @@ export default ({ getService }: FtrProviderContext): void => { .bulkCreateRules({ body: [getSimpleRule()] }) .expect(200); - expect(body).to.eql([ + expect(body).toEqual([ { error: { message: 'rule_id: "rule-1" already exists', diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts index f355e9ed61fc40..d91c1ab18b44ac 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts @@ -8,6 +8,7 @@ import expect from 'expect'; import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { BaseDefaultableFields } from '@kbn/security-solution-plugin/common/api/detection_engine'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; import { binaryToString, getCustomQueryRuleParams } from '../../../utils'; import { @@ -18,6 +19,7 @@ import { } from '../../../../../../common/utils/security_solution'; export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); + const securitySolutionApi = getService('securitySolutionApi'); const log = getService('log'); const es = getService('es'); @@ -63,6 +65,27 @@ export default ({ getService }: FtrProviderContext): void => { expect(exportedRule).toMatchObject(ruleToExport); }); + it('should export defaultable fields when values are set', async () => { + const defaultableFields: BaseDefaultableFields = { + related_integrations: [ + { package: 'package-a', version: '^1.2.3' }, + { package: 'package-b', integration: 'integration-b', version: '~1.1.1' }, + ], + }; + const ruleToExport = getCustomQueryRuleParams(defaultableFields); + + await securitySolutionApi.createRule({ body: ruleToExport }); + + const { body } = await securitySolutionApi + .exportRules({ query: {}, body: null }) + .expect(200) + .parse(binaryToString); + + const exportedRule = JSON.parse(body.toString().split(/\n/)[0]); + + expect(exportedRule).toMatchObject(defaultableFields); + }); + it('should have export summary reflecting a number of rules', async () => { await createRule(supertest, log, getCustomQueryRuleParams()); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules.ts index d1b2fb041f4bc3..f4fc373965df98 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules.ts @@ -8,6 +8,7 @@ import expect from 'expect'; import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { BaseDefaultableFields } from '@kbn/security-solution-plugin/common/api/detection_engine'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; import { getCustomQueryRuleParams, combineToNdJson, fetchRule } from '../../../utils'; import { @@ -19,6 +20,7 @@ import { export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); + const securitySolutionApi = getService('securitySolutionApi'); const log = getService('log'); const es = getService('es'); @@ -135,6 +137,33 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.errors[0].error.message).toBe('from: Failed to parse date-math expression'); }); + it('should be able to import rules with defaultable fields', async () => { + const defaultableFields: BaseDefaultableFields = { + related_integrations: [ + { package: 'package-a', version: '^1.2.3' }, + { package: 'package-b', integration: 'integration-b', version: '~1.1.1' }, + ], + }; + const ruleToImport = getCustomQueryRuleParams({ + ...defaultableFields, + rule_id: 'rule-1', + }); + const ndjson = combineToNdJson(ruleToImport); + + await securitySolutionApi + .importRules({ query: {} }) + .attach('file', Buffer.from(ndjson), 'rules.ndjson') + .expect(200); + + const { body: importedRule } = await securitySolutionApi + .readRule({ + query: { rule_id: 'rule-1' }, + }) + .expect(200); + + expect(importedRule).toMatchObject(ruleToImport); + }); + it('should be able to import two rules', async () => { const ndjson = combineToNdJson( getCustomQueryRuleParams({ diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts index 7abca99e6e0522..27990708215d33 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts @@ -5,12 +5,13 @@ * 2.0. */ -import expect from '@kbn/expect'; +import expect from 'expect'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; import { getSimpleRule, getSimpleRuleOutput, + getCustomQueryRuleParams, removeServerGeneratedProperties, removeServerGeneratedPropertiesIncludingRuleId, getSimpleRuleOutputWithoutRuleId, @@ -56,7 +57,40 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); + }); + + it('should patch defaultable fields', async () => { + const expectedRule = getCustomQueryRuleParams({ + rule_id: 'rule-1', + related_integrations: [ + { package: 'package-a', version: '^1.2.3' }, + { package: 'package-b', integration: 'integration-b', version: '~1.1.1' }, + ], + }); + + await securitySolutionApi.createRule({ + body: getCustomQueryRuleParams({ rule_id: 'rule-1' }), + }); + + const { body: patchedRuleResponse } = await securitySolutionApi + .patchRule({ + body: { + rule_id: 'rule-1', + related_integrations: expectedRule.related_integrations, + }, + }) + .expect(200); + + expect(patchedRuleResponse).toMatchObject(expectedRule); + + const { body: patchedRule } = await securitySolutionApi + .readRule({ + query: { rule_id: 'rule-1' }, + }) + .expect(200); + + expect(patchedRule).toMatchObject(expectedRule); }); it('@skipInServerless should return a "403 forbidden" using a rule_id of type "machine learning"', async () => { @@ -67,7 +101,7 @@ export default ({ getService }: FtrProviderContext) => { .patchRule({ body: { rule_id: 'rule-1', type: 'machine_learning' } }) .expect(403); - expect(body).to.eql({ + expect(body).toEqual({ message: 'Your license does not support machine learning. Please upgrade your license.', status_code: 403, }); @@ -90,7 +124,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); }); it('should patch a single rule property of name using the auto-generated id', async () => { @@ -107,7 +141,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); }); it('should not change the revision of a rule when it patches only enabled', async () => { @@ -123,7 +157,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); }); it('should change the revision of a rule when it patches enabled and another property', async () => { @@ -141,7 +175,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); }); it('should not change other properties when it does patches', async () => { @@ -167,7 +201,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); }); it('should give a 404 if it is given a fake id', async () => { @@ -177,7 +211,7 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(404); - expect(body).to.eql({ + expect(body).toEqual({ status_code: 404, message: 'id: "5096dec6-b6b9-4d8d-8f93-6c2602079d9d" not found', }); @@ -188,7 +222,7 @@ export default ({ getService }: FtrProviderContext) => { .patchRule({ body: { rule_id: 'fake_id', name: 'some other name' } }) .expect(404); - expect(body).to.eql({ + expect(body).toEqual({ status_code: 404, message: 'rule_id: "fake_id" not found', }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts index bb86ae5d17354f..ef3c944bf9931f 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts @@ -5,12 +5,13 @@ * 2.0. */ -import expect from '@kbn/expect'; +import expect from 'expect'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; import { getSimpleRule, getSimpleRuleOutput, + getCustomQueryRuleParams, removeServerGeneratedProperties, getSimpleRuleOutputWithoutRuleId, removeServerGeneratedPropertiesIncludingRuleId, @@ -55,7 +56,42 @@ export default ({ getService }: FtrProviderContext) => { outputRule.revision = 1; const bodyToCompare = removeServerGeneratedProperties(body[0]); const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); + }); + + it('should patch defaultable fields', async () => { + const expectedRule = getCustomQueryRuleParams({ + rule_id: 'rule-1', + related_integrations: [ + { package: 'package-a', version: '^1.2.3' }, + { package: 'package-b', integration: 'integration-b', version: '~1.1.1' }, + ], + }); + + await securitySolutionApi.createRule({ + body: getCustomQueryRuleParams({ rule_id: 'rule-1' }), + }); + + const { body: patchedRulesBulkResponse } = await securitySolutionApi + .bulkPatchRules({ + body: [ + { + rule_id: 'rule-1', + related_integrations: expectedRule.related_integrations, + }, + ], + }) + .expect(200); + + expect(patchedRulesBulkResponse[0]).toMatchObject(expectedRule); + + const { body: patchedRule } = await securitySolutionApi + .readRule({ + query: { rule_id: 'rule-1' }, + }) + .expect(200); + + expect(patchedRule).toMatchObject(expectedRule); }); it('should patch two rule properties of name using the two rules rule_id', async () => { @@ -84,8 +120,8 @@ export default ({ getService }: FtrProviderContext) => { const bodyToCompare1 = removeServerGeneratedProperties(body[0]); const bodyToCompare2 = removeServerGeneratedProperties(body[1]); - expect(bodyToCompare1).to.eql(expectedRule1); - expect(bodyToCompare2).to.eql(expectedRule2); + expect(bodyToCompare1).toEqual(expectedRule1); + expect(bodyToCompare2).toEqual(expectedRule2); }); it('should patch a single rule property of name using an id', async () => { @@ -101,7 +137,7 @@ export default ({ getService }: FtrProviderContext) => { outputRule.revision = 1; const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body[0]); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); }); it('should patch two rule properties of name using the two rules id', async () => { @@ -130,8 +166,8 @@ export default ({ getService }: FtrProviderContext) => { const bodyToCompare1 = removeServerGeneratedPropertiesIncludingRuleId(body[0]); const bodyToCompare2 = removeServerGeneratedPropertiesIncludingRuleId(body[1]); - expect(bodyToCompare1).to.eql(expectedRule); - expect(bodyToCompare2).to.eql(expectedRule2); + expect(bodyToCompare1).toEqual(expectedRule); + expect(bodyToCompare2).toEqual(expectedRule2); }); it('should patch a single rule property of name using the auto-generated id', async () => { @@ -148,7 +184,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body[0]); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); }); it('should not change the revision of a rule when it patches only enabled', async () => { @@ -164,7 +200,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body[0]); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); }); it('should change the revision of a rule when it patches enabled and another property', async () => { @@ -182,7 +218,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body[0]); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); }); it('should not change other properties when it does patches', async () => { @@ -208,7 +244,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body[0]); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); }); it('should return a 200 but give a 404 in the message if it is given a fake id', async () => { @@ -218,7 +254,7 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - expect(body).to.eql([ + expect(body).toEqual([ { id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', error: { @@ -234,7 +270,7 @@ export default ({ getService }: FtrProviderContext) => { .bulkPatchRules({ body: [{ rule_id: 'fake_id', name: 'some other name' }] }) .expect(200); - expect(body).to.eql([ + expect(body).toEqual([ { rule_id: 'fake_id', error: { status_code: 404, message: 'rule_id: "fake_id" not found' }, @@ -261,7 +297,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body[0]); - expect([bodyToCompare, body[1]]).to.eql([ + expect([bodyToCompare, body[1]]).toEqual([ expectedRule, { error: { @@ -292,7 +328,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body[0]); - expect([bodyToCompare, body[1]]).to.eql([ + expect([bodyToCompare, body[1]]).toEqual([ expectedRule, { error: { diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts index 301b4413805a96..08dfdac9a7e82e 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts @@ -5,11 +5,12 @@ * 2.0. */ -import expect from '@kbn/expect'; +import expect from 'expect'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; import { getSimpleRuleOutput, + getCustomQueryRuleParams, removeServerGeneratedProperties, removeServerGeneratedPropertiesIncludingRuleId, getSimpleRuleOutputWithoutRuleId, @@ -61,7 +62,37 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); + }); + + it('should update a rule with defaultable fields', async () => { + const expectedRule = getCustomQueryRuleParams({ + rule_id: 'rule-1', + related_integrations: [ + { package: 'package-a', version: '^1.2.3' }, + { package: 'package-b', integration: 'integration-b', version: '~1.1.1' }, + ], + }); + + await securitySolutionApi.createRule({ + body: getCustomQueryRuleParams({ rule_id: 'rule-1' }), + }); + + const { body: updatedRuleResponse } = await securitySolutionApi + .updateRule({ + body: expectedRule, + }) + .expect(200); + + expect(updatedRuleResponse).toMatchObject(expectedRule); + + const { body: updatedRule } = await securitySolutionApi + .readRule({ + query: { rule_id: 'rule-1' }, + }) + .expect(200); + + expect(updatedRule).toMatchObject(expectedRule); }); it('@skipInServerless should return a 403 forbidden if it is a machine learning job', async () => { @@ -75,7 +106,7 @@ export default ({ getService }: FtrProviderContext) => { const { body } = await securitySolutionApi.updateRule({ body: updatedRule }).expect(403); - expect(body).to.eql({ + expect(body).toEqual({ message: 'Your license does not support machine learning. Please upgrade your license.', status_code: 403, }); @@ -100,7 +131,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); }); it('should update a single rule property of name using the auto-generated id', async () => { @@ -120,7 +151,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); }); it('should change the revision of a rule when it updates enabled and another property', async () => { @@ -140,7 +171,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); }); it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => { @@ -165,7 +196,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); }); it('should give a 404 if it is given a fake id', async () => { @@ -175,7 +206,7 @@ export default ({ getService }: FtrProviderContext) => { const { body } = await securitySolutionApi.updateRule({ body: simpleRule }).expect(404); - expect(body).to.eql({ + expect(body).toEqual({ status_code: 404, message: 'id: "5096dec6-b6b9-4d8d-8f93-6c2602079d9d" not found', }); @@ -188,7 +219,7 @@ export default ({ getService }: FtrProviderContext) => { const { body } = await securitySolutionApi.updateRule({ body: simpleRule }).expect(404); - expect(body).to.eql({ + expect(body).toEqual({ status_code: 404, message: 'rule_id: "fake_id" not found', }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts index 4696a5d82444c5..d28d9efd413507 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts @@ -5,11 +5,12 @@ * 2.0. */ -import expect from '@kbn/expect'; +import expect from 'expect'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; import { getSimpleRuleOutput, + getCustomQueryRuleParams, removeServerGeneratedProperties, getSimpleRuleOutputWithoutRuleId, removeServerGeneratedPropertiesIncludingRuleId, @@ -60,7 +61,37 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body[0]); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); + }); + + it('should update a rule with defaultable fields', async () => { + const expectedRule = getCustomQueryRuleParams({ + rule_id: 'rule-1', + related_integrations: [ + { package: 'package-a', version: '^1.2.3' }, + { package: 'package-b', integration: 'integration-b', version: '~1.1.1' }, + ], + }); + + await securitySolutionApi.createRule({ + body: getCustomQueryRuleParams({ rule_id: 'rule-1' }), + }); + + const { body: updatedRulesBulkResponse } = await securitySolutionApi + .bulkUpdateRules({ + body: [expectedRule], + }) + .expect(200); + + expect(updatedRulesBulkResponse[0]).toMatchObject(expectedRule); + + const { body: updatedRule } = await securitySolutionApi + .readRule({ + query: { rule_id: 'rule-1' }, + }) + .expect(200); + + expect(updatedRule).toMatchObject(expectedRule); }); it('should update two rule properties of name using the two rules rule_id', async () => { @@ -92,8 +123,8 @@ export default ({ getService }: FtrProviderContext) => { const bodyToCompare1 = removeServerGeneratedProperties(body[0]); const bodyToCompare2 = removeServerGeneratedProperties(body[1]); - expect(bodyToCompare1).to.eql(expectedRule); - expect(bodyToCompare2).to.eql(expectedRule2); + expect(bodyToCompare1).toEqual(expectedRule); + expect(bodyToCompare2).toEqual(expectedRule2); }); it('should update a single rule property of name using an id', async () => { @@ -115,7 +146,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body[0]); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); }); it('should update two rule properties of name using the two rules id', async () => { @@ -149,8 +180,8 @@ export default ({ getService }: FtrProviderContext) => { const bodyToCompare1 = removeServerGeneratedPropertiesIncludingRuleId(body[0]); const bodyToCompare2 = removeServerGeneratedPropertiesIncludingRuleId(body[1]); - expect(bodyToCompare1).to.eql(expectedRule); - expect(bodyToCompare2).to.eql(expectedRule2); + expect(bodyToCompare1).toEqual(expectedRule); + expect(bodyToCompare2).toEqual(expectedRule2); }); it('should update a single rule property of name using the auto-generated id', async () => { @@ -172,7 +203,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body[0]); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); }); it('should change the revision of a rule when it updates enabled and another property', async () => { @@ -194,7 +225,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body[0]); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); }); it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => { @@ -221,7 +252,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body[0]); - expect(bodyToCompare).to.eql(expectedRule); + expect(bodyToCompare).toEqual(expectedRule); }); it('should return a 200 but give a 404 in the message if it is given a fake id', async () => { @@ -233,7 +264,7 @@ export default ({ getService }: FtrProviderContext) => { .bulkUpdateRules({ body: [ruleUpdate] }) .expect(200); - expect(body).to.eql([ + expect(body).toEqual([ { id: '1fd52120-d3a9-4e7a-b23c-96c0e1a74ae5', error: { @@ -253,7 +284,7 @@ export default ({ getService }: FtrProviderContext) => { .bulkUpdateRules({ body: [ruleUpdate] }) .expect(200); - expect(body).to.eql([ + expect(body).toEqual([ { rule_id: 'fake_id', error: { status_code: 404, message: 'rule_id: "fake_id" not found' }, @@ -283,7 +314,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body[0]); - expect([bodyToCompare, body[1]]).to.eql([ + expect([bodyToCompare, body[1]]).toEqual([ expectedRule, { error: { @@ -319,7 +350,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRule = updateUsername(outputRule, ELASTICSEARCH_USERNAME); const bodyToCompare = removeServerGeneratedProperties(body[0]); - expect([bodyToCompare, body[1]]).to.eql([ + expect([bodyToCompare, body[1]]).toEqual([ expectedRule, { error: { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/common_flows.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/common_flows.cy.ts index c9d83ae4d67e1f..a5903af58f1ee9 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/common_flows.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/common_flows.cy.ts @@ -27,6 +27,7 @@ import { fillFrom, fillNote, fillReferenceUrls, + fillRelatedIntegrations, fillRiskScore, fillRuleName, fillRuleTags, @@ -59,6 +60,7 @@ describe('Common rule creation flows', { tags: ['@ess', '@serverless'] }, () => it('Creates and enables a rule', function () { cy.log('Filling define section'); importSavedQuery(this.timelineId); + fillRelatedIntegrations(); cy.get(DEFINE_CONTINUE_BUTTON).click(); cy.log('Filling about section'); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/related_integrations/related_integrations.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/related_integrations/related_integrations.cy.ts index 413504800c2a7d..06e73f78ac2ad4 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/related_integrations/related_integrations.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/related_integrations/related_integrations.cy.ts @@ -5,8 +5,10 @@ * 2.0. */ -import { omit } from 'lodash'; -import { PerformRuleInstallationResponseBody } from '@kbn/security-solution-plugin/common/api/detection_engine'; +import { + PerformRuleInstallationResponseBody, + RelatedIntegration, +} from '@kbn/security-solution-plugin/common/api/detection_engine'; import { generateEvent } from '../../../../objects/event'; import { createDocument, deleteDataStream } from '../../../../tasks/api_calls/elasticsearch'; import { createRuleAssetSavedObject } from '../../../../helpers/rules'; @@ -50,37 +52,59 @@ import { describe('Related integrations', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => { const DATA_STREAM_NAME = 'logs-related-integrations-test'; const PREBUILT_RULE_NAME = 'Prebuilt rule with related integrations'; - const RULE_RELATED_INTEGRATIONS: IntegrationDefinition[] = [ + const RELATED_INTEGRATIONS: RelatedIntegration[] = [ + { + package: 'auditd', + version: '1.16.0', + }, { package: 'aws', version: '1.17.0', integration: 'cloudfront', - installed: true, - enabled: true, }, { package: 'aws', version: '1.17.0', integration: 'cloudtrail', - installed: true, - enabled: false, }, { package: 'aws', version: '1.17.0', integration: 'unknown', - installed: false, - enabled: false, }, - { package: 'system', version: '1.17.0', installed: true, enabled: true }, + { package: 'system', version: '1.17.0' }, ]; const PREBUILT_RULE = createRuleAssetSavedObject({ name: PREBUILT_RULE_NAME, index: [DATA_STREAM_NAME], query: '*:*', rule_id: 'rule_1', - related_integrations: RULE_RELATED_INTEGRATIONS.map((x) => omit(x, ['installed', 'enabled'])), + related_integrations: RELATED_INTEGRATIONS, }); + const EXPECTED_RELATED_INTEGRATIONS: ExpectedRelatedIntegration[] = [ + { + title: 'Auditd Logs', + status: 'Not installed', + }, + { + title: 'AWS Amazon cloudfront', + status: 'Enabled', + }, + { + title: 'AWS Aws cloudtrail', + status: 'Disabled', + }, + { + title: 'Aws Unknown', + }, + { + title: 'System', + status: 'Enabled', + }, + ]; + const EXPECTED_KNOWN_RELATED_INTEGRATIONS = EXPECTED_RELATED_INTEGRATIONS.filter((x) => + Boolean(x.status) + ); beforeEach(() => { login(); @@ -99,7 +123,7 @@ describe('Related integrations', { tags: ['@ess', '@serverless', '@skipInServerl it('should display a badge with the installed integrations', () => { cy.get(INTEGRATIONS_POPOVER).should( 'have.text', - `0/${RULE_RELATED_INTEGRATIONS.length} integrations` + `0/${EXPECTED_RELATED_INTEGRATIONS.length} integrations` ); }); @@ -108,15 +132,19 @@ describe('Related integrations', { tags: ['@ess', '@serverless', '@skipInServerl cy.get(INTEGRATIONS_POPOVER_TITLE).should( 'have.text', - `[${RULE_RELATED_INTEGRATIONS.length}] Related integrations available` + `[${EXPECTED_RELATED_INTEGRATIONS.length}] Related integrations available` + ); + cy.get(INTEGRATION_LINK).should('have.length', EXPECTED_RELATED_INTEGRATIONS.length); + cy.get(INTEGRATION_STATUS).should( + 'have.length', + EXPECTED_KNOWN_RELATED_INTEGRATIONS.length ); - cy.get(INTEGRATION_LINK).should('have.length', RULE_RELATED_INTEGRATIONS.length); - cy.get(INTEGRATION_STATUS).should('have.length', RULE_RELATED_INTEGRATIONS.length); - RULE_RELATED_INTEGRATIONS.forEach((integration, index) => { - cy.get(INTEGRATION_LINK).eq(index).contains(getIntegrationName(integration), { - matchCase: false, - }); + EXPECTED_RELATED_INTEGRATIONS.forEach((expected, index) => { + cy.get(INTEGRATION_LINK).eq(index).contains(expected.title); + }); + + EXPECTED_KNOWN_RELATED_INTEGRATIONS.forEach((_, index) => { cy.get(INTEGRATION_STATUS).eq(index).should('have.text', 'Not installed'); }); }); @@ -128,13 +156,17 @@ describe('Related integrations', { tags: ['@ess', '@serverless', '@skipInServerl }); it('should display the integrations in the definition section', () => { - cy.get(INTEGRATION_LINK).should('have.length', RULE_RELATED_INTEGRATIONS.length); - cy.get(INTEGRATION_STATUS).should('have.length', RULE_RELATED_INTEGRATIONS.length); + cy.get(INTEGRATION_LINK).should('have.length', EXPECTED_RELATED_INTEGRATIONS.length); + cy.get(INTEGRATION_STATUS).should( + 'have.length', + EXPECTED_KNOWN_RELATED_INTEGRATIONS.length + ); - RULE_RELATED_INTEGRATIONS.forEach((integration, index) => { - cy.get(INTEGRATION_LINK).eq(index).contains(getIntegrationName(integration), { - matchCase: false, - }); + EXPECTED_RELATED_INTEGRATIONS.forEach((expected, index) => { + cy.get(INTEGRATION_LINK).eq(index).contains(expected.title); + }); + + EXPECTED_KNOWN_RELATED_INTEGRATIONS.forEach((_, index) => { cy.get(INTEGRATION_STATUS).eq(index).should('have.text', 'Not installed'); }); }); @@ -165,12 +197,9 @@ describe('Related integrations', { tags: ['@ess', '@serverless', '@skipInServerl }); it('should display a badge with the installed integrations', () => { - const enabledIntegrations = RULE_RELATED_INTEGRATIONS.filter((x) => x.enabled).length; - const totalIntegrations = RULE_RELATED_INTEGRATIONS.length; - cy.get(INTEGRATIONS_POPOVER).should( 'have.text', - `${enabledIntegrations}/${totalIntegrations} integrations` + `2/${EXPECTED_RELATED_INTEGRATIONS.length} integrations` ); }); @@ -179,18 +208,20 @@ describe('Related integrations', { tags: ['@ess', '@serverless', '@skipInServerl cy.get(INTEGRATIONS_POPOVER_TITLE).should( 'have.text', - `[${RULE_RELATED_INTEGRATIONS.length}] Related integrations available` + `[${EXPECTED_RELATED_INTEGRATIONS.length}] Related integrations available` + ); + cy.get(INTEGRATION_LINK).should('have.length', EXPECTED_RELATED_INTEGRATIONS.length); + cy.get(INTEGRATION_STATUS).should( + 'have.length', + EXPECTED_KNOWN_RELATED_INTEGRATIONS.length ); - cy.get(INTEGRATION_LINK).should('have.length', RULE_RELATED_INTEGRATIONS.length); - cy.get(INTEGRATION_STATUS).should('have.length', RULE_RELATED_INTEGRATIONS.length); - RULE_RELATED_INTEGRATIONS.forEach((integration, index) => { - cy.get(INTEGRATION_LINK).eq(index).contains(getIntegrationName(integration), { - matchCase: false, - }); - cy.get(INTEGRATION_STATUS) - .eq(index) - .should('have.text', getIntegrationStatus(integration)); + EXPECTED_RELATED_INTEGRATIONS.forEach((expected, index) => { + cy.get(INTEGRATION_LINK).eq(index).contains(expected.title); + }); + + EXPECTED_KNOWN_RELATED_INTEGRATIONS.forEach((expected, index) => { + cy.get(INTEGRATION_STATUS).eq(index).should('have.text', expected.status); }); }); }); @@ -202,16 +233,18 @@ describe('Related integrations', { tags: ['@ess', '@serverless', '@skipInServerl }); it('should display the integrations in the definition section', () => { - cy.get(INTEGRATION_LINK).should('have.length', RULE_RELATED_INTEGRATIONS.length); - cy.get(INTEGRATION_STATUS).should('have.length', RULE_RELATED_INTEGRATIONS.length); + cy.get(INTEGRATION_LINK).should('have.length', EXPECTED_RELATED_INTEGRATIONS.length); + cy.get(INTEGRATION_STATUS).should( + 'have.length', + EXPECTED_KNOWN_RELATED_INTEGRATIONS.length + ); - RULE_RELATED_INTEGRATIONS.forEach((integration, index) => { - cy.get(INTEGRATION_LINK).eq(index).contains(getIntegrationName(integration), { - matchCase: false, - }); - cy.get(INTEGRATION_STATUS) - .eq(index) - .should('have.text', getIntegrationStatus(integration)); + EXPECTED_RELATED_INTEGRATIONS.forEach((expected, index) => { + cy.get(INTEGRATION_LINK).eq(index).contains(expected.title); + }); + + EXPECTED_KNOWN_RELATED_INTEGRATIONS.forEach((expected, index) => { + cy.get(INTEGRATION_STATUS).eq(index).should('have.text', expected.status); }); }); @@ -230,9 +263,7 @@ describe('Related integrations', { tags: ['@ess', '@serverless', '@skipInServerl size: 1, }).then((alertsResponse) => { expect(alertsResponse.body.hits.hits[0].fields).to.deep.equal({ - [RELATED_INTEGRATION_FIELD]: RULE_RELATED_INTEGRATIONS.map((x) => - omit(x, ['installed', 'enabled']) - ), + [RELATED_INTEGRATION_FIELD]: RELATED_INTEGRATIONS, }); }); }); @@ -263,13 +294,17 @@ describe('Related integrations', { tags: ['@ess', '@serverless', '@skipInServerl }); it('should display the integrations in the definition section', () => { - cy.get(INTEGRATION_LINK).should('have.length', RULE_RELATED_INTEGRATIONS.length); - cy.get(INTEGRATION_STATUS).should('have.length', RULE_RELATED_INTEGRATIONS.length); + cy.get(INTEGRATION_LINK).should('have.length', EXPECTED_RELATED_INTEGRATIONS.length); + cy.get(INTEGRATION_STATUS).should( + 'have.length', + EXPECTED_KNOWN_RELATED_INTEGRATIONS.length + ); - RULE_RELATED_INTEGRATIONS.forEach((integration, index) => { - cy.get(INTEGRATION_LINK).eq(index).contains(getIntegrationName(integration), { - matchCase: false, - }); + EXPECTED_RELATED_INTEGRATIONS.forEach((expected, index) => { + cy.get(INTEGRATION_LINK).eq(index).contains(expected.title); + }); + + EXPECTED_KNOWN_RELATED_INTEGRATIONS.forEach((_, index) => { cy.get(INTEGRATION_STATUS).eq(index).should('have.text', 'Not installed'); }); }); @@ -290,22 +325,9 @@ function visitFirstInstalledPrebuiltRuleDetailsPage(): void { ).then((response) => visitRuleDetailsPage(response.body.results.created[0].id)); } -interface IntegrationDefinition { - package: string; - version: string; - installed: boolean; - enabled: boolean; - integration?: string; -} - -function getIntegrationName(integration: IntegrationDefinition): string { - return `${integration.package} ${integration.integration ?? ''}`.trim(); -} - -function getIntegrationStatus(integration: IntegrationDefinition): string { - return `${integration.installed ? 'Installed' : 'Not installed'}${ - integration.enabled ? ': enabled' : '' - }`.trim(); +interface ExpectedRelatedIntegration { + title: string; + status?: string; } /** diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/entity_flyout.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/entity_flyout.cy.ts index 0b98ae36f1ec9c..9ccedac0c2504a 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/entity_flyout.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/entity_flyout.cy.ts @@ -32,7 +32,7 @@ import { ENTITY_DETAILS_FLYOUT_ASSET_CRITICALITY_SELECTOR, } from '../../screens/asset_criticality/flyouts'; import { deleteCriticality } from '../../tasks/api_calls/entity_analytics'; -import { mockFleetInstalledIntegrations } from '../../tasks/fleet_integrations'; +import { mockFleetIntegrations } from '../../tasks/fleet_integrations'; import { expandManagedDataEntraPanel, expandManagedDataOktaPanel, @@ -146,18 +146,22 @@ describe( // https://github.com/elastic/kibana/issues/179248 describe('Managed data section', { tags: ['@skipInServerlessMKI'] }, () => { beforeEach(() => { - mockFleetInstalledIntegrations([ + mockFleetIntegrations([ { package_name: ENTRA_ID_PACKAGE_NAME, - is_enabled: true, package_title: 'azure entra', - package_version: 'test_package_version', + latest_package_version: 'test_package_version', + installed_package_version: 'test_package_version', + is_installed: true, + is_enabled: true, }, { package_name: OKTA_PACKAGE_NAME, - is_enabled: true, package_title: 'okta', - package_version: 'test_package_version', + latest_package_version: 'test_package_version', + installed_package_version: 'test_package_version', + is_installed: true, + is_enabled: true, }, ]); }); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/common.ts b/x-pack/test/security_solution_cypress/cypress/screens/common.ts index 3d6aae97850188..b121badc9e20d7 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/common.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/common.ts @@ -8,3 +8,5 @@ export const TOOLTIP = '[role="tooltip"]'; export const BASIC_TABLE_LOADING = '.euiBasicTable.euiBasicTable-loading'; + +export const COMBO_BOX_OPTION = '.euiComboBoxOptionsList button[role="option"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts b/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts index ac474a56bd5b35..bf88869973ceed 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts @@ -125,6 +125,9 @@ export const EQL_OPTIONS_TIMESTAMP_INPUT = export const IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK = '[data-test-subj="importQueryFromSavedTimeline"]'; +export const RELATED_INTEGRATION_COMBO_BOX_INPUT = + '[data-test-subj="relatedIntegrationComboBox"] [data-test-subj="comboBoxSearchInput"]'; + export const INDICATOR_MATCH_TYPE = '[data-test-subj="threatMatchRuleType"]'; export const INPUT = '[data-test-subj="input"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts index 091d592dd04aea..f40cecee5a9815 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts @@ -125,6 +125,7 @@ import { ALERTS_INDEX_BUTTON, INVESTIGATIONS_INPUT, QUERY_BAR_ADD_FILTER, + RELATED_INTEGRATION_COMBO_BOX_INPUT, } from '../screens/create_new_rule'; import { INDEX_SELECTOR, @@ -147,7 +148,7 @@ import { ruleFields } from '../data/detection_engine'; import { waitForAlerts } from './alerts'; import { refreshPage } from './security_header'; import { EMPTY_ALERT_TABLE } from '../screens/alerts'; -import { TOOLTIP } from '../screens/common'; +import { COMBO_BOX_OPTION, TOOLTIP } from '../screens/common'; export const createAndEnableRule = () => { cy.get(CREATE_AND_ENABLE_BTN).click(); @@ -272,6 +273,17 @@ export const importSavedQuery = (timelineId: string) => { removeAlertsIndex(); }; +export const fillRelatedIntegrations = (): void => { + addFirstIntegration(); + addFirstIntegration(); +}; + +const addFirstIntegration = (): void => { + cy.get('button').contains('Add integration').click(); + cy.get(RELATED_INTEGRATION_COMBO_BOX_INPUT).last().should('be.enabled').click(); + cy.get(COMBO_BOX_OPTION).first().click(); +}; + export const fillRuleName = (ruleName: string = ruleFields.ruleName) => { cy.get(RULE_NAME_INPUT).clear({ force: true }); cy.get(RULE_NAME_INPUT).type(ruleName, { force: true }); diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/fleet_integrations.ts b/x-pack/test/security_solution_cypress/cypress/tasks/fleet_integrations.ts index b60c6a8c5622fa..7105ebc4df70f2 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/fleet_integrations.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/fleet_integrations.ts @@ -6,19 +6,19 @@ */ import { - GET_INSTALLED_INTEGRATIONS_URL, - InstalledIntegration, + GET_ALL_INTEGRATIONS_URL, + Integration, } from '@kbn/security-solution-plugin/common/api/detection_engine'; import { login } from './login'; import { visitGetStartedPage } from './navigation'; -export const mockFleetInstalledIntegrations = (integrations: InstalledIntegration[] = []) => { - cy.intercept('GET', `${GET_INSTALLED_INTEGRATIONS_URL}*`, { +export const mockFleetIntegrations = (integrations: Integration[] = []) => { + cy.intercept('GET', `${GET_ALL_INTEGRATIONS_URL}*`, { statusCode: 200, body: { - installed_integrations: integrations, + integrations, }, - }).as('installedIntegrations'); + }).as('integrations'); }; export const waitForFleetSetup = () => {