diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index 8623b8c3ca107..3c2a120d167d9 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -96,13 +96,37 @@ All columns that belong to the same layer pane group are sorted in the table. * *Text alignment* — Aligns the values in the cell to the *Left*, *Center*, or *Right*. +* *Color by value* — Applies color to the cell or text values. To change the color, click the *Edit colors* icon. + * *Hide column* — Hides the column for the field. * *Directly filter on click* — Turns column values into clickable links that allow you to filter or drill down into the data. * *Summary row* — Adds a row that displays the summary value. When specified, allows you to enter a *Summary label*. -* *Color by value* — Applies color to the cell or text values. To change the color, click *Edit*. +[float] +[[assign-colors-to-terms]] +===== Assign colors to terms + +preview::[] + +For term-based metrics, assign a color to each term with color mapping. + +. Create a custom table. + +. In the layer pane, select a *Rows* or *Metrics* field. + +. In the *Color by value* option, select *Cell* or *Text*. + +. Click the *Edit colors* icon. + +. Toggle the button to use the Color Mapping feature. + +. Select a color palette and mode. + +. Click *Add assignment* to assign a color to a specific term, or click *Add all unassigned terms* to assign colors to all terms. Assigning colors to dates is unsupported. + +. Configure color assignments. You can also select whether unassigned terms should be mapped to the selected color palette or a single color. [float] [[drag-and-drop-keyboard-navigation]] diff --git a/package.json b/package.json index afda7cd4c9125..0ad1d96d7b5db 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "@elastic/datemath": "5.0.3", "@elastic/ebt": "^1.1.1", "@elastic/ecs": "^8.11.1", - "@elastic/elasticsearch": "^8.15.0", + "@elastic/elasticsearch": "^8.15.1", "@elastic/ems-client": "8.5.3", "@elastic/eui": "97.3.0", "@elastic/filesaver": "1.1.2", @@ -1498,7 +1498,7 @@ "@octokit/rest": "^17.11.2", "@parcel/watcher": "^2.1.0", "@playwright/test": "=1.46.0", - "@redocly/cli": "^1.25.10", + "@redocly/cli": "^1.25.11", "@statoscope/webpack-plugin": "^5.28.2", "@storybook/addon-a11y": "^6.5.16", "@storybook/addon-actions": "^6.5.16", diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/catch_retryable_es_client_errors.test.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/catch_retryable_es_client_errors.test.ts index 1aeabb7e86dea..c84b30cf15774 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/catch_retryable_es_client_errors.test.ts +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/catch_retryable_es_client_errors.test.ts @@ -72,24 +72,25 @@ describe('catchRetryableEsClientErrors', () => { type: 'retryable_es_client_error', }); }); - it('ResponseError with retryable status code', async () => { - const statusCodes = [503, 401, 403, 408, 410, 429]; - return Promise.all( - statusCodes.map(async (status) => { - const error = new esErrors.ResponseError( - elasticsearchClientMock.createApiResponse({ - statusCode: status, - body: { error: { type: 'reason' } }, - }) - ); - expect( - ((await Promise.reject(error).catch(catchRetryableEsClientErrors)) as any).left - ).toMatchObject({ - message: 'reason', - type: 'retryable_es_client_error', - }); - }) - ); - }); + it.each([503, 401, 403, 408, 410, 429])( + 'ResponseError with retryable status code (%d)', + async (status) => { + const error = new esErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ + statusCode: status, + body: { error: { type: 'reason' } }, + }) + ); + expect( + ((await Promise.reject(error).catch(catchRetryableEsClientErrors)) as any).left + ).toMatchObject({ + message: + status === 410 + ? 'This API is unavailable in the version of Elasticsearch you are using.' + : 'reason', + type: 'retryable_es_client_error', + }); + } + ); }); }); diff --git a/packages/deeplinks/search/constants.ts b/packages/deeplinks/search/constants.ts index 9848bb0c3d42e..52f7bb201388e 100644 --- a/packages/deeplinks/search/constants.ts +++ b/packages/deeplinks/search/constants.ts @@ -25,3 +25,4 @@ export const SEARCH_ELASTICSEARCH = 'enterpriseSearchElasticsearch'; export const SEARCH_VECTOR_SEARCH = 'enterpriseSearchVectorSearch'; export const SEARCH_SEMANTIC_SEARCH = 'enterpriseSearchSemanticSearch'; export const SEARCH_AI_SEARCH = 'enterpriseSearchAISearch'; +export const SEARCH_INDICES_CREATE_INDEX = 'createIndex'; diff --git a/packages/deeplinks/search/deep_links.ts b/packages/deeplinks/search/deep_links.ts index 22dfb91bdff33..b23a86b3cc51c 100644 --- a/packages/deeplinks/search/deep_links.ts +++ b/packages/deeplinks/search/deep_links.ts @@ -22,6 +22,7 @@ import { SEARCH_HOMEPAGE, SEARCH_INDICES_START, SEARCH_INDICES, + SEARCH_INDICES_CREATE_INDEX, SEARCH_ELASTICSEARCH, SEARCH_VECTOR_SEARCH, SEARCH_SEMANTIC_SEARCH, @@ -55,6 +56,8 @@ export type AppsearchLinkId = 'engines'; export type RelevanceLinkId = 'inferenceEndpoints'; +export type SearchIndicesLinkId = typeof SEARCH_INDICES_CREATE_INDEX; + export type DeepLinkId = | EnterpriseSearchApp | EnterpriseSearchContentApp @@ -77,4 +80,5 @@ export type DeepLinkId = | SearchElasticsearch | SearchVectorSearch | SearchSemanticSearch - | SearchAISearch; + | SearchAISearch + | `${SearchIndices}:${SearchIndicesLinkId}`; diff --git a/packages/kbn-apm-synthtrace/src/lib/shared/base_client.ts b/packages/kbn-apm-synthtrace/src/lib/shared/base_client.ts index ed6d1b813184b..7fd639331af80 100644 --- a/packages/kbn-apm-synthtrace/src/lib/shared/base_client.ts +++ b/packages/kbn-apm-synthtrace/src/lib/shared/base_client.ts @@ -55,7 +55,6 @@ export class SynthtraceEsClient { await this.client.indices.resolveIndex({ name: this.indices.join(','), expand_wildcards: ['open', 'hidden'], - // @ts-expect-error ignore_unavailable is not in the type definition, but it is accepted by es ignore_unavailable: true, }) ).indices.map((index: { name: string }) => index.name) diff --git a/packages/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.test.ts b/packages/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.test.ts index 9e21c45f75b4b..c8880b9bfe678 100644 --- a/packages/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.test.ts +++ b/packages/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.test.ts @@ -78,15 +78,6 @@ describe('single line query', () => { }); }); - describe('SHOW', () => { - /** @todo Enable once show command args are parsed as columns. */ - test.skip('info page', () => { - const { text } = reprint('SHOW info'); - - expect(text).toBe('SHOW info'); - }); - }); - describe('STATS', () => { test('with aggregates assignment', () => { const { text } = reprint('FROM a | STATS var = agg(123, fn(true))'); @@ -100,6 +91,30 @@ describe('single line query', () => { expect(text).toBe('FROM a | STATS A(1), B(2) BY asdf'); }); }); + + describe('GROK', () => { + test('two basic arguments', () => { + const { text } = reprint('FROM search-movies | GROK Awards "text"'); + + expect(text).toBe('FROM search-movies | GROK Awards "text"'); + }); + }); + + describe('DISSECT', () => { + test('two basic arguments', () => { + const { text } = reprint('FROM index | DISSECT input "pattern"'); + + expect(text).toBe('FROM index | DISSECT input "pattern"'); + }); + + test('with APPEND_SEPARATOR option', () => { + const { text } = reprint( + 'FROM index | DISSECT input "pattern" APPEND_SEPARATOR=""' + ); + + expect(text).toBe('FROM index | DISSECT input "pattern" APPEND_SEPARATOR = ""'); + }); + }); }); describe('expressions', () => { diff --git a/packages/kbn-esql-ast/src/pretty_print/__tests__/wrapping_pretty_printer.test.ts b/packages/kbn-esql-ast/src/pretty_print/__tests__/wrapping_pretty_printer.test.ts index 2dfe239ce5b88..6422ae9a451af 100644 --- a/packages/kbn-esql-ast/src/pretty_print/__tests__/wrapping_pretty_printer.test.ts +++ b/packages/kbn-esql-ast/src/pretty_print/__tests__/wrapping_pretty_printer.test.ts @@ -19,6 +19,83 @@ const reprint = (src: string, opts?: WrappingPrettyPrinterOptions) => { return { text }; }; +describe('commands', () => { + describe('GROK', () => { + test('two basic arguments', () => { + const { text } = reprint('FROM search-movies | GROK Awards "text"'); + + expect(text).toBe('FROM search-movies | GROK Awards "text"'); + }); + + test('two long arguments', () => { + const { text } = reprint( + 'FROM search-movies | GROK AwardsAwardsAwardsAwardsAwardsAwardsAwardsAwards "texttexttexttexttexttexttexttexttexttexttexttexttexttexttext"' + ); + + expect('\n' + text).toBe(` +FROM search-movies + | GROK + AwardsAwardsAwardsAwardsAwardsAwardsAwardsAwards + "texttexttexttexttexttexttexttexttexttexttexttexttexttexttext"`); + }); + }); + + describe('DISSECT', () => { + test('two basic arguments', () => { + const { text } = reprint('FROM index | DISSECT input "pattern"'); + + expect(text).toBe('FROM index | DISSECT input "pattern"'); + }); + + test('two long arguments', () => { + const { text } = reprint( + 'FROM index | DISSECT InputInputInputInputInputInputInputInputInputInputInputInputInputInput "PatternPatternPatternPatternPatternPatternPatternPatternPatternPattern"' + ); + + expect('\n' + text).toBe(` +FROM index + | DISSECT + InputInputInputInputInputInputInputInputInputInputInputInputInputInput + "PatternPatternPatternPatternPatternPatternPatternPatternPatternPattern"`); + }); + + test('with APPEND_SEPARATOR option', () => { + const { text } = reprint( + 'FROM index | DISSECT input "pattern" APPEND_SEPARATOR=""' + ); + + expect(text).toBe('FROM index | DISSECT input "pattern" APPEND_SEPARATOR = ""'); + }); + + test('two long arguments with short APPEND_SEPARATOR option', () => { + const { text } = reprint( + 'FROM index | DISSECT InputInputInputInputInputInputInputInputInputInputInputInputInputInput "PatternPatternPatternPatternPatternPatternPatternPatternPatternPattern" APPEND_SEPARATOR="sep"' + ); + + expect('\n' + text).toBe(` +FROM index + | DISSECT + InputInputInputInputInputInputInputInputInputInputInputInputInputInput + "PatternPatternPatternPatternPatternPatternPatternPatternPatternPattern" + APPEND_SEPARATOR = "sep"`); + }); + + test('two long arguments with long APPEND_SEPARATOR option', () => { + const { text } = reprint( + 'FROM index | DISSECT InputInputInputInputInputInputInputInputInputInputInputInputInputInput "PatternPatternPatternPatternPatternPatternPatternPatternPatternPattern" APPEND_SEPARATOR=""' + ); + + expect('\n' + text).toBe(` +FROM index + | DISSECT + InputInputInputInputInputInputInputInputInputInputInputInputInputInput + "PatternPatternPatternPatternPatternPatternPatternPatternPatternPattern" + APPEND_SEPARATOR = + ""`); + }); + }); +}); + describe('casing', () => { test('can chose command name casing', () => { const query = 'FROM index | WHERE a == 123'; diff --git a/packages/kbn-esql-ast/src/pretty_print/basic_pretty_printer.ts b/packages/kbn-esql-ast/src/pretty_print/basic_pretty_printer.ts index 2f1e3439cd3a3..cf252825c243f 100644 --- a/packages/kbn-esql-ast/src/pretty_print/basic_pretty_printer.ts +++ b/packages/kbn-esql-ast/src/pretty_print/basic_pretty_printer.ts @@ -19,6 +19,7 @@ import { import { ESQLAstBaseItem, ESQLAstCommand, ESQLAstQueryExpression } from '../types'; import { ESQLAstExpressionNode, Visitor } from '../visitor'; import { resolveItem } from '../visitor/utils'; +import { commandOptionsWithEqualsSeparator, commandsWithNoCommaArgSeparator } from './constants'; import { LeafPrinter } from './leaf_printer'; export interface BasicPrettyPrinterOptions { @@ -378,7 +379,8 @@ export class BasicPrettyPrinter { args += (args ? ', ' : '') + arg; } - const argsFormatted = args ? ` ${args}` : ''; + const separator = commandOptionsWithEqualsSeparator.has(ctx.node.name) ? ' = ' : ' '; + const argsFormatted = args ? `${separator}${args}` : ''; const optionFormatted = `${option}${argsFormatted}`; return optionFormatted; @@ -392,7 +394,10 @@ export class BasicPrettyPrinter { let options = ''; for (const source of ctx.visitArguments()) { - args += (args ? ', ' : '') + source; + const needsSeparator = !!args; + const needsComma = !commandsWithNoCommaArgSeparator.has(ctx.node.name); + const separator = needsSeparator ? (needsComma ? ',' : '') + ' ' : ''; + args += separator + source; } for (const option of ctx.visitOptions()) { diff --git a/packages/kbn-esql-ast/src/pretty_print/constants.ts b/packages/kbn-esql-ast/src/pretty_print/constants.ts new file mode 100644 index 0000000000000..01208af98d025 --- /dev/null +++ b/packages/kbn-esql-ast/src/pretty_print/constants.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +/** + * This set tracks commands that don't use commas to separate their + * arguments. + * + * Normally ES|QL command arguments are separated by commas. + * + * ``` + * COMMAND arg1, arg2, arg3 + * ``` + * + * But there are some commands (namely `grok` and `dissect`) which don't + * use commas to separate their arguments. + * + * ``` + * GROK input "pattern" + * DISSECT input "pattern" + * ``` + */ +export const commandsWithNoCommaArgSeparator = new Set(['grok', 'dissect']); + +/** + * This set tracks command options which use an equals sign to separate + * the option label from the option value. + * + * Most ES|QL commands use a space to separate the option label from the + * option value. + * + * ``` + * COMMAND arg1, arg2, arg3 OPTION option + * FROM index METADATA _id + * ``` + * + * However, the `APPEND_SEPARATOR` in the `DISSECT` command uses an equals + * sign to separate the option label from the option value. + * + * ``` + * DISSECT input "pattern" APPEND_SEPARATOR = "separator" + * | + * | + * equals sign + * ``` + */ +export const commandOptionsWithEqualsSeparator = new Set(['append_separator']); diff --git a/packages/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts b/packages/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts index 91f65a389f0c3..2f863524740ee 100644 --- a/packages/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts +++ b/packages/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts @@ -20,6 +20,7 @@ import { } from '../visitor'; import { children, singleItems } from '../visitor/utils'; import { BasicPrettyPrinter, BasicPrettyPrinterOptions } from './basic_pretty_printer'; +import { commandOptionsWithEqualsSeparator, commandsWithNoCommaArgSeparator } from './constants'; import { getPrettyPrintStats } from './helpers'; import { LeafPrinter } from './leaf_printer'; @@ -259,6 +260,8 @@ export class WrappingPrettyPrinter { } } + const commaBetweenArgs = !commandsWithNoCommaArgSeparator.has(ctx.node.name); + if (!oneArgumentPerLine) { ARGS: for (const arg of singleItems(ctx.arguments())) { if (arg.type === 'option') { @@ -271,7 +274,8 @@ export class WrappingPrettyPrinter { if (formattedArgLength > largestArg) { largestArg = formattedArgLength; } - let separator = txt ? ',' : ''; + + let separator = txt ? (commaBetweenArgs ? ',' : '') : ''; let fragment = ''; if (needsWrap) { @@ -329,7 +333,7 @@ export class WrappingPrettyPrinter { const arg = ctx.visitExpression(args[i], { indent, remaining: this.opts.wrap - indent.length, - suffix: isLastArg ? '' : ',', + suffix: isLastArg ? '' : commaBetweenArgs ? ',' : '', }); const separator = isFirstArg ? '' : '\n'; const indentation = arg.indented ? '' : indent; @@ -557,8 +561,9 @@ export class WrappingPrettyPrinter { indent: inp.indent, remaining: inp.remaining - option.length - 1, }); - const argsFormatted = args.txt ? ` ${args.txt}` : ''; - const txt = `${option}${argsFormatted}`; + const argsFormatted = args.txt ? `${args.txt[0] === '\n' ? '' : ' '}${args.txt}` : ''; + const separator = commandOptionsWithEqualsSeparator.has(ctx.node.name) ? ' =' : ''; + const txt = `${option}${separator}${argsFormatted}`; return { txt, lines: args.lines }; }) diff --git a/src/plugins/controls/common/constants.ts b/src/plugins/controls/common/constants.ts index d1434d4df2ae0..afd6fe66f0df1 100644 --- a/src/plugins/controls/common/constants.ts +++ b/src/plugins/controls/common/constants.ts @@ -16,7 +16,7 @@ export const CONTROL_CHAINING_OPTIONS = { NONE: 'NONE', HIERARCHICAL: 'HIERARCHI export const DEFAULT_CONTROL_WIDTH: ControlWidth = CONTROL_WIDTH_OPTIONS.MEDIUM; export const DEFAULT_CONTROL_LABEL_POSITION: ControlLabelPosition = CONTROL_LABEL_POSITION_OPTIONS.ONE_LINE; -export const DEFAULT_CONTROL_GROW: boolean = true; +export const DEFAULT_CONTROL_GROW: boolean = false; export const DEFAULT_CONTROL_CHAINING: ControlGroupChainingSystem = CONTROL_CHAINING_OPTIONS.HIERARCHICAL; export const DEFAULT_IGNORE_PARENT_SETTINGS = { diff --git a/src/plugins/controls/public/control_group/components/control_group_editor.tsx b/src/plugins/controls/public/control_group/components/control_group_editor.tsx index 8f1ccb4d699b0..cb21c23bc9ce4 100644 --- a/src/plugins/controls/public/control_group/components/control_group_editor.tsx +++ b/src/plugins/controls/public/control_group/components/control_group_editor.tsx @@ -72,7 +72,7 @@ export const ControlGroupEditor = ({ onCancel, onSave, onDeleteAll, stateManager return ( <> - +

{ControlGroupStrings.management.getFlyoutTitle()}

@@ -80,7 +80,7 @@ export const ControlGroupEditor = ({ onCancel, onSave, onDeleteAll, stateManager { onCancel(); }} @@ -204,7 +204,7 @@ export const ControlGroupEditor = ({ onCancel, onSave, onDeleteAll, stateManager { diff --git a/src/plugins/controls/public/control_group/control_group_strings.tsx b/src/plugins/controls/public/control_group/control_group_strings.tsx index b8f6a11abf839..f5c92d987b271 100644 --- a/src/plugins/controls/public/control_group/control_group_strings.tsx +++ b/src/plugins/controls/public/control_group/control_group_strings.tsx @@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n'; export const ControlGroupStrings = { getSaveChangesTitle: () => i18n.translate('controls.controlGroup.manageControl.saveChangesTitle', { - defaultMessage: 'Save and close', + defaultMessage: 'Save', }), getCancelTitle: () => i18n.translate('controls.controlGroup.manageControl.cancelTitle', { diff --git a/src/plugins/controls/public/control_group/init_controls_manager.test.ts b/src/plugins/controls/public/control_group/init_controls_manager.test.ts index 29998325664bb..d88dc5452a0e5 100644 --- a/src/plugins/controls/public/control_group/init_controls_manager.test.ts +++ b/src/plugins/controls/public/control_group/init_controls_manager.test.ts @@ -263,7 +263,7 @@ describe('getNewControlState', () => { test('should contain defaults when there are no existing controls', () => { const controlsManager = initControlsManager({}, new BehaviorSubject({})); expect(controlsManager.getNewControlState()).toEqual({ - grow: true, + grow: false, width: 'medium', dataViewId: undefined, }); @@ -284,7 +284,7 @@ describe('getNewControlState', () => { new BehaviorSubject(intialControlsState) ); expect(controlsManager.getNewControlState()).toEqual({ - grow: true, + grow: false, width: 'medium', dataViewId: 'myOtherDataViewId', }); diff --git a/src/plugins/controls/public/control_group/open_edit_control_group_flyout.tsx b/src/plugins/controls/public/control_group/open_edit_control_group_flyout.tsx index 54e35ab271b34..459913d98de0b 100644 --- a/src/plugins/controls/public/control_group/open_edit_control_group_flyout.tsx +++ b/src/plugins/controls/public/control_group/open_edit_control_group_flyout.tsx @@ -101,6 +101,9 @@ export const openEditControlGroupFlyout = ( 'aria-label': i18n.translate('controls.controlGroup.manageControl', { defaultMessage: 'Edit control settings', }), + size: 'm', + maxWidth: 500, + paddingSize: 'm', outsideClickCloses: false, onClose: () => closeOverlay(overlay), } diff --git a/src/plugins/controls/public/controls/data_controls/data_control_constants.tsx b/src/plugins/controls/public/controls/data_controls/data_control_constants.tsx index 6b06bd8a52439..23d4c68f6c5dc 100644 --- a/src/plugins/controls/public/controls/data_controls/data_control_constants.tsx +++ b/src/plugins/controls/public/controls/data_controls/data_control_constants.tsx @@ -21,14 +21,6 @@ export const DataControlEditorStrings = { defaultMessage: 'Edit control', }), dataSource: { - getFormGroupTitle: () => - i18n.translate('controls.controlGroup.manageControl.dataSource.formGroupTitle', { - defaultMessage: 'Data source', - }), - getFormGroupDescription: () => - i18n.translate('controls.controlGroup.manageControl.dataSource.formGroupDescription', { - defaultMessage: 'Select the data view and field that you want to create a control for.', - }), getSelectDataViewMessage: () => i18n.translate('controls.controlGroup.manageControl.dataSource.selectDataViewMessage', { defaultMessage: 'Please select a data view', @@ -95,14 +87,6 @@ export const DataControlEditorStrings = { }, }, displaySettings: { - getFormGroupTitle: () => - i18n.translate('controls.controlGroup.manageControl.displaySettings.formGroupTitle', { - defaultMessage: 'Display settings', - }), - getFormGroupDescription: () => - i18n.translate('controls.controlGroup.manageControl.displaySettings.formGroupDescription', { - defaultMessage: 'Change how the control appears on your dashboard.', - }), getTitleInputTitle: () => i18n.translate('controls.controlGroup.manageControl.displaySettings.titleInputTitle', { defaultMessage: 'Label', @@ -133,7 +117,7 @@ export const DataControlEditorStrings = { }, getSaveChangesTitle: () => i18n.translate('controls.controlGroup.manageControl.saveChangesTitle', { - defaultMessage: 'Save and close', + defaultMessage: 'Save', }), getCancelTitle: () => i18n.translate('controls.controlGroup.manageControl.cancelTitle', { diff --git a/src/plugins/controls/public/controls/data_controls/data_control_editor.tsx b/src/plugins/controls/public/controls/data_controls/data_control_editor.tsx index 23fd95978ff82..a84425f350dc1 100644 --- a/src/plugins/controls/public/controls/data_controls/data_control_editor.tsx +++ b/src/plugins/controls/public/controls/data_controls/data_control_editor.tsx @@ -15,7 +15,6 @@ import { EuiButtonEmpty, EuiButtonGroup, EuiCallOut, - EuiDescribedFormGroup, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -250,20 +249,8 @@ export const DataControlEditor = - {DataControlEditorStrings.manageControl.controlTypeSettings.getFormGroupTitle( - controlFactory.getDisplayName() - )} - - } - description={DataControlEditorStrings.manageControl.controlTypeSettings.getFormGroupDescription( - controlFactory.getDisplayName() - )} - data-test-subj="control-editor-custom-settings" - > +
+ - +
); }, [fieldRegistry, controlFactory, initialState, editorState, controlGroupApi]); return ( <> - +

{!controlId // if no ID, then we are creating a new control ? DataControlEditorStrings.manageControl.getFlyoutCreateTitle() @@ -288,156 +275,144 @@ export const DataControlEditor = - {DataControlEditorStrings.manageControl.dataSource.getFormGroupTitle()}

} - description={DataControlEditorStrings.manageControl.dataSource.getFormGroupDescription()} - > - {!editorConfig?.hideDataViewSelector && ( - - {dataViewListError ? ( - -

{dataViewListError.message}

-
- ) : ( - { - setEditorState({ ...editorState, dataViewId: newDataViewId }); - setSelectedControlType(undefined); - }} - trigger={{ - label: - selectedDataView?.getName() ?? - DataControlEditorStrings.manageControl.dataSource.getSelectDataViewMessage(), - }} - selectableProps={{ isLoading: dataViewListLoading }} - /> - )} -
- )} - - - {fieldListError ? ( + {!editorConfig?.hideDataViewSelector && ( + + {dataViewListError ? ( -

{fieldListError.message}

+

{dataViewListError.message}

) : ( - { - const customPredicate = editorConfig?.fieldFilterPredicate?.(field) ?? true; - return Boolean(fieldRegistry?.[field.name]) && customPredicate; + { + setEditorState({ ...editorState, dataViewId: newDataViewId }); + setSelectedControlType(undefined); }} - selectedFieldName={editorState.fieldName} - dataView={selectedDataView} - onSelectField={(field) => { - setEditorState({ ...editorState, fieldName: field.name }); - - /** - * make sure that the new field is compatible with the selected control type and, if it's not, - * reset the selected control type to the **first** compatible control type - */ - const newCompatibleControlTypes = - fieldRegistry?.[field.name]?.compatibleControlTypes ?? []; - if ( - !selectedControlType || - !newCompatibleControlTypes.includes(selectedControlType!) - ) { - setSelectedControlType(newCompatibleControlTypes[0]); - } - - /** - * set the control title (i.e. the one set by the user) + default title (i.e. the field display name) - */ - const newDefaultTitle = field.displayName ?? field.name; - setDefaultPanelTitle(newDefaultTitle); - const currentTitle = editorState.title; - if (!currentTitle || currentTitle === newDefaultTitle) { - setPanelTitle(newDefaultTitle); - } - - setControlOptionsValid(true); // reset options state + trigger={{ + label: + selectedDataView?.getName() ?? + DataControlEditorStrings.manageControl.dataSource.getSelectDataViewMessage(), }} - selectableProps={{ isLoading: dataViewListLoading || dataViewLoading }} + selectableProps={{ isLoading: dataViewListLoading }} /> )}
+ )} + + + {fieldListError ? ( + +

{fieldListError.message}

+
+ ) : ( + { + const customPredicate = editorConfig?.fieldFilterPredicate?.(field) ?? true; + return Boolean(fieldRegistry?.[field.name]) && customPredicate; + }} + selectedFieldName={editorState.fieldName} + dataView={selectedDataView} + onSelectField={(field) => { + setEditorState({ ...editorState, fieldName: field.name }); + + /** + * make sure that the new field is compatible with the selected control type and, if it's not, + * reset the selected control type to the **first** compatible control type + */ + const newCompatibleControlTypes = + fieldRegistry?.[field.name]?.compatibleControlTypes ?? []; + if ( + !selectedControlType || + !newCompatibleControlTypes.includes(selectedControlType!) + ) { + setSelectedControlType(newCompatibleControlTypes[0]); + } + + /** + * set the control title (i.e. the one set by the user) + default title (i.e. the field display name) + */ + const newDefaultTitle = field.displayName ?? field.name; + setDefaultPanelTitle(newDefaultTitle); + const currentTitle = editorState.title; + if (!currentTitle || currentTitle === newDefaultTitle) { + setPanelTitle(newDefaultTitle); + } + + setControlOptionsValid(true); // reset options state + }} + selectableProps={{ isLoading: dataViewListLoading || dataViewLoading }} + /> + )} +
+ + {/* wrapping in `div` so that focus gets passed properly to the form row */} +
+ +
+
+ + { + setPanelTitle(e.target.value ?? ''); + setEditorState({ + ...editorState, + title: e.target.value === '' ? undefined : e.target.value, + }); + }} + /> + + {!editorConfig?.hideWidthSettings && ( - {/* wrapping in `div` so that focus gets passed properly to the form row */}
- + setEditorState({ ...editorState, width: newWidth as ControlWidth }) + } + /> + + setEditorState({ ...editorState, grow: !editorState.grow })} + data-test-subj="control-editor-grow-switch" />
- - {DataControlEditorStrings.manageControl.displaySettings.getFormGroupTitle()} - } - description={DataControlEditorStrings.manageControl.displaySettings.getFormGroupDescription()} - > - - { - setPanelTitle(e.target.value ?? ''); - setEditorState({ - ...editorState, - title: e.target.value === '' ? undefined : e.target.value, - }); - }} - /> - - {!editorConfig?.hideWidthSettings && ( - -
- - setEditorState({ ...editorState, width: newWidth as ControlWidth }) - } - /> - - setEditorState({ ...editorState, grow: !editorState.grow })} - data-test-subj="control-editor-grow-switch" - /> -
-
- )} -
+ )} {!editorConfig?.hideAdditionalSettings && CustomSettingsComponent} {controlId && ( <> @@ -464,7 +439,6 @@ export const DataControlEditor = { onCancel(editorState); }} @@ -476,7 +450,7 @@ export const DataControlEditor = closeOverlay(overlay), } ); diff --git a/src/plugins/controls/public/controls/data_controls/options_list_control/components/options_list_editor_options.tsx b/src/plugins/controls/public/controls/data_controls/options_list_control/components/options_list_editor_options.tsx index e9dad12be5623..f07a7cc6c58bf 100644 --- a/src/plugins/controls/public/controls/data_controls/options_list_control/components/options_list_editor_options.tsx +++ b/src/plugins/controls/public/controls/data_controls/options_list_control/components/options_list_editor_options.tsx @@ -131,6 +131,7 @@ export const OptionsListEditorOptions = ({ data-test-subj="optionsListControl__selectionOptionsRadioGroup" > { @@ -146,6 +147,7 @@ export const OptionsListEditorOptions = ({ data-test-subj="optionsListControl__searchOptionsRadioGroup" > { @@ -158,6 +160,7 @@ export const OptionsListEditorOptions = ({ )} { const newStep = event.target.valueAsNumber; diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/dashboard_panel_selection_flyout.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/dashboard_panel_selection_flyout.tsx index dbb86046def06..e6adece8ab36d 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/dashboard_panel_selection_flyout.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/dashboard_panel_selection_flyout.tsx @@ -125,7 +125,7 @@ export const DashboardPanelSelectionListFlyout: React.FC< return ( <> - +

{ @@ -281,7 +282,7 @@ export const DashboardPanelSelectionListFlyout: React.FC< diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx index 2cad63c442026..cf7f9c65c6618 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx @@ -43,7 +43,7 @@ export const EditorMenu = ({ createNewVisType, isDisabled }: EditorMenuProps) => function openDashboardPanelSelectionFlyout() { const flyoutPanelPaddingSize: ComponentProps< typeof DashboardPanelSelectionListFlyout - >['paddingSize'] = 'l'; + >['paddingSize'] = 'm'; const mount = toMountPoint( React.createElement(function () { diff --git a/src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.tsx b/src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.tsx index b83ebbcb49d66..b334dbcb5857a 100644 --- a/src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.tsx @@ -189,7 +189,7 @@ export const AddPanelFlyout = ({ return ( <> - +

{i18n.translate('embeddableApi.addPanel.Title', { defaultMessage: 'Add from library' })}

diff --git a/src/plugins/embeddable/public/add_panel_flyout/open_add_panel_flyout.tsx b/src/plugins/embeddable/public/add_panel_flyout/open_add_panel_flyout.tsx index 160289d0d1c2a..9ba3c00a73745 100644 --- a/src/plugins/embeddable/public/add_panel_flyout/open_add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/add_panel_flyout/open_add_panel_flyout.tsx @@ -52,6 +52,9 @@ export const openAddPanelFlyout = ({ if (onClose) onClose(); overlayRef.close(); }, + size: 'm', + maxWidth: 500, + paddingSize: 'm', 'data-test-subj': 'dashboardAddPanel', 'aria-labelledby': modalTitleId, } diff --git a/src/plugins/image_embeddable/public/components/image_editor/image_editor_flyout.test.tsx b/src/plugins/image_embeddable/public/components/image_editor/image_editor_flyout.test.tsx index 265f162d04f6c..f052f0526a945 100644 --- a/src/plugins/image_embeddable/public/components/image_editor/image_editor_flyout.test.tsx +++ b/src/plugins/image_embeddable/public/components/image_editor/image_editor_flyout.test.tsx @@ -43,11 +43,11 @@ const ImageEditor = (props: Partial) => { ); }; -test('should call onCancel when "Close" clicked', async () => { +test('should call onCancel when "Cancel" clicked', async () => { const onCancel = jest.fn(); const { getByText } = render(); - expect(getByText('Close')).toBeVisible(); - await userEvent.click(getByText('Close')); + expect(getByText('Cancel')).toBeVisible(); + await userEvent.click(getByText('Cancel')); expect(onCancel).toBeCalled(); }); diff --git a/src/plugins/image_embeddable/public/components/image_editor/image_editor_flyout.tsx b/src/plugins/image_embeddable/public/components/image_editor/image_editor_flyout.tsx index 2c57f25db6c8b..1a5ee3bc64e1d 100644 --- a/src/plugins/image_embeddable/public/components/image_editor/image_editor_flyout.tsx +++ b/src/plugins/image_embeddable/public/components/image_editor/image_editor_flyout.tsx @@ -121,7 +121,7 @@ export function ImageEditorFlyout(props: ImageEditorFlyoutProps) { return ( <> - +

{isEditing ? ( - - + + + setSrcType('file')} isSelected={srcType === 'file'}> - - + {srcType === 'file' && ( <> {isDraftImageConfigValid ? ( @@ -238,7 +238,7 @@ export function ImageEditorFlyout(props: ImageEditorFlyoutProps) { />

} - titleSize={'s'} + titleSize={'xs'} /> ) : ( )} - - + )} - - - - - - + diff --git a/src/plugins/image_embeddable/public/components/image_editor/open_image_editor.tsx b/src/plugins/image_embeddable/public/components/image_editor/open_image_editor.tsx index ae8ced88d14ef..f730147cb0d2c 100644 --- a/src/plugins/image_embeddable/public/components/image_editor/open_image_editor.tsx +++ b/src/plugins/image_embeddable/public/components/image_editor/open_image_editor.tsx @@ -79,6 +79,9 @@ export const openImageEditor = async ({ onClose: () => { onCancel(); }, + size: 'm', + maxWidth: 500, + paddingSize: 'm', ownFocus: true, 'data-test-subj': 'createImageEmbeddableFlyout', } diff --git a/src/plugins/links/public/components/dashboard_link/dashboard_link_destination_picker.tsx b/src/plugins/links/public/components/dashboard_link/dashboard_link_destination_picker.tsx index b062b9befa284..ab5b923327049 100644 --- a/src/plugins/links/public/components/dashboard_link/dashboard_link_destination_picker.tsx +++ b/src/plugins/links/public/components/dashboard_link/dashboard_link_destination_picker.tsx @@ -119,6 +119,7 @@ export const DashboardLinkDestinationPicker = ({ return ( onClose()} > - +

{link ? LinksStrings.editor.getEditLinkTitle() @@ -113,6 +113,7 @@ export const LinkEditor = ({ { @@ -131,6 +132,7 @@ export const LinkEditor = ({ /> onClose()} - iconType="cross" data-test-subj="links--linkEditor--closeBtn" > {LinksStrings.editor.getCancelButtonLabel()} @@ -160,6 +162,7 @@ export const LinkEditor = ({ { // this check should always be true, since the button is disabled otherwise - this is just for type safety diff --git a/src/plugins/links/public/components/editor/links_editor.scss b/src/plugins/links/public/components/editor/links_editor.scss index 02961c7d5f5cb..c33b95350df98 100644 --- a/src/plugins/links/public/components/editor/links_editor.scss +++ b/src/plugins/links/public/components/editor/links_editor.scss @@ -3,7 +3,7 @@ .linksPanelEditor { .linkEditor { @include euiFlyout; - max-inline-size: $euiSizeXXL * 18; // 40px * 18 = 720px + max-inline-size: $euiSizeXS * 125; // 4px * 125 = 500px &.in { animation: euiFlyoutOpenAnimation $euiAnimSpeedNormal $euiAnimSlightResistance; @@ -59,6 +59,9 @@ } .links_hoverActions { + background-color: $euiColorEmptyShade; + position: absolute; + right: $euiSizeL; opacity: 0; visibility: hidden; transition: visibility $euiAnimSpeedNormal, opacity $euiAnimSpeedNormal; diff --git a/src/plugins/links/public/components/editor/links_editor.tsx b/src/plugins/links/public/components/editor/links_editor.tsx index 93ca47e364c57..8fa33fd4ebcaa 100644 --- a/src/plugins/links/public/components/editor/links_editor.tsx +++ b/src/plugins/links/public/components/editor/links_editor.tsx @@ -167,7 +167,7 @@ const LinksEditor = ({ - +

{isEditingExisting ? LinksStrings.editor.panelEditor.getEditFlyoutTitle() @@ -251,7 +251,6 @@ const LinksEditor = ({ @@ -268,6 +267,7 @@ const LinksEditor = ({ data-test-subj="links--panelEditor--saveByReferenceTooltip" > - - - - - - - - - - - - - - + + + + + + + + + + + + ); diff --git a/src/plugins/links/public/components/external_link/external_link_destination_picker.tsx b/src/plugins/links/public/components/external_link/external_link_destination_picker.tsx index dfdc6e0589e6b..5b8522b39960e 100644 --- a/src/plugins/links/public/components/external_link/external_link_destination_picker.tsx +++ b/src/plugins/links/public/components/external_link/external_link_destination_picker.tsx @@ -54,6 +54,7 @@ export const ExternalLinkDestinationPicker = ({ return (
i18n.translate('links.editor.cancelButtonLabel', { - defaultMessage: 'Close', + defaultMessage: 'Cancel', }), panelEditor: { getLinksTitle: () => diff --git a/src/plugins/links/public/editor/open_editor_flyout.tsx b/src/plugins/links/public/editor/open_editor_flyout.tsx index 041672e89dbbe..87b1ab4e21ff8 100644 --- a/src/plugins/links/public/editor/open_editor_flyout.tsx +++ b/src/plugins/links/public/editor/open_editor_flyout.tsx @@ -137,7 +137,8 @@ export async function openEditorFlyout({ ), { id: flyoutId, - maxWidth: 720, + maxWidth: 500, + paddingSize: 'm', ownFocus: true, onClose: onCancel, outsideClickCloses: false, diff --git a/src/plugins/presentation_util/public/components/dashboard_drilldown_options/dashboard_drilldown_options.tsx b/src/plugins/presentation_util/public/components/dashboard_drilldown_options/dashboard_drilldown_options.tsx index 921560f7a2224..63ff89da2ec17 100644 --- a/src/plugins/presentation_util/public/components/dashboard_drilldown_options/dashboard_drilldown_options.tsx +++ b/src/plugins/presentation_util/public/components/dashboard_drilldown_options/dashboard_drilldown_options.tsx @@ -8,7 +8,7 @@ */ import React from 'react'; -import { EuiFormRow, EuiSwitch } from '@elastic/eui'; +import { EuiFormRow, EuiSpacer, EuiSwitch } from '@elastic/eui'; import { DashboardDrilldownOptions } from './types'; import { dashboardDrilldownConfigStrings } from '../../i18n/dashboard_drilldown_config'; @@ -24,32 +24,35 @@ export const DashboardDrilldownOptionsComponent = ({ }: DashboardDrilldownOptionsProps) => { return ( <> - - onOptionChange({ useCurrentFilters: !options.useCurrentFilters })} - data-test-subj="dashboardDrillDownOptions--useCurrentFilters--checkbox" - /> - - - onOptionChange({ useCurrentDateRange: !options.useCurrentDateRange })} - data-test-subj="dashboardDrillDownOptions--useCurrentDateRange--checkbox" - /> - - - onOptionChange({ openInNewTab: !options.openInNewTab })} - data-test-subj="dashboardDrillDownOptions--openInNewTab--checkbox" - /> + +
+ onOptionChange({ useCurrentFilters: !options.useCurrentFilters })} + data-test-subj="dashboardDrillDownOptions--useCurrentFilters--checkbox" + /> + + onOptionChange({ useCurrentDateRange: !options.useCurrentDateRange })} + data-test-subj="dashboardDrillDownOptions--useCurrentDateRange--checkbox" + /> + + onOptionChange({ openInNewTab: !options.openInNewTab })} + data-test-subj="dashboardDrillDownOptions--openInNewTab--checkbox" + /> +
); diff --git a/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx b/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx index e985e9bec357a..1c8466097bd98 100644 --- a/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx +++ b/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx @@ -52,6 +52,7 @@ export function DataViewPicker({ data-test-subj="open-data-view-picker" onClick={() => setPopoverIsOpen(!isPopoverOpen)} label={label} + size="s" fullWidth {...colorProp} {...rest} diff --git a/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx b/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx index daac202f21b66..0b81cfd66156d 100644 --- a/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx +++ b/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx @@ -140,6 +140,7 @@ export const FieldPicker = ({ placeholder: i18n.translate('presentationUtil.fieldSearch.searchPlaceHolder', { defaultMessage: 'Search field names', }), + compressed: true, disabled: Boolean(selectableProps?.isLoading), inputRef: setSearchRef, }} diff --git a/src/plugins/presentation_util/public/components/field_picker/field_type_filter.tsx b/src/plugins/presentation_util/public/components/field_picker/field_type_filter.tsx index d2e929b8a9a84..4212668599a0d 100644 --- a/src/plugins/presentation_util/public/components/field_picker/field_type_filter.tsx +++ b/src/plugins/presentation_util/public/components/field_picker/field_type_filter.tsx @@ -63,7 +63,7 @@ export function FieldTypeFilter({ ); return ( - + { return ( <> - - onOptionChange({ openInNewTab: !options.openInNewTab })} - data-test-subj="urlDrilldownOpenInNewTab" - /> - - - - {txtUrlTemplateEncodeUrl} - - {txtUrlTemplateEncodeDescription} - - } - checked={options.encodeUrl} - onChange={() => onOptionChange({ encodeUrl: !options.encodeUrl })} - data-test-subj="urlDrilldownEncodeUrl" - /> + +
+ onOptionChange({ openInNewTab: !options.openInNewTab })} + data-test-subj="urlDrilldownOpenInNewTab" + /> + + + {txtUrlTemplateEncodeUrl} + + {txtUrlTemplateEncodeDescription} + + } + checked={options.encodeUrl} + onChange={() => onOptionChange({ encodeUrl: !options.encodeUrl })} + data-test-subj="urlDrilldownEncodeUrl" + /> +
); diff --git a/test/api_integration/apis/data_views/fields_for_wildcard_route/response.ts b/test/api_integration/apis/data_views/fields_for_wildcard_route/response.ts index 2b0fa0dab4424..a80ca89ee4865 100644 --- a/test/api_integration/apis/data_views/fields_for_wildcard_route/response.ts +++ b/test/api_integration/apis/data_views/fields_for_wildcard_route/response.ts @@ -80,7 +80,8 @@ export default function ({ getService }: FtrProviderContext) { }, ]; - describe('fields_for_wildcard_route response', () => { + // Failing: See https://github.com/elastic/kibana/issues/199413 + describe.skip('fields_for_wildcard_route response', () => { before(() => esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index') ); diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/install_elser.ts b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/install_elser.ts index 037a9e809d1e1..09dc85b816191 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/install_elser.ts +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/install_elser.ts @@ -60,7 +60,6 @@ const waitUntilDeployed = async ({ model_id: modelId, }); const deploymentStats = statsRes.trained_model_stats[0]?.deployment_stats; - // @ts-expect-error deploymentStats.nodes not defined as array even if it is. if (!deploymentStats || deploymentStats.nodes.length === 0) { await sleep(delay); continue; diff --git a/x-pack/packages/kbn-cloud-security-posture/common/schema/graph/v1.ts b/x-pack/packages/kbn-cloud-security-posture/common/schema/graph/v1.ts index 3d37331b4cc5d..076c685aca5b9 100644 --- a/x-pack/packages/kbn-cloud-security-posture/common/schema/graph/v1.ts +++ b/x-pack/packages/kbn-cloud-security-posture/common/schema/graph/v1.ts @@ -6,14 +6,26 @@ */ import { schema } from '@kbn/config-schema'; +import { ApiMessageCode } from '../../types/graph/v1'; export const graphRequestSchema = schema.object({ + nodesLimit: schema.maybe(schema.number()), + showUnknownTarget: schema.maybe(schema.boolean()), query: schema.object({ - actorIds: schema.arrayOf(schema.string()), eventIds: schema.arrayOf(schema.string()), // TODO: use zod for range validation instead of config schema start: schema.oneOf([schema.number(), schema.string()]), end: schema.oneOf([schema.number(), schema.string()]), + esQuery: schema.maybe( + schema.object({ + bool: schema.object({ + filter: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + must: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + should: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + must_not: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + }), + }) + ), }), }); @@ -23,6 +35,9 @@ export const graphResponseSchema = () => schema.oneOf([entityNodeDataSchema, groupNodeDataSchema, labelNodeDataSchema]) ), edges: schema.arrayOf(edgeDataSchema), + messages: schema.maybe( + schema.arrayOf(schema.oneOf([schema.literal(ApiMessageCode.ReachedNodesLimit)])) + ), }); export const colorSchema = schema.oneOf([ diff --git a/x-pack/packages/kbn-cloud-security-posture/common/tsconfig.json b/x-pack/packages/kbn-cloud-security-posture/common/tsconfig.json index c7cf1e9208bfc..ebec9929559f0 100644 --- a/x-pack/packages/kbn-cloud-security-posture/common/tsconfig.json +++ b/x-pack/packages/kbn-cloud-security-posture/common/tsconfig.json @@ -20,5 +20,6 @@ "@kbn/i18n", "@kbn/analytics", "@kbn/usage-collection-plugin", + "@kbn/es-query", ] } diff --git a/x-pack/packages/kbn-cloud-security-posture/common/types/graph/v1.ts b/x-pack/packages/kbn-cloud-security-posture/common/types/graph/v1.ts index 48d1d1c49fd03..f97d11b34732c 100644 --- a/x-pack/packages/kbn-cloud-security-posture/common/types/graph/v1.ts +++ b/x-pack/packages/kbn-cloud-security-posture/common/types/graph/v1.ts @@ -6,6 +6,7 @@ */ import type { TypeOf } from '@kbn/config-schema'; +import type { BoolQuery } from '@kbn/es-query'; import { colorSchema, edgeDataSchema, @@ -17,13 +18,21 @@ import { nodeShapeSchema, } from '../../schema/graph/v1'; -export type GraphRequest = TypeOf; -export type GraphResponse = TypeOf; +export type GraphRequest = Omit, 'query.esQuery'> & { + query: { esQuery?: { bool: Partial } }; +}; +export type GraphResponse = Omit, 'messages'> & { + messages?: ApiMessageCode[]; +}; export type Color = typeof colorSchema.type; export type NodeShape = TypeOf; +export enum ApiMessageCode { + ReachedNodesLimit = 'REACHED_NODES_LIMIT', +} + export type EntityNodeDataModel = TypeOf; export type GroupNodeDataModel = TypeOf; diff --git a/x-pack/packages/ml/random_sampler_utils/src/random_sampler_wrapper.ts b/x-pack/packages/ml/random_sampler_utils/src/random_sampler_wrapper.ts index 5054833ac7dd0..39d26509422a2 100644 --- a/x-pack/packages/ml/random_sampler_utils/src/random_sampler_wrapper.ts +++ b/x-pack/packages/ml/random_sampler_utils/src/random_sampler_wrapper.ts @@ -69,7 +69,6 @@ export const createRandomSamplerWrapper = (options: RandomSamplerOptions) => { return { [aggName]: { - // @ts-expect-error `random_sampler` is not yet part of `AggregationsAggregationContainer` random_sampler: { probability, ...(options.seed ? { seed: options.seed } : {}), diff --git a/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts b/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts index 9e9744b33d940..9fb817b275a0d 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts @@ -10,6 +10,7 @@ import { graphResponseSchema, } from '@kbn/cloud-security-posture-common/schema/graph/latest'; import { transformError } from '@kbn/securitysolution-es-utils'; +import type { GraphRequest } from '@kbn/cloud-security-posture-common/types/graph/v1'; import { GRAPH_ROUTE_PATH } from '../../../common/constants'; import { CspRouter } from '../../types'; import { getGraph as getGraphV1 } from './v1'; @@ -39,26 +40,29 @@ export const defineGraphRoute = (router: CspRouter) => }, }, async (context, request, response) => { - const { actorIds, eventIds, start, end } = request.body.query; + const { nodesLimit, showUnknownTarget = false } = request.body; + const { eventIds, start, end, esQuery } = request.body.query as GraphRequest['query']; const cspContext = await context.csp; const spaceId = (await cspContext.spaces?.spacesService?.getActiveSpace(request))?.id; try { - const { nodes, edges } = await getGraphV1( - { + const resp = await getGraphV1({ + services: { logger: cspContext.logger, esClient: cspContext.esClient, }, - { - actorIds, + query: { eventIds, spaceId, start, end, - } - ); + esQuery, + }, + showUnknownTarget, + nodesLimit, + }); - return response.ok({ body: { nodes, edges } }); + return response.ok({ body: resp }); } catch (err) { const error = transformError(err); cspContext.logger.error(`Failed to fetch graph ${err}`); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/graph/types.ts b/x-pack/plugins/cloud_security_posture/server/routes/graph/types.ts deleted file mode 100644 index ba32664da6233..0000000000000 --- a/x-pack/plugins/cloud_security_posture/server/routes/graph/types.ts +++ /dev/null @@ -1,23 +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 type { - EdgeDataModel, - NodeDataModel, -} from '@kbn/cloud-security-posture-common/types/graph/latest'; -import type { Logger, IScopedClusterClient } from '@kbn/core/server'; -import type { Writable } from '@kbn/utility-types'; - -export interface GraphContextServices { - logger: Logger; - esClient: IScopedClusterClient; -} - -export interface GraphContext { - nodes: Array>; - edges: Array>; -} diff --git a/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts b/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts index 5102d153c1905..b14a2ba3e06a9 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts @@ -8,22 +8,27 @@ import { castArray } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; import type { Logger, IScopedClusterClient } from '@kbn/core/server'; +import { ApiMessageCode } from '@kbn/cloud-security-posture-common/types/graph/latest'; import type { + Color, EdgeDataModel, - NodeDataModel, EntityNodeDataModel, - LabelNodeDataModel, + GraphRequest, + GraphResponse, GroupNodeDataModel, -} from '@kbn/cloud-security-posture-common/types/graph/latest'; + LabelNodeDataModel, + NodeDataModel, +} from '@kbn/cloud-security-posture-common/types/graph/v1'; import type { EsqlToRecords } from '@elastic/elasticsearch/lib/helpers'; import type { Writable } from '@kbn/utility-types'; -import type { GraphContextServices, GraphContext } from './types'; + +type EsQuery = GraphRequest['query']['esQuery']; interface GraphEdge { badge: number; - ips: string[]; - hosts: string[]; - users: string[]; + ips?: string[] | string; + hosts?: string[] | string; + users?: string[] | string; actorIds: string[] | string; action: string; targetIds: string[] | string; @@ -36,50 +41,75 @@ interface LabelEdges { target: string; } -export const getGraph = async ( - services: GraphContextServices, +interface GraphContextServices { + logger: Logger; + esClient: IScopedClusterClient; +} + +interface GetGraphParams { + services: GraphContextServices; query: { - actorIds: string[]; eventIds: string[]; spaceId?: string; start: string | number; end: string | number; - } -): Promise<{ - nodes: NodeDataModel[]; - edges: EdgeDataModel[]; -}> => { - const { esClient, logger } = services; - const { actorIds, eventIds, spaceId = 'default', start, end } = query; - - logger.trace( - `Fetching graph for [eventIds: ${eventIds.join(', ')}] [actorIds: ${actorIds.join( - ', ' - )}] in [spaceId: ${spaceId}]` - ); + esQuery?: EsQuery; + }; + showUnknownTarget: boolean; + nodesLimit?: number; +} - const results = await fetchGraph({ esClient, logger, start, end, eventIds, actorIds }); +export const getGraph = async ({ + services: { esClient, logger }, + query: { eventIds, spaceId = 'default', start, end, esQuery }, + showUnknownTarget, + nodesLimit, +}: GetGraphParams): Promise> => { + logger.trace(`Fetching graph for [eventIds: ${eventIds.join(', ')}] in [spaceId: ${spaceId}]`); + + const results = await fetchGraph({ + esClient, + showUnknownTarget, + logger, + start, + end, + eventIds, + esQuery, + }); // Convert results into set of nodes and edges - const graphContext = parseRecords(logger, results.records); - - return { nodes: graphContext.nodes, edges: graphContext.edges }; + return parseRecords(logger, results.records, nodesLimit); }; interface ParseContext { - nodesMap: Record; - edgesMap: Record; - edgeLabelsNodes: Record; - labelEdges: Record; + readonly nodesLimit?: number; + readonly nodesMap: Record; + readonly edgesMap: Record; + readonly edgeLabelsNodes: Record; + readonly labelEdges: Record; + readonly messages: ApiMessageCode[]; + readonly logger: Logger; } -const parseRecords = (logger: Logger, records: GraphEdge[]): GraphContext => { - const ctx: ParseContext = { nodesMap: {}, edgeLabelsNodes: {}, edgesMap: {}, labelEdges: {} }; +const parseRecords = ( + logger: Logger, + records: GraphEdge[], + nodesLimit?: number +): Pick => { + const ctx: ParseContext = { + nodesLimit, + logger, + nodesMap: {}, + edgeLabelsNodes: {}, + edgesMap: {}, + labelEdges: {}, + messages: [], + }; - logger.trace(`Parsing records [length: ${records.length}]`); + logger.trace(`Parsing records [length: ${records.length}] [nodesLimit: ${nodesLimit ?? 'none'}]`); - createNodes(logger, records, ctx); - createEdgesAndGroups(logger, ctx); + createNodes(records, ctx); + createEdgesAndGroups(ctx); logger.trace( `Parsed [nodes: ${Object.keys(ctx.nodesMap).length}, edges: ${ @@ -90,7 +120,11 @@ const parseRecords = (logger: Logger, records: GraphEdge[]): GraphContext => { // Sort groups to be first (fixes minor layout issue) const nodes = sortNodes(ctx.nodesMap); - return { nodes, edges: Object.values(ctx.edgesMap) }; + return { + nodes, + edges: Object.values(ctx.edgesMap), + messages: ctx.messages.length > 0 ? ctx.messages : undefined, + }; }; const fetchGraph = async ({ @@ -98,15 +132,17 @@ const fetchGraph = async ({ logger, start, end, - actorIds, eventIds, + showUnknownTarget, + esQuery, }: { esClient: IScopedClusterClient; logger: Logger; start: string | number; end: string | number; - actorIds: string[]; eventIds: string[]; + showUnknownTarget: boolean; + esQuery?: EsQuery; }): Promise> => { const query = `from logs-* | WHERE event.action IS NOT NULL AND actor.entity.id IS NOT NULL @@ -124,59 +160,84 @@ const fetchGraph = async ({ targetIds = target.entity.id, eventOutcome = event.outcome, isAlert -| LIMIT 1000`; +| LIMIT 1000 +| SORT isAlert DESC`; logger.trace(`Executing query [${query}]`); return await esClient.asCurrentUser.helpers .esql({ columnar: false, - filter: { - bool: { - must: [ + filter: buildDslFilter(eventIds, showUnknownTarget, start, end, esQuery), + query, + // @ts-ignore - types are not up to date + params: [...eventIds.map((id, idx) => ({ [`al_id${idx}`]: id }))], + }) + .toRecords(); +}; + +const buildDslFilter = ( + eventIds: string[], + showUnknownTarget: boolean, + start: string | number, + end: string | number, + esQuery?: EsQuery +) => ({ + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: start, + lte: end, + }, + }, + }, + ...(showUnknownTarget + ? [] + : [ { - range: { - '@timestamp': { - gte: start, - lte: end, - }, + exists: { + field: 'target.entity.id', }, }, + ]), + { + bool: { + should: [ + ...(esQuery?.bool.filter?.length || + esQuery?.bool.must?.length || + esQuery?.bool.should?.length || + esQuery?.bool.must_not?.length + ? [esQuery] + : []), { - bool: { - should: [ - { - terms: { - 'event.id': eventIds, - }, - }, - { - terms: { - 'actor.entity.id': actorIds, - }, - }, - ], - minimum_should_match: 1, + terms: { + 'event.id': eventIds, }, }, ], + minimum_should_match: 1, }, }, - query, - // @ts-ignore - types are not up to date - params: [...eventIds.map((id, idx) => ({ [`al_id${idx}`]: id }))], - }) - .toRecords(); -}; + ], + }, +}); -const createNodes = ( - logger: Logger, - records: GraphEdge[], - context: Omit -) => { +const createNodes = (records: GraphEdge[], context: Omit) => { const { nodesMap, edgeLabelsNodes, labelEdges } = context; for (const record of records) { + if (context.nodesLimit !== undefined && Object.keys(nodesMap).length >= context.nodesLimit) { + context.logger.debug( + `Reached nodes limit [limit: ${context.nodesLimit}] [current: ${ + Object.keys(nodesMap).length + }]` + ); + context.messages.push(ApiMessageCode.ReachedNodesLimit); + break; + } + const { ips, hosts, users, actorIds, action, targetIds, isAlert, eventOutcome } = record; const actorIdsArray = castArray(actorIds); const targetIdsArray = castArray(targetIds); @@ -190,12 +251,6 @@ const createNodes = ( } }); - logger.trace( - `Parsing record [actorIds: ${actorIdsArray.join( - ', ' - )}, action: ${action}, targetIds: ${targetIdsArray.join(', ')}]` - ); - // Create entity nodes [...actorIdsArray, ...targetIdsArray].forEach((id) => { if (nodesMap[id] === undefined) { @@ -203,10 +258,13 @@ const createNodes = ( id, label: unknownTargets.includes(id) ? 'Unknown' : undefined, color: isAlert ? 'danger' : 'primary', - ...determineEntityNodeShape(id, ips ?? [], hosts ?? [], users ?? []), + ...determineEntityNodeShape( + id, + castArray(ips ?? []), + castArray(hosts ?? []), + castArray(users ?? []) + ), }; - - logger.trace(`Creating entity node [${id}]`); } }); @@ -226,8 +284,6 @@ const createNodes = ( shape: 'label', }; - logger.trace(`Creating label node [${labelNode.id}]`); - nodesMap[labelNode.id] = labelNode; edgeLabelsNodes[edgeId].push(labelNode.id); labelEdges[labelNode.id] = { source: actorId, target: targetId }; @@ -278,7 +334,7 @@ const sortNodes = (nodesMap: Record) => { return [...groupNodes, ...otherNodes]; }; -const createEdgesAndGroups = (logger: Logger, context: ParseContext) => { +const createEdgesAndGroups = (context: ParseContext) => { const { edgeLabelsNodes, edgesMap, nodesMap, labelEdges } = context; Object.entries(edgeLabelsNodes).forEach(([edgeId, edgeLabelsIds]) => { @@ -287,7 +343,6 @@ const createEdgesAndGroups = (logger: Logger, context: ParseContext) => { const edgeLabelId = edgeLabelsIds[0]; connectEntitiesAndLabelNode( - logger, edgesMap, nodesMap, labelEdges[edgeLabelId].source, @@ -300,44 +355,47 @@ const createEdgesAndGroups = (logger: Logger, context: ParseContext) => { shape: 'group', }; nodesMap[groupNode.id] = groupNode; + let groupEdgesColor: Color = 'primary'; + + edgeLabelsIds.forEach((edgeLabelId) => { + (nodesMap[edgeLabelId] as Writable).parentId = groupNode.id; + connectEntitiesAndLabelNode(edgesMap, nodesMap, groupNode.id, edgeLabelId, groupNode.id); + + if ((nodesMap[edgeLabelId] as LabelNodeDataModel).color === 'danger') { + groupEdgesColor = 'danger'; + } else if ( + (nodesMap[edgeLabelId] as LabelNodeDataModel).color === 'warning' && + groupEdgesColor !== 'danger' + ) { + // Use warning only if there's no danger color + groupEdgesColor = 'warning'; + } + }); connectEntitiesAndLabelNode( - logger, edgesMap, nodesMap, labelEdges[edgeLabelsIds[0]].source, groupNode.id, - labelEdges[edgeLabelsIds[0]].target + labelEdges[edgeLabelsIds[0]].target, + groupEdgesColor ); - - edgeLabelsIds.forEach((edgeLabelId) => { - (nodesMap[edgeLabelId] as Writable).parentId = groupNode.id; - connectEntitiesAndLabelNode( - logger, - edgesMap, - nodesMap, - groupNode.id, - edgeLabelId, - groupNode.id - ); - }); } }); }; const connectEntitiesAndLabelNode = ( - logger: Logger, edgesMap: Record, nodesMap: Record, sourceNodeId: string, labelNodeId: string, - targetNodeId: string + targetNodeId: string, + colorOverride?: Color ) => { [ - connectNodes(nodesMap, sourceNodeId, labelNodeId), - connectNodes(nodesMap, labelNodeId, targetNodeId), + connectNodes(nodesMap, sourceNodeId, labelNodeId, colorOverride), + connectNodes(nodesMap, labelNodeId, targetNodeId, colorOverride), ].forEach((edge) => { - logger.trace(`Connecting nodes [${edge.source} -> ${edge.target}]`); edgesMap[edge.id] = edge; }); }; @@ -345,7 +403,8 @@ const connectEntitiesAndLabelNode = ( const connectNodes = ( nodesMap: Record, sourceNodeId: string, - targetNodeId: string + targetNodeId: string, + colorOverride?: Color ): EdgeDataModel => { const sourceNode = nodesMap[sourceNodeId]; const targetNode = nodesMap[targetNodeId]; @@ -360,6 +419,6 @@ const connectNodes = ( id: `a(${sourceNodeId})-b(${targetNodeId})`, source: sourceNodeId, target: targetNodeId, - color, + color: colorOverride ?? color, }; }; diff --git a/x-pack/plugins/index_management/public/application/components/no_match/no_match.tsx b/x-pack/plugins/index_management/public/application/components/no_match/no_match.tsx index 7f5b3f4b4b7d5..15e306bb396b7 100644 --- a/x-pack/plugins/index_management/public/application/components/no_match/no_match.tsx +++ b/x-pack/plugins/index_management/public/application/components/no_match/no_match.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; import { CreateIndexButton } from '../../sections/home/index_list/create_index/create_index_button'; import { ExtensionsService } from '../../../services/extensions_service'; @@ -16,11 +17,13 @@ export const NoMatch = ({ filter, resetFilter, extensionsService, + share, }: { loadIndices: () => void; filter: string; resetFilter: () => void; extensionsService: ExtensionsService; + share?: SharePluginStart; }) => { if (filter) { return ( @@ -62,7 +65,7 @@ export const NoMatch = ({ if (extensionsService.emptyListContent) { return extensionsService.emptyListContent.renderContent({ - createIndexButton: , + createIndexButton: , }); } @@ -85,7 +88,7 @@ export const NoMatch = ({ />

} - actions={} + actions={} /> ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/create_index/create_index_button.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/create_index/create_index_button.tsx index 746d684f48b75..e7201ce5d44b3 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/create_index/create_index_button.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/create_index/create_index_button.tsx @@ -7,22 +7,32 @@ import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; import { EuiButton } from '@elastic/eui'; import { CreateIndexModal } from './create_index_modal'; -export const CreateIndexButton = ({ loadIndices }: { loadIndices: () => void }) => { +export interface CreateIndexButtonProps { + loadIndices: () => void; + share?: SharePluginStart; +} + +export const CreateIndexButton = ({ loadIndices, share }: CreateIndexButtonProps) => { const [createIndexModalOpen, setCreateIndexModalOpen] = useState(false); + const createIndexUrl = share?.url.locators.get('SEARCH_CREATE_INDEX')?.useUrl({}); + const actionProp = createIndexUrl + ? { href: createIndexUrl } + : { onClick: () => setCreateIndexModalOpen(true) }; return ( <> setCreateIndexModalOpen(true)} key="createIndexButton" data-test-subj="createIndexButton" data-telemetry-id="idxMgmt-indexList-createIndexButton" + {...actionProp} > - {({ services, config, core }) => { + {({ services, config, core, plugins }) => { const { extensionsService } = services; const { application, http } = core; + const { share } = plugins; const columnConfigs = getColumnConfigs({ showIndexStats: config.enableIndexStats, showSizeAndDocCount: config.enableSizeAndDocCount, @@ -669,7 +670,7 @@ export class IndexTable extends Component { )} - + @@ -714,6 +715,7 @@ export class IndexTable extends Component { filterChanged('')} extensionsService={extensionsService} diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/include_exclude_options.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/include_exclude_options.test.tsx index 87026e0613296..8df04b23a5435 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/include_exclude_options.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/include_exclude_options.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { fireEvent, render, screen, within } from '@testing-library/react'; +import { fireEvent, render, screen, within, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { IncludeExcludeRow, IncludeExcludeRowProps } from './include_exclude_options'; @@ -152,4 +152,168 @@ describe('IncludeExcludeComponent', () => { }); expect(onUpdateSpy).toHaveBeenCalledTimes(2); }); + + it('should prevent identical include and exclude values on change when making single selections', async () => { + renderIncludeExcludeRow({ + include: undefined, + exclude: undefined, + isNumberField: false, + tableRows, + }); + + await userEvent.click(screen.getByRole('combobox', { name: 'Include values' })); + await userEvent.click(screen.getByRole('option', { name: 'ABC' })); + expect(screen.getByTestId('lens-include-terms-combobox')).toHaveTextContent('ABC'); + + await userEvent.click(screen.getByRole('combobox', { name: 'Exclude values' })); + await userEvent.click(screen.getByRole('option', { name: 'ABC' })); + expect(screen.getByTestId('lens-exclude-terms-combobox')).toHaveTextContent('ABC'); + + expect(screen.getByTestId('lens-include-terms-combobox')).not.toHaveTextContent('ABC'); + + expect(onUpdateSpy).toHaveBeenCalledTimes(3); + }); + + it('should prevent identical include and exclude values on change when making multiple selections', async () => { + renderIncludeExcludeRow({ + include: undefined, + exclude: undefined, + isNumberField: false, + tableRows, + }); + + await userEvent.click(screen.getByRole('combobox', { name: 'Include values' })); + await userEvent.click(screen.getByRole('option', { name: 'ABC' })); + expect(screen.getByTestId('lens-include-terms-combobox')).toHaveTextContent('ABC'); + + await userEvent.click(screen.getByRole('combobox', { name: 'Include values' })); + await userEvent.click(screen.getByRole('option', { name: 'FEF' })); + expect(screen.getByTestId('lens-include-terms-combobox')).toHaveTextContent('FEF'); + + await userEvent.click(screen.getByRole('combobox', { name: 'Exclude values' })); + await userEvent.click(screen.getByRole('option', { name: 'ABC' })); + expect(screen.getByTestId('lens-include-terms-combobox')).not.toHaveTextContent('ABC'); + + expect(screen.getByTestId('lens-exclude-terms-combobox')).toHaveTextContent('ABC'); + + expect(onUpdateSpy).toHaveBeenCalledTimes(4); + }); + + it('should prevent identical include and exclude values on create option', async () => { + renderIncludeExcludeRow({ + include: undefined, + exclude: undefined, + isNumberField: false, + tableRows, + }); + + await userEvent.click(screen.getByRole('combobox', { name: 'Include values' })); + await userEvent.type(screen.getByRole('combobox', { name: 'Include values' }), 'test{enter}'); + expect(screen.getByTestId('lens-include-terms-combobox')).toHaveTextContent('test'); + + await userEvent.click(screen.getByRole('combobox', { name: 'Exclude values' })); + await userEvent.type(screen.getByRole('combobox', { name: 'Exclude values' }), 'test{enter}'); + expect(screen.getByTestId('lens-exclude-terms-combobox')).toHaveTextContent('test'); + + expect(screen.getByTestId('lens-include-terms-combobox')).not.toHaveTextContent('test'); + + expect(onUpdateSpy).toHaveBeenCalledTimes(3); + }); + + it('should prevent identical include and exclude values when creating multiple options', async () => { + renderIncludeExcludeRow({ + include: undefined, + exclude: undefined, + isNumberField: false, + tableRows, + }); + + await userEvent.click(screen.getByRole('combobox', { name: 'Include values' })); + await userEvent.type(screen.getByRole('combobox', { name: 'Include values' }), 'test{enter}'); + expect(screen.getByTestId('lens-include-terms-combobox')).toHaveTextContent('test'); + + await userEvent.type(screen.getByRole('combobox', { name: 'Include values' }), 'test1{enter}'); + expect(screen.getByTestId('lens-include-terms-combobox')).toHaveTextContent('test1'); + + await userEvent.click(screen.getByRole('combobox', { name: 'Exclude values' })); + await userEvent.type(screen.getByRole('combobox', { name: 'Exclude values' }), 'test1{enter}'); + expect(screen.getByTestId('lens-exclude-terms-combobox')).toHaveTextContent('test1'); + + expect(screen.getByTestId('lens-include-terms-combobox')).not.toHaveTextContent('test1'); + + expect(onUpdateSpy).toHaveBeenCalledTimes(4); + }); + + it('should prevent identical include value on exclude regex value change', async () => { + jest.useFakeTimers(); + + renderIncludeExcludeRow({ + include: [''], + exclude: [''], + includeIsRegex: true, + excludeIsRegex: true, + tableRows, + }); + + const includeRegexInput = screen.getByTestId('lens-include-terms-regex-input'); + const excludeRegexInput = screen.getByTestId('lens-exclude-terms-regex-input'); + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + + await user.type(includeRegexInput, 'test.*'); + act(() => { + jest.advanceTimersByTime(256); + }); + expect(includeRegexInput).toHaveValue('test.*'); + expect(onUpdateSpy).toHaveBeenCalledWith('include', ['test.*'], 'includeIsRegex', true); + + await user.type(excludeRegexInput, 'test.*'); + act(() => { + jest.advanceTimersByTime(256); + }); + expect(excludeRegexInput).toHaveValue('test.*'); + expect(onUpdateSpy).toHaveBeenCalledWith('exclude', ['test.*'], 'excludeIsRegex', true); + + expect(includeRegexInput).toHaveValue(''); + expect(onUpdateSpy).toHaveBeenCalledWith('include', [''], 'includeIsRegex', true); + + expect(onUpdateSpy).toHaveBeenCalledTimes(3); + + jest.useRealTimers(); + }); + + it('should prevent identical exclude value on include regex value change', async () => { + jest.useFakeTimers(); + + renderIncludeExcludeRow({ + include: [''], + exclude: [''], + includeIsRegex: true, + excludeIsRegex: true, + tableRows, + }); + + const includeRegexInput = screen.getByTestId('lens-include-terms-regex-input'); + const excludeRegexInput = screen.getByTestId('lens-exclude-terms-regex-input'); + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + + await user.type(excludeRegexInput, 'test.*'); + act(() => { + jest.advanceTimersByTime(256); + }); + expect(excludeRegexInput).toHaveValue('test.*'); + expect(onUpdateSpy).toHaveBeenCalledWith('exclude', ['test.*'], 'excludeIsRegex', true); + + await user.type(includeRegexInput, 'test.*'); + act(() => { + jest.advanceTimersByTime(256); + }); + expect(includeRegexInput).toHaveValue('test.*'); + expect(onUpdateSpy).toHaveBeenCalledWith('include', ['test.*'], 'includeIsRegex', true); + + expect(excludeRegexInput).toHaveValue(''); + expect(onUpdateSpy).toHaveBeenCalledWith('exclude', [''], 'excludeIsRegex', true); + + expect(onUpdateSpy).toHaveBeenCalledTimes(3); + jest.useRealTimers(); + }); }); diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/include_exclude_options.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/include_exclude_options.tsx index 41f521088af94..b2a8abb62c1ae 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/include_exclude_options.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/include_exclude_options.tsx @@ -99,68 +99,77 @@ export const IncludeExcludeRow = ({ selectedOptions: IncludeExcludeOptions[], operation: 'include' | 'exclude' ) => { - const options = { - ...includeExcludeSelectedOptions, - [operation]: selectedOptions, - }; - setIncludeExcludeSelectedOptions(options); - const terms = selectedOptions.map((option) => { - if (!Number.isNaN(Number(option.label))) { - return Number(option.label); - } - return option.label; + const otherOperation = operation === 'include' ? 'exclude' : 'include'; + const otherSelectedOptions = includeExcludeSelectedOptions[otherOperation] ?? []; + const hasIdenticalOptions = selectedOptions.some((option) => { + return otherSelectedOptions.some((otherOption) => otherOption.label === option.label); }); - const param = `${operation}IsRegex`; - updateParams(operation, terms, param, false); - }; - - const onCreateOption = ( - searchValue: string, - flattenedOptions: IncludeExcludeOptions[] = [], - operation: 'include' | 'exclude' - ) => { - const newOption = { - label: searchValue, - }; - let includeExcludeOptions = []; + const otherSelectedNonIdenticalOptions = hasIdenticalOptions + ? otherSelectedOptions.filter( + (otherOption) => !selectedOptions.some((option) => option.label === otherOption.label) + ) + : otherSelectedOptions; - const includeORExcludeSelectedOptions = includeExcludeSelectedOptions[operation] ?? []; - includeExcludeOptions = [...includeORExcludeSelectedOptions, newOption]; const options = { - ...includeExcludeSelectedOptions, - [operation]: includeExcludeOptions, + [otherOperation]: otherSelectedNonIdenticalOptions, + [operation]: selectedOptions, }; setIncludeExcludeSelectedOptions(options); - const terms = includeExcludeOptions.map((option) => { - if (!Number.isNaN(Number(option.label))) { - return Number(option.label); - } - return option.label; - }); + const getTerms = (updatedSelectedOptions: IncludeExcludeOptions[]) => + updatedSelectedOptions.map((option) => { + if (!Number.isNaN(Number(option.label))) { + return Number(option.label); + } + return option.label; + }); + + const terms = getTerms(selectedOptions); const param = `${operation}IsRegex`; updateParams(operation, terms, param, false); + + if (hasIdenticalOptions) { + const otherTerms = getTerms(otherSelectedNonIdenticalOptions); + const otherParam = `${otherOperation}IsRegex`; + updateParams(otherOperation, otherTerms, otherParam, false); + } + }; + + const onCreateOption = (searchValue: string, operation: 'include' | 'exclude') => { + const newOption = { label: searchValue }; + const selectedOptions = [...(includeExcludeSelectedOptions[operation] ?? []), newOption]; + onChangeIncludeExcludeOptions(selectedOptions, operation); }; const onIncludeRegexChangeToDebounce = useCallback( (newIncludeValue: string | number | undefined) => { + const isEqualToExcludeValue = newIncludeValue === regex.exclude; + const excludeValue = isEqualToExcludeValue ? '' : regex.exclude; setRegex({ - ...regex, + exclude: excludeValue, include: newIncludeValue, }); updateParams('include', [newIncludeValue ?? ''], 'includeIsRegex', true); + if (isEqualToExcludeValue) { + updateParams('exclude', [''], 'excludeIsRegex', true); + } }, [regex, updateParams] ); const onExcludeRegexChangeToDebounce = useCallback( (newExcludeValue: string | number | undefined) => { + const isEqualToIncludeValue = newExcludeValue === regex.include; + const includeValue = isEqualToIncludeValue ? '' : regex.include; setRegex({ - ...regex, + include: includeValue, exclude: newExcludeValue, }); updateParams('exclude', [newExcludeValue ?? ''], 'excludeIsRegex', true); + if (isEqualToIncludeValue) { + updateParams('include', [''], 'includeIsRegex', true); + } }, [regex, updateParams] ); @@ -247,9 +256,7 @@ export const IncludeExcludeRow = ({ options={termsOptions} selectedOptions={includeExcludeSelectedOptions.include} onChange={(options) => onChangeIncludeExcludeOptions(options, 'include')} - onCreateOption={(searchValue, options) => - onCreateOption(searchValue, options, 'include') - } + onCreateOption={(searchValue) => onCreateOption(searchValue, 'include')} isClearable={true} data-test-subj="lens-include-terms-combobox" autoFocus @@ -300,6 +307,7 @@ export const IncludeExcludeRow = ({ defaultMessage: 'Enter a regex to filter values', } )} + data-test-subj="lens-exclude-terms-regex-input" value={excludeRegexValue} onChange={(e) => { onExcludeRegexValueChange(e.target.value); @@ -322,9 +330,7 @@ export const IncludeExcludeRow = ({ options={termsOptions} selectedOptions={includeExcludeSelectedOptions.exclude} onChange={(options) => onChangeIncludeExcludeOptions(options, 'exclude')} - onCreateOption={(searchValue, options) => - onCreateOption(searchValue, options, 'exclude') - } + onCreateOption={(searchValue) => onCreateOption(searchValue, 'exclude')} isClearable={true} data-test-subj="lens-exclude-terms-combobox" autoFocus diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts index 0a3632efe9195..abdffd19eca76 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts @@ -34,7 +34,7 @@ export const transformElasticNamedSearchToListItem = ({ }: TransformElasticMSearchToListItemOptions): SearchListItemArraySchema => { return value.map((singleValue, index) => { const matchingHits = response.hits.hits.filter((hit) => { - if (hit.matched_queries != null) { + if (hit.matched_queries != null && Array.isArray(hit.matched_queries)) { return hit.matched_queries.some((matchedQuery) => { const [matchedQueryIndex] = matchedQuery.split('.'); return matchedQueryIndex === `${index}`; diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/_chart_tooltip.scss b/x-pack/plugins/ml/public/application/components/chart_tooltip/_chart_tooltip.scss deleted file mode 100644 index 9f9f16dff7e13..0000000000000 --- a/x-pack/plugins/ml/public/application/components/chart_tooltip/_chart_tooltip.scss +++ /dev/null @@ -1,47 +0,0 @@ -.mlChartTooltip { - @include euiToolTipStyle('s'); - @include euiFontSizeXS; - padding: 0; - transition: opacity $euiAnimSpeedNormal; - pointer-events: none; - user-select: none; - max-width: 512px; - - &__list { - margin: $euiSizeXS; - padding-bottom: $euiSizeXS; - } - - &__header { - font-weight: $euiFontWeightBold; - padding: $euiSizeXS ($euiSizeXS * 2); - margin-bottom: $euiSizeXS; - border-bottom: $euiBorderThin solid transparentize($euiBorderColor, .8); - } - - &__item { - display: flex; - padding: 3px; - box-sizing: border-box; - border-left: $euiSizeXS solid transparent; - } - - &__label { - min-width: 1px; - } - - &__value { - font-weight: $euiFontWeightBold; - text-align: right; - font-feature-settings: 'tnum'; - margin-left: 8px; - } - - &__rowHighlighted { - background-color: transparentize($euiColorGhost, .9); - } - - &--hidden { - opacity: 0; - } -} diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/_index.scss b/x-pack/plugins/ml/public/application/components/chart_tooltip/_index.scss deleted file mode 100644 index 11b36a0a21001..0000000000000 --- a/x-pack/plugins/ml/public/application/components/chart_tooltip/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'chart_tooltip'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx index 0c6fe9095f4e2..f279175d01107 100644 --- a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx +++ b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx @@ -9,14 +9,14 @@ import type { FC } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import classNames from 'classnames'; import TooltipTrigger from 'react-popper-tooltip'; +import type { ChildrenArg, TooltipTriggerProps } from 'react-popper-tooltip/dist/types'; + import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import type { TooltipValueFormatter } from '@elastic/charts'; -import './_index.scss'; - -import type { ChildrenArg, TooltipTriggerProps } from 'react-popper-tooltip/dist/types'; import type { ChartTooltipValue, TooltipData } from './chart_tooltip_service'; import { ChartTooltipService } from './chart_tooltip_service'; +import { useChartTooltipStyles } from './chart_tooltip_styles'; const renderHeader = (headerData?: ChartTooltipValue, formatter?: TooltipValueFormatter) => { if (!headerData) { @@ -30,17 +30,26 @@ const renderHeader = (headerData?: ChartTooltipValue, formatter?: TooltipValueFo * Pure component for rendering the tooltip content with a custom layout across the ML plugin. */ export const FormattedTooltip: FC<{ tooltipData: TooltipData }> = ({ tooltipData }) => { + const { + mlChartTooltip, + mlChartTooltipList, + mlChartTooltipHeader, + mlChartTooltipItem, + mlChartTooltipLabel, + mlChartTooltipValue, + } = useChartTooltipStyles(); + return ( -
+
{tooltipData.length > 0 && tooltipData[0].skipHeader === undefined && ( -
{renderHeader(tooltipData[0])}
+
{renderHeader(tooltipData[0])}
)} {tooltipData.length > 1 && ( -
+
{tooltipData .slice(1) .map(({ label, value, color, isHighlighted, seriesIdentifier, valueAccessor }) => { - const classes = classNames('mlChartTooltip__item', { + const classes = classNames({ // eslint-disable-next-line @typescript-eslint/naming-convention echTooltip__rowHighlighted: isHighlighted, }); @@ -52,16 +61,21 @@ export const FormattedTooltip: FC<{ tooltipData: TooltipData }> = ({ tooltipData return (
- + {label} - + {renderValue} diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_styles.ts b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_styles.ts new file mode 100644 index 0000000000000..c53bdb5242f3c --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_styles.ts @@ -0,0 +1,65 @@ +/* + * 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 { css } from '@emotion/react'; + +import { mathWithUnits, transparentize, useEuiTheme } from '@elastic/eui'; +// @ts-expect-error style types not defined +import { euiToolTipStyles } from '@elastic/eui/lib/components/tool_tip/tool_tip.styles'; + +import { useCurrentEuiThemeVars } from '@kbn/ml-kibana-theme'; + +import { useMlKibana } from '../../contexts/kibana'; + +export const useChartTooltipStyles = () => { + const euiThemeContext = useEuiTheme(); + const { + services: { theme }, + } = useMlKibana(); + const { euiTheme } = useCurrentEuiThemeVars(theme); + const euiStyles = euiToolTipStyles(euiThemeContext); + + return { + mlChartTooltip: css([ + euiStyles.euiToolTip, + { + fontSize: euiTheme.euiFontSizeXS, + padding: 0, + transition: `opacity ${euiTheme.euiAnimSpeedNormal}`, + pointerEvents: 'none', + userSelect: 'none', + maxWidth: '512px', + position: 'relative', + }, + ]), + mlChartTooltipList: css({ + margin: euiTheme.euiSizeXS, + paddingBottom: euiTheme.euiSizeXS, + }), + mlChartTooltipHeader: css({ + fontWeight: euiTheme.euiFontWeightBold, + padding: `${euiTheme.euiSizeXS} ${mathWithUnits(euiTheme.euiSizeS, (x) => x * 2)}`, + marginBottom: euiTheme.euiSizeXS, + borderBottom: `1px solid ${transparentize(euiTheme.euiBorderColor, 0.8)}`, + }), + mlChartTooltipItem: css({ + display: 'flex', + padding: '3px', + boxSizing: 'border-box', + borderLeft: `${euiTheme.euiSizeXS} solid transparent`, + }), + mlChartTooltipLabel: css({ + minWidth: '1px', + }), + mlChartTooltipValue: css({ + fontWeight: euiTheme.euiFontWeightBold, + textAlign: 'right', + fontFeatureSettings: 'tnum', + marginLeft: '8px', + }), + }; +}; diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.scss b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.scss deleted file mode 100644 index 322cdb4971f05..0000000000000 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.scss +++ /dev/null @@ -1,8 +0,0 @@ -.mlScatterplotMatrix { - overflow-x: auto; - - .vega-bind span { - font-size: $euiFontSizeXS; - padding: 0 $euiSizeXS; - } -} \ No newline at end of file diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx index dab7dc4117083..763addd4aaa87 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx @@ -7,6 +7,7 @@ import type { FC } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { css } from '@emotion/react'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { EuiComboBoxOptionOption } from '@elastic/eui'; @@ -35,6 +36,7 @@ import { type RuntimeMappings, } from '@kbn/ml-runtime-field-utils'; import { getProcessedFields } from '@kbn/ml-data-grid'; +import { euiThemeVars } from '@kbn/ui-theme'; import { useCurrentThemeVars, useMlApi, useMlKibana } from '../../contexts/kibana'; @@ -48,7 +50,17 @@ import { OUTLIER_SCORE_FIELD, } from './scatterplot_matrix_vega_lite_spec'; -import './scatterplot_matrix.scss'; +const cssOverrides = css({ + // Prevent the chart from overflowing the container + overflowX: 'auto', + // Overrides for the outlier threshold slider + '.vega-bind': { + span: { + fontSize: euiThemeVars.euiFontSizeXS, + padding: `0 ${euiThemeVars.euiSizeXS}`, + }, + }, +}); const SCATTERPLOT_MATRIX_DEFAULT_FIELDS = 4; const SCATTERPLOT_MATRIX_DEFAULT_FETCH_SIZE = 1000; @@ -413,7 +425,7 @@ export const ScatterplotMatrix: FC = ({ ) : (
diff --git a/x-pack/plugins/ml/public/application/model_management/expanded_row.tsx b/x-pack/plugins/ml/public/application/model_management/expanded_row.tsx index e9db6b4e4f590..f7e95e3eda52c 100644 --- a/x-pack/plugins/ml/public/application/model_management/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/model_management/expanded_row.tsx @@ -182,6 +182,7 @@ export const ExpandedRow: FC = ({ item }) => { key: `${perDeploymentStat.deployment_id}_${nodeName}`, ...perDeploymentStat, ...modelSizeStats, + // @ts-expect-error `throughput_last_minute` is not declared in ES Types node: { ...pick(n, [ 'average_inference_time_ms', diff --git a/x-pack/plugins/ml/server/models/model_management/memory_usage.ts b/x-pack/plugins/ml/server/models/model_management/memory_usage.ts index 6e2931dbbe06b..6e9121a133bb8 100644 --- a/x-pack/plugins/ml/server/models/model_management/memory_usage.ts +++ b/x-pack/plugins/ml/server/models/model_management/memory_usage.ts @@ -181,6 +181,7 @@ export class MemoryUsageService { const mlNodes = Object.entries(response.nodes).filter(([, node]) => node.roles.includes('ml')); + // @ts-expect-error `throughput_last_minute` is not declared in ES Types const nodeDeploymentStatsResponses: NodeDeploymentStatsResponse[] = mlNodes.map( ([nodeId, node]) => { const nodeFields = pick(node, NODE_FIELDS) as RequiredNodeFields; diff --git a/x-pack/plugins/search_indices/public/analytics/constants.ts b/x-pack/plugins/search_indices/public/analytics/constants.ts index d64019d6ef676..0da7aedf19328 100644 --- a/x-pack/plugins/search_indices/public/analytics/constants.ts +++ b/x-pack/plugins/search_indices/public/analytics/constants.ts @@ -12,9 +12,9 @@ export enum AnalyticsEvents { startCreateIndexPageModifyIndexName = 'start_modify_index_name', startCreateIndexClick = 'start_create_index', startCreateIndexLanguageSelect = 'start_code_lang_select', + startCreateIndexRunInConsole = 'start_cta_run_in_console', startCreateIndexCodeCopyInstall = 'start_code_copy_install', startCreateIndexCodeCopy = 'start_code_copy', - startCreateIndexRunInConsole = 'start_cta_run_in_console', startCreateIndexCreatedRedirect = 'start_index_created_api', startFileUploadClick = 'start_file_upload', indexDetailsInstallCodeCopy = 'index_details_code_copy_install', @@ -23,4 +23,15 @@ export enum AnalyticsEvents { indexDetailsNavDataTab = 'index_details_nav_data_tab', indexDetailsNavSettingsTab = 'index_details_nav_settings_tab', indexDetailsNavMappingsTab = 'index_details_nav_mappings_tab', + createIndexPageOpened = 'create_index_page_opened', + createIndexShowCodeClick = 'create_index_show_code', + createIndexShowUIClick = 'create_index_show_create_index_ui', + createIndexPageModifyIndexName = 'create_index_modify_index_name', + createIndexCreateIndexClick = 'create_index_click_create', + createIndexLanguageSelect = 'create_index_code_lang_select', + createIndexRunInConsole = 'create_index_run_in_console', + createIndexCodeCopyInstall = 'create_index_copy_install', + createIndexCodeCopy = 'create_index_code_copy', + createIndexFileUploadClick = 'create_index_file_upload', + createIndexIndexCreatedRedirect = 'create_index_created_api', } diff --git a/x-pack/plugins/search_indices/public/components/create_index/create_index.tsx b/x-pack/plugins/search_indices/public/components/create_index/create_index.tsx new file mode 100644 index 0000000000000..d8ce8073c691e --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/create_index/create_index.tsx @@ -0,0 +1,114 @@ +/* + * 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, useState } from 'react'; + +import type { IndicesStatusResponse, UserStartPrivilegesResponse } from '../../../common'; + +import { AnalyticsEvents } from '../../analytics/constants'; +import { AvailableLanguages } from '../../code_examples'; +import { useKibana } from '../../hooks/use_kibana'; +import { useUsageTracker } from '../../hooks/use_usage_tracker'; +import { CreateIndexFormState } from '../../types'; +import { generateRandomIndexName } from '../../utils/indices'; +import { getDefaultCodingLanguage } from '../../utils/language'; + +import { CreateIndexPanel } from '../shared/create_index_panel'; + +import { CreateIndexCodeView } from './create_index_code_view'; +import { CreateIndexUIView } from './create_index_ui_view'; + +function initCreateIndexState() { + const defaultIndexName = generateRandomIndexName(); + return { + indexName: defaultIndexName, + defaultIndexName, + codingLanguage: getDefaultCodingLanguage(), + }; +} + +export interface CreateIndexProps { + indicesData?: IndicesStatusResponse; + userPrivileges?: UserStartPrivilegesResponse; +} + +enum CreateIndexViewMode { + UI = 'ui', + Code = 'code', +} + +export const CreateIndex = ({ indicesData, userPrivileges }: CreateIndexProps) => { + const { application } = useKibana().services; + const [createIndexView, setCreateIndexView] = useState( + userPrivileges?.privileges.canCreateIndex === false + ? CreateIndexViewMode.Code + : CreateIndexViewMode.UI + ); + const [formState, setFormState] = useState(initCreateIndexState); + const usageTracker = useUsageTracker(); + const onChangeView = useCallback( + (id: string) => { + switch (id) { + case CreateIndexViewMode.UI: + usageTracker.click(AnalyticsEvents.createIndexShowUIClick); + setCreateIndexView(CreateIndexViewMode.UI); + return; + case CreateIndexViewMode.Code: + usageTracker.click(AnalyticsEvents.createIndexShowCodeClick); + setCreateIndexView(CreateIndexViewMode.Code); + return; + } + }, + [usageTracker] + ); + const onChangeCodingLanguage = useCallback( + (language: AvailableLanguages) => { + setFormState({ + ...formState, + codingLanguage: language, + }); + usageTracker.count([ + AnalyticsEvents.createIndexLanguageSelect, + `${AnalyticsEvents.createIndexLanguageSelect}_${language}`, + ]); + }, + [usageTracker, formState, setFormState] + ); + const onClose = useCallback(() => { + application.navigateToApp('management', { deepLinkId: 'index_management' }); + }, [application]); + + return ( + + {createIndexView === CreateIndexViewMode.UI && ( + + )} + {createIndexView === CreateIndexViewMode.Code && ( + + )} + + ); +}; diff --git a/x-pack/plugins/search_indices/public/components/create_index/create_index_code_view.tsx b/x-pack/plugins/search_indices/public/components/create_index/create_index_code_view.tsx new file mode 100644 index 0000000000000..cdadfbdc146f6 --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/create_index/create_index_code_view.tsx @@ -0,0 +1,26 @@ +/* + * 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 type { IndicesStatusResponse } from '../../../common'; +import { + CreateIndexCodeView as SharedCreateIndexCodeView, + CreateIndexCodeViewProps as SharedCreateIndexCodeViewProps, +} from '../shared/create_index_code_view'; + +import { useIndicesRedirect } from './hooks/use_indices_redirect'; + +export interface CreateIndexCodeViewProps extends SharedCreateIndexCodeViewProps { + indicesData?: IndicesStatusResponse; +} + +export const CreateIndexCodeView = ({ indicesData, ...props }: CreateIndexCodeViewProps) => { + useIndicesRedirect(indicesData); + + return ; +}; diff --git a/x-pack/plugins/search_indices/public/components/create_index/create_index_page.tsx b/x-pack/plugins/search_indices/public/components/create_index/create_index_page.tsx new file mode 100644 index 0000000000000..d8601e95760d7 --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/create_index/create_index_page.tsx @@ -0,0 +1,60 @@ +/* + * 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, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiLoadingLogo, EuiPageTemplate } from '@elastic/eui'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; + +import { useKibana } from '../../hooks/use_kibana'; +import { useIndicesStatusQuery } from '../../hooks/api/use_indices_status'; +import { useUserPrivilegesQuery } from '../../hooks/api/use_user_permissions'; +import { LoadIndicesStatusError } from '../shared/load_indices_status_error'; + +import { CreateIndex } from './create_index'; +import { usePageChrome } from '../../hooks/use_page_chrome'; +import { IndexManagementBreadcrumbs } from '../shared/breadcrumbs'; + +const CreateIndexLabel = i18n.translate('xpack.searchIndices.createIndex.docTitle', { + defaultMessage: 'Create Index', +}); + +export const CreateIndexPage = () => { + const { console: consolePlugin } = useKibana().services; + const { + data: indicesData, + isInitialLoading, + isError: hasIndicesStatusFetchError, + error: indicesFetchError, + } = useIndicesStatusQuery(); + const { data: userPrivileges } = useUserPrivilegesQuery(); + + const embeddableConsole = useMemo( + () => (consolePlugin?.EmbeddableConsole ? : null), + [consolePlugin] + ); + usePageChrome(CreateIndexLabel, [...IndexManagementBreadcrumbs, { text: CreateIndexLabel }]); + + return ( + + + {isInitialLoading && } + {hasIndicesStatusFetchError && } + {!isInitialLoading && !hasIndicesStatusFetchError && ( + + )} + + {embeddableConsole} + + ); +}; diff --git a/x-pack/plugins/search_indices/public/components/create_index/create_index_ui_view.tsx b/x-pack/plugins/search_indices/public/components/create_index/create_index_ui_view.tsx new file mode 100644 index 0000000000000..08073c0e84794 --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/create_index/create_index_ui_view.tsx @@ -0,0 +1,76 @@ +/* + * 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, useState } from 'react'; + +import type { UserStartPrivilegesResponse } from '../../../common'; +import { AnalyticsEvents } from '../../analytics/constants'; +import { CreateIndexFormState } from '../../types'; +import { CreateIndexForm } from '../shared/create_index_form'; +import { useUsageTracker } from '../../hooks/use_usage_tracker'; +import { isValidIndexName } from '../../utils/indices'; +import { useCreateIndex } from '../shared/hooks/use_create_index'; + +import { useKibana } from '../../hooks/use_kibana'; + +export interface CreateIndexUIViewProps { + formState: CreateIndexFormState; + setFormState: (value: CreateIndexFormState) => void; + userPrivileges?: UserStartPrivilegesResponse; +} + +export const CreateIndexUIView = ({ + formState, + setFormState, + userPrivileges, +}: CreateIndexUIViewProps) => { + const [indexNameHasError, setIndexNameHasError] = useState(false); + const { application } = useKibana().services; + const usageTracker = useUsageTracker(); + const { createIndex, isLoading } = useCreateIndex(); + const onIndexNameChange = (e: React.ChangeEvent) => { + const newIndexName = e.target.value; + setFormState({ ...formState, indexName: e.target.value }); + const invalidIndexName = !isValidIndexName(newIndexName); + if (indexNameHasError !== invalidIndexName) { + setIndexNameHasError(invalidIndexName); + } + }; + const onCreateIndex = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + if (!isValidIndexName(formState.indexName)) { + return; + } + usageTracker.click(AnalyticsEvents.createIndexCreateIndexClick); + + if (formState.defaultIndexName !== formState.indexName) { + usageTracker.click(AnalyticsEvents.createIndexPageModifyIndexName); + } + + createIndex({ indexName: formState.indexName }); + }, + [usageTracker, createIndex, formState.indexName, formState.defaultIndexName] + ); + const onFileUpload = useCallback(() => { + usageTracker.click(AnalyticsEvents.createIndexFileUploadClick); + application.navigateToApp('ml', { path: 'filedatavisualizer' }); + }, [usageTracker, application]); + + return ( + + ); +}; diff --git a/x-pack/plugins/search_indices/public/components/create_index/hooks/use_indices_redirect.tsx b/x-pack/plugins/search_indices/public/components/create_index/hooks/use_indices_redirect.tsx new file mode 100644 index 0000000000000..4246976209a9f --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/create_index/hooks/use_indices_redirect.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useState } from 'react'; + +import type { IndicesStatusResponse } from '../../../../common'; + +import { useKibana } from '../../../hooks/use_kibana'; + +import { getFirstNewIndexName } from '../../../utils/indices'; +import { navigateToIndexDetails } from '../../utils'; +import { useUsageTracker } from '../../../contexts/usage_tracker_context'; +import { AnalyticsEvents } from '../../../analytics/constants'; + +export const useIndicesRedirect = (indicesStatus?: IndicesStatusResponse) => { + const { application, http } = useKibana().services; + const [initialStatus, setInitialStatus] = useState(undefined); + const [hasDoneRedirect, setHasDoneRedirect] = useState(() => false); + const usageTracker = useUsageTracker(); + return useEffect(() => { + if (hasDoneRedirect) { + return; + } + if (!indicesStatus) { + return; + } + if (initialStatus === undefined) { + setInitialStatus(indicesStatus); + return; + } + const newIndexName = getFirstNewIndexName(initialStatus.indexNames, indicesStatus.indexNames); + if (newIndexName) { + navigateToIndexDetails(application, http, newIndexName); + setHasDoneRedirect(true); + usageTracker.click(AnalyticsEvents.createIndexIndexCreatedRedirect); + return; + } + }, [ + application, + http, + indicesStatus, + initialStatus, + setHasDoneRedirect, + usageTracker, + hasDoneRedirect, + ]); +}; diff --git a/x-pack/plugins/search_indices/public/components/indices/details_page.tsx b/x-pack/plugins/search_indices/public/components/indices/details_page.tsx index ad5e174dd6e4a..c672bb51493f6 100644 --- a/x-pack/plugins/search_indices/public/components/indices/details_page.tsx +++ b/x-pack/plugins/search_indices/public/components/indices/details_page.tsx @@ -37,20 +37,14 @@ import { SearchIndexDetailsPageMenuItemPopover } from './details_page_menu_item' import { useIndexDocumentSearch } from '../../hooks/api/use_document_search'; import { useUsageTracker } from '../../contexts/usage_tracker_context'; import { AnalyticsEvents } from '../../analytics/constants'; +import { usePageChrome } from '../../hooks/use_page_chrome'; +import { IndexManagementBreadcrumbs } from '../shared/breadcrumbs'; export const SearchIndexDetailsPage = () => { const indexName = decodeURIComponent(useParams<{ indexName: string }>().indexName); const tabId = decodeURIComponent(useParams<{ tabId: string }>().tabId); - const { - console: consolePlugin, - docLinks, - application, - history, - share, - chrome, - serverless, - } = useKibana().services; + const { console: consolePlugin, docLinks, application, history, share } = useKibana().services; const { data: index, refetch, @@ -82,23 +76,12 @@ export const SearchIndexDetailsPage = () => { setHasDocuments(!(!isInitialLoading && indexDocuments?.results?.data.length === 0)); }, [indexDocuments, isInitialLoading, setHasDocuments, setDocumentsLoading]); - useEffect(() => { - chrome.docTitle.change(indexName); - - if (serverless) { - serverless.setBreadcrumbs([ - { - text: i18n.translate('xpack.searchIndices.indexBreadcrumbLabel', { - defaultMessage: 'Index Management', - }), - href: '/app/management/data/index_management/indices', - }, - { - text: indexName, - }, - ]); - } - }, [chrome, indexName, serverless]); + usePageChrome(indexName, [ + ...IndexManagementBreadcrumbs, + { + text: indexName, + }, + ]); const usageTracker = useUsageTracker(); diff --git a/x-pack/plugins/search_indices/public/components/indices/indices_router.tsx b/x-pack/plugins/search_indices/public/components/indices_router.tsx similarity index 80% rename from x-pack/plugins/search_indices/public/components/indices/indices_router.tsx rename to x-pack/plugins/search_indices/public/components/indices_router.tsx index 51527a7d2ef8e..56ccb3c0674e6 100644 --- a/x-pack/plugins/search_indices/public/components/indices/indices_router.tsx +++ b/x-pack/plugins/search_indices/public/components/indices_router.tsx @@ -7,13 +7,17 @@ import React from 'react'; import { Route, Router, Routes } from '@kbn/shared-ux-router'; import { Redirect } from 'react-router-dom'; -import { useKibana } from '../../hooks/use_kibana'; + +import { useKibana } from '../hooks/use_kibana'; import { SearchIndexDetailsTabs, SEARCH_INDICES_DETAILS_PATH, SEARCH_INDICES_DETAILS_TABS_PATH, -} from '../../routes'; -import { SearchIndexDetailsPage } from './details_page'; + CREATE_INDEX_PATH, +} from '../routes'; +import { SearchIndexDetailsPage } from './indices/details_page'; +import { CreateIndexPage } from './create_index/create_index_page'; + export const SearchIndicesRouter: React.FC = () => { const { application, history } = useKibana().services; return ( @@ -29,6 +33,7 @@ export const SearchIndicesRouter: React.FC = () => { /> + { application.navigateToApp('elasticsearchStart'); diff --git a/x-pack/plugins/search_indices/public/components/start/api_key_callout.tsx b/x-pack/plugins/search_indices/public/components/shared/api_key_callout.tsx similarity index 82% rename from x-pack/plugins/search_indices/public/components/start/api_key_callout.tsx rename to x-pack/plugins/search_indices/public/components/shared/api_key_callout.tsx index 65363e9f73225..1fe6c6d1a7ed7 100644 --- a/x-pack/plugins/search_indices/public/components/start/api_key_callout.tsx +++ b/x-pack/plugins/search_indices/public/components/shared/api_key_callout.tsx @@ -17,19 +17,19 @@ interface APIKeyCalloutProps { export const APIKeyCallout = ({ apiKey }: APIKeyCalloutProps) => { const title = apiKey - ? i18n.translate('xpack.searchIndices.startPage.codeView.apiKeyTitle', { + ? i18n.translate('xpack.searchIndices.shared.codeView.apiKeyTitle', { defaultMessage: 'Copy your API key', }) - : i18n.translate('xpack.searchIndices.startPage.codeView.explicitGenerate.apiKeyTitle', { + : i18n.translate('xpack.searchIndices.shared.codeView.explicitGenerate.apiKeyTitle', { defaultMessage: 'Create an API key', }); const description = apiKey - ? i18n.translate('xpack.searchIndices.startPage.codeView.apiKeyDescription', { + ? i18n.translate('xpack.searchIndices.shared.codeView.apiKeyDescription', { defaultMessage: 'Make sure you keep it somewhere safe. You won’t be able to retrieve it later.', }) - : i18n.translate('xpack.searchIndices.startPage.codeView.explicitGenerate.apiKeyDescription', { + : i18n.translate('xpack.searchIndices.shared.codeView.explicitGenerate.apiKeyDescription', { defaultMessage: 'Create an API key to connect to Elasticsearch.', }); diff --git a/x-pack/plugins/search_indices/public/components/shared/breadcrumbs.ts b/x-pack/plugins/search_indices/public/components/shared/breadcrumbs.ts new file mode 100644 index 0000000000000..2805100d6cabb --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/shared/breadcrumbs.ts @@ -0,0 +1,24 @@ +/* + * 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 { ChromeBreadcrumb } from '@kbn/core-chrome-browser'; +import { i18n } from '@kbn/i18n'; + +export const IndexManagementBreadcrumbs: ChromeBreadcrumb[] = [ + { + text: i18n.translate('xpack.searchIndices.breadcrumbs.indexManagement.label', { + defaultMessage: 'Index Management', + }), + href: '/app/management/data/index_management', + }, + { + text: i18n.translate('xpack.searchIndices.breadcrumbs.indexManagement.indices.label', { + defaultMessage: 'Indices', + }), + href: '/app/management/data/index_management/indices', + }, +]; diff --git a/x-pack/plugins/search_indices/public/components/start/create_index_code.tsx b/x-pack/plugins/search_indices/public/components/shared/create_index_code_view.tsx similarity index 67% rename from x-pack/plugins/search_indices/public/components/start/create_index_code.tsx rename to x-pack/plugins/search_indices/public/components/shared/create_index_code_view.tsx index fadfe1c7dcb90..14e9162fb9706 100644 --- a/x-pack/plugins/search_indices/public/components/start/create_index_code.tsx +++ b/x-pack/plugins/search_indices/public/components/shared/create_index_code_view.tsx @@ -4,61 +4,55 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { TryInConsoleButton } from '@kbn/try-in-console'; import { useSearchApiKey } from '@kbn/search-api-keys-components'; -import { AnalyticsEvents } from '../../analytics/constants'; import { Languages, AvailableLanguages, LanguageOptions } from '../../code_examples'; import { useUsageTracker } from '../../hooks/use_usage_tracker'; import { useKibana } from '../../hooks/use_kibana'; import { useElasticsearchUrl } from '../../hooks/use_elasticsearch_url'; -import { CodeSample } from '../shared/code_sample'; -import { LanguageSelector } from '../shared/language_selector'; - -import { CreateIndexFormState } from './types'; -import { useStartPageCodingExamples } from './hooks/use_coding_examples'; import { APIKeyCallout } from './api_key_callout'; +import { CodeSample } from './code_sample'; +import { useCreateIndexCodingExamples } from './hooks/use_create_index_coding_examples'; +import { LanguageSelector } from './language_selector'; export interface CreateIndexCodeViewProps { - createIndexForm: CreateIndexFormState; + selectedLanguage: AvailableLanguages; + indexName: string; changeCodingLanguage: (language: AvailableLanguages) => void; canCreateApiKey?: boolean; + analyticsEvents: { + runInConsole: string; + installCommands: string; + createIndex: string; + }; } export const CreateIndexCodeView = ({ - createIndexForm, - changeCodingLanguage, + analyticsEvents, canCreateApiKey, + changeCodingLanguage, + indexName, + selectedLanguage, }: CreateIndexCodeViewProps) => { const { application, share, console: consolePlugin } = useKibana().services; const usageTracker = useUsageTracker(); - const selectedCodeExamples = useStartPageCodingExamples(); + const selectedCodeExamples = useCreateIndexCodingExamples(); - const { codingLanguage: selectedLanguage } = createIndexForm; - const onSelectLanguage = useCallback( - (value: AvailableLanguages) => { - changeCodingLanguage(value); - usageTracker.count([ - AnalyticsEvents.startCreateIndexLanguageSelect, - `${AnalyticsEvents.startCreateIndexLanguageSelect}_${value}`, - ]); - }, - [usageTracker, changeCodingLanguage] - ); const elasticsearchUrl = useElasticsearchUrl(); const { apiKey, apiKeyIsVisible } = useSearchApiKey(); const codeParams = useMemo(() => { return { - indexName: createIndexForm.indexName || undefined, + indexName: indexName || undefined, elasticsearchURL: elasticsearchUrl, apiKey: apiKeyIsVisible && apiKey ? apiKey : undefined, }; - }, [createIndexForm.indexName, elasticsearchUrl, apiKeyIsVisible, apiKey]); + }, [indexName, elasticsearchUrl, apiKeyIsVisible, apiKey]); const selectedCodeExample = useMemo(() => { return selectedCodeExamples[selectedLanguage]; }, [selectedLanguage, selectedCodeExamples]); @@ -75,7 +69,7 @@ export const CreateIndexCodeView = ({ @@ -87,8 +81,8 @@ export const CreateIndexCodeView = ({ telemetryId={`${selectedLanguage}_create_index`} onClick={() => { usageTracker.click([ - AnalyticsEvents.startCreateIndexRunInConsole, - `${AnalyticsEvents.startCreateIndexRunInConsole}_${selectedLanguage}`, + analyticsEvents.runInConsole, + `${analyticsEvents.runInConsole}_${selectedLanguage}`, ]); }} /> @@ -102,8 +96,8 @@ export const CreateIndexCodeView = ({ code={selectedCodeExample.installCommand} onCodeCopyClick={() => { usageTracker.click([ - AnalyticsEvents.startCreateIndexCodeCopyInstall, - `${AnalyticsEvents.startCreateIndexCodeCopyInstall}_${selectedLanguage}`, + analyticsEvents.installCommands, + `${analyticsEvents.installCommands}_${selectedLanguage}`, ]); }} /> @@ -116,9 +110,9 @@ export const CreateIndexCodeView = ({ code={selectedCodeExample.createIndex(codeParams)} onCodeCopyClick={() => { usageTracker.click([ - AnalyticsEvents.startCreateIndexCodeCopy, - `${AnalyticsEvents.startCreateIndexCodeCopy}_${selectedLanguage}`, - `${AnalyticsEvents.startCreateIndexCodeCopy}_${selectedLanguage}_${selectedCodeExamples.exampleType}`, + analyticsEvents.createIndex, + `${analyticsEvents.createIndex}_${selectedLanguage}`, + `${analyticsEvents.createIndex}_${selectedLanguage}_${selectedCodeExamples.exampleType}`, ]); }} /> diff --git a/x-pack/plugins/search_indices/public/components/shared/create_index_form.tsx b/x-pack/plugins/search_indices/public/components/shared/create_index_form.tsx new file mode 100644 index 0000000000000..ba2f83cb273da --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/shared/create_index_form.tsx @@ -0,0 +1,165 @@ +/* + * 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 { + EuiButton, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiHorizontalRule, + EuiIcon, + EuiLink, + EuiPanel, + EuiSpacer, + EuiText, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import type { UserStartPrivilegesResponse } from '../../../common'; + +export interface CreateIndexFormProps { + indexName: string; + indexNameHasError: boolean; + isLoading: boolean; + onCreateIndex: (e: React.FormEvent) => void; + onFileUpload: () => void; + onIndexNameChange: (e: React.ChangeEvent) => void; + showAPIKeyCreateLabel: boolean; + userPrivileges?: UserStartPrivilegesResponse; +} + +export const CreateIndexForm = ({ + indexName, + indexNameHasError, + isLoading, + onCreateIndex, + onFileUpload, + onIndexNameChange, + showAPIKeyCreateLabel, + userPrivileges, +}: CreateIndexFormProps) => { + return ( + <> + + + + + + + + + {i18n.translate('xpack.searchIndices.shared.createIndex.permissionTooltip', { + defaultMessage: 'You do not have permission to create an index.', + })} +

+ ) : undefined + } + > + + {i18n.translate('xpack.searchIndices.shared.createIndex.action.text', { + defaultMessage: 'Create my index', + })} + +
+
+ + {showAPIKeyCreateLabel && ( + + + +

+ {i18n.translate( + 'xpack.searchIndices.shared.createIndex.apiKeyCreation.description', + { + defaultMessage: "We'll create an API key for this index", + } + )} +

+
+
+ )} +
+
+
+ + + + + + + + +

+ + {i18n.translate('xpack.searchIndices.shared.createIndex.fileUpload.link', { + defaultMessage: 'Upload a file', + })} + + ), + }} + /> +

+
+
+
+
+ + ); +}; diff --git a/x-pack/plugins/search_indices/public/components/shared/create_index_panel.tsx b/x-pack/plugins/search_indices/public/components/shared/create_index_panel.tsx new file mode 100644 index 0000000000000..8c353ebab6bd8 --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/shared/create_index_panel.tsx @@ -0,0 +1,271 @@ +/* + * 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, { useMemo } from 'react'; +import { + EuiButtonEmpty, + EuiButtonGroup, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPanel, + EuiSpacer, + EuiText, + EuiTextAlign, + EuiTitle, + useEuiTheme, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { docLinks } from '../../../common/doc_links'; +import { useKibana } from '../../hooks/use_kibana'; +import { CreateIndexViewMode } from '../../types'; + +const MAX_WIDTH = '650px'; + +export interface CreateIndexPanelProps { + children: React.ReactNode | React.ReactNode[]; + createIndexView: CreateIndexViewMode; + onChangeView: (id: string) => void; + onClose: () => void; + showCallouts?: boolean; + showSkip?: boolean; + title?: React.ReactNode; +} + +export const CreateIndexPanel = ({ + children, + createIndexView, + onChangeView, + onClose, + showCallouts, + showSkip, + title, +}: CreateIndexPanelProps) => { + const { cloud, http } = useKibana().services; + const { euiTheme } = useEuiTheme(); + + const o11yTrialLink = useMemo(() => { + if (cloud && cloud.isServerlessEnabled) { + const baseUrl = cloud?.projectsUrl ?? 'https://cloud.elastic.co/projects/'; + return `${baseUrl}create/observability/start`; + } + return http.basePath.prepend('/app/observability/onboarding'); + }, [cloud, http]); + + return ( + <> + + + + + + + + + + + +

+ {i18n.translate('xpack.searchIndices.shared.createIndex.pageTitle', { + defaultMessage: 'Elasticsearch', + })} +

+
+
+
+ + +

+ {i18n.translate('xpack.searchIndices.shared.createIndex.pageDescription', { + defaultMessage: 'Get started with Elasticsearch', + })} +

+
+
+ + + + + + +

+ {title ?? + i18n.translate('xpack.searchIndices.shared.createIndex.defaultTitle', { + defaultMessage: 'Create an index', + })} +

+
+
+ + + +
+ +

+ {i18n.translate('xpack.searchIndices.shared.createIndex.description', { + defaultMessage: + 'An index stores your data and defines the schema, or field mappings, for your searches', + })} +

+
+ {children} +
+
+ {showCallouts && ( + <> + + + + +
+ {i18n.translate( + 'xpack.searchIndices.shared.createIndex.observabilityCallout.title', + { + defaultMessage: 'Looking to store your logs or metrics data?', + } + )} +
+
+
+ + + + + {i18n.translate( + 'xpack.searchIndices.shared.createIndex.observabilityCallout.logs.button', + { + defaultMessage: 'Collect and analyze logs', + } + )} + + + + {i18n.translate( + 'xpack.searchIndices.shared.createIndex.observabilityCallout.logs.subTitle', + { + defaultMessage: 'Explore Logstash and Beats', + } + )} + + + + + or + + + + {i18n.translate( + 'xpack.searchIndices.shared.createIndex.observabilityCallout.o11yTrial.button', + { + defaultMessage: 'Start an Observability trial', + } + )} + + + + {i18n.translate( + 'xpack.searchIndices.shared.createIndex.observabilityCallout.o11yTrial.subTitle', + { + defaultMessage: 'Powerful performance monitoring', + } + )} + + + + +
+ + )} +
+ {showSkip === true && ( + <> + + + + {i18n.translate('xpack.searchIndices.shared.createIndex.skipLabel', { + defaultMessage: 'Skip', + })} + + + + )} + + ); +}; diff --git a/x-pack/plugins/search_indices/public/components/start/hooks/use_create_index.tsx b/x-pack/plugins/search_indices/public/components/shared/hooks/use_create_index.tsx similarity index 94% rename from x-pack/plugins/search_indices/public/components/start/hooks/use_create_index.tsx rename to x-pack/plugins/search_indices/public/components/shared/hooks/use_create_index.tsx index 8daafec5573ca..537aa3cc4b987 100644 --- a/x-pack/plugins/search_indices/public/components/start/hooks/use_create_index.tsx +++ b/x-pack/plugins/search_indices/public/components/shared/hooks/use_create_index.tsx @@ -11,7 +11,7 @@ import { useCreateIndex as useCreateIndexApi } from '../../../hooks/api/use_crea import { useKibana } from '../../../hooks/use_kibana'; -import { navigateToIndexDetails } from './utils'; +import { navigateToIndexDetails } from '../../utils'; export const useCreateIndex = () => { const { application, http } = useKibana().services; diff --git a/x-pack/plugins/search_indices/public/components/start/hooks/use_coding_examples.tsx b/x-pack/plugins/search_indices/public/components/shared/hooks/use_create_index_coding_examples.tsx similarity index 87% rename from x-pack/plugins/search_indices/public/components/start/hooks/use_coding_examples.tsx rename to x-pack/plugins/search_indices/public/components/shared/hooks/use_create_index_coding_examples.tsx index 1a351d10943f2..fb1cb6a7eab52 100644 --- a/x-pack/plugins/search_indices/public/components/start/hooks/use_coding_examples.tsx +++ b/x-pack/plugins/search_indices/public/components/shared/hooks/use_create_index_coding_examples.tsx @@ -8,7 +8,7 @@ import { CreateIndexCodeExamples } from '../../../types'; import { DenseVectorSeverlessCodeExamples } from '../../../code_examples/create_index'; -export const useStartPageCodingExamples = (): CreateIndexCodeExamples => { +export const useCreateIndexCodingExamples = (): CreateIndexCodeExamples => { // TODO: in the future this will be dynamic based on the onboarding token // or project sub-type return DenseVectorSeverlessCodeExamples; diff --git a/x-pack/plugins/search_indices/public/components/start/status_error.tsx b/x-pack/plugins/search_indices/public/components/shared/load_indices_status_error.tsx similarity index 79% rename from x-pack/plugins/search_indices/public/components/start/status_error.tsx rename to x-pack/plugins/search_indices/public/components/shared/load_indices_status_error.tsx index 7e41e37d5cd94..58e1867cc577d 100644 --- a/x-pack/plugins/search_indices/public/components/start/status_error.tsx +++ b/x-pack/plugins/search_indices/public/components/shared/load_indices_status_error.tsx @@ -15,14 +15,14 @@ export interface StartPageErrorProps { error: unknown; } -export const StartPageError = ({ error }: StartPageErrorProps) => { +export const LoadIndicesStatusError = ({ error }: StartPageErrorProps) => { return ( - {i18n.translate('xpack.searchIndices.startPage.statusFetchError.title', { + {i18n.translate('xpack.searchIndices.shared.statusFetchError.title', { defaultMessage: 'Error loading indices', })}

@@ -31,7 +31,7 @@ export const StartPageError = ({ error }: StartPageErrorProps) => { {getErrorMessage( error, - i18n.translate('xpack.searchIndices.startPage.statusFetchError.unknownError', { + i18n.translate('xpack.searchIndices.shared.statusFetchError.unknownError', { defaultMessage: 'Unknown error fetching indices.', }) )} diff --git a/x-pack/plugins/search_indices/public/components/start/create_index.tsx b/x-pack/plugins/search_indices/public/components/start/create_index.tsx index 788bd1e36f2ee..e7ca978fb8eaa 100644 --- a/x-pack/plugins/search_indices/public/components/start/create_index.tsx +++ b/x-pack/plugins/search_indices/public/components/start/create_index.tsx @@ -6,52 +6,29 @@ */ import React, { useCallback, useState } from 'react'; -import { - EuiButton, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiForm, - EuiFormRow, - EuiHorizontalRule, - EuiIcon, - EuiLink, - EuiPanel, - EuiSpacer, - EuiText, - EuiToolTip, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; import type { UserStartPrivilegesResponse } from '../../../common'; import { AnalyticsEvents } from '../../analytics/constants'; import { useUsageTracker } from '../../hooks/use_usage_tracker'; +import { CreateIndexFormState } from '../../types'; import { isValidIndexName } from '../../utils/indices'; -import { useCreateIndex } from './hooks/use_create_index'; +import { useCreateIndex } from '../shared/hooks/use_create_index'; +import { CreateIndexForm } from '../shared/create_index_form'; -import { CreateIndexFormState } from './types'; import { useKibana } from '../../hooks/use_kibana'; -const CREATE_INDEX_CONTENT = i18n.translate( - 'xpack.searchIndices.startPage.createIndex.action.text', - { - defaultMessage: 'Create my index', - } -); - -export interface CreateIndexFormProps { +export interface CreateIndexUIViewProps { formState: CreateIndexFormState; setFormState: React.Dispatch>; userPrivileges?: UserStartPrivilegesResponse; } -export const CreateIndexForm = ({ +export const CreateIndexUIView = ({ userPrivileges, formState, setFormState, -}: CreateIndexFormProps) => { +}: CreateIndexUIViewProps) => { const { application } = useKibana().services; const [indexNameHasError, setIndexNameHasError] = useState(false); const usageTracker = useUsageTracker(); @@ -86,129 +63,15 @@ export const CreateIndexForm = ({ }, [usageTracker, application]); return ( - <> - - - - - - - - {userPrivileges?.privileges?.canCreateIndex === false ? ( - - {i18n.translate('xpack.searchIndices.startPage.createIndex.permissionTooltip', { - defaultMessage: 'You do not have permission to create an index.', - })} -

- } - > - - {CREATE_INDEX_CONTENT} - -
- ) : ( - - {CREATE_INDEX_CONTENT} - - )} -
- - {userPrivileges?.privileges?.canCreateApiKeys && ( - - - -

- {i18n.translate( - 'xpack.searchIndices.startPage.createIndex.apiKeyCreation.description', - { - defaultMessage: "We'll create an API key for this index", - } - )} -

-
-
- )} -
-
-
- - - - - - - - -

- - {i18n.translate( - 'xpack.searchIndices.startPage.createIndex.fileUpload.link', - { - defaultMessage: 'Upload a file', - } - )} - - ), - }} - /> -

-
-
-
-
- + ); }; diff --git a/x-pack/plugins/search_indices/public/components/start/elasticsearch_start.tsx b/x-pack/plugins/search_indices/public/components/start/elasticsearch_start.tsx index 42b021043cb34..3f3063ddb150e 100644 --- a/x-pack/plugins/search_indices/public/components/start/elasticsearch_start.tsx +++ b/x-pack/plugins/search_indices/public/components/start/elasticsearch_start.tsx @@ -5,23 +5,10 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { - EuiButtonEmpty, - EuiButtonGroup, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiPanel, - EuiSpacer, - EuiText, - EuiTextAlign, - EuiTitle, -} from '@elastic/eui'; +import React, { useCallback, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import type { IndicesStatusResponse, UserStartPrivilegesResponse } from '../../../common'; -import { docLinks } from '../../../common/doc_links'; import { AnalyticsEvents } from '../../analytics/constants'; import { AvailableLanguages } from '../../code_examples'; @@ -29,9 +16,11 @@ import { useUsageTracker } from '../../hooks/use_usage_tracker'; import { generateRandomIndexName } from '../../utils/indices'; import { getDefaultCodingLanguage } from '../../utils/language'; -import { CreateIndexForm } from './create_index'; -import { CreateIndexCodeView } from './create_index_code'; -import { CreateIndexFormState } from './types'; +import { CreateIndexUIView } from './create_index'; +import { CreateIndexCodeView } from '../shared/create_index_code_view'; +import { CreateIndexFormState, CreateIndexViewMode } from '../../types'; + +import { CreateIndexPanel } from '../shared/create_index_panel'; import { useKibana } from '../../hooks/use_kibana'; function initCreateIndexState(): CreateIndexFormState { @@ -43,21 +32,17 @@ function initCreateIndexState(): CreateIndexFormState { }; } -const MAX_WIDTH = '650px'; - -enum CreateIndexView { - UI = 'ui', - Code = 'code', -} export interface ElasticsearchStartProps { indicesData?: IndicesStatusResponse; userPrivileges?: UserStartPrivilegesResponse; } export const ElasticsearchStart = ({ userPrivileges }: ElasticsearchStartProps) => { - const { cloud, http } = useKibana().services; - const [createIndexView, setCreateIndexView] = useState( - userPrivileges?.privileges.canCreateIndex === false ? CreateIndexView.Code : CreateIndexView.UI + const { application } = useKibana().services; + const [createIndexView, setCreateIndexViewMode] = useState( + userPrivileges?.privileges.canCreateIndex === false + ? CreateIndexViewMode.Code + : CreateIndexViewMode.UI ); const [formState, setFormState] = useState(initCreateIndexState); const usageTracker = useUsageTracker(); @@ -68,28 +53,20 @@ export const ElasticsearchStart = ({ userPrivileges }: ElasticsearchStartProps) useEffect(() => { if (userPrivileges === undefined) return; if (userPrivileges.privileges.canCreateIndex === false) { - setCreateIndexView(CreateIndexView.Code); + setCreateIndexViewMode(CreateIndexViewMode.Code); } }, [userPrivileges]); - const o11yTrialLink = useMemo(() => { - if (cloud && cloud.isServerlessEnabled) { - const baseUrl = cloud?.projectsUrl ?? 'https://cloud.elastic.co/projects/'; - return `${baseUrl}create/observability/start`; - } - return http.basePath.prepend('/app/observability/onboarding'); - }, [cloud, http]); - const onChangeView = useCallback( (id: string) => { switch (id) { - case CreateIndexView.UI: + case CreateIndexViewMode.UI: usageTracker.click(AnalyticsEvents.startPageShowCreateIndexUIClick); - setCreateIndexView(CreateIndexView.UI); + setCreateIndexViewMode(CreateIndexViewMode.UI); return; - case CreateIndexView.Code: + case CreateIndexViewMode.Code: usageTracker.click(AnalyticsEvents.startPageShowCodeClick); - setCreateIndexView(CreateIndexView.Code); + setCreateIndexViewMode(CreateIndexViewMode.Code); return; } }, @@ -101,178 +78,48 @@ export const ElasticsearchStart = ({ userPrivileges }: ElasticsearchStartProps) ...formState, codingLanguage: language, }); + usageTracker.count([ + AnalyticsEvents.startCreateIndexLanguageSelect, + `${AnalyticsEvents.startCreateIndexLanguageSelect}_${language}`, + ]); }, - [formState, setFormState] + [usageTracker, formState, setFormState] ); + const onClose = useCallback(() => { + application.navigateToApp('management', { deepLinkId: 'index_management' }); + }, [application]); return ( - - - - - - - - -

- {i18n.translate('xpack.searchIndices.startPage.pageTitle', { - defaultMessage: 'Elasticsearch', - })} -

-
-
-
- - -

- {i18n.translate('xpack.searchIndices.startPage.pageDescription', { - defaultMessage: 'Vectorize, search, and visualize your data', - })} -

-
-
- - - - - - -

- {i18n.translate('xpack.searchIndices.startPage.createIndex.title', { - defaultMessage: 'Create your first index', - })} -

-
-
- - - -
- -

- {i18n.translate('xpack.searchIndices.startPage.createIndex.description', { - defaultMessage: - 'An index stores your data and defines the schema, or field mappings, for your searches', - })} -

-
- {createIndexView === CreateIndexView.UI && ( - - )} - {createIndexView === CreateIndexView.Code && ( - - )} -
-
- - - - -
- {i18n.translate('xpack.searchIndices.startPage.observabilityCallout.title', { - defaultMessage: 'Looking to store your logs or metrics data?', - })} -
-
-
- - - - - {i18n.translate('xpack.searchIndices.startPage.observabilityCallout.logs.button', { - defaultMessage: 'Collect and analyze logs', - })} - - - - {i18n.translate( - 'xpack.searchIndices.startPage.observabilityCallout.logs.subTitle', - { - defaultMessage: 'Explore Logstash and Beats', - } - )} - - - - - or - - - - {i18n.translate( - 'xpack.searchIndices.startPage.observabilityCallout.o11yTrial.button', - { - defaultMessage: 'Start an Observability trial', - } - )} - - - - {i18n.translate( - 'xpack.searchIndices.startPage.observabilityCallout.o11yTrial.subTitle', - { - defaultMessage: 'Powerful performance monitoring', - } - )} - - - - -
-
+ {createIndexView === CreateIndexViewMode.UI && ( + + )} + {createIndexView === CreateIndexViewMode.Code && ( + + )} + ); }; diff --git a/x-pack/plugins/search_indices/public/components/start/hooks/use_indices_redirect.tsx b/x-pack/plugins/search_indices/public/components/start/hooks/use_indices_redirect.tsx index 899d44da2afa8..6909b1117e327 100644 --- a/x-pack/plugins/search_indices/public/components/start/hooks/use_indices_redirect.tsx +++ b/x-pack/plugins/search_indices/public/components/start/hooks/use_indices_redirect.tsx @@ -11,7 +11,7 @@ import type { IndicesStatusResponse } from '../../../../common'; import { useKibana } from '../../../hooks/use_kibana'; -import { navigateToIndexDetails } from './utils'; +import { navigateToIndexDetails } from '../../utils'; import { useUsageTracker } from '../../../contexts/usage_tracker_context'; import { AnalyticsEvents } from '../../../analytics/constants'; diff --git a/x-pack/plugins/search_indices/public/components/start/start_page.tsx b/x-pack/plugins/search_indices/public/components/start/start_page.tsx index 4a848f580d22f..4dabec2e5fa98 100644 --- a/x-pack/plugins/search_indices/public/components/start/start_page.tsx +++ b/x-pack/plugins/search_indices/public/components/start/start_page.tsx @@ -6,6 +6,7 @@ */ import React, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; import { EuiLoadingLogo, EuiPageTemplate } from '@elastic/eui'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; @@ -16,7 +17,13 @@ import { useUserPrivilegesQuery } from '../../hooks/api/use_user_permissions'; import { useIndicesRedirect } from './hooks/use_indices_redirect'; import { ElasticsearchStart } from './elasticsearch_start'; -import { StartPageError } from './status_error'; +import { LoadIndicesStatusError } from '../shared/load_indices_status_error'; +import { IndexManagementBreadcrumbs } from '../shared/breadcrumbs'; +import { usePageChrome } from '../../hooks/use_page_chrome'; + +const PageTitle = i18n.translate('xpack.searchIndices.startPage.docTitle', { + defaultMessage: 'Create your first index', +}); export const ElasticsearchStartPage = () => { const { console: consolePlugin } = useKibana().services; @@ -27,6 +34,7 @@ export const ElasticsearchStartPage = () => { error: indicesFetchError, } = useIndicesStatusQuery(); const { data: userPrivileges } = useUserPrivilegesQuery(); + usePageChrome(PageTitle, [...IndexManagementBreadcrumbs, { text: PageTitle }]); const embeddableConsole = useMemo( () => (consolePlugin?.EmbeddableConsole ? : null), @@ -43,7 +51,7 @@ export const ElasticsearchStartPage = () => { > {isInitialLoading && } - {hasIndicesStatusFetchError && } + {hasIndicesStatusFetchError && } {!isInitialLoading && !hasIndicesStatusFetchError && ( )} diff --git a/x-pack/plugins/search_indices/public/components/start/types.ts b/x-pack/plugins/search_indices/public/components/start/types.ts deleted file mode 100644 index 4c0235ec515f1..0000000000000 --- a/x-pack/plugins/search_indices/public/components/start/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { AvailableLanguages } from '../../code_examples'; - -export interface CreateIndexFormState { - indexName: string; - defaultIndexName: string; - codingLanguage: AvailableLanguages; -} diff --git a/x-pack/plugins/search_indices/public/components/start/hooks/utils.ts b/x-pack/plugins/search_indices/public/components/utils.ts similarity index 66% rename from x-pack/plugins/search_indices/public/components/start/hooks/utils.ts rename to x-pack/plugins/search_indices/public/components/utils.ts index ed8b6f40e51fd..235c03b9faab8 100644 --- a/x-pack/plugins/search_indices/public/components/start/hooks/utils.ts +++ b/x-pack/plugins/search_indices/public/components/utils.ts @@ -5,12 +5,15 @@ * 2.0. */ +import { generatePath } from 'react-router-dom'; + import type { ApplicationStart, HttpSetup } from '@kbn/core/public'; +import { INDICES_APP_BASE, SEARCH_INDICES_DETAILS_PATH } from '../routes'; -const INDEX_DETAILS_PATH = '/app/elasticsearch/indices/index_details'; +const INDEX_DETAILS_FULL_PATH = `${INDICES_APP_BASE}${SEARCH_INDICES_DETAILS_PATH}`; function getIndexDetailsPath(http: HttpSetup, indexName: string) { - return http.basePath.prepend(`${INDEX_DETAILS_PATH}/${encodeURIComponent(indexName)}`); + return http.basePath.prepend(generatePath(INDEX_DETAILS_FULL_PATH, { indexName })); } export const navigateToIndexDetails = ( diff --git a/x-pack/plugins/search_indices/public/hooks/use_page_chrome.ts b/x-pack/plugins/search_indices/public/hooks/use_page_chrome.ts new file mode 100644 index 0000000000000..fae438b502a08 --- /dev/null +++ b/x-pack/plugins/search_indices/public/hooks/use_page_chrome.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 { useEffect } from 'react'; +import type { ChromeBreadcrumb } from '@kbn/core-chrome-browser'; +import { useKibana } from './use_kibana'; + +export const usePageChrome = (docTitle: string, breadcrumbs: ChromeBreadcrumb[]) => { + const { chrome, http, serverless } = useKibana().services; + + useEffect(() => { + chrome.docTitle.change(docTitle); + const newBreadcrumbs = breadcrumbs.map((breadcrumb) => { + if (breadcrumb.href && http.basePath.get().length > 0) { + breadcrumb.href = http.basePath.prepend(breadcrumb.href); + } + return breadcrumb; + }); + if (serverless) { + serverless.setBreadcrumbs(newBreadcrumbs); + } else { + chrome.setBreadcrumbs(newBreadcrumbs); + } + return () => { + // clear manually set breadcrumbs + if (serverless) { + serverless.setBreadcrumbs([]); + } else { + chrome.setBreadcrumbs([]); + } + }; + }, [breadcrumbs, chrome, docTitle, http.basePath, serverless]); +}; diff --git a/x-pack/plugins/search_indices/public/locators.ts b/x-pack/plugins/search_indices/public/locators.ts new file mode 100644 index 0000000000000..587ce51f2c82d --- /dev/null +++ b/x-pack/plugins/search_indices/public/locators.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 { LocatorDefinition } from '@kbn/share-plugin/common'; +import type { SharePluginSetup } from '@kbn/share-plugin/public'; +import type { SerializableRecord } from '@kbn/utility-types'; + +import { INDICES_APP_ID } from '../common'; +import { CREATE_INDEX_PATH } from './routes'; + +export function registerLocators(share: SharePluginSetup) { + share.url.locators.create(new CreateIndexLocatorDefinition()); +} + +class CreateIndexLocatorDefinition implements LocatorDefinition { + public readonly getLocation = async () => { + return { + app: INDICES_APP_ID, + path: CREATE_INDEX_PATH, + state: {}, + }; + }; + + public readonly id = 'SEARCH_CREATE_INDEX'; +} diff --git a/x-pack/plugins/search_indices/public/plugin.ts b/x-pack/plugins/search_indices/public/plugin.ts index c9b5c8f4c7659..b92fbaa5e7f45 100644 --- a/x-pack/plugins/search_indices/public/plugin.ts +++ b/x-pack/plugins/search_indices/public/plugin.ts @@ -6,10 +6,12 @@ */ import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; +import { SEARCH_INDICES_CREATE_INDEX } from '@kbn/deeplinks-search/constants'; import { i18n } from '@kbn/i18n'; import { docLinks } from '../common/doc_links'; import type { + AppPluginSetupDependencies, SearchIndicesAppPluginStartDependencies, SearchIndicesPluginSetup, SearchIndicesPluginStart, @@ -17,7 +19,13 @@ import type { } from './types'; import { initQueryClient } from './services/query_client'; import { INDICES_APP_ID, START_APP_ID } from '../common'; -import { INDICES_APP_BASE, START_APP_BASE, SearchIndexDetailsTabValues } from './routes'; +import { + CREATE_INDEX_PATH, + INDICES_APP_BASE, + START_APP_BASE, + SearchIndexDetailsTabValues, +} from './routes'; +import { registerLocators } from './locators'; export class SearchIndicesPlugin implements Plugin @@ -25,7 +33,8 @@ export class SearchIndicesPlugin private pluginEnabled: boolean = false; public setup( - core: CoreSetup + core: CoreSetup, + plugins: AppPluginSetupDependencies ): SearchIndicesPluginSetup { this.pluginEnabled = true; @@ -51,12 +60,21 @@ export class SearchIndicesPlugin core.application.register({ id: INDICES_APP_ID, appRoute: INDICES_APP_BASE, + deepLinks: [ + { + id: SEARCH_INDICES_CREATE_INDEX, + path: CREATE_INDEX_PATH, + title: i18n.translate('xpack.searchIndices.elasticsearchIndices.createIndexTitle', { + defaultMessage: 'Create index', + }), + }, + ], title: i18n.translate('xpack.searchIndices.elasticsearchIndices.startAppTitle', { defaultMessage: 'Elasticsearch Indices', }), async mount({ element, history }) { const { renderApp } = await import('./application'); - const { SearchIndicesRouter } = await import('./components/indices/indices_router'); + const { SearchIndicesRouter } = await import('./components/indices_router'); const [coreStart, depsStart] = await core.getStartServices(); const startDeps: SearchIndicesServicesContextDeps = { ...depsStart, @@ -66,6 +84,8 @@ export class SearchIndicesPlugin }, }); + registerLocators(plugins.share); + return { enabled: true, startAppId: START_APP_ID, diff --git a/x-pack/plugins/search_indices/public/routes.ts b/x-pack/plugins/search_indices/public/routes.ts index 057891d63226d..86d05fb73032d 100644 --- a/x-pack/plugins/search_indices/public/routes.ts +++ b/x-pack/plugins/search_indices/public/routes.ts @@ -13,6 +13,7 @@ export enum SearchIndexDetailsTabs { MAPPINGS = 'mappings', SETTINGS = 'settings', } +export const CREATE_INDEX_PATH = `${ROOT_PATH}create`; export const SearchIndexDetailsTabValues: string[] = Object.values(SearchIndexDetailsTabs); export const START_APP_BASE = '/app/elasticsearch/start'; diff --git a/x-pack/plugins/search_indices/public/types.ts b/x-pack/plugins/search_indices/public/types.ts index cfc732adff45f..ccf4d56e13a67 100644 --- a/x-pack/plugins/search_indices/public/types.ts +++ b/x-pack/plugins/search_indices/public/types.ts @@ -5,19 +5,25 @@ * 2.0. */ -import type { CloudStart } from '@kbn/cloud-plugin/public'; -import type { ConsolePluginStart } from '@kbn/console-plugin/public'; +import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public'; +import type { ConsolePluginSetup, ConsolePluginStart } from '@kbn/console-plugin/public'; import type { AppMountParameters, CoreStart } from '@kbn/core/public'; -import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; -import type { SharePluginStart } from '@kbn/share-plugin/public'; -import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; +import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; +import type { + UsageCollectionSetup, + UsageCollectionStart, +} from '@kbn/usage-collection-plugin/public'; import type { MappingProperty, MappingPropertyBase, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { IndexManagementPluginStart } from '@kbn/index-management-shared-types'; +import type { + IndexManagementPluginSetup, + IndexManagementPluginStart, +} from '@kbn/index-management-shared-types'; import type { AppDeepLinkId } from '@kbn/core-chrome-browser'; -import { ServerlessPluginStart } from '@kbn/serverless/public'; +import type { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public'; +import type { AvailableLanguages } from './code_examples'; export interface SearchIndicesPluginSetup { enabled: boolean; @@ -31,14 +37,20 @@ export interface SearchIndicesPluginStart { startRoute: string; } -export interface AppPluginStartDependencies { - navigation: NavigationPublicPluginStart; +export interface AppPluginSetupDependencies { + console?: ConsolePluginSetup; + cloud?: CloudSetup; + indexManagement: IndexManagementPluginSetup; + share: SharePluginSetup; + serverless?: ServerlessPluginSetup; + usageCollection?: UsageCollectionSetup; } export interface SearchIndicesAppPluginStartDependencies { console?: ConsolePluginStart; cloud?: CloudStart; share: SharePluginStart; + serverless?: ServerlessPluginStart; usageCollection?: UsageCollectionStart; indexManagement: IndexManagementPluginStart; } @@ -50,8 +62,6 @@ export interface SearchIndicesServicesContextDeps { export type SearchIndicesServicesContext = CoreStart & SearchIndicesAppPluginStartDependencies & { history: AppMountParameters['history']; - indexManagement: IndexManagementPluginStart; - serverless: ServerlessPluginStart; }; export interface AppUsageTracker { @@ -123,3 +133,14 @@ export interface IngestDataCodeExamples { python: IngestDataCodeDefinition; javascript: IngestDataCodeDefinition; } + +export interface CreateIndexFormState { + indexName: string; + defaultIndexName: string; + codingLanguage: AvailableLanguages; +} + +export enum CreateIndexViewMode { + UI = 'ui', + Code = 'code', +} diff --git a/x-pack/plugins/search_indices/public/utils/indices.test.ts b/x-pack/plugins/search_indices/public/utils/indices.test.ts index 8ddd7cbb56fc5..a3b6654a1209e 100644 --- a/x-pack/plugins/search_indices/public/utils/indices.test.ts +++ b/x-pack/plugins/search_indices/public/utils/indices.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { generateRandomIndexName, isValidIndexName } from './indices'; +import { generateRandomIndexName, isValidIndexName, getFirstNewIndexName } from './indices'; describe('indices utils', function () { describe('generateRandomIndexName', function () { @@ -46,4 +46,31 @@ describe('indices utils', function () { expect(isValidIndexName(indexName)).toBe(true); }); }); + + describe('getFirstNewIndexName', function () { + it('returns undefined when lists are the same', () => { + expect(getFirstNewIndexName([], [])).toEqual(undefined); + expect(getFirstNewIndexName(['index'], ['index'])).toEqual(undefined); + expect(getFirstNewIndexName(['index', 'test'], ['index', 'test'])).toEqual(undefined); + }); + + it('returns new item when it exists', () => { + expect(getFirstNewIndexName([], ['index'])).toEqual('index'); + expect(getFirstNewIndexName(['index'], ['index', 'test'])).toEqual('test'); + expect(getFirstNewIndexName(['index', 'test'], ['index', 'test', 'unit-test'])).toEqual( + 'unit-test' + ); + expect(getFirstNewIndexName(['index', 'test'], ['unit-test', 'index', 'test'])).toEqual( + 'unit-test' + ); + }); + it('returns first new item when it multiple new indices exists', () => { + expect(getFirstNewIndexName([], ['index', 'test'])).toEqual('index'); + expect(getFirstNewIndexName(['index'], ['test', 'index', 'unit-test'])).toEqual('test'); + }); + it('can handle old indices being removed', () => { + expect(getFirstNewIndexName(['index'], ['test'])).toEqual('test'); + expect(getFirstNewIndexName(['test', 'index', 'unit-test'], ['index', 'new'])).toEqual('new'); + }); + }); }); diff --git a/x-pack/plugins/search_indices/public/utils/indices.ts b/x-pack/plugins/search_indices/public/utils/indices.ts index 21c6e672af08f..3812eea8757b9 100644 --- a/x-pack/plugins/search_indices/public/utils/indices.ts +++ b/x-pack/plugins/search_indices/public/utils/indices.ts @@ -35,3 +35,12 @@ export function generateRandomIndexName( return result; } + +export function getFirstNewIndexName(startingIndexNames: string[], currentIndexNames: string[]) { + for (const index of currentIndexNames) { + if (startingIndexNames.indexOf(index) === -1) { + return index; + } + } + return undefined; +} diff --git a/x-pack/plugins/search_indices/tsconfig.json b/x-pack/plugins/search_indices/tsconfig.json index 61b82f4485492..341dd230cee5f 100644 --- a/x-pack/plugins/search_indices/tsconfig.json +++ b/x-pack/plugins/search_indices/tsconfig.json @@ -12,7 +12,6 @@ ], "kbn_references": [ "@kbn/core", - "@kbn/navigation-plugin", "@kbn/config-schema", "@kbn/core-elasticsearch-server", "@kbn/logging", @@ -39,7 +38,8 @@ "@kbn/search-shared-ui", "@kbn/deeplinks-search", "@kbn/core-chrome-browser", - "@kbn/serverless" + "@kbn/serverless", + "@kbn/utility-types" ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/search_playground/public/components/setup_page/create_index_button.tsx b/x-pack/plugins/search_playground/public/components/setup_page/create_index_button.tsx index d4a165f56266c..5bc9f84c833fb 100644 --- a/x-pack/plugins/search_playground/public/components/setup_page/create_index_button.tsx +++ b/x-pack/plugins/search_playground/public/components/setup_page/create_index_button.tsx @@ -16,7 +16,9 @@ export const CreateIndexButton: React.FC = () => { services: { application, share }, } = useKibana(); const createIndexLocator = useMemo( - () => share.url.locators.get('CREATE_INDEX_LOCATOR_ID'), + () => + share.url.locators.get('CREATE_INDEX_LOCATOR_ID') ?? + share.url.locators.get('SEARCH_CREATE_INDEX'), [share.url.locators] ); const handleNavigateToIndex = useCallback(async () => { diff --git a/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/exporting.md b/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/exporting.md new file mode 100644 index 0000000000000..f4cbc66779a81 --- /dev/null +++ b/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/exporting.md @@ -0,0 +1,65 @@ +# Prebuilt Rule Export + +This is a test plan for the exporting of prebuilt rules. This feature is an aspect of `Milestone 2` of the [Rule Immutability/Customization](https://github.com/elastic/security-team/issues/1974) epic. + +Status: `in progress`. + +## Useful information + +### Tickets + +- [Rule Immutability/Customization](https://github.com/elastic/security-team/issues/1974) +- [Rule Exporting Feature](https://github.com/elastic/kibana/issues/180167#issue-2227974379) +- [Rule Export API PR](https://github.com/elastic/kibana/pull/194498) + +### Terminology + +- **prebuilt rule**: A rule contained in our `Prebuilt Security Detection Rules` integration in Fleet. +- **custom rule**: A rule defined by the user, which has no relation to the prebuilt rules +- **rule source, or ruleSource**: A field on the rule that defines the rule's categorization + +## Scenarios + +### Core Functionality + +#### Scenario: Exporting prebuilt rule individually +```Gherkin +Given a space with prebuilt rules installed +When the user selects "Export rule" from the "All actions" dropdown on the rule's page +Then the rule should be exported as an NDJSON file +And it should include an "immutable" field with a value of true +And its "ruleSource" "type" should be "external" +And its "ruleSource" "isCustomized" value should depend on whether the rule was customized +``` + +#### Scenario: Exporting prebuilt rules in bulk +```Gherkin +Given a space with prebuilt rules installed +When the user selects prebuilt rules in the alerts table +And chooses "Export" from bulk actions +Then the selected rules should be exported as an NDJSON file +And they should include an "immutable" field with a value of true +And their "ruleSource" "type" should be "external" +And their "ruleSource" "isCustomized" should depend on whether the rule was customized +``` + +#### Scenario: Exporting both prebuilt and custom rules in bulk +```Gherkin +Given a space with prebuilt and custom rules installed +When the user selects prebuilt rules in the alerts table +And chooses "Export" from bulk actions +Then the selected rules should be exported as an NDJSON file +And the prebuilt rules should include an "immutable" field with a value of true +And the custom rules should include an "immutable" field with a value of false +And the prebuilt rules' "ruleSource" "type" should be "external" +And the custom rules' "ruleSource" "type" should be "internal" +``` + +### Error Handling + +#### Scenario: Exporting beyond the export limit +```Gherkin +Given a space with prebuilt and custom rules installed +And the number of rules is greater than the export limit (defaults to 10_000) +Then the request should be rejected as a bad request +``` diff --git a/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/importing.md b/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/importing.md new file mode 100644 index 0000000000000..0c947d0a52b95 --- /dev/null +++ b/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/importing.md @@ -0,0 +1,127 @@ +# Prebuilt Rule Import + +This is a test plan for the importing of prebuilt rules. This feature is an aspect of `Milestone 2` of the [Rule Immutability/Customization](https://github.com/elastic/security-team/issues/1974) epic. + +Status: `in progress`. + +## Useful information + +### Tickets + +- [Rule Immutability/Customization](https://github.com/elastic/security-team/issues/1974) +- [Rule Importing Feature](https://github.com/elastic/kibana/issues/180168) +- [Rule Import API PR](https://github.com/elastic/kibana/pull/190198) + +### Terminology + +- **prebuilt rule**: A rule contained in our `Prebuilt Security Detection Rules` integration in Fleet. +- **custom rule**: A rule defined by the user, which has no relation to the prebuilt rules +- **rule source, or ruleSource**: A field on the rule that defines the rule's categorization + +## Scenarios + +### Core Functionality + +#### Scenario: Importing an unmodified prebuilt rule with a matching rule_id and version + +```Gherkin +Given the import payload contains a prebuilt rule with a matching rule_id and version, identical to the published rule +When the user imports the rule +Then the rule should be created or updated +And the ruleSource type should be "external" +And isCustomized should be false +``` + +#### Scenario: Importing a customized prebuilt rule with a matching rule_id and version + +```Gherkin +Given the import payload contains a prebuilt rule with a matching rule_id and version, modified from the published version +When the user imports the rule +Then the rule should be created or updated +And the ruleSource type should be "external" +And isCustomized should be true +``` + +#### Scenario: Importing a prebuilt rule with a matching rule_id but no matching version + +```Gherkin +Given the import payload contains a prebuilt rule with a matching rule_id but no matching version +When the user imports the rule +Then the rule should be created or updated +And the ruleSource type should be "external" +And isCustomized should be true +``` + +#### Scenario: Importing a prebuilt rule with a non-existent rule_id + +```Gherkin +Given the import payload contains a prebuilt rule with a non-existent rule_id +When the user imports the rule +Then the rule should be created +And the ruleSource type should be "internal" +``` + +#### Scenario: Importing a prebuilt rule without a rule_id field + +```Gherkin +Given the import payload contains a prebuilt rule without a rule_id field +When the user imports the rule +Then the import should be rejected with a message "rule_id field is required" +``` + +#### Scenario: Importing a prebuilt rule with a matching rule_id but missing a version field + +```Gherkin +Given the import payload contains a prebuilt rule without a version field +When the user imports the rule +Then the import should be rejected with a message "version field is required" +``` + +#### Scenario: Importing an existing custom rule missing a version field + +```Gherkin +Given the import payload contains an existing custom rule without a version field +When the user imports the rule +Then the rule should be updated +And the ruleSource type should be "internal" +And the "version" field should be set to the existing rule's "version" +``` + +#### Scenario: Importing a new custom rule missing a version field + +```Gherkin +Given the import payload contains a new custom rule without a version field +When the user imports the rule +Then the rule should be created +And the ruleSource type should be "internal" +And the "version" field should be set to 1 +``` + +#### Scenario: Importing a rule with overwrite flag set to true + +```Gherkin +Given the import payload contains a rule with an existing rule_id +And the overwrite flag is set to true +When the user imports the rule +Then the rule should be overwritten +And the ruleSource type should be calculated based on the rule_id and version +``` + +#### Scenario: Importing a rule with overwrite flag set to false + +```Gherkin +Given the import payload contains a rule with an existing rule_id +And the overwrite flag is set to false +When the user imports the rule +Then the import should be rejected with a message "rule_id already exists" +``` + +#### Scenario: Importing both custom and prebuilt rules + +```Gherkin +Given the import payload contains modified and unmodified, custom and prebuilt rules +When the user imports the rule +Then custom rules should be created or updated, with versions defaulted to 1 +And prebuilt rules should be created or updated, +And prebuilt rules missing versions should be rejected +``` diff --git a/x-pack/plugins/security_solution/public/app/actions/telemetry.test.ts b/x-pack/plugins/security_solution/public/app/actions/telemetry.test.ts index 9b22aa03fbeaf..28d4b90f4d9ee 100644 --- a/x-pack/plugins/security_solution/public/app/actions/telemetry.test.ts +++ b/x-pack/plugins/security_solution/public/app/actions/telemetry.test.ts @@ -9,6 +9,7 @@ import type { StartServices } from '../../types'; import { enhanceActionWithTelemetry } from './telemetry'; import { createAction } from '@kbn/ui-actions-plugin/public'; import type { CellActionExecutionContext } from '@kbn/cell-actions'; +import { AppEventTypes } from '../../common/lib/telemetry'; const actionId = 'test_action_id'; const displayName = 'test-actions'; @@ -29,13 +30,13 @@ const context = { describe('enhanceActionWithTelemetry', () => { it('calls telemetry report when the action is executed', () => { - const telemetry = { reportCellActionClicked: jest.fn() }; + const telemetry = { reportEvent: jest.fn() }; const services = { telemetry } as unknown as StartServices; const enhancedAction = enhanceActionWithTelemetry(action, services); enhancedAction.execute(context); - expect(telemetry.reportCellActionClicked).toHaveBeenCalledWith({ + expect(telemetry.reportEvent).toHaveBeenCalledWith(AppEventTypes.CellActionClicked, { displayName, actionId, fieldName, diff --git a/x-pack/plugins/security_solution/public/app/actions/telemetry.ts b/x-pack/plugins/security_solution/public/app/actions/telemetry.ts index 319194e57f81a..0c69f7b233666 100644 --- a/x-pack/plugins/security_solution/public/app/actions/telemetry.ts +++ b/x-pack/plugins/security_solution/public/app/actions/telemetry.ts @@ -9,6 +9,7 @@ import type { CellAction, CellActionExecutionContext } from '@kbn/cell-actions'; import type { ActionExecutionContext } from '@kbn/ui-actions-plugin/public'; import type { StartServices } from '../../types'; import type { SecurityCellActionExecutionContext } from './types'; +import { AppEventTypes } from '../../common/lib/telemetry'; export const enhanceActionWithTelemetry = ( action: CellAction, @@ -19,7 +20,7 @@ export const enhanceActionWithTelemetry = ( const enhancedExecute = ( context: ActionExecutionContext ): Promise => { - telemetry.reportCellActionClicked({ + telemetry.reportEvent(AppEventTypes.CellActionClicked, { actionId: rest.id, displayName: rest.getDisplayName(context), fieldName: context.data.map(({ field }) => field.name).join(', '), diff --git a/x-pack/plugins/security_solution/public/assistant/use_assistant_telemetry/index.test.tsx b/x-pack/plugins/security_solution/public/assistant/use_assistant_telemetry/index.test.tsx index d714781ee11c9..b90db333742a4 100644 --- a/x-pack/plugins/security_solution/public/assistant/use_assistant_telemetry/index.test.tsx +++ b/x-pack/plugins/security_solution/public/assistant/use_assistant_telemetry/index.test.tsx @@ -9,6 +9,7 @@ import { renderHook } from '@testing-library/react-hooks'; import { useAssistantTelemetry } from '.'; import { BASE_SECURITY_CONVERSATIONS } from '../content/conversations'; import { createTelemetryServiceMock } from '../../common/lib/telemetry/telemetry_service.mock'; +import { AssistantEventTypes } from '../../common/lib/telemetry'; const customId = `My Convo`; const mockedConversations = { @@ -20,15 +21,9 @@ const mockedConversations = { messages: [], }, }; -const reportAssistantInvoked = jest.fn(); -const reportAssistantMessageSent = jest.fn(); -const reportAssistantQuickPrompt = jest.fn(); + const mockedTelemetry = { ...createTelemetryServiceMock(), - reportAssistantInvoked, - reportAssistantMessageSent, - reportAssistantQuickPrompt, - reportAssistantSettingToggled: () => {}, }; jest.mock('../../common/lib/kibana', () => { @@ -55,9 +50,9 @@ jest.mock('@kbn/elastic-assistant', () => ({ })); const trackingFns = [ - 'reportAssistantInvoked', - 'reportAssistantMessageSent', - 'reportAssistantQuickPrompt', + { name: 'reportAssistantInvoked', eventType: AssistantEventTypes.AssistantInvoked }, + { name: 'reportAssistantMessageSent', eventType: AssistantEventTypes.AssistantMessageSent }, + { name: 'reportAssistantQuickPrompt', eventType: AssistantEventTypes.AssistantQuickPrompt }, ]; describe('useAssistantTelemetry', () => { @@ -67,7 +62,7 @@ describe('useAssistantTelemetry', () => { it('should return the expected telemetry object with tracking functions', () => { const { result } = renderHook(() => useAssistantTelemetry()); trackingFns.forEach((fn) => { - expect(result.current).toHaveProperty(fn); + expect(result.current).toHaveProperty(fn.name); }); }); @@ -76,11 +71,11 @@ describe('useAssistantTelemetry', () => { const { result } = renderHook(() => useAssistantTelemetry()); const validId = Object.keys(mockedConversations)[0]; // @ts-ignore - const trackingFn = result.current[fn]; + const trackingFn = result.current[fn.name]; await trackingFn({ conversationId: validId, invokedBy: 'shortcut' }); // @ts-ignore - const trackingMockedFn = mockedTelemetry[fn]; - expect(trackingMockedFn).toHaveBeenCalledWith({ + const trackingMockedFn = mockedTelemetry.reportEvent; + expect(trackingMockedFn).toHaveBeenCalledWith(fn.eventType, { conversationId: validId, invokedBy: 'shortcut', }); @@ -89,11 +84,11 @@ describe('useAssistantTelemetry', () => { it('Should call tracking with "Custom" id when tracking is called with an isDefault=false conversation id', async () => { const { result } = renderHook(() => useAssistantTelemetry()); // @ts-ignore - const trackingFn = result.current[fn]; + const trackingFn = result.current[fn.name]; await trackingFn({ conversationId: customId, invokedBy: 'shortcut' }); // @ts-ignore - const trackingMockedFn = mockedTelemetry[fn]; - expect(trackingMockedFn).toHaveBeenCalledWith({ + const trackingMockedFn = mockedTelemetry.reportEvent; + expect(trackingMockedFn).toHaveBeenCalledWith(fn.eventType, { conversationId: 'Custom', invokedBy: 'shortcut', }); @@ -102,11 +97,11 @@ describe('useAssistantTelemetry', () => { it('Should call tracking with "Custom" id when tracking is called with an unknown conversation id', async () => { const { result } = renderHook(() => useAssistantTelemetry()); // @ts-ignore - const trackingFn = result.current[fn]; + const trackingFn = result.current[fn.name]; await trackingFn({ conversationId: '123', invokedBy: 'shortcut' }); // @ts-ignore - const trackingMockedFn = mockedTelemetry[fn]; - expect(trackingMockedFn).toHaveBeenCalledWith({ + const trackingMockedFn = mockedTelemetry.reportEvent; + expect(trackingMockedFn).toHaveBeenCalledWith(fn.eventType, { conversationId: 'Custom', invokedBy: 'shortcut', }); diff --git a/x-pack/plugins/security_solution/public/assistant/use_assistant_telemetry/index.tsx b/x-pack/plugins/security_solution/public/assistant/use_assistant_telemetry/index.tsx index 543eac554beba..04bfc8bdcd640 100644 --- a/x-pack/plugins/security_solution/public/assistant/use_assistant_telemetry/index.tsx +++ b/x-pack/plugins/security_solution/public/assistant/use_assistant_telemetry/index.tsx @@ -9,7 +9,13 @@ import { type AssistantTelemetry } from '@kbn/elastic-assistant'; import { useCallback } from 'react'; import { useKibana } from '../../common/lib/kibana'; import { useBaseConversations } from '../use_conversation_store'; - +import type { + ReportAssistantInvokedParams, + ReportAssistantMessageSentParams, + ReportAssistantQuickPromptParams, + ReportAssistantSettingToggledParams, +} from '../../common/lib/telemetry'; +import { AssistantEventTypes } from '../../common/lib/telemetry'; export const useAssistantTelemetry = (): AssistantTelemetry => { const { services: { telemetry }, @@ -27,27 +33,30 @@ export const useAssistantTelemetry = (): AssistantTelemetry => { const reportTelemetry = useCallback( async ({ - fn, + eventType, params: { conversationId, ...rest }, - }: // eslint-disable-next-line @typescript-eslint/no-explicit-any - any): Promise<{ - fn: keyof AssistantTelemetry; - params: AssistantTelemetry[keyof AssistantTelemetry]; - }> => - fn({ + }: { + eventType: AssistantEventTypes; + params: + | ReportAssistantInvokedParams + | ReportAssistantMessageSentParams + | ReportAssistantQuickPromptParams; + }) => + telemetry.reportEvent(eventType, { ...rest, conversationId: await getAnonymizedConversationTitle(conversationId), }), - [getAnonymizedConversationTitle] + [getAnonymizedConversationTitle, telemetry] ); return { - reportAssistantInvoked: (params) => - reportTelemetry({ fn: telemetry.reportAssistantInvoked, params }), - reportAssistantMessageSent: (params) => - reportTelemetry({ fn: telemetry.reportAssistantMessageSent, params }), - reportAssistantQuickPrompt: (params) => - reportTelemetry({ fn: telemetry.reportAssistantQuickPrompt, params }), - reportAssistantSettingToggled: (params) => telemetry.reportAssistantSettingToggled(params), + reportAssistantInvoked: (params: ReportAssistantInvokedParams) => + reportTelemetry({ eventType: AssistantEventTypes.AssistantInvoked, params }), + reportAssistantMessageSent: (params: ReportAssistantMessageSentParams) => + reportTelemetry({ eventType: AssistantEventTypes.AssistantMessageSent, params }), + reportAssistantQuickPrompt: (params: ReportAssistantQuickPromptParams) => + reportTelemetry({ eventType: AssistantEventTypes.AssistantQuickPrompt, params }), + reportAssistantSettingToggled: (params: ReportAssistantSettingToggledParams) => + telemetry.reportEvent(AssistantEventTypes.AssistantSettingToggled, params), }; }; diff --git a/x-pack/plugins/security_solution/public/cases/pages/index.tsx b/x-pack/plugins/security_solution/public/cases/pages/index.tsx index 77fc7db0c0a8a..787dfc973c5d2 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/index.tsx @@ -25,6 +25,7 @@ import * as timelineMarkdownPlugin from '../../common/components/markdown_editor import { useFetchAlertData } from './use_fetch_alert_data'; import { useUpsellingMessage } from '../../common/hooks/use_upselling'; import { useFetchNotes } from '../../notes/hooks/use_fetch_notes'; +import { DocumentEventTypes } from '../../common/lib/telemetry'; const CaseContainerComponent: React.FC = () => { const { cases, telemetry } = useKibana().services; @@ -47,7 +48,7 @@ const CaseContainerComponent: React.FC = () => { }, }, }); - telemetry.reportDetailsFlyoutOpened({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { location: TimelineId.casePage, panel: 'right', }); diff --git a/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx b/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx index aebf34f094027..a56010182f138 100644 --- a/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx @@ -24,6 +24,7 @@ import type { ColumnHeaderOptions, OnRowSelected } from '../../../../../common/t import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features'; import { useTourContext } from '../../guided_onboarding_tour'; import { AlertsCasesTourSteps, SecurityStepId } from '../../guided_onboarding_tour/tour_config'; +import { NotesEventTypes, DocumentEventTypes } from '../../../lib/telemetry'; import { getMappedNonEcsValue } from '../../../utils/get_mapped_non_ecs_value'; export type RowActionProps = EuiDataGridCellValueElementProps & { @@ -109,7 +110,7 @@ const RowActionComponent = ({ }, }, }); - telemetry.reportDetailsFlyoutOpened({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { location: tableId, panel: 'right', }); @@ -137,10 +138,10 @@ const RowActionComponent = ({ }, }, }); - telemetry.reportOpenNoteInExpandableFlyoutClicked({ + telemetry.reportEvent(NotesEventTypes.OpenNoteInExpandableFlyoutClicked, { location: tableId, }); - telemetry.reportDetailsFlyoutOpened({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { location: tableId, panel: 'left', }); diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.tsx index 0648dd60d84f9..83042c2f4fbf4 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.tsx @@ -41,6 +41,7 @@ import { import type { HostsTableType } from '../../../explore/hosts/store/model'; import type { UsersTableType } from '../../../explore/users/store/model'; import { useGetSecuritySolutionLinkProps, withSecuritySolutionLink } from './link_props'; +import { EntityEventTypes } from '../../lib/telemetry'; export { useSecuritySolutionLinkProps, type GetSecuritySolutionLinkProps } from './link_props'; export { LinkButton, LinkAnchor } from './helpers'; @@ -94,7 +95,7 @@ const UserDetailsLinkComponent: React.FC<{ const onClick = useCallback( (e: SyntheticEvent) => { - telemetry.reportEntityDetailsClicked({ entity: 'user' }); + telemetry.reportEvent(EntityEventTypes.EntityDetailsClicked, { entity: 'user' }); const callback = onClickParam ?? goToUsersDetails; callback(e); }, @@ -171,7 +172,7 @@ const HostDetailsLinkComponent: React.FC = ({ const onClick = useCallback( (e: SyntheticEvent) => { - telemetry.reportEntityDetailsClicked({ entity: 'host' }); + telemetry.reportEvent(EntityEventTypes.EntityDetailsClicked, { entity: 'host' }); const callback = onClickParam ?? goToHostDetails; callback(e); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_enable_data_feed.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_enable_data_feed.test.tsx index 0801ec37f6ae6..5d1d2ab2eaba7 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_enable_data_feed.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_enable_data_feed.test.tsx @@ -11,7 +11,7 @@ import { TestProviders } from '../../../mock'; import type { SecurityJob } from '../types'; import { createTelemetryServiceMock } from '../../../lib/telemetry/telemetry_service.mock'; -import { ML_JOB_TELEMETRY_STATUS } from '../../../lib/telemetry'; +import { ML_JOB_TELEMETRY_STATUS, EntityEventTypes } from '../../../lib/telemetry'; const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} @@ -188,14 +188,14 @@ describe('useSecurityJobsHelpers', () => { await result.current.enableDatafeed(JOB, TIMESTAMP); }); - expect(mockedTelemetry.reportMLJobUpdate).toHaveBeenCalledWith({ + expect(mockedTelemetry.reportEvent).toHaveBeenCalledWith(EntityEventTypes.MLJobUpdate, { status: ML_JOB_TELEMETRY_STATUS.moduleInstalled, isElasticJob: true, jobId, moduleId, }); - expect(mockedTelemetry.reportMLJobUpdate).toHaveBeenCalledWith({ + expect(mockedTelemetry.reportEvent).toHaveBeenCalledWith(EntityEventTypes.MLJobUpdate, { status: ML_JOB_TELEMETRY_STATUS.started, isElasticJob: true, jobId, @@ -211,7 +211,7 @@ describe('useSecurityJobsHelpers', () => { await result.current.enableDatafeed({ ...JOB, isInstalled: true }, TIMESTAMP); }); - expect(mockedTelemetry.reportMLJobUpdate).toHaveBeenCalledWith({ + expect(mockedTelemetry.reportEvent).toHaveBeenCalledWith(EntityEventTypes.MLJobUpdate, { status: ML_JOB_TELEMETRY_STATUS.startError, errorMessage: 'Start job failure - test_error', isElasticJob: true, @@ -228,7 +228,7 @@ describe('useSecurityJobsHelpers', () => { await result.current.enableDatafeed(JOB, TIMESTAMP); }); - expect(mockedTelemetry.reportMLJobUpdate).toHaveBeenCalledWith({ + expect(mockedTelemetry.reportEvent).toHaveBeenCalledWith(EntityEventTypes.MLJobUpdate, { status: ML_JOB_TELEMETRY_STATUS.installationError, errorMessage: 'Create job failure - test_error', isElasticJob: true, @@ -295,7 +295,7 @@ describe('useSecurityJobsHelpers', () => { await result.current.disableDatafeed({ ...JOB, isInstalled: true }); }); - expect(mockedTelemetry.reportMLJobUpdate).toHaveBeenCalledWith({ + expect(mockedTelemetry.reportEvent).toHaveBeenCalledWith(EntityEventTypes.MLJobUpdate, { status: ML_JOB_TELEMETRY_STATUS.stopped, isElasticJob: true, jobId, @@ -311,7 +311,7 @@ describe('useSecurityJobsHelpers', () => { await result.current.disableDatafeed({ ...JOB, isInstalled: true }); }); - expect(mockedTelemetry.reportMLJobUpdate).toHaveBeenCalledWith({ + expect(mockedTelemetry.reportEvent).toHaveBeenCalledWith(EntityEventTypes.MLJobUpdate, { status: ML_JOB_TELEMETRY_STATUS.stopError, errorMessage: 'Stop job failure - test_error', isElasticJob: true, diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_enable_data_feed.ts b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_enable_data_feed.ts index 393e132436c38..ab966770aaf33 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_enable_data_feed.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_enable_data_feed.ts @@ -13,6 +13,7 @@ import { METRIC_TYPE, ML_JOB_TELEMETRY_STATUS, TELEMETRY_EVENT, + EntityEventTypes, track, } from '../../../lib/telemetry'; @@ -43,7 +44,7 @@ export const useEnableDataFeed = () => { jobIdErrorFilter: [job.id], groups: job.groups, }); - telemetry.reportMLJobUpdate({ + telemetry.reportEvent(EntityEventTypes.MLJobUpdate, { jobId: job.id, isElasticJob: job.isElasticJob, moduleId: job.moduleId, @@ -52,7 +53,7 @@ export const useEnableDataFeed = () => { } catch (error) { setIsLoading(false); addError(error, { title: i18n.CREATE_JOB_FAILURE }); - telemetry.reportMLJobUpdate({ + telemetry.reportEvent(EntityEventTypes.MLJobUpdate, { jobId: job.id, isElasticJob: job.isElasticJob, moduleId: job.moduleId, @@ -82,7 +83,7 @@ export const useEnableDataFeed = () => { throw new Error(response[datafeedId].error); } - telemetry.reportMLJobUpdate({ + telemetry.reportEvent(EntityEventTypes.MLJobUpdate, { jobId: job.id, isElasticJob: job.isElasticJob, status: ML_JOB_TELEMETRY_STATUS.started, @@ -92,7 +93,7 @@ export const useEnableDataFeed = () => { } catch (error) { track(METRIC_TYPE.COUNT, TELEMETRY_EVENT.JOB_ENABLE_FAILURE); addError(error, { title: i18n.START_JOB_FAILURE }); - telemetry.reportMLJobUpdate({ + telemetry.reportEvent(EntityEventTypes.MLJobUpdate, { jobId: job.id, isElasticJob: job.isElasticJob, status: ML_JOB_TELEMETRY_STATUS.startError, @@ -124,7 +125,7 @@ export const useEnableDataFeed = () => { throw new Error(response.error); } - telemetry.reportMLJobUpdate({ + telemetry.reportEvent(EntityEventTypes.MLJobUpdate, { jobId: job.id, isElasticJob: job.isElasticJob, status: ML_JOB_TELEMETRY_STATUS.stopped, @@ -134,7 +135,7 @@ export const useEnableDataFeed = () => { } catch (error) { track(METRIC_TYPE.COUNT, TELEMETRY_EVENT.JOB_DISABLE_FAILURE); addError(error, { title: i18n.STOP_JOB_FAILURE }); - telemetry.reportMLJobUpdate({ + telemetry.reportEvent(EntityEventTypes.MLJobUpdate, { jobId: job.id, isElasticJob: job.isElasticJob, status: ML_JOB_TELEMETRY_STATUS.stopError, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.test.ts index c8c675f0f40a5..c20d1f4623fa7 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.test.ts @@ -137,12 +137,12 @@ describe('useBreadcrumbsNav', () => { }); it('should create breadcrumbs onClick handler', () => { - const reportBreadcrumbClickedMock = jest.fn(); + const reportEventMock = jest.fn(); (kibanaLib.useKibana as jest.Mock).mockImplementation(() => ({ services: { telemetry: { - reportBreadcrumbClicked: reportBreadcrumbClickedMock, + reportEvent: reportEventMock, }, }, })); @@ -157,6 +157,6 @@ describe('useBreadcrumbsNav', () => { expect(event.preventDefault).toHaveBeenCalled(); expect(mockDispatch).toHaveBeenCalled(); - expect(reportBreadcrumbClickedMock).toHaveBeenCalled(); + expect(reportEventMock).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.ts index 7825435fafcad..aae96ddd07dc2 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.ts @@ -16,7 +16,7 @@ import { timelineActions } from '../../../../timelines/store'; import { TimelineId } from '../../../../../common/types/timeline'; import type { GetSecuritySolutionUrl } from '../../link_to'; import { useGetSecuritySolutionUrl } from '../../link_to'; -import type { TelemetryClientStart } from '../../../lib/telemetry'; +import { AppEventTypes, type TelemetryServiceStart } from '../../../lib/telemetry'; import { useKibana, useNavigateTo, type NavigateTo } from '../../../lib/kibana'; import { useRouteSpy } from '../../../utils/route/use_route_spy'; import { updateBreadcrumbsNav } from '../../../breadcrumbs'; @@ -68,7 +68,7 @@ const addOnClicksHandlers = ( breadcrumbs: ChromeBreadcrumb[], dispatch: Dispatch, navigateTo: NavigateTo, - telemetry: TelemetryClientStart + telemetry: TelemetryServiceStart ): ChromeBreadcrumb[] => breadcrumbs.map((breadcrumb) => ({ ...breadcrumb, @@ -89,13 +89,13 @@ const createOnClickHandler = href: string, dispatch: Dispatch, navigateTo: NavigateTo, - telemetry: TelemetryClientStart, + telemetry: TelemetryServiceStart, title: React.ReactNode ) => (ev: SyntheticEvent) => { ev.preventDefault(); if (typeof title === 'string') { - telemetry.reportBreadcrumbClicked({ title }); + telemetry.reportEvent(AppEventTypes.BreadcrumbClicked, { title }); } dispatch(timelineActions.showTimeline({ id: TimelineId.active, show: false })); navigateTo({ url: href }); diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts index 5d0e9bcfd918a..08bc1d4a62a83 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts @@ -52,52 +52,3 @@ export enum TELEMETRY_EVENT { // AI assistant on rule creation form OPEN_ASSISTANT_ON_RULE_QUERY_ERROR = 'open_assistant_on_rule_query_error', } - -export enum TelemetryEventTypes { - AlertsGroupingChanged = 'Alerts Grouping Changed', - AlertsGroupingToggled = 'Alerts Grouping Toggled', - AlertsGroupingTakeAction = 'Alerts Grouping Take Action', - BreadcrumbClicked = 'Breadcrumb Clicked', - AssistantInvoked = 'Assistant Invoked', - AssistantMessageSent = 'Assistant Message Sent', - AssistantQuickPrompt = 'Assistant Quick Prompt', - AssistantSettingToggled = 'Assistant Setting Toggled', - AssetCriticalityCsvPreviewGenerated = 'Asset Criticality Csv Preview Generated', - AssetCriticalityFileSelected = 'Asset Criticality File Selected', - AssetCriticalityCsvImported = 'Asset Criticality CSV Imported', - EntityDetailsClicked = 'Entity Details Clicked', - EntityAlertsClicked = 'Entity Alerts Clicked', - EntityRiskFiltered = 'Entity Risk Filtered', - EntityStoreEnablementToggleClicked = 'Entity Store Enablement Toggle Clicked', - EntityStoreDashboardInitButtonClicked = 'Entity Store Initialization Button Clicked', - MLJobUpdate = 'ML Job Update', - AddRiskInputToTimelineClicked = 'Add Risk Input To Timeline Clicked', - ToggleRiskSummaryClicked = 'Toggle Risk Summary Clicked', - RiskInputsExpandedFlyoutOpened = 'Risk Inputs Expanded Flyout Opened', - CellActionClicked = 'Cell Action Clicked', - AnomaliesCountClicked = 'Anomalies Count Clicked', - DataQualityIndexChecked = 'Data Quality Index Checked', - DataQualityCheckAllCompleted = 'Data Quality Check All Completed', - DetailsFlyoutOpened = 'Details Flyout Opened', - DetailsFlyoutTabClicked = 'Details Flyout Tabs Clicked', - OnboardingHubStepOpen = 'Onboarding Hub Step Open', - OnboardingHubStepFinished = 'Onboarding Hub Step Finished', - OnboardingHubStepLinkClicked = 'Onboarding Hub Step Link Clicked', - ManualRuleRunOpenModal = 'Manual Rule Run Open Modal', - ManualRuleRunExecute = 'Manual Rule Run Execute', - ManualRuleRunCancelJob = 'Manual Rule Run Cancel Job', - EventLogFilterByRunType = 'Event Log Filter By Run Type', - EventLogShowSourceEventDateRange = 'Event Log -> Show Source -> Event Date Range', - OpenNoteInExpandableFlyoutClicked = 'Open Note In Expandable Flyout Clicked', - AddNoteFromExpandableFlyoutClicked = 'Add Note From Expandable Flyout Clicked', - PreviewRule = 'Preview rule', -} - -export enum ML_JOB_TELEMETRY_STATUS { - started = 'started', - startError = 'start_error', - stopped = 'stopped', - stopError = 'stop_error', - moduleInstalled = 'module_installed', - installationError = 'installationError', -} diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/ai_assistant/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/ai_assistant/index.ts index 117d6216ed2ab..70d2eb82a2c91 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/ai_assistant/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/ai_assistant/index.ts @@ -5,11 +5,11 @@ * 2.0. */ -import type { TelemetryEvent } from '../../types'; -import { TelemetryEventTypes } from '../../constants'; +import type { AssistantTelemetryEvent } from './types'; +import { AssistantEventTypes } from './types'; -export const assistantInvokedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.AssistantInvoked, +export const assistantInvokedEvent: AssistantTelemetryEvent = { + eventType: AssistantEventTypes.AssistantInvoked, schema: { conversationId: { type: 'keyword', @@ -28,8 +28,8 @@ export const assistantInvokedEvent: TelemetryEvent = { }, }; -export const assistantMessageSentEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.AssistantMessageSent, +export const assistantMessageSentEvent: AssistantTelemetryEvent = { + eventType: AssistantEventTypes.AssistantMessageSent, schema: { conversationId: { type: 'keyword', @@ -75,8 +75,8 @@ export const assistantMessageSentEvent: TelemetryEvent = { }, }; -export const assistantQuickPrompt: TelemetryEvent = { - eventType: TelemetryEventTypes.AssistantQuickPrompt, +export const assistantQuickPrompt: AssistantTelemetryEvent = { + eventType: AssistantEventTypes.AssistantQuickPrompt, schema: { conversationId: { type: 'keyword', @@ -95,8 +95,8 @@ export const assistantQuickPrompt: TelemetryEvent = { }, }; -export const assistantSettingToggledEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.AssistantSettingToggled, +export const assistantSettingToggledEvent: AssistantTelemetryEvent = { + eventType: AssistantEventTypes.AssistantSettingToggled, schema: { alertsCountUpdated: { type: 'boolean', @@ -114,3 +114,10 @@ export const assistantSettingToggledEvent: TelemetryEvent = { }, }, }; + +export const assistantTelemetryEvents = [ + assistantInvokedEvent, + assistantMessageSentEvent, + assistantQuickPrompt, + assistantSettingToggledEvent, +]; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/ai_assistant/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/ai_assistant/types.ts index 2dd6bf6215dbf..894494575f9af 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/ai_assistant/types.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/ai_assistant/types.ts @@ -6,7 +6,13 @@ */ import type { RootSchema } from '@kbn/core/public'; -import type { TelemetryEventTypes } from '../../constants'; + +export enum AssistantEventTypes { + AssistantInvoked = 'Assistant Invoked', + AssistantMessageSent = 'Assistant Message Sent', + AssistantQuickPrompt = 'Assistant Quick Prompt', + AssistantSettingToggled = 'Assistant Setting Toggled', +} export interface ReportAssistantInvokedParams { conversationId: string; @@ -32,26 +38,14 @@ export interface ReportAssistantSettingToggledParams { assistantStreamingEnabled?: boolean; } -export type ReportAssistantTelemetryEventParams = - | ReportAssistantInvokedParams - | ReportAssistantMessageSentParams - | ReportAssistantSettingToggledParams - | ReportAssistantQuickPromptParams; - -export type AssistantTelemetryEvent = - | { - eventType: TelemetryEventTypes.AssistantInvoked; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.AssistantSettingToggled; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.AssistantMessageSent; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.AssistantQuickPrompt; - schema: RootSchema; - }; +export interface AssistantTelemetryEventsMap { + [AssistantEventTypes.AssistantInvoked]: ReportAssistantInvokedParams; + [AssistantEventTypes.AssistantMessageSent]: ReportAssistantMessageSentParams; + [AssistantEventTypes.AssistantQuickPrompt]: ReportAssistantQuickPromptParams; + [AssistantEventTypes.AssistantSettingToggled]: ReportAssistantSettingToggledParams; +} + +export interface AssistantTelemetryEvent { + eventType: AssistantEventTypes; + schema: RootSchema; +} diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/alerts_grouping/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/alerts_grouping/index.ts index 7c990dc75776e..3e2119205b77c 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/alerts_grouping/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/alerts_grouping/index.ts @@ -5,11 +5,11 @@ * 2.0. */ -import type { TelemetryEvent } from '../../types'; -import { TelemetryEventTypes } from '../../constants'; +import type { AlertsGroupingTelemetryEvent } from './types'; +import { AlertsEventTypes } from './types'; -export const alertsGroupingToggledEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.AlertsGroupingToggled, +export const alertsGroupingToggledEvent: AlertsGroupingTelemetryEvent = { + eventType: AlertsEventTypes.AlertsGroupingToggled, schema: { isOpen: { type: 'boolean', @@ -35,8 +35,8 @@ export const alertsGroupingToggledEvent: TelemetryEvent = { }, }; -export const alertsGroupingChangedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.AlertsGroupingChanged, +export const alertsGroupingChangedEvent: AlertsGroupingTelemetryEvent = { + eventType: AlertsEventTypes.AlertsGroupingChanged, schema: { tableId: { type: 'keyword', @@ -55,8 +55,8 @@ export const alertsGroupingChangedEvent: TelemetryEvent = { }, }; -export const alertsGroupingTakeActionEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.AlertsGroupingTakeAction, +export const alertsGroupingTakeActionEvent: AlertsGroupingTelemetryEvent = { + eventType: AlertsEventTypes.AlertsGroupingTakeAction, schema: { tableId: { type: 'keyword', @@ -88,3 +88,9 @@ export const alertsGroupingTakeActionEvent: TelemetryEvent = { }, }, }; + +export const alertsTelemetryEvents = [ + alertsGroupingToggledEvent, + alertsGroupingChangedEvent, + alertsGroupingTakeActionEvent, +]; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/alerts_grouping/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/alerts_grouping/types.ts index d2b5e227ee66a..924ddd4d1987f 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/alerts_grouping/types.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/alerts_grouping/types.ts @@ -6,41 +6,38 @@ */ import type { RootSchema } from '@kbn/core/public'; -import type { TelemetryEventTypes } from '../../constants'; -export interface ReportAlertsGroupingChangedParams { +export enum AlertsEventTypes { + AlertsGroupingChanged = 'Alerts Grouping Changed', + AlertsGroupingToggled = 'Alerts Grouping Toggled', + AlertsGroupingTakeAction = 'Alerts Grouping Take Action', +} + +interface ReportAlertsGroupingChangedParams { tableId: string; groupByField: string; } -export interface ReportAlertsGroupingToggledParams { +interface ReportAlertsGroupingToggledParams { isOpen: boolean; tableId: string; groupNumber: number; } -export interface ReportAlertsTakeActionParams { +interface ReportAlertsTakeActionParams { tableId: string; groupNumber: number; status: 'open' | 'closed' | 'acknowledged'; groupByField: string; } -export type ReportAlertsGroupingTelemetryEventParams = - | ReportAlertsGroupingChangedParams - | ReportAlertsGroupingToggledParams - | ReportAlertsTakeActionParams; +export interface AlertsGroupingTelemetryEventsMap { + [AlertsEventTypes.AlertsGroupingChanged]: ReportAlertsGroupingChangedParams; + [AlertsEventTypes.AlertsGroupingToggled]: ReportAlertsGroupingToggledParams; + [AlertsEventTypes.AlertsGroupingTakeAction]: ReportAlertsTakeActionParams; +} -export type AlertsGroupingTelemetryEvent = - | { - eventType: TelemetryEventTypes.AlertsGroupingToggled; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.AlertsGroupingChanged; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.AlertsGroupingTakeAction; - schema: RootSchema; - }; +export interface AlertsGroupingTelemetryEvent { + eventType: AlertsEventTypes; + schema: RootSchema; +} diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/app/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/app/index.ts new file mode 100644 index 0000000000000..d00d1df5f8306 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/app/index.ts @@ -0,0 +1,58 @@ +/* + * 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 { AppTelemetryEvent } from './types'; +import { AppEventTypes } from './types'; + +const cellActionClickedEvent: AppTelemetryEvent = { + eventType: AppEventTypes.CellActionClicked, + schema: { + fieldName: { + type: 'keyword', + _meta: { + description: 'Field Name', + optional: false, + }, + }, + actionId: { + type: 'keyword', + _meta: { + description: 'Action id', + optional: false, + }, + }, + displayName: { + type: 'keyword', + _meta: { + description: 'User friendly action name', + optional: false, + }, + }, + metadata: { + type: 'pass_through', + _meta: { + description: 'Action metadata', + optional: true, + }, + }, + }, +}; + +const breadCrumbClickedEvent: AppTelemetryEvent = { + eventType: AppEventTypes.BreadcrumbClicked, + schema: { + title: { + type: 'keyword', + _meta: { + description: 'Breadcrumb title', + optional: false, + }, + }, + }, +}; + +export const appTelemetryEvents = [cellActionClickedEvent, breadCrumbClickedEvent]; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/app/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/app/types.ts new file mode 100644 index 0000000000000..f42e689cc3fdb --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/app/types.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { RootSchema } from '@kbn/core/public'; +import type { SecurityCellActionMetadata } from '../../../../../app/actions/types'; + +export enum AppEventTypes { + CellActionClicked = 'Cell Action Clicked', + BreadcrumbClicked = 'Breadcrumb Clicked', +} + +interface ReportCellActionClickedParams { + metadata: SecurityCellActionMetadata | undefined; + displayName: string; + actionId: string; + fieldName: string; +} + +interface ReportBreadcrumbClickedParams { + title: string; +} + +export interface AppTelemetryEventsMap { + [AppEventTypes.CellActionClicked]: ReportCellActionClickedParams; + [AppEventTypes.BreadcrumbClicked]: ReportBreadcrumbClickedParams; +} + +export interface AppTelemetryEvent { + eventType: AppEventTypes; + schema: RootSchema; +} diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/data_quality/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/data_quality/index.ts index 1a3a88cbd2f57..16e8a3e1eaa64 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/data_quality/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/data_quality/index.ts @@ -5,14 +5,10 @@ * 2.0. */ -import { TelemetryEventTypes } from '../../constants'; -import type { - DataQualityTelemetryCheckAllCompletedEvent, - DataQualityTelemetryIndexCheckedEvent, -} from '../../types'; +import { DataQualityEventTypes, type DataQualityTelemetryEvents } from './types'; -export const dataQualityIndexCheckedEvent: DataQualityTelemetryIndexCheckedEvent = { - eventType: TelemetryEventTypes.DataQualityIndexChecked, +export const dataQualityIndexCheckedEvent: DataQualityTelemetryEvents = { + eventType: DataQualityEventTypes.DataQualityIndexChecked, schema: { batchId: { type: 'keyword', @@ -163,8 +159,8 @@ export const dataQualityIndexCheckedEvent: DataQualityTelemetryIndexCheckedEvent }, }; -export const dataQualityCheckAllClickedEvent: DataQualityTelemetryCheckAllCompletedEvent = { - eventType: TelemetryEventTypes.DataQualityCheckAllCompleted, +export const dataQualityCheckAllClickedEvent: DataQualityTelemetryEvents = { + eventType: DataQualityEventTypes.DataQualityCheckAllCompleted, schema: { batchId: { type: 'keyword', @@ -259,3 +255,8 @@ export const dataQualityCheckAllClickedEvent: DataQualityTelemetryCheckAllComple }, }, }; + +export const dataQualityTelemetryEvents = [ + dataQualityIndexCheckedEvent, + dataQualityCheckAllClickedEvent, +]; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/data_quality/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/data_quality/types.ts index 9e1d012811e3b..a6eca7eafc9c5 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/data_quality/types.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/data_quality/types.ts @@ -6,7 +6,11 @@ */ import type { RootSchema } from '@kbn/core/public'; -import type { TelemetryEventTypes } from '../../constants'; + +export enum DataQualityEventTypes { + DataQualityIndexChecked = 'Data Quality Index Checked', + DataQualityCheckAllCompleted = 'Data Quality Check All Completed', +} export type ReportDataQualityIndexCheckedParams = ReportDataQualityCheckAllCompletedParams & { errorCount?: number; @@ -34,16 +38,12 @@ export interface ReportDataQualityCheckAllCompletedParams { timeConsumedMs?: number; } -export interface DataQualityTelemetryIndexCheckedEvent { - eventType: TelemetryEventTypes.DataQualityIndexChecked; - schema: RootSchema; +export interface DataQualityTelemetryEventsMap { + [DataQualityEventTypes.DataQualityIndexChecked]: ReportDataQualityIndexCheckedParams; + [DataQualityEventTypes.DataQualityCheckAllCompleted]: ReportDataQualityCheckAllCompletedParams; } -export interface DataQualityTelemetryCheckAllCompletedEvent { - eventType: TelemetryEventTypes.DataQualityCheckAllCompleted; - schema: RootSchema; +export interface DataQualityTelemetryEvents { + eventType: DataQualityEventTypes; + schema: RootSchema; } - -export type DataQualityTelemetryEvents = - | DataQualityTelemetryIndexCheckedEvent - | DataQualityTelemetryCheckAllCompletedEvent; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/document_details/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/document_details/index.ts index ba59cf5130dc2..6cb27693464b2 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/document_details/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/document_details/index.ts @@ -5,11 +5,11 @@ * 2.0. */ -import type { TelemetryEvent } from '../../types'; -import { TelemetryEventTypes } from '../../constants'; +import type { DocumentDetailsTelemetryEvent } from './types'; +import { DocumentEventTypes } from './types'; -export const DocumentDetailsFlyoutOpenedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.DetailsFlyoutOpened, +export const DocumentDetailsFlyoutOpenedEvent: DocumentDetailsTelemetryEvent = { + eventType: DocumentEventTypes.DetailsFlyoutOpened, schema: { location: { type: 'text', @@ -28,8 +28,8 @@ export const DocumentDetailsFlyoutOpenedEvent: TelemetryEvent = { }, }; -export const DocumentDetailsTabClickedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.DetailsFlyoutTabClicked, +export const DocumentDetailsTabClickedEvent: DocumentDetailsTelemetryEvent = { + eventType: DocumentEventTypes.DetailsFlyoutTabClicked, schema: { location: { type: 'text', @@ -54,3 +54,8 @@ export const DocumentDetailsTabClickedEvent: TelemetryEvent = { }, }, }; + +export const documentTelemetryEvents = [ + DocumentDetailsFlyoutOpenedEvent, + DocumentDetailsTabClickedEvent, +]; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/document_details/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/document_details/types.ts index 7a3ff374eae3c..603b169e77403 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/document_details/types.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/document_details/types.ts @@ -6,29 +6,29 @@ */ import type { RootSchema } from '@kbn/core/public'; -import type { TelemetryEventTypes } from '../../constants'; -export interface ReportDetailsFlyoutOpenedParams { +export enum DocumentEventTypes { + DetailsFlyoutOpened = 'Details Flyout Opened', + DetailsFlyoutTabClicked = 'Details Flyout Tabs Clicked', +} + +interface ReportDetailsFlyoutOpenedParams { location: string; panel: 'left' | 'right' | 'preview'; } -export interface ReportDetailsFlyoutTabClickedParams { +interface ReportDetailsFlyoutTabClickedParams { location: string; panel: 'left' | 'right'; tabId: string; } -export type ReportDocumentDetailsTelemetryEventParams = - | ReportDetailsFlyoutOpenedParams - | ReportDetailsFlyoutTabClickedParams; +export interface DocumentDetailsTelemetryEventsMap { + [DocumentEventTypes.DetailsFlyoutOpened]: ReportDetailsFlyoutOpenedParams; + [DocumentEventTypes.DetailsFlyoutTabClicked]: ReportDetailsFlyoutTabClickedParams; +} -export type DocumentDetailsTelemetryEvents = - | { - eventType: TelemetryEventTypes.DetailsFlyoutOpened; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.DetailsFlyoutTabClicked; - schema: RootSchema; - }; +export interface DocumentDetailsTelemetryEvent { + eventType: DocumentEventTypes; + schema: RootSchema; +} diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/entity_analytics/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/entity_analytics/index.ts index 5a45970de6af1..771957d7a8829 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/entity_analytics/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/entity_analytics/index.ts @@ -5,11 +5,11 @@ * 2.0. */ -import type { TelemetryEvent } from '../../types'; -import { TelemetryEventTypes } from '../../constants'; +import type { EntityAnalyticsTelemetryEvent } from './types'; +import { EntityEventTypes } from './types'; -export const entityClickedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.EntityDetailsClicked, +export const entityClickedEvent: EntityAnalyticsTelemetryEvent = { + eventType: EntityEventTypes.EntityDetailsClicked, schema: { entity: { type: 'keyword', @@ -21,8 +21,8 @@ export const entityClickedEvent: TelemetryEvent = { }, }; -export const entityAlertsClickedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.EntityAlertsClicked, +export const entityAlertsClickedEvent: EntityAnalyticsTelemetryEvent = { + eventType: EntityEventTypes.EntityAlertsClicked, schema: { entity: { type: 'keyword', @@ -34,8 +34,8 @@ export const entityAlertsClickedEvent: TelemetryEvent = { }, }; -export const entityRiskFilteredEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.EntityRiskFiltered, +export const entityRiskFilteredEvent: EntityAnalyticsTelemetryEvent = { + eventType: EntityEventTypes.EntityRiskFiltered, schema: { entity: { type: 'keyword', @@ -54,8 +54,8 @@ export const entityRiskFilteredEvent: TelemetryEvent = { }, }; -export const toggleRiskSummaryClickedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.ToggleRiskSummaryClicked, +export const toggleRiskSummaryClickedEvent: EntityAnalyticsTelemetryEvent = { + eventType: EntityEventTypes.ToggleRiskSummaryClicked, schema: { entity: { type: 'keyword', @@ -74,8 +74,8 @@ export const toggleRiskSummaryClickedEvent: TelemetryEvent = { }, }; -export const RiskInputsExpandedFlyoutOpenedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.RiskInputsExpandedFlyoutOpened, +export const RiskInputsExpandedFlyoutOpenedEvent: EntityAnalyticsTelemetryEvent = { + eventType: EntityEventTypes.RiskInputsExpandedFlyoutOpened, schema: { entity: { type: 'keyword', @@ -87,8 +87,8 @@ export const RiskInputsExpandedFlyoutOpenedEvent: TelemetryEvent = { }, }; -export const addRiskInputToTimelineClickedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.AddRiskInputToTimelineClicked, +export const addRiskInputToTimelineClickedEvent: EntityAnalyticsTelemetryEvent = { + eventType: EntityEventTypes.AddRiskInputToTimelineClicked, schema: { quantity: { type: 'integer', @@ -100,8 +100,8 @@ export const addRiskInputToTimelineClickedEvent: TelemetryEvent = { }, }; -export const assetCriticalityFileSelectedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.AssetCriticalityFileSelected, +export const assetCriticalityFileSelectedEvent: EntityAnalyticsTelemetryEvent = { + eventType: EntityEventTypes.AssetCriticalityFileSelected, schema: { valid: { type: 'boolean', @@ -131,8 +131,8 @@ export const assetCriticalityFileSelectedEvent: TelemetryEvent = { }, }; -export const assetCriticalityCsvPreviewGeneratedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.AssetCriticalityCsvPreviewGenerated, +export const assetCriticalityCsvPreviewGeneratedEvent: EntityAnalyticsTelemetryEvent = { + eventType: EntityEventTypes.AssetCriticalityCsvPreviewGenerated, schema: { file: { properties: { @@ -198,8 +198,8 @@ export const assetCriticalityCsvPreviewGeneratedEvent: TelemetryEvent = { }, }; -export const assetCriticalityCsvImportedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.AssetCriticalityCsvImported, +export const assetCriticalityCsvImportedEvent: EntityAnalyticsTelemetryEvent = { + eventType: EntityEventTypes.AssetCriticalityCsvImported, schema: { file: { properties: { @@ -215,8 +215,8 @@ export const assetCriticalityCsvImportedEvent: TelemetryEvent = { }, }; -export const entityStoreInitEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.EntityStoreDashboardInitButtonClicked, +export const entityStoreInitEvent: EntityAnalyticsTelemetryEvent = { + eventType: EntityEventTypes.EntityStoreDashboardInitButtonClicked, schema: { timestamp: { type: 'date', @@ -228,8 +228,8 @@ export const entityStoreInitEvent: TelemetryEvent = { }, }; -export const entityStoreEnablementEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.EntityStoreEnablementToggleClicked, +export const entityStoreEnablementEvent: EntityAnalyticsTelemetryEvent = { + eventType: EntityEventTypes.EntityStoreEnablementToggleClicked, schema: { timestamp: { type: 'date', @@ -247,3 +247,80 @@ export const entityStoreEnablementEvent: TelemetryEvent = { }, }, }; + +const mlJobUpdateEvent: EntityAnalyticsTelemetryEvent = { + eventType: EntityEventTypes.MLJobUpdate, + schema: { + jobId: { + type: 'keyword', + _meta: { + description: 'Job id', + optional: false, + }, + }, + isElasticJob: { + type: 'boolean', + _meta: { + description: 'If true the job is one of the pre-configure security solution modules', + optional: false, + }, + }, + moduleId: { + type: 'keyword', + _meta: { + description: 'Module id', + optional: true, + }, + }, + status: { + type: 'keyword', + _meta: { + description: 'It describes what has changed in the job.', + optional: false, + }, + }, + errorMessage: { + type: 'text', + _meta: { + description: 'Error message', + optional: true, + }, + }, + }, +}; + +const anomaliesCountClickedEvent: EntityAnalyticsTelemetryEvent = { + eventType: EntityEventTypes.AnomaliesCountClicked, + schema: { + jobId: { + type: 'keyword', + _meta: { + description: 'Job id', + optional: false, + }, + }, + count: { + type: 'integer', + _meta: { + description: 'Number of anomalies', + optional: false, + }, + }, + }, +}; + +export const entityTelemetryEvents = [ + entityClickedEvent, + entityAlertsClickedEvent, + entityRiskFilteredEvent, + assetCriticalityCsvPreviewGeneratedEvent, + assetCriticalityFileSelectedEvent, + assetCriticalityCsvImportedEvent, + entityStoreEnablementEvent, + entityStoreInitEvent, + toggleRiskSummaryClickedEvent, + RiskInputsExpandedFlyoutOpenedEvent, + addRiskInputToTimelineClickedEvent, + mlJobUpdateEvent, + anomaliesCountClickedEvent, +]; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/entity_analytics/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/entity_analytics/types.ts index 3313e99a31184..3051d675b6b19 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/entity_analytics/types.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/entity_analytics/types.ts @@ -7,29 +7,52 @@ import type { RootSchema } from '@kbn/core/public'; import type { RiskSeverity } from '../../../../../../common/search_strategy'; -import type { TelemetryEventTypes } from '../../constants'; +export enum EntityEventTypes { + EntityDetailsClicked = 'Entity Details Clicked', + EntityAlertsClicked = 'Entity Alerts Clicked', + EntityRiskFiltered = 'Entity Risk Filtered', + EntityStoreEnablementToggleClicked = 'Entity Store Enablement Toggle Clicked', + EntityStoreDashboardInitButtonClicked = 'Entity Store Initialization Button Clicked', + ToggleRiskSummaryClicked = 'Toggle Risk Summary Clicked', + AddRiskInputToTimelineClicked = 'Add Risk Input To Timeline Clicked', + RiskInputsExpandedFlyoutOpened = 'Risk Inputs Expanded Flyout Opened', + AssetCriticalityCsvPreviewGenerated = 'Asset Criticality Csv Preview Generated', + AssetCriticalityFileSelected = 'Asset Criticality File Selected', + AssetCriticalityCsvImported = 'Asset Criticality CSV Imported', + AnomaliesCountClicked = 'Anomalies Count Clicked', + MLJobUpdate = 'ML Job Update', +} + +export enum ML_JOB_TELEMETRY_STATUS { + started = 'started', + startError = 'start_error', + stopped = 'stopped', + stopError = 'stop_error', + moduleInstalled = 'module_installed', + installationError = 'installationError', +} interface EntityParam { entity: 'host' | 'user'; } -export type ReportEntityDetailsClickedParams = EntityParam; -export type ReportEntityAlertsClickedParams = EntityParam; -export interface ReportEntityRiskFilteredParams extends Partial { +type ReportEntityDetailsClickedParams = EntityParam; +type ReportEntityAlertsClickedParams = EntityParam; +interface ReportEntityRiskFilteredParams extends Partial { selectedSeverity: RiskSeverity; } -export interface ReportToggleRiskSummaryClickedParams extends EntityParam { +interface ReportToggleRiskSummaryClickedParams extends EntityParam { action: 'show' | 'hide'; } -export type ReportRiskInputsExpandedFlyoutOpenedParams = EntityParam; +type ReportRiskInputsExpandedFlyoutOpenedParams = EntityParam; -export interface ReportAddRiskInputToTimelineClickedParams { +interface ReportAddRiskInputToTimelineClickedParams { quantity: number; } -export interface ReportAssetCriticalityFileSelectedParams { +interface ReportAssetCriticalityFileSelectedParams { valid: boolean; errorCode?: string; file: { @@ -37,7 +60,7 @@ export interface ReportAssetCriticalityFileSelectedParams { }; } -export interface ReportAssetCriticalityCsvPreviewGeneratedParams { +interface ReportAssetCriticalityCsvPreviewGeneratedParams { file: { size: number; }; @@ -53,76 +76,51 @@ export interface ReportAssetCriticalityCsvPreviewGeneratedParams { }; } -export interface ReportAssetCriticalityCsvImportedParams { +interface ReportAssetCriticalityCsvImportedParams { file: { size: number; }; } -export interface ReportEntityStoreEnablementParams { +interface ReportAnomaliesCountClickedParams { + jobId: string; + count: number; +} + +interface ReportEntityStoreEnablementParams { timestamp: string; action: 'start' | 'stop'; } -export interface ReportEntityStoreInitParams { +interface ReportEntityStoreInitParams { timestamp: string; } -export type ReportEntityAnalyticsTelemetryEventParams = - | ReportEntityDetailsClickedParams - | ReportEntityAlertsClickedParams - | ReportEntityRiskFilteredParams - | ReportToggleRiskSummaryClickedParams - | ReportRiskInputsExpandedFlyoutOpenedParams - | ReportAddRiskInputToTimelineClickedParams - | ReportAssetCriticalityCsvPreviewGeneratedParams - | ReportAssetCriticalityFileSelectedParams - | ReportAssetCriticalityCsvImportedParams - | ReportEntityStoreEnablementParams - | ReportEntityStoreInitParams; - -export type EntityAnalyticsTelemetryEvent = - | { - eventType: TelemetryEventTypes.EntityDetailsClicked; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.EntityAlertsClicked; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.EntityRiskFiltered; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.AddRiskInputToTimelineClicked; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.ToggleRiskSummaryClicked; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.RiskInputsExpandedFlyoutOpened; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.AssetCriticalityCsvPreviewGenerated; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.AssetCriticalityFileSelected; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.AssetCriticalityCsvImported; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.EntityStoreEnablementToggleClicked; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.EntityStoreDashboardInitButtonClicked; - schema: RootSchema; - }; +interface ReportMLJobUpdateParams { + jobId: string; + isElasticJob: boolean; + status: ML_JOB_TELEMETRY_STATUS; + moduleId?: string; + errorMessage?: string; +} + +export interface EntityAnalyticsTelemetryEventsMap { + [EntityEventTypes.EntityDetailsClicked]: ReportEntityDetailsClickedParams; + [EntityEventTypes.EntityAlertsClicked]: ReportEntityAlertsClickedParams; + [EntityEventTypes.EntityRiskFiltered]: ReportEntityRiskFilteredParams; + [EntityEventTypes.EntityStoreEnablementToggleClicked]: ReportEntityStoreEnablementParams; + [EntityEventTypes.EntityStoreDashboardInitButtonClicked]: ReportEntityStoreInitParams; + [EntityEventTypes.ToggleRiskSummaryClicked]: ReportToggleRiskSummaryClickedParams; + [EntityEventTypes.AddRiskInputToTimelineClicked]: ReportAddRiskInputToTimelineClickedParams; + [EntityEventTypes.RiskInputsExpandedFlyoutOpened]: ReportRiskInputsExpandedFlyoutOpenedParams; + [EntityEventTypes.AssetCriticalityCsvPreviewGenerated]: ReportAssetCriticalityCsvPreviewGeneratedParams; + [EntityEventTypes.AssetCriticalityFileSelected]: ReportAssetCriticalityFileSelectedParams; + [EntityEventTypes.AssetCriticalityCsvImported]: ReportAssetCriticalityCsvImportedParams; + [EntityEventTypes.AnomaliesCountClicked]: ReportAnomaliesCountClickedParams; + [EntityEventTypes.MLJobUpdate]: ReportMLJobUpdateParams; +} + +export interface EntityAnalyticsTelemetryEvent { + eventType: EntityEventTypes; + schema: RootSchema; +} diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/event_log/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/event_log/index.ts index c30efcee10cfc..7e34afa0aec66 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/event_log/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/event_log/index.ts @@ -6,10 +6,10 @@ */ import type { EventLogTelemetryEvent } from './types'; -import { TelemetryEventTypes } from '../../constants'; +import { EventLogEventTypes } from './types'; export const eventLogFilterByRunTypeEvent: EventLogTelemetryEvent = { - eventType: TelemetryEventTypes.EventLogFilterByRunType, + eventType: EventLogEventTypes.EventLogFilterByRunType, schema: { runType: { type: 'array', @@ -24,7 +24,7 @@ export const eventLogFilterByRunTypeEvent: EventLogTelemetryEvent = { }; export const eventLogShowSourceEventDateRangeEvent: EventLogTelemetryEvent = { - eventType: TelemetryEventTypes.EventLogShowSourceEventDateRange, + eventType: EventLogEventTypes.EventLogShowSourceEventDateRange, schema: { isVisible: { type: 'boolean', @@ -35,3 +35,8 @@ export const eventLogShowSourceEventDateRangeEvent: EventLogTelemetryEvent = { }, }, }; + +export const eventLogTelemetryEvents = [ + eventLogFilterByRunTypeEvent, + eventLogShowSourceEventDateRangeEvent, +]; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/event_log/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/event_log/types.ts index b196c9010b258..a2a32290ce400 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/event_log/types.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/event_log/types.ts @@ -5,25 +5,24 @@ * 2.0. */ import type { RootSchema } from '@kbn/core/public'; -import type { TelemetryEventTypes } from '../../constants'; -export interface ReportEventLogFilterByRunTypeParams { +export enum EventLogEventTypes { + EventLogFilterByRunType = 'Event Log Filter By Run Type', + EventLogShowSourceEventDateRange = 'Event Log -> Show Source -> Event Date Range', +} +interface ReportEventLogFilterByRunTypeParams { runType: string[]; } -export interface ReportEventLogShowSourceEventDateRangeParams { +interface ReportEventLogShowSourceEventDateRangeParams { isVisible: boolean; } -export type ReportEventLogTelemetryEventParams = - | ReportEventLogFilterByRunTypeParams - | ReportEventLogShowSourceEventDateRangeParams; +export interface EventLogTelemetryEventsMap { + [EventLogEventTypes.EventLogFilterByRunType]: ReportEventLogFilterByRunTypeParams; + [EventLogEventTypes.EventLogShowSourceEventDateRange]: ReportEventLogShowSourceEventDateRangeParams; +} -export type EventLogTelemetryEvent = - | { - eventType: TelemetryEventTypes.EventLogFilterByRunType; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.EventLogShowSourceEventDateRange; - schema: RootSchema; - }; +export interface EventLogTelemetryEvent { + eventType: EventLogEventTypes; + schema: RootSchema; +} diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/manual_rule_run/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/manual_rule_run/index.ts index a1476944d9806..3bc616dca1cf0 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/manual_rule_run/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/manual_rule_run/index.ts @@ -5,11 +5,11 @@ * 2.0. */ -import type { TelemetryEvent } from '../../types'; -import { TelemetryEventTypes } from '../../constants'; +import type { ManualRuleRunTelemetryEvent } from './types'; +import { ManualRuleRunEventTypes } from './types'; -export const manualRuleRunOpenModalEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.ManualRuleRunOpenModal, +export const manualRuleRunOpenModalEvent: ManualRuleRunTelemetryEvent = { + eventType: ManualRuleRunEventTypes.ManualRuleRunOpenModal, schema: { type: { type: 'keyword', @@ -21,8 +21,8 @@ export const manualRuleRunOpenModalEvent: TelemetryEvent = { }, }; -export const manualRuleRunExecuteEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.ManualRuleRunExecute, +export const manualRuleRunExecuteEvent: ManualRuleRunTelemetryEvent = { + eventType: ManualRuleRunEventTypes.ManualRuleRunExecute, schema: { rangeInMs: { type: 'integer', @@ -50,8 +50,8 @@ export const manualRuleRunExecuteEvent: TelemetryEvent = { }, }; -export const manualRuleRunCancelJobEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.ManualRuleRunCancelJob, +export const manualRuleRunCancelJobEvent: ManualRuleRunTelemetryEvent = { + eventType: ManualRuleRunEventTypes.ManualRuleRunCancelJob, schema: { totalTasks: { type: 'integer', @@ -77,3 +77,9 @@ export const manualRuleRunCancelJobEvent: TelemetryEvent = { }, }, }; + +export const manualRuleRunTelemetryEvents = [ + manualRuleRunCancelJobEvent, + manualRuleRunExecuteEvent, + manualRuleRunOpenModalEvent, +]; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/manual_rule_run/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/manual_rule_run/types.ts index a58b0adf45503..231b555408e56 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/manual_rule_run/types.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/manual_rule_run/types.ts @@ -5,39 +5,35 @@ * 2.0. */ import type { RootSchema } from '@kbn/core/public'; -import type { TelemetryEventTypes } from '../../constants'; -export interface ReportManualRuleRunOpenModalParams { +export enum ManualRuleRunEventTypes { + ManualRuleRunOpenModal = 'Manual Rule Run Open Modal', + ManualRuleRunExecute = 'Manual Rule Run Execute', + ManualRuleRunCancelJob = 'Manual Rule Run Cancel Job', +} +interface ReportManualRuleRunOpenModalParams { type: 'single' | 'bulk'; } -export interface ReportManualRuleRunExecuteParams { +interface ReportManualRuleRunExecuteParams { rangeInMs: number; rulesCount: number; status: 'success' | 'error'; } -export interface ReportManualRuleRunCancelJobParams { +interface ReportManualRuleRunCancelJobParams { totalTasks: number; completedTasks: number; errorTasks: number; } -export type ReportManualRuleRunTelemetryEventParams = - | ReportManualRuleRunOpenModalParams - | ReportManualRuleRunExecuteParams - | ReportManualRuleRunCancelJobParams; +export interface ManualRuleRunTelemetryEventsMap { + [ManualRuleRunEventTypes.ManualRuleRunOpenModal]: ReportManualRuleRunOpenModalParams; + [ManualRuleRunEventTypes.ManualRuleRunExecute]: ReportManualRuleRunExecuteParams; + [ManualRuleRunEventTypes.ManualRuleRunCancelJob]: ReportManualRuleRunCancelJobParams; +} -export type ManualRuleRunTelemetryEvent = - | { - eventType: TelemetryEventTypes.ManualRuleRunOpenModal; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.ManualRuleRunExecute; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.ManualRuleRunCancelJob; - schema: RootSchema; - }; +export interface ManualRuleRunTelemetryEvent { + eventType: ManualRuleRunEventTypes; + schema: RootSchema; +} diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/notes/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/notes/index.ts index c560f69730d36..94c9c350e0109 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/notes/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/notes/index.ts @@ -5,11 +5,11 @@ * 2.0. */ -import type { TelemetryEvent } from '../../types'; -import { TelemetryEventTypes } from '../../constants'; +import type { NotesTelemetryEvent } from './types'; +import { NotesEventTypes } from './types'; -export const openNoteInExpandableFlyoutClickedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.OpenNoteInExpandableFlyoutClicked, +export const openNoteInExpandableFlyoutClickedEvent: NotesTelemetryEvent = { + eventType: NotesEventTypes.OpenNoteInExpandableFlyoutClicked, schema: { location: { type: 'text', @@ -21,8 +21,8 @@ export const openNoteInExpandableFlyoutClickedEvent: TelemetryEvent = { }, }; -export const addNoteFromExpandableFlyoutClickedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.AddNoteFromExpandableFlyoutClicked, +export const addNoteFromExpandableFlyoutClickedEvent: NotesTelemetryEvent = { + eventType: NotesEventTypes.AddNoteFromExpandableFlyoutClicked, schema: { isRelatedToATimeline: { type: 'boolean', @@ -33,3 +33,8 @@ export const addNoteFromExpandableFlyoutClickedEvent: TelemetryEvent = { }, }, }; + +export const notesTelemetryEvents = [ + openNoteInExpandableFlyoutClickedEvent, + addNoteFromExpandableFlyoutClickedEvent, +]; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/notes/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/notes/types.ts index a785f2f8493e1..76440215c8079 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/notes/types.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/notes/types.ts @@ -6,26 +6,26 @@ */ import type { RootSchema } from '@kbn/core/public'; -import type { TelemetryEventTypes } from '../../constants'; -export interface OpenNoteInExpandableFlyoutClickedParams { +interface OpenNoteInExpandableFlyoutClickedParams { location: string; } -export interface AddNoteFromExpandableFlyoutClickedParams { +interface AddNoteFromExpandableFlyoutClickedParams { isRelatedToATimeline: boolean; } -export type NotesTelemetryEventParams = - | OpenNoteInExpandableFlyoutClickedParams - | AddNoteFromExpandableFlyoutClickedParams; +export enum NotesEventTypes { + OpenNoteInExpandableFlyoutClicked = 'Open Note In Expandable Flyout Clicked', + AddNoteFromExpandableFlyoutClicked = 'Add Note From Expandable Flyout Clicked', +} + +export interface NotesTelemetryEventsMap { + [NotesEventTypes.OpenNoteInExpandableFlyoutClicked]: OpenNoteInExpandableFlyoutClickedParams; + [NotesEventTypes.AddNoteFromExpandableFlyoutClicked]: AddNoteFromExpandableFlyoutClickedParams; +} -export type NotesTelemetryEvents = - | { - eventType: TelemetryEventTypes.OpenNoteInExpandableFlyoutClicked; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.AddNoteFromExpandableFlyoutClicked; - schema: RootSchema; - }; +export interface NotesTelemetryEvent { + eventType: NotesEventTypes; + schema: RootSchema; +} diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/onboarding/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/onboarding/index.ts index dacb0c9483281..75a35e2d61c57 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/onboarding/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/onboarding/index.ts @@ -5,11 +5,11 @@ * 2.0. */ -import type { TelemetryEvent } from '../../types'; -import { TelemetryEventTypes } from '../../constants'; +import type { OnboardingHubTelemetryEvent } from './types'; +import { OnboardingHubEventTypes } from './types'; -export const onboardingHubStepOpenEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.OnboardingHubStepOpen, +export const onboardingHubStepOpenEvent: OnboardingHubTelemetryEvent = { + eventType: OnboardingHubEventTypes.OnboardingHubStepOpen, schema: { stepId: { type: 'keyword', @@ -28,8 +28,8 @@ export const onboardingHubStepOpenEvent: TelemetryEvent = { }, }; -export const onboardingHubStepLinkClickedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.OnboardingHubStepLinkClicked, +export const onboardingHubStepLinkClickedEvent: OnboardingHubTelemetryEvent = { + eventType: OnboardingHubEventTypes.OnboardingHubStepLinkClicked, schema: { originStepId: { type: 'keyword', @@ -48,8 +48,8 @@ export const onboardingHubStepLinkClickedEvent: TelemetryEvent = { }, }; -export const onboardingHubStepFinishedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.OnboardingHubStepFinished, +export const onboardingHubStepFinishedEvent: OnboardingHubTelemetryEvent = { + eventType: OnboardingHubEventTypes.OnboardingHubStepFinished, schema: { stepId: { type: 'keyword', @@ -74,3 +74,9 @@ export const onboardingHubStepFinishedEvent: TelemetryEvent = { }, }, }; + +export const onboardingHubTelemetryEvents = [ + onboardingHubStepOpenEvent, + onboardingHubStepLinkClickedEvent, + onboardingHubStepFinishedEvent, +]; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/onboarding/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/onboarding/types.ts index 224635715b324..d11e9800e16fe 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/onboarding/types.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/onboarding/types.ts @@ -5,30 +5,25 @@ * 2.0. */ import type { RootSchema } from '@kbn/core/public'; -import type { TelemetryEventTypes } from '../../constants'; -export type OnboardingHubStepOpenTrigger = 'navigation' | 'click'; +export enum OnboardingHubEventTypes { + OnboardingHubStepOpen = 'Onboarding Hub Step Open', + OnboardingHubStepFinished = 'Onboarding Hub Step Finished', + OnboardingHubStepLinkClicked = 'Onboarding Hub Step Link Clicked', +} + +type OnboardingHubStepOpenTrigger = 'navigation' | 'click'; -export interface OnboardingHubStepOpenParams { +interface OnboardingHubStepOpenParams { stepId: string; trigger: OnboardingHubStepOpenTrigger; } -export interface OnboardingHubStepOpen { - eventType: TelemetryEventTypes.OnboardingHubStepOpen; - schema: RootSchema; -} - export interface OnboardingHubStepLinkClickedParams { originStepId: string; stepLinkId: string; } -export interface OnboardingHubStepLinkClicked { - eventType: TelemetryEventTypes.OnboardingHubStepLinkClicked; - schema: RootSchema; -} - export type OnboardingHubStepFinishedTrigger = 'auto_check' | 'click'; export interface OnboardingHubStepFinishedParams { @@ -37,12 +32,13 @@ export interface OnboardingHubStepFinishedParams { trigger: OnboardingHubStepFinishedTrigger; } -export interface OnboardingHubStepFinished { - eventType: TelemetryEventTypes.OnboardingHubStepFinished; - schema: RootSchema; +export interface OnboardingHubTelemetryEventsMap { + [OnboardingHubEventTypes.OnboardingHubStepOpen]: OnboardingHubStepOpenParams; + [OnboardingHubEventTypes.OnboardingHubStepFinished]: OnboardingHubStepFinishedParams; + [OnboardingHubEventTypes.OnboardingHubStepLinkClicked]: OnboardingHubStepLinkClickedParams; } -export type OnboardingHubTelemetryEvent = - | OnboardingHubStepOpen - | OnboardingHubStepFinished - | OnboardingHubStepLinkClicked; +export interface OnboardingHubTelemetryEvent { + eventType: OnboardingHubEventTypes; + schema: RootSchema; +} diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/preview_rule/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/preview_rule/index.ts index 12d721c45e2c0..f34380935b0ed 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/preview_rule/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/preview_rule/index.ts @@ -5,11 +5,11 @@ * 2.0. */ -import type { TelemetryEvent } from '../../types'; -import { TelemetryEventTypes } from '../../constants'; +import type { PreviewRuleTelemetryEvent } from './types'; +import { PreviewRuleEventTypes } from './types'; -export const previewRuleEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.PreviewRule, +export const previewRuleEvent: PreviewRuleTelemetryEvent = { + eventType: PreviewRuleEventTypes.PreviewRule, schema: { ruleType: { type: 'keyword', @@ -27,3 +27,5 @@ export const previewRuleEvent: TelemetryEvent = { }, }, }; + +export const previewRuleTelemetryEvents = [previewRuleEvent]; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/preview_rule/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/preview_rule/types.ts index e5523080088fc..876378e24553b 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/preview_rule/types.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/preview_rule/types.ts @@ -7,14 +7,21 @@ import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import type { RootSchema } from '@kbn/core/public'; -import type { TelemetryEventTypes } from '../../constants'; -export interface PreviewRuleParams { +interface PreviewRuleParams { ruleType: Type; loggedRequestsEnabled: boolean; } +export enum PreviewRuleEventTypes { + PreviewRule = 'Preview rule', +} + +export interface PreviewRuleTelemetryEventsMap { + [PreviewRuleEventTypes.PreviewRule]: PreviewRuleParams; +} + export interface PreviewRuleTelemetryEvent { - eventType: TelemetryEventTypes.PreviewRule; - schema: RootSchema; + eventType: PreviewRuleEventTypes; + schema: RootSchema; } diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts index 3e7c9f1138391..b610f6e77dda1 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts @@ -4,198 +4,28 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { TelemetryEvent } from '../types'; -import { TelemetryEventTypes } from '../constants'; -import { - alertsGroupingChangedEvent, - alertsGroupingTakeActionEvent, - alertsGroupingToggledEvent, -} from './alerts_grouping'; -import { - entityAlertsClickedEvent, - entityClickedEvent, - entityRiskFilteredEvent, - addRiskInputToTimelineClickedEvent, - RiskInputsExpandedFlyoutOpenedEvent, - toggleRiskSummaryClickedEvent, - assetCriticalityCsvPreviewGeneratedEvent, - assetCriticalityFileSelectedEvent, - assetCriticalityCsvImportedEvent, - entityStoreEnablementEvent, - entityStoreInitEvent, -} from './entity_analytics'; -import { - assistantInvokedEvent, - assistantSettingToggledEvent, - assistantMessageSentEvent, - assistantQuickPrompt, -} from './ai_assistant'; -import { dataQualityIndexCheckedEvent, dataQualityCheckAllClickedEvent } from './data_quality'; -import { - DocumentDetailsFlyoutOpenedEvent, - DocumentDetailsTabClickedEvent, -} from './document_details'; -import { - onboardingHubStepFinishedEvent, - onboardingHubStepLinkClickedEvent, - onboardingHubStepOpenEvent, -} from './onboarding'; -import { - manualRuleRunCancelJobEvent, - manualRuleRunExecuteEvent, - manualRuleRunOpenModalEvent, -} from './manual_rule_run'; -import { eventLogFilterByRunTypeEvent, eventLogShowSourceEventDateRangeEvent } from './event_log'; -import { - addNoteFromExpandableFlyoutClickedEvent, - openNoteInExpandableFlyoutClickedEvent, -} from './notes'; -import { previewRuleEvent } from './preview_rule'; - -const mlJobUpdateEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.MLJobUpdate, - schema: { - jobId: { - type: 'keyword', - _meta: { - description: 'Job id', - optional: false, - }, - }, - isElasticJob: { - type: 'boolean', - _meta: { - description: 'If true the job is one of the pre-configure security solution modules', - optional: false, - }, - }, - moduleId: { - type: 'keyword', - _meta: { - description: 'Module id', - optional: true, - }, - }, - status: { - type: 'keyword', - _meta: { - description: 'It describes what has changed in the job.', - optional: false, - }, - }, - errorMessage: { - type: 'text', - _meta: { - description: 'Error message', - optional: true, - }, - }, - }, -}; - -const cellActionClickedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.CellActionClicked, - schema: { - fieldName: { - type: 'keyword', - _meta: { - description: 'Field Name', - optional: false, - }, - }, - actionId: { - type: 'keyword', - _meta: { - description: 'Action id', - optional: false, - }, - }, - displayName: { - type: 'keyword', - _meta: { - description: 'User friendly action name', - optional: false, - }, - }, - metadata: { - type: 'pass_through', - _meta: { - description: 'Action metadata', - optional: true, - }, - }, - }, -}; - -const anomaliesCountClickedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.AnomaliesCountClicked, - schema: { - jobId: { - type: 'keyword', - _meta: { - description: 'Job id', - optional: false, - }, - }, - count: { - type: 'integer', - _meta: { - description: 'Number of anomalies', - optional: false, - }, - }, - }, -}; - -const breadCrumbClickedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.BreadcrumbClicked, - schema: { - title: { - type: 'keyword', - _meta: { - description: 'Breadcrumb title', - optional: false, - }, - }, - }, -}; +import { assistantTelemetryEvents } from './ai_assistant'; +import { alertsTelemetryEvents } from './alerts_grouping'; +import { appTelemetryEvents } from './app'; +import { dataQualityTelemetryEvents } from './data_quality'; +import { documentTelemetryEvents } from './document_details'; +import { entityTelemetryEvents } from './entity_analytics'; +import { eventLogTelemetryEvents } from './event_log'; +import { manualRuleRunTelemetryEvents } from './manual_rule_run'; +import { notesTelemetryEvents } from './notes'; +import { onboardingHubTelemetryEvents } from './onboarding'; +import { previewRuleTelemetryEvents } from './preview_rule'; export const telemetryEvents = [ - alertsGroupingToggledEvent, - alertsGroupingChangedEvent, - alertsGroupingTakeActionEvent, - assistantInvokedEvent, - assistantMessageSentEvent, - assistantQuickPrompt, - assistantSettingToggledEvent, - entityClickedEvent, - entityAlertsClickedEvent, - entityRiskFilteredEvent, - assetCriticalityCsvPreviewGeneratedEvent, - assetCriticalityFileSelectedEvent, - assetCriticalityCsvImportedEvent, - entityStoreEnablementEvent, - entityStoreInitEvent, - toggleRiskSummaryClickedEvent, - RiskInputsExpandedFlyoutOpenedEvent, - addRiskInputToTimelineClickedEvent, - mlJobUpdateEvent, - cellActionClickedEvent, - anomaliesCountClickedEvent, - dataQualityIndexCheckedEvent, - dataQualityCheckAllClickedEvent, - breadCrumbClickedEvent, - DocumentDetailsFlyoutOpenedEvent, - DocumentDetailsTabClickedEvent, - onboardingHubStepOpenEvent, - onboardingHubStepLinkClickedEvent, - onboardingHubStepFinishedEvent, - manualRuleRunCancelJobEvent, - manualRuleRunExecuteEvent, - manualRuleRunOpenModalEvent, - eventLogFilterByRunTypeEvent, - eventLogShowSourceEventDateRangeEvent, - openNoteInExpandableFlyoutClickedEvent, - addNoteFromExpandableFlyoutClickedEvent, - previewRuleEvent, + ...assistantTelemetryEvents, + ...alertsTelemetryEvents, + ...previewRuleTelemetryEvents, + ...entityTelemetryEvents, + ...dataQualityTelemetryEvents, + ...documentTelemetryEvents, + ...onboardingHubTelemetryEvents, + ...manualRuleRunTelemetryEvents, + ...eventLogTelemetryEvents, + ...notesTelemetryEvents, + ...appTelemetryEvents, ]; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts index 5a6818c712de5..a8df452d512a0 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts @@ -5,23 +5,9 @@ * 2.0. */ -import type { AlertWorkflowStatus } from '../../types'; export { telemetryMiddleware } from './middleware'; export * from './constants'; -export * from './telemetry_client'; export * from './telemetry_service'; export * from './track'; export * from './types'; - -export const getTelemetryEvent = { - groupedAlertsTakeAction: ({ - tableId, - groupNumber, - status, - }: { - tableId: string; - groupNumber: number; - status: AlertWorkflowStatus; - }) => `alerts_table_${tableId}_group-${groupNumber}_mark-${status}`, -}; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts deleted file mode 100644 index 87d4b215543dc..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts +++ /dev/null @@ -1,48 +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 type { TelemetryClientStart } from './types'; - -export const createTelemetryClientMock = (): jest.Mocked => ({ - reportAlertsGroupingChanged: jest.fn(), - reportAlertsGroupingToggled: jest.fn(), - reportAlertsGroupingTakeAction: jest.fn(), - reportAssistantInvoked: jest.fn(), - reportAssistantMessageSent: jest.fn(), - reportAssistantQuickPrompt: jest.fn(), - reportAssistantSettingToggled: jest.fn(), - reportEntityDetailsClicked: jest.fn(), - reportEntityAlertsClicked: jest.fn(), - reportEntityRiskFiltered: jest.fn(), - reportMLJobUpdate: jest.fn(), - reportCellActionClicked: jest.fn(), - reportAnomaliesCountClicked: jest.fn(), - reportDataQualityIndexChecked: jest.fn(), - reportDataQualityCheckAllCompleted: jest.fn(), - reportBreadcrumbClicked: jest.fn(), - reportToggleRiskSummaryClicked: jest.fn(), - reportRiskInputsExpandedFlyoutOpened: jest.fn(), - reportAddRiskInputToTimelineClicked: jest.fn(), - reportDetailsFlyoutOpened: jest.fn(), - reportDetailsFlyoutTabClicked: jest.fn(), - reportOnboardingHubStepOpen: jest.fn(), - reportOnboardingHubStepLinkClicked: jest.fn(), - reportOnboardingHubStepFinished: jest.fn(), - reportAssetCriticalityCsvPreviewGenerated: jest.fn(), - reportAssetCriticalityFileSelected: jest.fn(), - reportAssetCriticalityCsvImported: jest.fn(), - reportEventLogFilterByRunType: jest.fn(), - reportEventLogShowSourceEventDateRange: jest.fn(), - reportManualRuleRunCancelJob: jest.fn(), - reportManualRuleRunExecute: jest.fn(), - reportManualRuleRunOpenModal: jest.fn(), - reportOpenNoteInExpandableFlyoutClicked: jest.fn(), - reportAddNoteFromExpandableFlyoutClicked: jest.fn(), - reportPreviewRule: jest.fn(), - reportEntityStoreEnablement: jest.fn(), - reportEntityStoreInit: jest.fn(), -}); diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts deleted file mode 100644 index 689209f284dbb..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { AnalyticsServiceSetup } from '@kbn/core-analytics-server'; -import type { - AddNoteFromExpandableFlyoutClickedParams, - OpenNoteInExpandableFlyoutClickedParams, -} from './events/notes/types'; -import type { - TelemetryClientStart, - ReportAlertsGroupingChangedParams, - ReportAlertsGroupingToggledParams, - ReportAlertsTakeActionParams, - ReportEntityDetailsClickedParams, - ReportEntityAlertsClickedParams, - ReportEntityRiskFilteredParams, - ReportMLJobUpdateParams, - ReportCellActionClickedParams, - ReportAnomaliesCountClickedParams, - ReportDataQualityIndexCheckedParams, - ReportDataQualityCheckAllCompletedParams, - ReportBreadcrumbClickedParams, - ReportAssistantInvokedParams, - ReportAssistantMessageSentParams, - ReportAssistantQuickPromptParams, - ReportAssistantSettingToggledParams, - ReportRiskInputsExpandedFlyoutOpenedParams, - ReportToggleRiskSummaryClickedParams, - ReportDetailsFlyoutOpenedParams, - ReportDetailsFlyoutTabClickedParams, - ReportAssetCriticalityCsvPreviewGeneratedParams, - ReportAssetCriticalityFileSelectedParams, - ReportAssetCriticalityCsvImportedParams, - ReportAddRiskInputToTimelineClickedParams, - OnboardingHubStepLinkClickedParams, - OnboardingHubStepOpenParams, - OnboardingHubStepFinishedParams, - ReportManualRuleRunCancelJobParams, - ReportManualRuleRunExecuteParams, - ReportManualRuleRunOpenModalParams, - ReportEventLogShowSourceEventDateRangeParams, - ReportEventLogFilterByRunTypeParams, - PreviewRuleParams, - ReportEntityStoreEnablementParams, - ReportEntityStoreInitParams, -} from './types'; -import { TelemetryEventTypes } from './constants'; - -/** - * Client which aggregate all the available telemetry tracking functions - * for the plugin - */ -export class TelemetryClient implements TelemetryClientStart { - constructor(private analytics: AnalyticsServiceSetup) {} - - public reportAlertsGroupingChanged = (params: ReportAlertsGroupingChangedParams) => { - this.analytics.reportEvent(TelemetryEventTypes.AlertsGroupingChanged, params); - }; - - public reportAlertsGroupingToggled = (params: ReportAlertsGroupingToggledParams) => { - this.analytics.reportEvent(TelemetryEventTypes.AlertsGroupingToggled, params); - }; - - public reportAlertsGroupingTakeAction = (params: ReportAlertsTakeActionParams) => { - this.analytics.reportEvent(TelemetryEventTypes.AlertsGroupingTakeAction, params); - }; - - public reportAssistantInvoked = (params: ReportAssistantInvokedParams) => { - this.analytics.reportEvent(TelemetryEventTypes.AssistantInvoked, params); - }; - - public reportAssistantMessageSent = (params: ReportAssistantMessageSentParams) => { - this.analytics.reportEvent(TelemetryEventTypes.AssistantMessageSent, params); - }; - - public reportAssistantQuickPrompt = (params: ReportAssistantQuickPromptParams) => { - this.analytics.reportEvent(TelemetryEventTypes.AssistantQuickPrompt, params); - }; - - public reportAssistantSettingToggled = (params: ReportAssistantSettingToggledParams) => { - this.analytics.reportEvent(TelemetryEventTypes.AssistantSettingToggled, params); - }; - - public reportEntityDetailsClicked = ({ entity }: ReportEntityDetailsClickedParams) => { - this.analytics.reportEvent(TelemetryEventTypes.EntityDetailsClicked, { - entity, - }); - }; - - public reportEntityAlertsClicked = ({ entity }: ReportEntityAlertsClickedParams) => { - this.analytics.reportEvent(TelemetryEventTypes.EntityAlertsClicked, { - entity, - }); - }; - - public reportEntityRiskFiltered = ({ - entity, - selectedSeverity, - }: ReportEntityRiskFilteredParams) => { - this.analytics.reportEvent(TelemetryEventTypes.EntityRiskFiltered, { - entity, - selectedSeverity, - }); - }; - - public reportAssetCriticalityCsvPreviewGenerated = ( - params: ReportAssetCriticalityCsvPreviewGeneratedParams - ) => { - this.analytics.reportEvent(TelemetryEventTypes.AssetCriticalityCsvPreviewGenerated, params); - }; - - public reportAssetCriticalityFileSelected = ( - params: ReportAssetCriticalityFileSelectedParams - ) => { - this.analytics.reportEvent(TelemetryEventTypes.AssetCriticalityFileSelected, params); - }; - - public reportAssetCriticalityCsvImported = (params: ReportAssetCriticalityCsvImportedParams) => { - this.analytics.reportEvent(TelemetryEventTypes.AssetCriticalityCsvImported, params); - }; - - public reportMLJobUpdate = (params: ReportMLJobUpdateParams) => { - this.analytics.reportEvent(TelemetryEventTypes.MLJobUpdate, params); - }; - - reportToggleRiskSummaryClicked(params: ReportToggleRiskSummaryClickedParams): void { - this.analytics.reportEvent(TelemetryEventTypes.ToggleRiskSummaryClicked, params); - } - reportRiskInputsExpandedFlyoutOpened(params: ReportRiskInputsExpandedFlyoutOpenedParams): void { - this.analytics.reportEvent(TelemetryEventTypes.RiskInputsExpandedFlyoutOpened, params); - } - reportAddRiskInputToTimelineClicked(params: ReportAddRiskInputToTimelineClickedParams): void { - this.analytics.reportEvent(TelemetryEventTypes.AddRiskInputToTimelineClicked, params); - } - - public reportCellActionClicked = (params: ReportCellActionClickedParams) => { - this.analytics.reportEvent(TelemetryEventTypes.CellActionClicked, params); - }; - - public reportAnomaliesCountClicked = (params: ReportAnomaliesCountClickedParams) => { - this.analytics.reportEvent(TelemetryEventTypes.AnomaliesCountClicked, params); - }; - - public reportDataQualityIndexChecked = (params: ReportDataQualityIndexCheckedParams) => { - this.analytics.reportEvent(TelemetryEventTypes.DataQualityIndexChecked, params); - }; - - public reportDataQualityCheckAllCompleted = ( - params: ReportDataQualityCheckAllCompletedParams - ) => { - this.analytics.reportEvent(TelemetryEventTypes.DataQualityCheckAllCompleted, params); - }; - - public reportBreadcrumbClicked = ({ title }: ReportBreadcrumbClickedParams) => { - this.analytics.reportEvent(TelemetryEventTypes.BreadcrumbClicked, { - title, - }); - }; - - public reportDetailsFlyoutOpened = (params: ReportDetailsFlyoutOpenedParams) => { - this.analytics.reportEvent(TelemetryEventTypes.DetailsFlyoutOpened, params); - }; - - public reportDetailsFlyoutTabClicked = (params: ReportDetailsFlyoutTabClickedParams) => { - this.analytics.reportEvent(TelemetryEventTypes.DetailsFlyoutTabClicked, params); - }; - - public reportOnboardingHubStepOpen = (params: OnboardingHubStepOpenParams) => { - this.analytics.reportEvent(TelemetryEventTypes.OnboardingHubStepOpen, params); - }; - - public reportOnboardingHubStepFinished = (params: OnboardingHubStepFinishedParams) => { - this.analytics.reportEvent(TelemetryEventTypes.OnboardingHubStepFinished, params); - }; - - public reportOnboardingHubStepLinkClicked = (params: OnboardingHubStepLinkClickedParams) => { - this.analytics.reportEvent(TelemetryEventTypes.OnboardingHubStepLinkClicked, params); - }; - - public reportManualRuleRunOpenModal = (params: ReportManualRuleRunOpenModalParams) => { - this.analytics.reportEvent(TelemetryEventTypes.ManualRuleRunOpenModal, params); - }; - - public reportManualRuleRunExecute = (params: ReportManualRuleRunExecuteParams) => { - this.analytics.reportEvent(TelemetryEventTypes.ManualRuleRunExecute, params); - }; - - public reportManualRuleRunCancelJob = (params: ReportManualRuleRunCancelJobParams) => { - this.analytics.reportEvent(TelemetryEventTypes.ManualRuleRunCancelJob, params); - }; - - public reportEventLogFilterByRunType = (params: ReportEventLogFilterByRunTypeParams) => { - this.analytics.reportEvent(TelemetryEventTypes.EventLogFilterByRunType, params); - }; - - public reportEventLogShowSourceEventDateRange( - params: ReportEventLogShowSourceEventDateRangeParams - ): void { - this.analytics.reportEvent(TelemetryEventTypes.EventLogShowSourceEventDateRange, params); - } - - public reportOpenNoteInExpandableFlyoutClicked = ( - params: OpenNoteInExpandableFlyoutClickedParams - ) => { - this.analytics.reportEvent(TelemetryEventTypes.OpenNoteInExpandableFlyoutClicked, params); - }; - - public reportAddNoteFromExpandableFlyoutClicked = ( - params: AddNoteFromExpandableFlyoutClickedParams - ) => { - this.analytics.reportEvent(TelemetryEventTypes.AddNoteFromExpandableFlyoutClicked, params); - }; - - public reportPreviewRule = (params: PreviewRuleParams) => { - this.analytics.reportEvent(TelemetryEventTypes.PreviewRule, params); - }; - - public reportEntityStoreEnablement = (params: ReportEntityStoreEnablementParams) => { - this.analytics.reportEvent(TelemetryEventTypes.EntityStoreEnablementToggleClicked, params); - }; - - public reportEntityStoreInit = (params: ReportEntityStoreInitParams) => { - this.analytics.reportEvent(TelemetryEventTypes.EntityStoreDashboardInitButtonClicked, params); - }; -} diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.mock.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.mock.ts index 519ba4527560b..30b8a0c434c5f 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.mock.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.mock.ts @@ -5,6 +5,4 @@ * 2.0. */ -import { createTelemetryClientMock } from './telemetry_client.mock'; - -export const createTelemetryServiceMock = () => createTelemetryClientMock(); +export const createTelemetryServiceMock = () => ({ reportEvent: jest.fn() }); diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.test.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.test.ts index 9079c6bf4f650..486aa241290a8 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.test.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.test.ts @@ -8,7 +8,7 @@ import { coreMock } from '@kbn/core/server/mocks'; import { telemetryEvents } from './events/telemetry_events'; import { TelemetryService } from './telemetry_service'; -import { TelemetryEventTypes } from './constants'; +import { AlertsEventTypes } from './types'; describe('TelemetryService', () => { let service: TelemetryService; @@ -41,17 +41,12 @@ describe('TelemetryService', () => { }); describe('#start()', () => { - it('should return all the available tracking methods', () => { + it('should return the tracking method', () => { const setupParams = getSetupParams(); service.setup(setupParams); const telemetry = service.start(); - expect(telemetry).toHaveProperty('reportAlertsGroupingChanged'); - expect(telemetry).toHaveProperty('reportAlertsGroupingToggled'); - expect(telemetry).toHaveProperty('reportAlertsGroupingTakeAction'); - - expect(telemetry).toHaveProperty('reportDetailsFlyoutOpened'); - expect(telemetry).toHaveProperty('reportDetailsFlyoutTabClicked'); + expect(telemetry).toHaveProperty('reportEvent'); }); }); @@ -61,7 +56,7 @@ describe('TelemetryService', () => { service.setup(setupParams); const telemetry = service.start(); - telemetry.reportAlertsGroupingTakeAction({ + telemetry.reportEvent(AlertsEventTypes.AlertsGroupingTakeAction, { tableId: 'test-groupingId', groupNumber: 0, status: 'closed', @@ -70,7 +65,7 @@ describe('TelemetryService', () => { expect(setupParams.analytics.reportEvent).toHaveBeenCalledTimes(1); expect(setupParams.analytics.reportEvent).toHaveBeenCalledWith( - TelemetryEventTypes.AlertsGroupingTakeAction, + AlertsEventTypes.AlertsGroupingTakeAction, { tableId: 'test-groupingId', groupNumber: 0, diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.ts index d4c100d5fe407..a1bf49394af9e 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.ts @@ -8,13 +8,18 @@ import type { AnalyticsServiceSetup } from '@kbn/core-analytics-server'; import { of } from 'rxjs'; import type { + TelemetryEventTypeData, + TelemetryEventTypes, TelemetryServiceSetupParams, - TelemetryClientStart, - TelemetryEventParams, } from './types'; import { telemetryEvents } from './events/telemetry_events'; -import { TelemetryClient } from './telemetry_client'; +export interface TelemetryServiceStart { + reportEvent: ( + eventType: T, + eventData: TelemetryEventTypeData + ) => void; +} /** * Service that interacts with the Core's analytics module * to trigger custom event for Security Solution plugin features @@ -41,17 +46,19 @@ export class TelemetryService { }); } telemetryEvents.forEach((eventConfig) => - analytics.registerEventType(eventConfig) + analytics.registerEventType>(eventConfig) ); } - public start(): TelemetryClientStart { - if (!this.analytics) { + public start(): TelemetryServiceStart { + const reportEvent = this.analytics?.reportEvent.bind(this.analytics); + + if (!this.analytics || !reportEvent) { throw new Error( 'The TelemetryService.setup() method has not been invoked, be sure to call it during the plugin setup.' ); } - return new TelemetryClient(this.analytics); + return { reportEvent }; } } diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts index 95896bf74a6a7..9cd56ebcb60f4 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts @@ -5,77 +5,41 @@ * 2.0. */ -import type { AnalyticsServiceSetup, RootSchema } from '@kbn/core/public'; -import type { SecurityCellActionMetadata } from '../../../app/actions/types'; -import type { ML_JOB_TELEMETRY_STATUS, TelemetryEventTypes } from './constants'; +import type { AnalyticsServiceSetup } from '@kbn/core/public'; import type { - AlertsGroupingTelemetryEvent, - ReportAlertsGroupingChangedParams, - ReportAlertsGroupingTelemetryEventParams, - ReportAlertsGroupingToggledParams, - ReportAlertsTakeActionParams, + AlertsEventTypes, + AlertsGroupingTelemetryEventsMap, } from './events/alerts_grouping/types'; import type { - ReportDataQualityCheckAllCompletedParams, - ReportDataQualityIndexCheckedParams, - DataQualityTelemetryEvents, + DataQualityEventTypes, + DataQualityTelemetryEventsMap, } from './events/data_quality/types'; import type { - EntityAnalyticsTelemetryEvent, - ReportAddRiskInputToTimelineClickedParams, - ReportEntityAlertsClickedParams, - ReportEntityAnalyticsTelemetryEventParams, - ReportEntityDetailsClickedParams, - ReportEntityRiskFilteredParams, - ReportRiskInputsExpandedFlyoutOpenedParams, - ReportToggleRiskSummaryClickedParams, - ReportAssetCriticalityCsvPreviewGeneratedParams, - ReportAssetCriticalityFileSelectedParams, - ReportAssetCriticalityCsvImportedParams, - ReportEntityStoreEnablementParams, - ReportEntityStoreInitParams, + EntityAnalyticsTelemetryEventsMap, + EntityEventTypes, } from './events/entity_analytics/types'; +import type { AssistantEventTypes, AssistantTelemetryEventsMap } from './events/ai_assistant/types'; import type { - AssistantTelemetryEvent, - ReportAssistantTelemetryEventParams, - ReportAssistantInvokedParams, - ReportAssistantQuickPromptParams, - ReportAssistantMessageSentParams, - ReportAssistantSettingToggledParams, -} from './events/ai_assistant/types'; -import type { - DocumentDetailsTelemetryEvents, - ReportDocumentDetailsTelemetryEventParams, - ReportDetailsFlyoutOpenedParams, - ReportDetailsFlyoutTabClickedParams, + DocumentDetailsTelemetryEventsMap, + DocumentEventTypes, } from './events/document_details/types'; import type { - OnboardingHubStepFinishedParams, - OnboardingHubStepLinkClickedParams, - OnboardingHubStepOpenParams, - OnboardingHubTelemetryEvent, + OnboardingHubEventTypes, + OnboardingHubTelemetryEventsMap, } from './events/onboarding/types'; import type { - ManualRuleRunTelemetryEvent, - ReportManualRuleRunOpenModalParams, - ReportManualRuleRunExecuteParams, - ReportManualRuleRunCancelJobParams, - ReportManualRuleRunTelemetryEventParams, + ManualRuleRunEventTypes, + ManualRuleRunTelemetryEventsMap, } from './events/manual_rule_run/types'; +import type { EventLogEventTypes, EventLogTelemetryEventsMap } from './events/event_log/types'; +import type { NotesEventTypes, NotesTelemetryEventsMap } from './events/notes/types'; import type { - EventLogTelemetryEvent, - ReportEventLogFilterByRunTypeParams, - ReportEventLogShowSourceEventDateRangeParams, - ReportEventLogTelemetryEventParams, -} from './events/event_log/types'; -import type { - AddNoteFromExpandableFlyoutClickedParams, - NotesTelemetryEventParams, - NotesTelemetryEvents, - OpenNoteInExpandableFlyoutClickedParams, -} from './events/notes/types'; -import type { PreviewRuleParams, PreviewRuleTelemetryEvent } from './events/preview_rule/types'; + PreviewRuleEventTypes, + PreviewRuleTelemetryEventsMap, +} from './events/preview_rule/types'; +import type { AppEventTypes, AppTelemetryEventsMap } from './events/app/types'; +export * from './events/app/types'; export * from './events/ai_assistant/types'; export * from './events/alerts_grouping/types'; export * from './events/data_quality/types'; @@ -85,142 +49,46 @@ export * from './events/document_details/types'; export * from './events/manual_rule_run/types'; export * from './events/event_log/types'; export * from './events/preview_rule/types'; +export * from './events/notes/types'; export interface TelemetryServiceSetupParams { analytics: AnalyticsServiceSetup; } -export interface ReportMLJobUpdateParams { - jobId: string; - isElasticJob: boolean; - status: ML_JOB_TELEMETRY_STATUS; - moduleId?: string; - errorMessage?: string; -} - -export interface ReportCellActionClickedParams { - metadata: SecurityCellActionMetadata | undefined; - displayName: string; - actionId: string; - fieldName: string; -} - -export interface ReportAnomaliesCountClickedParams { - jobId: string; - count: number; -} - -export interface ReportBreadcrumbClickedParams { - title: string; -} - -export type TelemetryEventParams = - | ReportAlertsGroupingTelemetryEventParams - | ReportAssistantTelemetryEventParams - | ReportEntityAnalyticsTelemetryEventParams - | ReportMLJobUpdateParams - | ReportCellActionClickedParams - | ReportAnomaliesCountClickedParams - | ReportDataQualityIndexCheckedParams - | ReportDataQualityCheckAllCompletedParams - | ReportBreadcrumbClickedParams - | ReportDocumentDetailsTelemetryEventParams - | OnboardingHubStepOpenParams - | OnboardingHubStepFinishedParams - | OnboardingHubStepLinkClickedParams - | ReportManualRuleRunTelemetryEventParams - | ReportEventLogTelemetryEventParams - | PreviewRuleParams - | NotesTelemetryEventParams; - -export interface TelemetryClientStart { - reportAlertsGroupingChanged(params: ReportAlertsGroupingChangedParams): void; - reportAlertsGroupingToggled(params: ReportAlertsGroupingToggledParams): void; - reportAlertsGroupingTakeAction(params: ReportAlertsTakeActionParams): void; - - // Assistant - reportAssistantInvoked(params: ReportAssistantInvokedParams): void; - reportAssistantMessageSent(params: ReportAssistantMessageSentParams): void; - reportAssistantQuickPrompt(params: ReportAssistantQuickPromptParams): void; - reportAssistantSettingToggled(params: ReportAssistantSettingToggledParams): void; - - // Entity Analytics - reportEntityDetailsClicked(params: ReportEntityDetailsClickedParams): void; - reportEntityAlertsClicked(params: ReportEntityAlertsClickedParams): void; - reportEntityRiskFiltered(params: ReportEntityRiskFilteredParams): void; - reportMLJobUpdate(params: ReportMLJobUpdateParams): void; - // Entity Analytics inside Entity Flyout - reportToggleRiskSummaryClicked(params: ReportToggleRiskSummaryClickedParams): void; - reportRiskInputsExpandedFlyoutOpened(params: ReportRiskInputsExpandedFlyoutOpenedParams): void; - reportAddRiskInputToTimelineClicked(params: ReportAddRiskInputToTimelineClickedParams): void; - // Entity Analytics Asset Criticality - reportAssetCriticalityFileSelected(params: ReportAssetCriticalityFileSelectedParams): void; - reportAssetCriticalityCsvPreviewGenerated( - params: ReportAssetCriticalityCsvPreviewGeneratedParams - ): void; - reportAssetCriticalityCsvImported(params: ReportAssetCriticalityCsvImportedParams): void; - reportCellActionClicked(params: ReportCellActionClickedParams): void; - // Entity Analytics Entity Store - reportEntityStoreEnablement(params: ReportEntityStoreEnablementParams): void; - reportEntityStoreInit(params: ReportEntityStoreInitParams): void; - - reportAnomaliesCountClicked(params: ReportAnomaliesCountClickedParams): void; - reportDataQualityIndexChecked(params: ReportDataQualityIndexCheckedParams): void; - reportDataQualityCheckAllCompleted(params: ReportDataQualityCheckAllCompletedParams): void; - reportBreadcrumbClicked(params: ReportBreadcrumbClickedParams): void; - - // document details flyout - reportDetailsFlyoutOpened(params: ReportDetailsFlyoutOpenedParams): void; - reportDetailsFlyoutTabClicked(params: ReportDetailsFlyoutTabClickedParams): void; - - // onboarding hub - reportOnboardingHubStepOpen(params: OnboardingHubStepOpenParams): void; - reportOnboardingHubStepFinished(params: OnboardingHubStepFinishedParams): void; - reportOnboardingHubStepLinkClicked(params: OnboardingHubStepLinkClickedParams): void; - - // manual rule run - reportManualRuleRunOpenModal(params: ReportManualRuleRunOpenModalParams): void; - reportManualRuleRunExecute(params: ReportManualRuleRunExecuteParams): void; - reportManualRuleRunCancelJob(params: ReportManualRuleRunCancelJobParams): void; - - // event log - reportEventLogFilterByRunType(params: ReportEventLogFilterByRunTypeParams): void; - reportEventLogShowSourceEventDateRange( - params: ReportEventLogShowSourceEventDateRangeParams - ): void; - - // new notes - reportOpenNoteInExpandableFlyoutClicked(params: OpenNoteInExpandableFlyoutClickedParams): void; - reportAddNoteFromExpandableFlyoutClicked(params: AddNoteFromExpandableFlyoutClickedParams): void; - - // preview rule - reportPreviewRule(params: PreviewRuleParams): void; -} - -export type TelemetryEvent = - | AssistantTelemetryEvent - | AlertsGroupingTelemetryEvent - | EntityAnalyticsTelemetryEvent - | DataQualityTelemetryEvents - | DocumentDetailsTelemetryEvents - | { - eventType: TelemetryEventTypes.MLJobUpdate; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.CellActionClicked; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.AnomaliesCountClicked; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.BreadcrumbClicked; - schema: RootSchema; - } - | OnboardingHubTelemetryEvent - | ManualRuleRunTelemetryEvent - | EventLogTelemetryEvent - | PreviewRuleTelemetryEvent - | NotesTelemetryEvents; +// Combine all event type data +export type TelemetryEventTypeData = T extends AssistantEventTypes + ? AssistantTelemetryEventsMap[T] + : T extends AlertsEventTypes + ? AlertsGroupingTelemetryEventsMap[T] + : T extends PreviewRuleEventTypes + ? PreviewRuleTelemetryEventsMap[T] + : T extends EntityEventTypes + ? EntityAnalyticsTelemetryEventsMap[T] + : T extends DataQualityEventTypes + ? DataQualityTelemetryEventsMap[T] + : T extends DocumentEventTypes + ? DocumentDetailsTelemetryEventsMap[T] + : T extends OnboardingHubEventTypes + ? OnboardingHubTelemetryEventsMap[T] + : T extends ManualRuleRunEventTypes + ? ManualRuleRunTelemetryEventsMap[T] + : T extends EventLogEventTypes + ? EventLogTelemetryEventsMap[T] + : T extends NotesEventTypes + ? NotesTelemetryEventsMap[T] + : T extends AppEventTypes + ? AppTelemetryEventsMap[T] + : never; + +export type TelemetryEventTypes = + | AssistantEventTypes + | AlertsEventTypes + | PreviewRuleEventTypes + | EntityEventTypes + | DataQualityEventTypes + | DocumentEventTypes + | OnboardingHubEventTypes + | ManualRuleRunEventTypes + | EventLogEventTypes + | NotesEventTypes + | AppEventTypes; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts index 018e2602aa170..79257b4fef3da 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts @@ -18,6 +18,7 @@ import { transformOutput } from '../../../../detections/containers/detection_eng import type { TimeframePreviewOptions } from '../../../../detections/pages/detection_engine/rules/types'; import { usePreviewInvocationCount } from './use_preview_invocation_count'; import * as i18n from './translations'; +import { PreviewRuleEventTypes } from '../../../../common/lib/telemetry'; const emptyPreviewRule: RulePreviewResponse = { previewId: undefined, @@ -58,7 +59,7 @@ export const usePreviewRule = ({ const createPreviewId = async () => { if (rule != null) { try { - telemetry.reportPreviewRule({ + telemetry.reportEvent(PreviewRuleEventTypes.PreviewRule, { loggedRequestsEnabled: enableLoggedRequests ?? false, ruleType: rule.type, }); 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 cc8f2abda9c4e..9bcf35fdb13ca 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 @@ -202,7 +202,9 @@ const onOpenTimeline = jest.fn(); const COMBO_BOX_TOGGLE_BUTTON_TEST_ID = 'comboBoxToggleListButton'; const VERSION_INPUT_TEST_ID = 'relatedIntegrationVersionDependency'; -describe('StepDefineRule', () => { +// Failing: See https://github.com/elastic/kibana/issues/199648 +// Failing: See https://github.com/elastic/kibana/issues/199700 +describe.skip('StepDefineRule', () => { beforeEach(() => { jest.clearAllMocks(); mockUseRuleFromTimeline.mockReturnValue({ onOpenTimeline, loading: false }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/execution_log_table.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/execution_log_table.test.tsx index 4ed02135143ff..9a9cb57713359 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/execution_log_table.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/execution_log_table.test.tsx @@ -32,7 +32,7 @@ jest.mock('../../../../../common/hooks/use_experimental_features', () => { }); const mockTelemetry = { - reportEventLogShowSourceEventDateRange: jest.fn(), + reportEvent: jest.fn(), }; const mockedUseKibana = { @@ -91,6 +91,6 @@ describe('ExecutionLogTable', () => { fireEvent.click(switchButton); - expect(mockTelemetry.reportEventLogShowSourceEventDateRange).toHaveBeenCalled(); + expect(mockTelemetry.reportEvent).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/execution_log_table.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/execution_log_table.tsx index 4546e55522ce5..296323213a6db 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/execution_log_table.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/execution_log_table.tsx @@ -85,6 +85,7 @@ import { getSourceEventTimeRangeColumns, } from './execution_log_columns'; import { ExecutionLogSearchBar } from './execution_log_search_bar'; +import { EventLogEventTypes } from '../../../../../common/lib/telemetry'; const EXECUTION_UUID_FIELD_NAME = 'kibana.alert.rule.execution.uuid'; @@ -470,7 +471,7 @@ const ExecutionLogTableComponent: React.FC = ({ (e: EuiSwitchEvent) => { const isVisible = e.target.checked; onShowSourceEventTimeRange(isVisible); - telemetry.reportEventLogShowSourceEventDateRange({ + telemetry.reportEvent(EventLogEventTypes.EventLogShowSourceEventDateRange, { isVisible, }); }, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/components/rule_backfills_info/stop_backfill.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/components/rule_backfills_info/stop_backfill.test.tsx index b2cdd83d6f43f..faf19fd5dd24e 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/components/rule_backfills_info/stop_backfill.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/components/rule_backfills_info/stop_backfill.test.tsx @@ -14,6 +14,7 @@ import { TestProviders } from '../../../../common/mock'; import { useKibana } from '../../../../common/lib/kibana'; import * as i18n from '../../translations'; import type { BackfillRow } from '../../types'; +import { ManualRuleRunEventTypes } from '../../../../common/lib/telemetry'; jest.mock('../../../../common/hooks/use_app_toasts'); jest.mock('../../api/hooks/use_delete_backfill'); @@ -25,7 +26,7 @@ const mockUseKibana = useKibana as jest.Mock; describe('StopBackfill', () => { const mockTelemetry = { - reportManualRuleRunCancelJob: jest.fn(), + reportEvent: jest.fn(), }; const addSuccess = jest.fn(); @@ -90,11 +91,14 @@ describe('StopBackfill', () => { fireEvent.click(getByTestId('confirmModalConfirmButton')); await waitFor(() => { - expect(mockTelemetry.reportManualRuleRunCancelJob).toHaveBeenCalledWith({ - totalTasks: backfill.total, - completedTasks: backfill.complete, - errorTasks: backfill.error, - }); + expect(mockTelemetry.reportEvent).toHaveBeenCalledWith( + ManualRuleRunEventTypes.ManualRuleRunCancelJob, + { + totalTasks: backfill.total, + completedTasks: backfill.complete, + errorTasks: backfill.error, + } + ); }); expect(addSuccess).toHaveBeenCalledWith(i18n.BACKFILLS_TABLE_STOP_CONFIRMATION_SUCCESS); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/components/rule_backfills_info/stop_backfill.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/components/rule_backfills_info/stop_backfill.tsx index 84acf0b014d60..51d09dc323d89 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/components/rule_backfills_info/stop_backfill.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/components/rule_backfills_info/stop_backfill.tsx @@ -12,6 +12,7 @@ import { useDeleteBackfill } from '../../api/hooks/use_delete_backfill'; import * as i18n from '../../translations'; import type { BackfillRow } from '../../types'; import { useKibana } from '../../../../common/lib/kibana'; +import { ManualRuleRunEventTypes } from '../../../../common/lib/telemetry'; export const StopBackfill = ({ backfill }: { backfill: BackfillRow }) => { const { telemetry } = useKibana().services; @@ -19,7 +20,7 @@ export const StopBackfill = ({ backfill }: { backfill: BackfillRow }) => { const deleteBackfillMutation = useDeleteBackfill({ onSuccess: () => { closeModal(); - telemetry.reportManualRuleRunCancelJob({ + telemetry.reportEvent(ManualRuleRunEventTypes.ManualRuleRunCancelJob, { totalTasks: backfill.total, completedTasks: backfill.complete, errorTasks: backfill.error, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/logic/use_schedule_rule_run.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/logic/use_schedule_rule_run.test.tsx index 36bdf8a8bf821..ce70bc08bd722 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/logic/use_schedule_rule_run.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/logic/use_schedule_rule_run.test.tsx @@ -11,6 +11,7 @@ import { useKibana } from '../../../common/lib/kibana'; import { useKibana as mockUseKibana } from '../../../common/lib/kibana/__mocks__'; import { TestProviders } from '../../../common/mock'; import { useScheduleRuleRun } from './use_schedule_rule_run'; +import { ManualRuleRunEventTypes } from '../../../common/lib/telemetry'; const mockUseScheduleRuleRunMutation = jest.fn(); @@ -28,7 +29,7 @@ const mockedUseKibana = { services: { ...mockUseKibana().services, telemetry: { - reportManualRuleRunExecute: jest.fn(), + reportEvent: jest.fn(), }, }, }; @@ -61,7 +62,7 @@ describe('When using the `useScheduleRuleRun()` hook', () => { ); }); - it('should call reportManualRuleRunExecute with success status on success', async () => { + it('should call reportEvent with success status on success', async () => { const { result, waitFor } = renderHook(() => useScheduleRuleRun(), { wrapper: TestProviders, }); @@ -77,14 +78,17 @@ describe('When using the `useScheduleRuleRun()` hook', () => { return mockUseScheduleRuleRunMutation.mock.calls.length > 0; }); - expect(mockedUseKibana.services.telemetry.reportManualRuleRunExecute).toHaveBeenCalledWith({ - rangeInMs: timeRange.endDate.diff(timeRange.startDate), - status: 'success', - rulesCount: 1, - }); + expect(mockedUseKibana.services.telemetry.reportEvent).toHaveBeenCalledWith( + ManualRuleRunEventTypes.ManualRuleRunExecute, + { + rangeInMs: timeRange.endDate.diff(timeRange.startDate), + status: 'success', + rulesCount: 1, + } + ); }); - it('should call reportManualRuleRunExecute with error status on failure', async () => { + it('should call reportEvent with error status on failure', async () => { const { result, waitFor } = renderHook(() => useScheduleRuleRun(), { wrapper: TestProviders, }); @@ -100,10 +104,13 @@ describe('When using the `useScheduleRuleRun()` hook', () => { return mockUseScheduleRuleRunMutation.mock.calls.length > 0; }); - expect(mockedUseKibana.services.telemetry.reportManualRuleRunExecute).toHaveBeenCalledWith({ - rangeInMs: timeRange.endDate.diff(timeRange.startDate), - status: 'error', - rulesCount: 1, - }); + expect(mockedUseKibana.services.telemetry.reportEvent).toHaveBeenCalledWith( + ManualRuleRunEventTypes.ManualRuleRunExecute, + { + rangeInMs: timeRange.endDate.diff(timeRange.startDate), + status: 'error', + rulesCount: 1, + } + ); }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/logic/use_schedule_rule_run.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/logic/use_schedule_rule_run.ts index 7599d8685d3c0..94f85a30b3ceb 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/logic/use_schedule_rule_run.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/logic/use_schedule_rule_run.ts @@ -12,6 +12,7 @@ import { useScheduleRuleRunMutation } from '../api/hooks/use_schedule_rule_run_m import type { ScheduleBackfillProps } from '../types'; import * as i18n from '../translations'; +import { ManualRuleRunEventTypes } from '../../../common/lib/telemetry'; export function useScheduleRuleRun() { const { mutateAsync } = useScheduleRuleRunMutation(); @@ -22,7 +23,7 @@ export function useScheduleRuleRun() { async (options: ScheduleBackfillProps) => { try { const results = await mutateAsync(options); - telemetry.reportManualRuleRunExecute({ + telemetry.reportEvent(ManualRuleRunEventTypes.ManualRuleRunExecute, { rangeInMs: options.timeRange.endDate.diff(options.timeRange.startDate), status: 'success', rulesCount: options.ruleIds.length, @@ -31,7 +32,7 @@ export function useScheduleRuleRun() { return results; } catch (error) { addError(error, { title: i18n.BACKFILL_SCHEDULE_ERROR_TITLE }); - telemetry.reportManualRuleRunExecute({ + telemetry.reportEvent(ManualRuleRunEventTypes.ManualRuleRunExecute, { rangeInMs: options.timeRange.endDate.diff(options.timeRange.startDate), status: 'error', rulesCount: options.ruleIds.length, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx index 68e58b4db073f..22f10605b3685 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx @@ -45,6 +45,7 @@ import type { ExecuteBulkActionsDryRun } from './use_bulk_actions_dry_run'; import { computeDryRunEditPayload } from './utils/compute_dry_run_edit_payload'; import { transformExportDetailsToDryRunResult } from './utils/dry_run_result'; import { prepareSearchParams } from './utils/prepare_search_params'; +import { ManualRuleRunEventTypes } from '../../../../../common/lib/telemetry'; interface UseBulkActionsArgs { filterOptions: FilterOptions; @@ -234,7 +235,7 @@ export const useBulkActions = ({ } const modalManualRuleRunConfirmationResult = await showManualRuleRunConfirmation(); - startServices.telemetry.reportManualRuleRunOpenModal({ + startServices.telemetry.reportEvent(ManualRuleRunEventTypes.ManualRuleRunOpenModal, { type: 'bulk', }); if (modalManualRuleRunConfirmationResult === null) { @@ -252,7 +253,7 @@ export const useBulkActions = ({ }, }); - startServices.telemetry.reportManualRuleRunExecute({ + startServices.telemetry.reportEvent(ManualRuleRunEventTypes.ManualRuleRunExecute, { rangeInMs: modalManualRuleRunConfirmationResult.endDate.diff( modalManualRuleRunConfirmationResult.startDate ), diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rules_table_actions.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rules_table_actions.tsx index 4cc7a03426657..f3d0930d7c1fe 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rules_table_actions.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rules_table_actions.tsx @@ -25,6 +25,7 @@ import { useDownloadExportedRules } from '../../../rule_management/logic/bulk_ac import { useHasActionsPrivileges } from './use_has_actions_privileges'; import type { TimeRange } from '../../../rule_gaps/types'; import { useScheduleRuleRun } from '../../../rule_gaps/logic/use_schedule_rule_run'; +import { ManualRuleRunEventTypes } from '../../../../common/lib/telemetry'; export const useRulesTableActions = ({ showExceptionsDuplicateConfirmation, @@ -126,7 +127,7 @@ export const useRulesTableActions = ({ onClick: async (rule: Rule) => { startTransaction({ name: SINGLE_RULE_ACTIONS.MANUAL_RULE_RUN }); const modalManualRuleRunConfirmationResult = await showManualRuleRunConfirmation(); - telemetry.reportManualRuleRunOpenModal({ + telemetry.reportEvent(ManualRuleRunEventTypes.ManualRuleRunOpenModal, { type: 'single', }); if (modalManualRuleRunConfirmationResult === null) { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/execution_run_type_filter/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/execution_run_type_filter/index.test.tsx index 50c35e7a6e529..a11246b4f52ba 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/execution_run_type_filter/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/execution_run_type_filter/index.test.tsx @@ -10,11 +10,12 @@ import { render, screen, fireEvent } from '@testing-library/react'; import { ExecutionRunTypeFilter } from '.'; import { RuleRunTypeEnum } from '../../../../../../../common/api/detection_engine/rule_monitoring'; import { useKibana } from '../../../../../../common/lib/kibana'; +import { EventLogEventTypes } from '../../../../../../common/lib/telemetry'; jest.mock('../../../../../../common/lib/kibana'); const mockTelemetry = { - reportEventLogFilterByRunType: jest.fn(), + reportEvent: jest.fn(), }; const mockUseKibana = useKibana as jest.Mock; @@ -28,7 +29,7 @@ mockUseKibana.mockReturnValue({ const items = [RuleRunTypeEnum.backfill, RuleRunTypeEnum.standard]; describe('ExecutionRunTypeFilter', () => { - it('calls telemetry.reportEventLogFilterByRunType on selection change', () => { + it('calls telemetry.reportEvent on selection change', () => { const handleChange = jest.fn(); render(); @@ -40,8 +41,11 @@ describe('ExecutionRunTypeFilter', () => { fireEvent.click(manualRun); expect(handleChange).toHaveBeenCalledWith([RuleRunTypeEnum.backfill]); - expect(mockTelemetry.reportEventLogFilterByRunType).toHaveBeenCalledWith({ - runType: [RuleRunTypeEnum.backfill], - }); + expect(mockTelemetry.reportEvent).toHaveBeenCalledWith( + EventLogEventTypes.EventLogFilterByRunType, + { + runType: [RuleRunTypeEnum.backfill], + } + ); }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/execution_run_type_filter/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/execution_run_type_filter/index.tsx index 9f144410a7590..4e1c44517c058 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/execution_run_type_filter/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/execution_run_type_filter/index.tsx @@ -15,6 +15,7 @@ import { RULE_EXECUTION_TYPE_STANDARD, } from '../../../../../../common/translations'; import { useKibana } from '../../../../../../common/lib/kibana'; +import { EventLogEventTypes } from '../../../../../../common/lib/telemetry'; interface ExecutionRunTypeFilterProps { items: RuleRunType[]; @@ -42,7 +43,9 @@ const ExecutionRunTypeFilterComponent: React.FC = ( const handleSelectionChange = useCallback( (types: RuleRunType[]) => { onChange(types); - telemetry.reportEventLogFilterByRunType({ runType: types }); + telemetry.reportEvent(EventLogEventTypes.EventLogFilterByRunType, { + runType: types, + }); }, [onChange, telemetry] ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.test.tsx index cf57c9d59b080..5392d730c9e0d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.test.tsx @@ -20,6 +20,7 @@ import { useKibana as mockUseKibana } from '../../../common/lib/kibana/__mocks__ import { createTelemetryServiceMock } from '../../../common/lib/telemetry/telemetry_service.mock'; import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_query'; import { getQuery, groupingSearchResponse } from './grouping_settings/mock'; +import { AlertsEventTypes } from '../../../common/lib/telemetry'; jest.mock('../../containers/detection_engine/alerts/use_query'); jest.mock('../../../sourcerer/containers'); @@ -553,17 +554,23 @@ describe('GroupedAlertsTable', () => { fireEvent.click(getByTestId('group-selector-dropdown')); fireEvent.click(getByTestId('panel-user.name')); - expect(mockedTelemetry.reportAlertsGroupingChanged).toHaveBeenCalledWith({ - groupByField: 'user.name', - tableId: testProps.tableId, - }); + expect(mockedTelemetry.reportEvent).toHaveBeenCalledWith( + AlertsEventTypes.AlertsGroupingChanged, + { + groupByField: 'user.name', + tableId: testProps.tableId, + } + ); fireEvent.click(getByTestId('group-selector-dropdown')); fireEvent.click(getByTestId('panel-host.name')); - expect(mockedTelemetry.reportAlertsGroupingChanged).toHaveBeenCalledWith({ - groupByField: 'host.name', - tableId: testProps.tableId, - }); + expect(mockedTelemetry.reportEvent).toHaveBeenCalledWith( + AlertsEventTypes.AlertsGroupingChanged, + { + groupByField: 'host.name', + tableId: testProps.tableId, + } + ); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx index a1cbdc8004727..c4dd142fc71b3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx @@ -25,7 +25,7 @@ import type { RunTimeMappings } from '../../../sourcerer/store/model'; import { renderGroupPanel, getStats } from './grouping_settings'; import { useKibana } from '../../../common/lib/kibana'; import { GroupedSubLevel } from './alerts_sub_grouping'; -import { track } from '../../../common/lib/telemetry'; +import { AlertsEventTypes, track } from '../../../common/lib/telemetry'; export interface AlertsTableComponentProps { currentAlertStatusFilterValue?: Status[]; @@ -80,14 +80,18 @@ const GroupedAlertsTableComponent: React.FC = (props) const { onGroupChange, onGroupToggle } = useMemo( () => ({ onGroupChange: ({ groupByField, tableId }: { groupByField: string; tableId: string }) => { - telemetry.reportAlertsGroupingChanged({ groupByField, tableId }); + telemetry.reportEvent(AlertsEventTypes.AlertsGroupingChanged, { groupByField, tableId }); }, onGroupToggle: (param: { isOpen: boolean; groupName?: string | undefined; groupNumber: number; groupingId: string; - }) => telemetry.reportAlertsGroupingToggled({ ...param, tableId: param.groupingId }), + }) => + telemetry.reportEvent(AlertsEventTypes.AlertsGroupingToggled, { + ...param, + tableId: param.groupingId, + }), }), [telemetry] ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx index 6a7b11ee0e192..e5e753b1c7763 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx @@ -28,7 +28,7 @@ import { import { FILTER_ACKNOWLEDGED, FILTER_CLOSED, FILTER_OPEN } from '../../../../../common/types'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import * as i18n from '../translations'; -import { getTelemetryEvent, METRIC_TYPE, track } from '../../../../common/lib/telemetry'; +import { AlertsEventTypes, METRIC_TYPE, track } from '../../../../common/lib/telemetry'; import type { StartServices } from '../../../../types'; export interface TakeActionsProps { @@ -36,6 +36,18 @@ export interface TakeActionsProps { showAlertStatusActions?: boolean; } +const getTelemetryEvent = { + groupedAlertsTakeAction: ({ + tableId, + groupNumber, + status, + }: { + tableId: string; + groupNumber: number; + status: AlertWorkflowStatus; + }) => `alerts_table_${tableId}_group-${groupNumber}_mark-${status}`, +}; + export const useGroupTakeActionsItems = ({ currentStatus, showAlertStatusActions = true, @@ -58,7 +70,7 @@ export const useGroupTakeActionsItems = ({ status: 'open' | 'closed' | 'acknowledged'; groupByField: string; }) => { - telemetry.reportAlertsGroupingTakeAction(params); + telemetry.reportEvent(AlertsEventTypes.AlertsGroupingTakeAction, params); }, [telemetry] ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx index e1ff950bc5e32..408a13fd1a9cf 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx @@ -16,6 +16,7 @@ import { RuleActionsOverflow } from '.'; import { mockRule } from '../../../../detection_engine/rule_management_ui/components/rules_table/__mocks__/mock'; import { TestProviders } from '../../../../common/mock'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { ManualRuleRunEventTypes } from '../../../../common/lib/telemetry'; const showBulkDuplicateExceptionsConfirmation = () => Promise.resolve(null); const showManualRuleRunConfirmation = () => Promise.resolve(null); @@ -28,7 +29,7 @@ jest.mock('../../../../detection_engine/rule_management/logic/bulk_actions/use_b jest.mock('../../../../detection_engine/rule_gaps/logic/use_schedule_rule_run'); jest.mock('../../../../common/lib/apm/use_start_transaction'); jest.mock('../../../../common/hooks/use_app_toasts'); -const mockReportManualRuleRunOpenModal = jest.fn(); +const mockReportEvent = jest.fn(); jest.mock('../../../../common/lib/kibana', () => { const actual = jest.requireActual('../../../../common/lib/kibana'); return { @@ -36,8 +37,8 @@ jest.mock('../../../../common/lib/kibana', () => { useKibana: jest.fn().mockReturnValue({ services: { telemetry: { - reportManualRuleRunOpenModal: (params: { type: 'single' | 'bulk' }) => - mockReportManualRuleRunOpenModal(params), + reportEvent: (eventType: ManualRuleRunEventTypes, params: { type: 'single' | 'bulk' }) => + mockReportEvent(eventType, params), }, application: { navigateToApp: jest.fn(), @@ -274,7 +275,7 @@ describe('RuleActionsOverflow', () => { expect(getByTestId('rules-details-popover')).not.toHaveTextContent(/.+/); }); - test('it calls telemetry.reportManualRuleRunOpenModal when rules-details-manual-rule-run is clicked', async () => { + test('it calls telemetry.reportEvent when rules-details-manual-rule-run is clicked', async () => { const { getByTestId } = render( { fireEvent.click(getByTestId('rules-details-manual-rule-run')); await waitFor(() => { - expect(mockReportManualRuleRunOpenModal).toHaveBeenCalledWith({ - type: 'single', - }); + expect(mockReportEvent).toHaveBeenCalledWith( + ManualRuleRunEventTypes.ManualRuleRunOpenModal, + { + type: 'single', + } + ); }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx index 68defd759938f..a786b95979d43 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx @@ -34,6 +34,7 @@ import { import { useDownloadExportedRules } from '../../../../detection_engine/rule_management/logic/bulk_actions/use_download_exported_rules'; import * as i18nActions from '../../../pages/detection_engine/rules/translations'; import * as i18n from './translations'; +import { ManualRuleRunEventTypes } from '../../../../common/lib/telemetry'; const MyEuiButtonIcon = styled(EuiButtonIcon)` &.euiButtonIcon { @@ -161,7 +162,7 @@ const RuleActionsOverflowComponent = ({ startTransaction({ name: SINGLE_RULE_ACTIONS.MANUAL_RULE_RUN }); closePopover(); const modalManualRuleRunConfirmationResult = await showManualRuleRunConfirmation(); - telemetry.reportManualRuleRunOpenModal({ + telemetry.reportEvent(ManualRuleRunEventTypes.ManualRuleRunOpenModal, { type: 'single', }); if (modalManualRuleRunConfirmationResult === null) { diff --git a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx index dbd47281c5f2a..484162cbf7d42 100644 --- a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx +++ b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx @@ -20,7 +20,7 @@ import { useSourcererDataView } from '../../../sourcerer/containers'; import { SourcererScopeName } from '../../../sourcerer/store/model'; import { updateGroups } from '../../../common/store/grouping/actions'; import { useKibana } from '../../../common/lib/kibana'; -import { METRIC_TYPE, track } from '../../../common/lib/telemetry'; +import { METRIC_TYPE, AlertsEventTypes, track } from '../../../common/lib/telemetry'; import { useDataTableFilters } from '../../../common/hooks/use_data_table_filters'; import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; import { RightTopMenu } from '../../../common/components/events_viewer/right_top_menu'; @@ -47,7 +47,10 @@ export const getPersistentControlsHook = (tableId: TableId) => { METRIC_TYPE.CLICK, getTelemetryEvent.groupChanged({ groupingId: tableId, selected: groupSelection }) ); - telemetry.reportAlertsGroupingChanged({ groupByField: groupSelection, tableId }); + telemetry.reportEvent(AlertsEventTypes.AlertsGroupingChanged, { + groupByField: groupSelection, + tableId, + }); }, [telemetry] ); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/asset_criticality_file_uploader.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/asset_criticality_file_uploader.tsx index 0acd1f831ca71..9c76c1e5f5082 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/asset_criticality_file_uploader.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/asset_criticality_file_uploader.tsx @@ -16,6 +16,7 @@ import { AssetCriticalityResultStep } from './components/result_step'; import { useEntityAnalyticsRoutes } from '../../api/api'; import { useFileValidation, useNavigationSteps } from './hooks'; import type { OnCompleteParams } from './types'; +import { EntityEventTypes } from '../../../common/lib/telemetry'; export const AssetCriticalityFileUploader: React.FC = () => { const [state, dispatch] = useReducer(reducer, INITIAL_STATE); @@ -24,7 +25,7 @@ export const AssetCriticalityFileUploader: React.FC = () => { const onValidationComplete = useCallback( ({ validatedFile, processingStartTime, processingEndTime, tookMs }: OnCompleteParams) => { - telemetry.reportAssetCriticalityCsvPreviewGenerated({ + telemetry.reportEvent(EntityEventTypes.AssetCriticalityCsvPreviewGenerated, { file: { size: validatedFile.size, }, diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/validation_step.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/validation_step.test.tsx index ca1dcfb6e7f42..f3200b5c6ee44 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/validation_step.test.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/validation_step.test.tsx @@ -20,7 +20,7 @@ jest.mock('../../../../common/lib/kibana/kibana_react', () => ({ useKibana: () => ({ services: { telemetry: { - reportAssetCriticalityCsvImported: jest.fn(), + reportEvent: jest.fn(), }, }, }), diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/validation_step.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/validation_step.tsx index 538e19f4d5fd6..c4dadc756c15b 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/validation_step.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/validation_step.tsx @@ -23,6 +23,7 @@ import { downloadBlob } from '../../../../common/utils/download_blob'; import { useKibana } from '../../../../common/lib/kibana/kibana_react'; import type { ValidatedFile } from '../types'; import { buildAnnotationsFromError } from '../helpers'; +import { EntityEventTypes } from '../../../../common/lib/telemetry'; export interface AssetCriticalityValidationStepProps { validatedFile: ValidatedFile; @@ -42,7 +43,7 @@ export const AssetCriticalityValidationStep: React.FC { - telemetry.reportAssetCriticalityCsvImported({ + telemetry.reportEvent(EntityEventTypes.AssetCriticalityCsvImported, { file: { size: fileSize, }, diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/hooks.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/hooks.ts index 107ba6348ac70..0472f5002fcb1 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/hooks.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/hooks.ts @@ -17,6 +17,7 @@ import { useKibana } from '../../../common/lib/kibana'; import type { OnCompleteParams } from './types'; import type { ReducerState } from './reducer'; import { getStepStatus, isValidationStep } from './helpers'; +import { EntityEventTypes } from '../../../common/lib/telemetry'; interface UseFileChangeCbParams { onError: (errorMessage: string, file: File) => void; @@ -35,7 +36,7 @@ export const useFileValidation = ({ onError, onComplete }: UseFileChangeCbParams }, file: File ) => { - telemetry.reportAssetCriticalityFileSelected({ + telemetry.reportEvent(EntityEventTypes.AssetCriticalityFileSelected, { valid: false, errorCode: error.code, file: { @@ -62,7 +63,7 @@ export const useFileValidation = ({ onError, onComplete }: UseFileChangeCbParams return; } - telemetry.reportAssetCriticalityFileSelected({ + telemetry.reportEvent(EntityEventTypes.AssetCriticalityFileSelected, { valid: true, file: { size: file.size, diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_analytics_anomalies/anomalies_count_link.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_analytics_anomalies/anomalies_count_link.test.tsx index 8400578b85c4f..32234547ca628 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_analytics_anomalies/anomalies_count_link.test.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_analytics_anomalies/anomalies_count_link.test.tsx @@ -10,6 +10,7 @@ import { AnomalyEntity } from '../../../common/components/ml/anomaly/use_anomali import { createTelemetryServiceMock } from '../../../common/lib/telemetry/telemetry_service.mock'; import { TestProviders } from '../../../common/mock'; import { AnomaliesCountLink } from './anomalies_count_link'; +import { EntityEventTypes } from '../../../common/lib/telemetry'; const mockedTelemetry = createTelemetryServiceMock(); jest.mock('../../../common/lib/kibana', () => { @@ -37,6 +38,9 @@ describe('AnomaliesCountLink', () => { fireEvent.click(getByRole('button')); - expect(mockedTelemetry.reportAnomaliesCountClicked).toHaveBeenLastCalledWith({ jobId, count }); + expect(mockedTelemetry.reportEvent).toHaveBeenLastCalledWith( + EntityEventTypes.AnomaliesCountClicked, + { jobId, count } + ); }); }); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_analytics_anomalies/anomalies_count_link.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_analytics_anomalies/anomalies_count_link.tsx index bb32564acb1b9..6068d0ece6676 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_analytics_anomalies/anomalies_count_link.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_analytics_anomalies/anomalies_count_link.tsx @@ -15,6 +15,7 @@ import { HostsType } from '../../../explore/hosts/store/model'; import { UsersType } from '../../../explore/users/store/model'; import { useKibana } from '../../../common/lib/kibana'; +import { EntityEventTypes } from '../../../common/lib/telemetry'; export const AnomaliesCountLink = ({ count, @@ -36,7 +37,7 @@ export const AnomaliesCountLink = ({ const onClick = useCallback(() => { if (!jobId) return; - telemetry.reportAnomaliesCountClicked({ + telemetry.reportEvent(EntityEventTypes.AnomaliesCountClicked, { jobId, count, }); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_analytics_risk_score/index.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_analytics_risk_score/index.tsx index ffa1afffdf21b..08ce79cc74c17 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_analytics_risk_score/index.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_analytics_risk_score/index.tsx @@ -38,6 +38,7 @@ import { useRiskScore } from '../../api/hooks/use_risk_score'; import { UserPanelKey } from '../../../flyout/entity_details/user_right'; import { RiskEnginePrivilegesCallOut } from '../risk_engine_privileges_callout'; import { useMissingRiskEnginePrivileges } from '../../hooks/use_missing_risk_engine_privileges'; +import { EntityEventTypes } from '../../../common/lib/telemetry'; export const ENTITY_RISK_SCORE_TABLE_ID = 'entity-risk-score-table'; @@ -51,7 +52,7 @@ const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskSc const openEntityOnAlertsPage = useCallback( (entityName: string) => { - telemetry.reportEntityAlertsClicked({ entity: riskEntity }); + telemetry.reportEvent(EntityEventTypes.EntityAlertsClicked, { entity: riskEntity }); openAlertsPageWithFilters([ { title: getRiskEntityTranslation(riskEntity), diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions.ts index b12f82128c824..a8a7f60e075ae 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions.ts @@ -17,6 +17,7 @@ import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { useAddBulkToTimelineAction } from '../../../../detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline'; import { useKibana } from '../../../../common/lib/kibana/kibana_react'; import type { InputAlert } from '../../../hooks/use_risk_contributing_alerts'; +import { EntityEventTypes } from '../../../../common/lib/telemetry'; /** * The returned actions only support alerts risk inputs. @@ -61,7 +62,7 @@ export const useRiskInputActions = (inputs: InputAlert[], closePopover: () => vo }, addToNewTimeline: () => { - telemetry.reportAddRiskInputToTimelineClicked({ + telemetry.reportEvent(EntityEventTypes.AddRiskInputToTimelineClicked, { quantity: inputs.length, }); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts index 21e73241451e5..8aefbe2b44af1 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts @@ -17,6 +17,7 @@ import type { } from '../../../../../common/api/entity_analytics'; import { useEntityStoreRoutes } from '../../../api/entity_store'; import { ENTITY_STORE_ENGINE_STATUS, useEntityEngineStatus } from './use_entity_engine_status'; +import { EntityEventTypes } from '../../../../common/lib/telemetry'; const ENTITY_STORE_ENABLEMENT_INIT = 'ENTITY_STORE_ENABLEMENT_INIT'; @@ -49,7 +50,7 @@ export const useEntityStoreEnablement = () => { }); const enable = useCallback(() => { - telemetry?.reportEntityStoreInit({ + telemetry?.reportEvent(EntityEventTypes.EntityStoreDashboardInitButtonClicked, { timestamp: new Date().toISOString(), }); return initialize().then(() => setPolling(true)); @@ -76,7 +77,7 @@ export const useInitEntityEngineMutation = (options?: UseMutationOptions<{}>) => const { initEntityStore } = useEntityStoreRoutes(); return useMutation( () => { - telemetry?.reportEntityStoreEnablement({ + telemetry?.reportEvent(EntityEventTypes.EntityStoreEnablementToggleClicked, { timestamp: new Date().toISOString(), action: 'start', }); @@ -106,7 +107,7 @@ export const useStopEntityEngineMutation = (options?: UseMutationOptions<{}>) => const { stopEntityStore } = useEntityStoreRoutes(); return useMutation( () => { - telemetry?.reportEntityStoreEnablement({ + telemetry?.reportEvent(EntityEventTypes.EntityStoreEnablementToggleClicked, { timestamp: new Date().toISOString(), action: 'stop', }); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx index 2dec7d07ce6e8..0c42543a7f91e 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx @@ -43,6 +43,7 @@ import { LENS_VISUALIZATION_MIN_WIDTH, SUMMARY_TABLE_MIN_WIDTH, } from './common'; +import { EntityEventTypes } from '../../../common/lib/telemetry'; export interface RiskSummaryProps { riskScoreData: RiskScoreState; @@ -84,7 +85,7 @@ const FlyoutRiskSummaryComponent = ({ (isOpen: boolean) => { const entity = isUserRiskData(riskData) ? 'user' : 'host'; - telemetry.reportToggleRiskSummaryClicked({ + telemetry.reportEvent(EntityEventTypes.ToggleRiskSummaryClicked, { entity, action: isOpen ? 'show' : 'hide', }); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/severity/severity_filter.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/severity/severity_filter.test.tsx index 8adbc2c7578df..c50d5a1be7747 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/severity/severity_filter.test.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/severity/severity_filter.test.tsx @@ -24,7 +24,7 @@ jest.mock('../../../common/lib/kibana', () => { describe('SeverityFilter', () => { beforeEach(() => { - mockedTelemetry.reportEntityRiskFiltered.mockClear(); + mockedTelemetry.reportEvent.mockClear(); }); it('sends telemetry when selecting a classification', () => { @@ -38,7 +38,7 @@ describe('SeverityFilter', () => { fireEvent.click(getByTestId('risk-filter-item-Unknown')); - expect(mockedTelemetry.reportEntityRiskFiltered).toHaveBeenCalledTimes(1); + expect(mockedTelemetry.reportEvent).toHaveBeenCalledTimes(1); }); it('does not send telemetry when deselecting a classification', () => { @@ -61,6 +61,6 @@ describe('SeverityFilter', () => { fireEvent.click(getByTestId('risk-filter-popoverButton')); fireEvent.click(getByTestId('risk-filter-item-Unknown')); - expect(mockedTelemetry.reportEntityRiskFiltered).toHaveBeenCalledTimes(0); + expect(mockedTelemetry.reportEvent).toHaveBeenCalledTimes(0); }); }); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/severity/severity_filter.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/severity/severity_filter.tsx index 6aa150e40afae..6da522658894d 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/severity/severity_filter.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/severity/severity_filter.tsx @@ -13,6 +13,7 @@ import type { RiskScoreEntity, RiskSeverity } from '../../../../common/search_st import { RiskScoreLevel } from './common'; import { ENTITY_RISK_LEVEL } from '../risk_score/translations'; import { useKibana } from '../../../common/lib/kibana'; +import { EntityEventTypes } from '../../../common/lib/telemetry'; export interface SeverityFilterProps { riskEntity?: RiskScoreEntity; @@ -35,7 +36,7 @@ export const SeverityFilter: React.FC = ({ >( (newSelection, changedSeverity, changedStatus) => { if (changedStatus === 'on') { - telemetry.reportEntityRiskFiltered({ + telemetry.reportEvent(EntityEventTypes.EntityRiskFiltered, { entity: riskEntity, selectedSeverity: changedSeverity, }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx index c315e991d9f06..e3fd2fef8bc6e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx @@ -71,6 +71,7 @@ import type { NarrowDateRange } from '../../../../common/components/ml/types'; import { MisconfigurationsInsight } from '../../shared/components/misconfiguration_insight'; import { VulnerabilitiesInsight } from '../../shared/components/vulnerabilities_insight'; import { AlertCountInsight } from '../../shared/components/alert_count_insight'; +import { DocumentEventTypes } from '../../../../common/lib/telemetry'; const HOST_DETAILS_ID = 'entities-hosts-details'; const RELATED_USERS_ID = 'entities-hosts-related-users'; @@ -134,7 +135,7 @@ export const HostDetails: React.FC = ({ hostName, timestamp, s banner: HOST_PREVIEW_BANNER, }, }); - telemetry.reportDetailsFlyoutOpened({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { location: scopeId, panel: 'preview', }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/session_view.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/session_view.tsx index 38bf50a679ee2..3b45cd71b0a6f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/session_view.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/session_view.tsx @@ -28,6 +28,7 @@ import { ALERT_PREVIEW_BANNER } from '../../preview/constants'; import { useLicense } from '../../../../common/hooks/use_license'; import { useSessionPreview } from '../../right/hooks/use_session_preview'; import { SessionViewNoDataMessage } from '../../shared/components/session_view_no_data_message'; +import { DocumentEventTypes } from '../../../../common/lib/telemetry'; export const SESSION_VIEW_ID = 'session-view'; @@ -74,7 +75,7 @@ export const SessionView: FC = () => { isPreviewMode: true, }, }); - telemetry.reportDetailsFlyoutOpened({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { location: scopeId, panel: 'preview', }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx index 2f98c641b5954..e88cbb54f9471 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx @@ -69,6 +69,7 @@ import { PreviewLink } from '../../../shared/components/preview_link'; import type { NarrowDateRange } from '../../../../common/components/ml/types'; import { MisconfigurationsInsight } from '../../shared/components/misconfiguration_insight'; import { AlertCountInsight } from '../../shared/components/alert_count_insight'; +import { DocumentEventTypes } from '../../../../common/lib/telemetry'; const USER_DETAILS_ID = 'entities-users-details'; const RELATED_HOSTS_ID = 'entities-users-related-hosts'; @@ -133,7 +134,7 @@ export const UserDetails: React.FC = ({ userName, timestamp, s banner: USER_PREVIEW_BANNER, }, }); - telemetry.reportDetailsFlyoutOpened({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { location: scopeId, panel: 'preview', }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/index.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/index.tsx index 56375426c5f68..6dcf9da06d2b6 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/index.tsx @@ -22,6 +22,7 @@ import { getField } from '../shared/utils'; import { EventKind } from '../shared/constants/event_kinds'; import { useDocumentDetailsContext } from '../shared/context'; import type { DocumentDetailsProps } from '../shared/types'; +import { DocumentEventTypes } from '../../../common/lib/telemetry/types'; export type LeftPanelPaths = 'visualize' | 'insights' | 'investigation' | 'response' | 'notes'; export const LeftPanelVisualizeTab: LeftPanelPaths = 'visualize'; @@ -75,7 +76,7 @@ export const LeftPanel: FC> = memo(({ path }) => { scopeId, }, }); - telemetry.reportDetailsFlyoutTabClicked({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutTabClicked, { location: scopeId, panel: 'left', tabId, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/insights_tab.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/insights_tab.tsx index 3917de03e2e34..0982e10485ba3 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/insights_tab.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/insights_tab.tsx @@ -31,6 +31,7 @@ import { PREVALENCE_TAB_ID, PrevalenceDetails } from '../components/prevalence_d import { CORRELATIONS_TAB_ID, CorrelationsDetails } from '../components/correlations_details'; import { getField } from '../../shared/utils'; import { EventKind } from '../../shared/constants/event_kinds'; +import { DocumentEventTypes } from '../../../../common/lib/telemetry'; const ENTITIES_TAB_ID = 'entity'; @@ -113,7 +114,7 @@ export const InsightsTab = memo(() => { scopeId, }, }); - telemetry.reportDetailsFlyoutTabClicked({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutTabClicked, { location: scopeId, panel: 'left', tabId: optionId, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/preview/footer.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/preview/footer.tsx index 0201332888675..b2df6c096e279 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/preview/footer.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/preview/footer.tsx @@ -16,6 +16,7 @@ import { DocumentDetailsRightPanelKey } from '../shared/constants/panel_keys'; import { useDocumentDetailsContext } from '../shared/context'; import { PREVIEW_FOOTER_TEST_ID, PREVIEW_FOOTER_LINK_TEST_ID } from './test_ids'; import { useKibana } from '../../../common/lib/kibana'; +import { DocumentEventTypes } from '../../../common/lib/telemetry'; /** * Footer at the bottom of preview panel with a link to open document details flyout @@ -41,7 +42,7 @@ export const PreviewPanelFooter = () => { }, }, }); - telemetry.reportDetailsFlyoutOpened({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { location: scopeId, panel: 'right', }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_description.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_description.tsx index 2d2dfddbbbabe..91d5059e5d60a 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_description.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_description.tsx @@ -22,6 +22,7 @@ import { RULE_SUMMARY_BUTTON_TEST_ID, } from './test_ids'; import { RULE_PREVIEW_BANNER, RulePreviewPanelKey } from '../../../rule_details/right'; +import { DocumentEventTypes } from '../../../../common/lib/telemetry'; /** * Displays the rule description of a signal document. @@ -42,7 +43,7 @@ export const AlertDescription: FC = () => { isPreviewMode: true, }, }); - telemetry.reportDetailsFlyoutOpened({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { location: scopeId, panel: 'preview', }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx index be65593364593..af9e8dca1f24f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx @@ -32,7 +32,6 @@ export const GraphPreviewContainer: React.FC = () => { const graphFetchQuery = useFetchGraphData({ req: { query: { - actorIds: [], eventIds, start: DEFAULT_FROM, end: DEFAULT_TO, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/reason.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/reason.tsx index c4b0e6e26a820..fc6db84ad4393 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/reason.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/reason.tsx @@ -22,6 +22,7 @@ import { } from './test_ids'; import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { useDocumentDetailsContext } from '../../shared/context'; +import { DocumentEventTypes } from '../../../../common/lib/telemetry'; export const ALERT_REASON_BANNER = { title: i18n.translate( @@ -55,7 +56,7 @@ export const Reason: FC = () => { banner: ALERT_REASON_BANNER, }, }); - telemetry.reportDetailsFlyoutOpened({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { location: scopeId, panel: 'preview', }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.test.tsx new file mode 100644 index 0000000000000..c22ec0caa82c5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.test.tsx @@ -0,0 +1,89 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { useFetchGraphData } from './use_fetch_graph_data'; + +const mockUseQuery = jest.fn(); + +jest.mock('@tanstack/react-query', () => { + return { + useQuery: (...args: unknown[]) => mockUseQuery(...args), + }; +}); + +describe('useFetchGraphData', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('Should pass default options when options are not provided', () => { + renderHook(() => { + return useFetchGraphData({ + req: { + query: { + eventIds: [], + start: '2021-09-01T00:00:00.000Z', + end: '2021-09-01T23:59:59.999Z', + }, + }, + }); + }); + + expect(mockUseQuery.mock.calls).toHaveLength(1); + expect(mockUseQuery.mock.calls[0][2]).toEqual({ + enabled: true, + refetchOnWindowFocus: true, + }); + }); + + it('Should should not be enabled when enabled set to false', () => { + renderHook(() => { + return useFetchGraphData({ + req: { + query: { + eventIds: [], + start: '2021-09-01T00:00:00.000Z', + end: '2021-09-01T23:59:59.999Z', + }, + }, + options: { + enabled: false, + }, + }); + }); + + expect(mockUseQuery.mock.calls).toHaveLength(1); + expect(mockUseQuery.mock.calls[0][2]).toEqual({ + enabled: false, + refetchOnWindowFocus: true, + }); + }); + + it('Should should not be refetchOnWindowFocus when refetchOnWindowFocus set to false', () => { + renderHook(() => { + return useFetchGraphData({ + req: { + query: { + eventIds: [], + start: '2021-09-01T00:00:00.000Z', + end: '2021-09-01T23:59:59.999Z', + }, + }, + options: { + refetchOnWindowFocus: false, + }, + }); + }); + + expect(mockUseQuery.mock.calls).toHaveLength(1); + expect(mockUseQuery.mock.calls[0][2]).toEqual({ + enabled: true, + refetchOnWindowFocus: false, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.ts index 2304cfb8d4fd2..9a0e270a9b2e0 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.ts @@ -10,6 +10,7 @@ import type { GraphRequest, GraphResponse, } from '@kbn/cloud-security-posture-common/types/graph/latest'; +import { useMemo } from 'react'; import { EVENT_GRAPH_VISUALIZATION_API } from '../../../../../common/constants'; import { useHttp } from '../../../../common/lib/kibana'; @@ -30,6 +31,11 @@ export interface UseFetchGraphDataParams { * Defaults to true. */ enabled?: boolean; + /** + * If true, the query will refetch on window focus. + * Defaults to true. + */ + refetchOnWindowFocus?: boolean; }; } @@ -61,18 +67,25 @@ export const useFetchGraphData = ({ req, options, }: UseFetchGraphDataParams): UseFetchGraphDataResult => { - const { actorIds, eventIds, start, end } = req.query; + const { eventIds, start, end, esQuery } = req.query; const http = useHttp(); + const QUERY_KEY = useMemo( + () => ['useFetchGraphData', eventIds, start, end, esQuery], + [end, esQuery, eventIds, start] + ); const { isLoading, isError, data } = useQuery( - ['useFetchGraphData', actorIds, eventIds, start, end], + QUERY_KEY, () => { return http.post(EVENT_GRAPH_VISUALIZATION_API, { version: '1', body: JSON.stringify(req), }); }, - options + { + enabled: options?.enabled ?? true, + refetchOnWindowFocus: options?.refetchOnWindowFocus ?? true, + } ); return { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/index.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/index.tsx index 56c24d9562091..9d3262ce1ff3b 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/index.tsx @@ -20,6 +20,7 @@ import { PanelContent } from './content'; import type { RightPanelTabType } from './tabs'; import { PanelFooter } from './footer'; import { useFlyoutIsExpandable } from './hooks/use_flyout_is_expandable'; +import { DocumentEventTypes } from '../../../common/lib/telemetry'; export type RightPanelPaths = 'overview' | 'table' | 'json'; @@ -53,7 +54,7 @@ export const RightPanel: FC> = memo(({ path }) => // saving which tab is currently selected in the right panel in local storage storage.set(FLYOUT_STORAGE_KEYS.RIGHT_PANEL_SELECTED_TABS, tabId); - telemetry.reportDetailsFlyoutTabClicked({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutTabClicked, { location: scopeId, panel: 'right', tabId, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/navigation.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/navigation.tsx index b4f12fbabf94f..c3ee6a7d7a51a 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/navigation.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/navigation.tsx @@ -13,6 +13,7 @@ import { HeaderActions } from './components/header_actions'; import { FlyoutNavigation } from '../../shared/components/flyout_navigation'; import { DocumentDetailsLeftPanelKey } from '../shared/constants/panel_keys'; import { useDocumentDetailsContext } from '../shared/context'; +import { DocumentEventTypes } from '../../../common/lib/telemetry'; interface PanelNavigationProps { /** @@ -35,7 +36,7 @@ export const PanelNavigation: FC = memo(({ flyoutIsExpanda scopeId, }, }); - telemetry.reportDetailsFlyoutOpened({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { location: scopeId, panel: 'left', }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_analyzer.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_analyzer.tsx index 516a43332d29f..a4539ed7e6415 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_analyzer.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_analyzer.tsx @@ -19,6 +19,7 @@ import { } from '../constants/panel_keys'; import { Flyouts } from '../constants/flyouts'; import { isTimelineScope } from '../../../../helpers'; +import { DocumentEventTypes } from '../../../../common/lib/telemetry'; export interface UseNavigateToAnalyzerParams { /** @@ -107,7 +108,7 @@ export const useNavigateToAnalyzer = ({ if (isFlyoutOpen) { openLeftPanel(left); openPreviewPanel(preview); - telemetry.reportDetailsFlyoutTabClicked({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutTabClicked, { location: scopeId, panel: 'left', tabId: 'visualize', @@ -118,7 +119,7 @@ export const useNavigateToAnalyzer = ({ left, preview, }); - telemetry.reportDetailsFlyoutOpened({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { location: scopeId, panel: 'left', }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_session_view.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_session_view.tsx index b8234321217e6..f0b2733998c97 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_session_view.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_session_view.tsx @@ -12,6 +12,7 @@ import type { Maybe } from '@kbn/timelines-plugin/common/search_strategy/common' import { useKibana } from '../../../../common/lib/kibana'; import { SESSION_VIEW_ID } from '../../left/components/session_view'; import { DocumentDetailsLeftPanelKey, DocumentDetailsRightPanelKey } from '../constants/panel_keys'; +import { DocumentEventTypes } from '../../../../common/lib/telemetry'; export interface UseNavigateToSessionViewParams { /** @@ -83,7 +84,7 @@ export const useNavigateToSessionView = ({ const navigateToSessionView = useCallback(() => { if (isFlyoutOpen) { openLeftPanel(left); - telemetry.reportDetailsFlyoutTabClicked({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutTabClicked, { location: scopeId, panel: 'left', tabId: 'visualize', @@ -93,7 +94,7 @@ export const useNavigateToSessionView = ({ right, left, }); - telemetry.reportDetailsFlyoutOpened({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { location: scopeId, panel: 'left', }); diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx index adc54b58f75cb..83fa75474a1cc 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx @@ -35,6 +35,7 @@ import { useObservedHost } from './hooks/use_observed_host'; import { HostDetailsPanelKey } from '../host_details_left'; import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; import { HostPreviewPanelFooter } from '../host_preview/footer'; +import { EntityEventTypes } from '../../../common/lib/telemetry'; export interface HostPanelProps extends Record { contextID: string; @@ -130,7 +131,7 @@ export const HostPanel = ({ const openTabPanel = useCallback( (tab?: EntityDetailsLeftPanelTab) => { - telemetry.reportRiskInputsExpandedFlyoutOpened({ + telemetry.reportEvent(EntityEventTypes.RiskInputsExpandedFlyoutOpened, { entity: 'host', }); diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx index 3a60c06e3faea..42c8664b2ac0c 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx @@ -33,6 +33,7 @@ import { UserDetailsPanelKey } from '../user_details_left'; import { useObservedUser } from './hooks/use_observed_user'; import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; import { UserPreviewPanelFooter } from '../user_preview/footer'; +import { EntityEventTypes } from '../../../common/lib/telemetry'; export interface UserPanelProps extends Record { contextID: string; @@ -123,7 +124,7 @@ export const UserPanel = ({ const { openLeftPanel } = useExpandableFlyoutApi(); const openPanelTab = useCallback( (tab?: EntityDetailsLeftPanelTab) => { - telemetry.reportRiskInputsExpandedFlyoutOpened({ + telemetry.reportEvent(EntityEventTypes.RiskInputsExpandedFlyoutOpened, { entity: 'user', }); diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.tsx index fc51a4e64e6c0..b6a4ea33ba4bc 100644 --- a/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.tsx +++ b/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.tsx @@ -24,6 +24,7 @@ import { UserPreviewPanelKey } from '../../entity_details/user_right'; import { USER_PREVIEW_BANNER } from '../../document_details/right/components/user_entity_overview'; import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../network_details'; import { RulePreviewPanelKey, RULE_PREVIEW_BANNER } from '../../rule_details/right'; +import { DocumentEventTypes } from '../../../common/lib/telemetry'; const PREVIEW_FIELDS = [HOST_NAME_FIELD_NAME, USER_NAME_FIELD_NAME, SIGNAL_RULE_NAME_FIELD_NAME]; @@ -133,7 +134,7 @@ export const PreviewLink: FC = ({ id: previewParams.id, params: previewParams.params, }); - telemetry.reportDetailsFlyoutOpened({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { location: scopeId, panel: 'preview', }); diff --git a/x-pack/plugins/security_solution/public/notes/components/add_note.tsx b/x-pack/plugins/security_solution/public/notes/components/add_note.tsx index 78a84064467f6..5d1ef2ce4d8e1 100644 --- a/x-pack/plugins/security_solution/public/notes/components/add_note.tsx +++ b/x-pack/plugins/security_solution/public/notes/components/add_note.tsx @@ -28,6 +28,7 @@ import { userClosedCreateErrorToast, } from '../store/notes.slice'; import { MarkdownEditor } from '../../common/components/markdown_editor'; +import { NotesEventTypes } from '../../common/lib/telemetry'; export const MARKDOWN_ARIA_LABEL = i18n.translate( 'xpack.securitySolution.notes.addNote.markdownAriaLabel', @@ -96,7 +97,7 @@ export const AddNote = memo( if (onNoteAdd) { onNoteAdd(); } - telemetry.reportAddNoteFromExpandableFlyoutClicked({ + telemetry.reportEvent(NotesEventTypes.AddNoteFromExpandableFlyoutClicked, { isRelatedToATimeline: timelineId != null, }); setEditorValue(''); diff --git a/x-pack/plugins/security_solution/public/notes/components/open_flyout_button.tsx b/x-pack/plugins/security_solution/public/notes/components/open_flyout_button.tsx index 85e9e24c6f26e..65e6389fc2fd1 100644 --- a/x-pack/plugins/security_solution/public/notes/components/open_flyout_button.tsx +++ b/x-pack/plugins/security_solution/public/notes/components/open_flyout_button.tsx @@ -16,6 +16,7 @@ import { useSourcererDataView } from '../../sourcerer/containers'; import { SourcererScopeName } from '../../sourcerer/store/model'; import { useKibana } from '../../common/lib/kibana'; import { DocumentDetailsRightPanelKey } from '../../flyout/document_details/shared/constants/panel_keys'; +import { DocumentEventTypes } from '../../common/lib/telemetry'; export const OPEN_FLYOUT_BUTTON = i18n.translate( 'xpack.securitySolution.notes.openFlyoutButtonLabel', @@ -61,7 +62,7 @@ export const OpenFlyoutButtonIcon = memo( }, }, }); - telemetry.reportDetailsFlyoutOpened({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { location: timelineId, panel: 'right', }); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx index dda17e18c087e..2a6597628a26d 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx @@ -9,6 +9,7 @@ import type { PropsWithChildren } from 'react'; import React, { createContext, useContext, useMemo } from 'react'; import { useKibana } from '../../common/lib/kibana/kibana_react'; import type { OnboardingCardId } from '../constants'; +import { OnboardingHubEventTypes } from '../../common/lib/telemetry'; export interface OnboardingContextValue { spaceId: string; @@ -26,19 +27,19 @@ export const OnboardingContextProvider: React.FC ({ spaceId, reportCardOpen: (cardId, { auto = false } = {}) => { - telemetry.reportOnboardingHubStepOpen({ + telemetry.reportEvent(OnboardingHubEventTypes.OnboardingHubStepOpen, { stepId: cardId, trigger: auto ? 'navigation' : 'click', }); }, reportCardComplete: (cardId, { auto = false } = {}) => { - telemetry.reportOnboardingHubStepFinished({ + telemetry.reportEvent(OnboardingHubEventTypes.OnboardingHubStepFinished, { stepId: cardId, trigger: auto ? 'auto_check' : 'click', }); }, reportCardLinkClicked: (cardId, linkId: string) => { - telemetry.reportOnboardingHubStepLinkClicked({ + telemetry.reportEvent(OnboardingHubEventTypes.OnboardingHubStepLinkClicked, { originStepId: cardId, stepLinkId: linkId, }); diff --git a/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx b/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx index 67dcc3848f02a..e785e58435432 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx @@ -28,9 +28,10 @@ import { KibanaServices, useKibana, useToasts, useUiSetting$ } from '../../commo import { SpyRoute } from '../../common/utils/route/spy_routes'; import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index'; import * as i18n from './translations'; -import type { - ReportDataQualityCheckAllCompletedParams, - ReportDataQualityIndexCheckedParams, +import { + type ReportDataQualityCheckAllCompletedParams, + type ReportDataQualityIndexCheckedParams, + DataQualityEventTypes, } from '../../common/lib/telemetry'; const LOCAL_STORAGE_KEY = 'dataQualityDashboardLastChecked'; @@ -118,14 +119,14 @@ const DataQualityComponent: React.FC = () => { const reportDataQualityIndexChecked = useCallback( (params: ReportDataQualityIndexCheckedParams) => { - telemetry.reportDataQualityIndexChecked(params); + telemetry.reportEvent(DataQualityEventTypes.DataQualityIndexChecked, params); }, [telemetry] ); const reportDataQualityCheckAllCompleted = useCallback( (params: ReportDataQualityCheckAllCompletedParams) => { - telemetry.reportDataQualityCheckAllCompleted(params); + telemetry.reportEvent(DataQualityEventTypes.DataQualityCheckAllCompleted, params); }, [telemetry] ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx index fe53c36d3f049..19f98aff651fa 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx @@ -34,6 +34,7 @@ import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { useSourcererDataView } from '../../../../sourcerer/containers'; import { useDeleteNote } from './hooks/use_delete_note'; import { getTimelineNoteSelector } from '../../timeline/tabs/notes/selectors'; +import { DocumentEventTypes } from '../../../../common/lib/telemetry'; export const NotePreviewsContainer = styled.section` padding-top: ${({ theme }) => `${theme.eui.euiSizeS}`}; @@ -66,7 +67,7 @@ const ToggleEventDetailsButtonComponent: React.FC }, }, }); - telemetry.reportDetailsFlyoutOpened({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { location: timelineId, panel: 'right', }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx index 602d2353f342f..5c4a592d99a7d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx @@ -46,6 +46,7 @@ import { useTimelineControlColumn } from '../shared/use_timeline_control_columns import { LeftPanelNotesTab } from '../../../../../flyout/document_details/left'; import { useNotesInFlyout } from '../../properties/use_notes_in_flyout'; import { NotesFlyout } from '../../properties/notes_flyout'; +import { NotesEventTypes, DocumentEventTypes } from '../../../../../common/lib/telemetry'; import { TimelineRefetch } from '../../refetch_timeline'; export type Props = TimelineTabCommonProps & PropsFromRedux; @@ -161,10 +162,10 @@ export const EqlTabContentComponent: React.FC = ({ }, }, }); - telemetry.reportOpenNoteInExpandableFlyoutClicked({ + telemetry.reportEvent(NotesEventTypes.OpenNoteInExpandableFlyoutClicked, { location: timelineId, }); - telemetry.reportDetailsFlyoutOpened({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { location: timelineId, panel: 'left', }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx index 1f2360daa051d..0b2553d23ac5e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx @@ -36,6 +36,7 @@ import { useTimelineControlColumn } from '../shared/use_timeline_control_columns import { LeftPanelNotesTab } from '../../../../../flyout/document_details/left'; import { useNotesInFlyout } from '../../properties/use_notes_in_flyout'; import { NotesFlyout } from '../../properties/notes_flyout'; +import { NotesEventTypes, DocumentEventTypes } from '../../../../../common/lib/telemetry'; import { defaultUdtHeaders } from '../../body/column_headers/default_headers'; interface PinnedFilter { @@ -190,10 +191,10 @@ export const PinnedTabContentComponent: React.FC = ({ }, }, }); - telemetry.reportOpenNoteInExpandableFlyoutClicked({ + telemetry.reportEvent(NotesEventTypes.OpenNoteInExpandableFlyoutClicked, { location: timelineId, }); - telemetry.reportDetailsFlyoutOpened({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { location: timelineId, panel: 'left', }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx index 8ea1db39a3618..ec61c67a3954a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx @@ -49,6 +49,7 @@ import { useTimelineColumns } from '../shared/use_timeline_columns'; import { useTimelineControlColumn } from '../shared/use_timeline_control_columns'; import { NotesFlyout } from '../../properties/notes_flyout'; import { useNotesInFlyout } from '../../properties/use_notes_in_flyout'; +import { DocumentEventTypes, NotesEventTypes } from '../../../../../common/lib/telemetry'; const compareQueryProps = (prevProps: Props, nextProps: Props) => prevProps.kqlMode === nextProps.kqlMode && @@ -228,10 +229,10 @@ export const QueryTabContentComponent: React.FC = ({ }, }, }); - telemetry.reportOpenNoteInExpandableFlyoutClicked({ + telemetry.reportEvent(NotesEventTypes.OpenNoteInExpandableFlyoutClicked, { location: timelineId, }); - telemetry.reportDetailsFlyoutOpened({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { location: timelineId, panel: 'left', }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx index c711208cb6806..67b9fd50eac22 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx @@ -34,6 +34,7 @@ import { useUserPrivileges } from '../../../../../common/components/user_privile import { timelineActions, timelineSelectors } from '../../../../store'; import { timelineDefaults } from '../../../../store/defaults'; import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; +import { DocumentEventTypes } from '../../../../../common/lib/telemetry'; import { isFullScreen } from '../../helpers'; const FullScreenButtonIcon = styled(EuiButtonIcon)` @@ -287,7 +288,7 @@ export const useSessionView = ({ scopeId, height }: { scopeId: string; height?: }, }, }); - telemetry.reportDetailsFlyoutOpened({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { location: scopeId, panel: 'right', }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx index 875c147d6a700..fa5b83f23576a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx @@ -48,6 +48,7 @@ import { transformTimelineItemToUnifiedRows } from '../utils'; import { TimelineEventDetailRow } from './timeline_event_detail_row'; import { CustomTimelineDataGridBody } from './custom_timeline_data_grid_body'; import { TIMELINE_EVENT_DETAIL_ROW_ID } from '../../body/constants'; +import { DocumentEventTypes } from '../../../../../common/lib/telemetry/types'; export const SAMPLE_SIZE_SETTING = 500; const DataGridMemoized = React.memo(UnifiedDataTable); @@ -165,7 +166,7 @@ export const TimelineDataTableComponent: React.FC = memo( }, }, }); - telemetry.reportDetailsFlyoutOpened({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { location: timelineId, panel: 'right', }); diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index 6ac8b349b74c5..55fce6a46dba8 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -84,7 +84,6 @@ import type { Assets } from './assets'; import type { Investigations } from './investigations'; import type { MachineLearning } from './machine_learning'; -import type { TelemetryClientStart } from './common/lib/telemetry'; import type { Dashboards } from './dashboards'; import type { BreadcrumbsNav } from './common/breadcrumbs/types'; import type { TopValuesPopoverService } from './app/components/top_values_popover/top_values_popover_service'; @@ -93,6 +92,7 @@ import type { SetComponents, GetComponents$ } from './contract_components'; import type { ConfigSettings } from '../common/config_settings'; import type { OnboardingService } from './onboarding/service'; import type { SolutionNavigation } from './app/solution_navigation/solution_navigation'; +import type { TelemetryServiceStart } from './common/lib/telemetry'; export interface SetupPlugins { cloud?: CloudSetup; @@ -188,7 +188,7 @@ export type StartServices = CoreStart & getPluginWrapper: () => typeof SecuritySolutionTemplateWrapper; }; contentManagement: ContentManagementPublicStart; - telemetry: TelemetryClientStart; + telemetry: TelemetryServiceStart; customDataService: DataPublicPluginStart; topValuesPopover: TopValuesPopoverService; timelineDataService: DataPublicPluginStart; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.ts index 196ffc71162db..8ba1de0bb70d9 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.ts @@ -27,7 +27,12 @@ export function registerActionAuditLogRoutes( .get({ access: 'public', path: ENDPOINT_ACTION_LOG_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/details.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/details.ts index 96b466a251cf4..1d524b08aefce 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/details.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/details.ts @@ -32,7 +32,12 @@ export const registerActionDetailsRoutes = ( .get({ access: 'public', path: ACTION_DETAILS_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.ts index 2e16c57886f7d..29aa6f4bba3d8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.ts @@ -38,7 +38,12 @@ export const registerActionFileDownloadRoutes = ( // we need to enable setting the version number via query params enableQueryVersion: true, path: ACTION_AGENT_FILE_DOWNLOAD_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.ts index 1cb4e95e1eaf1..63118a64fc453 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.ts @@ -74,7 +74,12 @@ export const registerActionFileInfoRoute = ( .get({ access: 'public', path: ACTION_AGENT_FILE_INFO_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.ts index a858909f5e2ed..05e5d77eb945b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.ts @@ -30,7 +30,12 @@ export function registerActionListRoutes( .get({ access: 'public', path: BASE_ENDPOINT_ACTION_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts index b6eb2376bd1cb..0fc90c7589b99 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts @@ -80,7 +80,12 @@ export function registerResponseActionRoutes( .post({ access: 'public', path: ISOLATE_HOST_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { @@ -99,7 +104,12 @@ export function registerResponseActionRoutes( .post({ access: 'public', path: UNISOLATE_HOST_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { @@ -119,7 +129,12 @@ export function registerResponseActionRoutes( .post({ access: 'public', path: ISOLATE_HOST_ROUTE_V2, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { @@ -139,7 +154,12 @@ export function registerResponseActionRoutes( .post({ access: 'public', path: UNISOLATE_HOST_ROUTE_V2, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { @@ -159,7 +179,12 @@ export function registerResponseActionRoutes( .post({ access: 'public', path: KILL_PROCESS_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { @@ -182,7 +207,12 @@ export function registerResponseActionRoutes( .post({ access: 'public', path: SUSPEND_PROCESS_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { @@ -205,7 +235,12 @@ export function registerResponseActionRoutes( .post({ access: 'public', path: GET_PROCESSES_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { @@ -225,7 +260,12 @@ export function registerResponseActionRoutes( .post({ access: 'public', path: GET_FILE_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { @@ -245,7 +285,12 @@ export function registerResponseActionRoutes( .post({ access: 'public', path: EXECUTE_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { @@ -265,9 +310,14 @@ export function registerResponseActionRoutes( .post({ access: 'public', path: UPLOAD_ROUTE, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, options: { authRequired: true, - tags: ['access:securitySolution'], + body: { accepts: ['multipart/form-data'], output: 'stream', @@ -293,7 +343,12 @@ export function registerResponseActionRoutes( .post({ access: 'public', path: SCAN_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/state.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/state.ts index 3ea4d9fa35753..c2d5b850d6bb2 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/state.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/state.ts @@ -32,7 +32,12 @@ export function registerActionStateRoutes( .get({ access: 'public', path: ACTION_STATE_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.ts index 6172bf07d2320..8a245dfb451ea 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.ts @@ -29,7 +29,12 @@ export function registerActionStatusRoutes( .get({ access: 'public', path: ACTION_STATUS_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/agent/agent_status_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/agent/agent_status_handler.ts index e6ea2f7595785..7ca24156b45fd 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/agent/agent_status_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/agent/agent_status_handler.ts @@ -27,7 +27,12 @@ export const registerAgentStatusRoute = ( .get({ access: 'internal', path: AGENT_STATUS_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts index 2f6e46d1d7727..3f028719fe5ad 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts @@ -54,7 +54,12 @@ export function registerEndpointRoutes( .get({ access: 'public', path: HOST_METADATA_LIST_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { @@ -94,7 +99,12 @@ export function registerEndpointRoutes( .get({ access: 'public', path: METADATA_TRANSFORMS_STATUS_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }) @@ -114,7 +124,12 @@ export function registerEndpointRoutes( .get({ access: 'internal', path: METADATA_TRANSFORMS_STATUS_INTERNAL_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index 00054964e4401..6010c56557273 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -240,8 +240,8 @@ describe('test endpoint routes', () => { }); expect(routeConfig.options).toEqual({ authRequired: true, - tags: ['access:securitySolution'], }); + expect(routeConfig.security?.authz).toEqual({ requiredPrivileges: ['securitySolution'] }); expect(mockResponse.ok).toBeCalled(); const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as MetadataListResponse; expect(endpointResultList.data.length).toEqual(1); @@ -614,8 +614,8 @@ describe('test endpoint routes', () => { expect(esClientMock.transform.getTransformStats).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, - tags: ['access:securitySolution'], }); + expect(routeConfig.security?.authz).toEqual({ requiredPrivileges: ['securitySolution'] }); expect(mockResponse.ok).toBeCalled(); const response = mockResponse.ok.mock.calls[0][0]?.body as TransformGetTransformStatsResponse; expect(response.count).toEqual(expectedResponse.count); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/protection_updates_note/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/protection_updates_note/index.ts index 7b28ccfcf9fe7..4355684407bb1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/protection_updates_note/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/protection_updates_note/index.ts @@ -25,7 +25,12 @@ export function registerProtectionUpdatesNoteRoutes( .post({ access: 'public', path: PROTECTION_UPDATES_NOTE_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { @@ -45,7 +50,12 @@ export function registerProtectionUpdatesNoteRoutes( .get({ access: 'public', path: PROTECTION_UPDATES_NOTE_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.ts index 677fb004ee862..bbee33114534b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.ts @@ -42,7 +42,12 @@ export function registerEndpointSuggestionsRoutes( .post({ access: 'public', path: SUGGESTIONS_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }) @@ -64,7 +69,12 @@ export function registerEndpointSuggestionsRoutes( .post({ access: 'internal', path: SUGGESTIONS_INTERNAL_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/enrich_signal_threat_matches.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/enrich_signal_threat_matches.ts index 8f98eab1a93e9..0d9882fe8aec3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/enrich_signal_threat_matches.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/enrich_signal_threat_matches.ts @@ -24,8 +24,10 @@ export const groupAndMergeSignalMatches = (signalHits: SignalSourceHit[]): Signa if (existingSignalHit == null) { acc[signalId] = signalHit; } else { - const existingQueries = existingSignalHit?.matched_queries ?? []; - const newQueries = signalHit.matched_queries ?? []; + const existingQueries = Array.isArray(existingSignalHit?.matched_queries) + ? existingSignalHit.matched_queries + : []; + const newQueries = Array.isArray(signalHit.matched_queries) ? signalHit.matched_queries : []; existingSignalHit.matched_queries = [...existingQueries, ...newQueries]; acc[signalId] = existingSignalHit; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_signals_map_from_threat_index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_signals_map_from_threat_index.ts index 309516a57335c..9694d37aab0ab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_signals_map_from_threat_index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_signals_map_from_threat_index.ts @@ -90,7 +90,9 @@ export async function getSignalsQueryMapFromThreatIndex( while (maxThreatsReachedMap.size < eventsCount && threatList?.hits.hits.length > 0) { threatList.hits.hits.forEach((threatHit) => { - const matchedQueries = threatHit?.matched_queries || []; + const matchedQueries = Array.isArray(threatHit?.matched_queries) + ? threatHit.matched_queries + : []; matchedQueries.forEach((matchedQuery) => { const decodedQuery = decodeThreatMatchNamedQuery(matchedQuery); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/utils.ts index da72d121c371c..347ea5d1d94c6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/utils.ts @@ -189,7 +189,9 @@ export const decodeThreatMatchNamedQuery = (encoded: string): DecodedThreatNamed export const extractNamedQueries = ( hit: SignalSourceHit | ThreatListItem ): DecodedThreatNamedQuery[] => - hit.matched_queries?.map((match) => decodeThreatMatchNamedQuery(match)) ?? []; + Array.isArray(hit.matched_queries) + ? hit.matched_queries.map((match) => decodeThreatMatchNamedQuery(match)) + : []; export const buildExecutionIntervalValidator: (interval: string) => () => void = (interval) => { const intervalDuration = parseInterval(interval); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts index e15aceb8a713b..54af298d11a3e 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts @@ -46,16 +46,19 @@ export const buildIndicatorShouldClauses = ( export const buildIndicatorEnrichments = (hits: estypes.SearchHit[]): CtiEnrichment[] => { return hits.flatMap(({ matched_queries: matchedQueries, ...hit }) => { return ( - matchedQueries?.reduce((enrichments, matchedQuery) => { - if (isValidEventField(matchedQuery)) { - enrichments.push({ - ...hit.fields, - ...buildIndicatorMatchedFields(hit, matchedQuery), - }); - } + (Array.isArray(matchedQueries) ? matchedQueries : [])?.reduce( + (enrichments, matchedQuery) => { + if (isValidEventField(matchedQuery)) { + enrichments.push({ + ...hit.fields, + ...buildIndicatorMatchedFields(hit, matchedQuery), + }); + } - return enrichments; - }, []) ?? [] + return enrichments; + }, + [] + ) ?? [] ); }); }; diff --git a/x-pack/plugins/serverless_search/public/navigation_tree.ts b/x-pack/plugins/serverless_search/public/navigation_tree.ts index b0f5a4658e7d2..066ab8e8c093e 100644 --- a/x-pack/plugins/serverless_search/public/navigation_tree.ts +++ b/x-pack/plugins/serverless_search/public/navigation_tree.ts @@ -20,14 +20,6 @@ export const navigationTree = (): NavigationTreeDefinition => ({ isCollapsible: false, breadcrumbStatus: 'hidden', children: [ - { - id: 'home', - title: i18n.translate('xpack.serverlessSearch.nav.home', { - defaultMessage: 'Home', - }), - link: 'elasticsearchStart', - spaceBefore: 'm', - }, { id: 'data', title: i18n.translate('xpack.serverlessSearch.nav.data', { @@ -47,9 +39,8 @@ export const navigationTree = (): NavigationTreeDefinition => ({ pathNameSerialized.startsWith( prepend('/app/management/data/index_management/') ) || - pathNameSerialized.startsWith( - prepend('/app/elasticsearch/indices/index_details/') - ) + pathNameSerialized.startsWith(prepend('/app/elasticsearch/indices')) || + pathNameSerialized.startsWith(prepend('/app/elasticsearch/start')) ); }, }, diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 45960195be216..cd820d1e70780 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -29,7 +29,7 @@ import { TaskManagerConfig } from './config'; import { createInitialMiddleware, addMiddlewareToChain, Middleware } from './lib/middleware'; import { removeIfExists } from './lib/remove_if_exists'; import { setupSavedObjects, BACKGROUND_TASK_NODE_SO_NAME, TASK_SO_NAME } from './saved_objects'; -import { TaskDefinitionRegistry, TaskTypeDictionary, REMOVED_TYPES } from './task_type_dictionary'; +import { TaskDefinitionRegistry, TaskTypeDictionary } from './task_type_dictionary'; import { AggregationOpts, FetchResult, SearchOpts, TaskStore } from './task_store'; import { createManagedConfiguration } from './lib/create_managed_configuration'; import { TaskScheduling } from './task_scheduling'; @@ -45,6 +45,10 @@ import { metricsStream, Metrics } from './metrics'; import { TaskManagerMetricsCollector } from './metrics/task_metrics_collector'; import { TaskPartitioner } from './lib/task_partitioner'; import { getDefaultCapacity } from './lib/get_default_capacity'; +import { + registerMarkRemovedTasksAsUnrecognizedDefinition, + scheduleMarkRemovedTasksAsUnrecognizedDefinition, +} from './removed_tasks/mark_removed_tasks_as_unrecognized'; export interface TaskManagerSetupContract { /** @@ -221,6 +225,11 @@ export class TaskManagerPlugin } registerDeleteInactiveNodesTaskDefinition(this.logger, core.getStartServices, this.definitions); + registerMarkRemovedTasksAsUnrecognizedDefinition( + this.logger, + core.getStartServices, + this.definitions + ); if (this.config.unsafe.exclude_task_types.length) { this.logger.warn( @@ -332,7 +341,6 @@ export class TaskManagerPlugin this.taskPollingLifecycle = new TaskPollingLifecycle({ config: this.config!, definitions: this.definitions, - unusedTypes: REMOVED_TYPES, logger: this.logger, executionContext, taskStore, @@ -384,6 +392,7 @@ export class TaskManagerPlugin }); scheduleDeleteInactiveNodesTaskDefinition(this.logger, taskScheduling).catch(() => {}); + scheduleMarkRemovedTasksAsUnrecognizedDefinition(this.logger, taskScheduling).catch(() => {}); return { fetch: (opts: SearchOpts): Promise => taskStore.fetch(opts), diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts index 1f244f7f4c8a5..a408bd3f634d9 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts @@ -106,7 +106,6 @@ describe('TaskPollingLifecycle', () => { }, taskStore: mockTaskStore, logger: taskManagerLogger, - unusedTypes: [], definitions: new TaskTypeDictionary(taskManagerLogger), middleware: createInitialMiddleware(), startingCapacity: 20, diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.ts index 0b1710ae7fa2f..fb6776fa34f28 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.ts @@ -55,7 +55,6 @@ export interface ITaskEventEmitter { export type TaskPollingLifecycleOpts = { logger: Logger; definitions: TaskTypeDictionary; - unusedTypes: string[]; taskStore: TaskStore; config: TaskManagerConfig; middleware: Middleware; @@ -115,7 +114,6 @@ export class TaskPollingLifecycle implements ITaskEventEmitter this.pool.availableCapacity(taskType), taskPartitioner, diff --git a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts index 76df8b7ae5584..fa1d1f749985b 100644 --- a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts +++ b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts @@ -70,7 +70,6 @@ describe('mark_available_tasks_as_claimed', () => { fieldUpdates, claimableTaskTypes: definitions.getAllTypes(), skippedTaskTypes: [], - unusedTaskTypes: [], taskMaxAttempts: Array.from(definitions).reduce((accumulator, [type, { maxAttempts }]) => { return { ...accumulator, [type]: maxAttempts || defaultMaxAttempts }; }, {}), @@ -153,8 +152,6 @@ if (doc['task.runAt'].size()!=0) { ctx._source.task.status = "claiming"; ${Object.keys(fieldUpdates) .map((field) => `ctx._source.task.${field}=params.fieldUpdates.${field};`) .join(' ')} - } else if (params.unusedTaskTypes.contains(ctx._source.task.taskType)) { - ctx._source.task.status = "unrecognized"; } else { ctx.op = "noop"; }`, @@ -167,7 +164,6 @@ if (doc['task.runAt'].size()!=0) { }, claimableTaskTypes: ['sampleTask', 'otherTask'], skippedTaskTypes: [], - unusedTaskTypes: [], taskMaxAttempts: { sampleTask: 5, otherTask: 1, @@ -242,7 +238,6 @@ if (doc['task.runAt'].size()!=0) { fieldUpdates, claimableTaskTypes: ['foo', 'bar'], skippedTaskTypes: [], - unusedTaskTypes: [], taskMaxAttempts: { foo: 5, bar: 2, diff --git a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts index 4e138545aec25..ec99c6ad5bf80 100644 --- a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts +++ b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts @@ -202,7 +202,6 @@ export interface UpdateFieldsAndMarkAsFailedOpts { }; claimableTaskTypes: string[]; skippedTaskTypes: string[]; - unusedTaskTypes: string[]; taskMaxAttempts: { [field: string]: number }; } @@ -210,7 +209,6 @@ export const updateFieldsAndMarkAsFailed = ({ fieldUpdates, claimableTaskTypes, skippedTaskTypes, - unusedTaskTypes, taskMaxAttempts, }: UpdateFieldsAndMarkAsFailedOpts): ScriptClause => { const setScheduledAtScript = `if(ctx._source.task.retryAt != null && ZonedDateTime.parse(ctx._source.task.retryAt).toInstant().toEpochMilli() < params.now) { @@ -227,8 +225,6 @@ export const updateFieldsAndMarkAsFailed = ({ source: ` if (params.claimableTaskTypes.contains(ctx._source.task.taskType)) { ${setScheduledAtAndMarkAsClaimed} - } else if (params.unusedTaskTypes.contains(ctx._source.task.taskType)) { - ctx._source.task.status = "unrecognized"; } else { ctx.op = "noop"; }`, @@ -238,7 +234,6 @@ export const updateFieldsAndMarkAsFailed = ({ fieldUpdates, claimableTaskTypes, skippedTaskTypes, - unusedTaskTypes, taskMaxAttempts, }, }; diff --git a/x-pack/plugins/task_manager/server/queries/task_claiming.test.ts b/x-pack/plugins/task_manager/server/queries/task_claiming.test.ts index 437af8e007bdb..629e3464399c7 100644 --- a/x-pack/plugins/task_manager/server/queries/task_claiming.test.ts +++ b/x-pack/plugins/task_manager/server/queries/task_claiming.test.ts @@ -83,7 +83,6 @@ describe('TaskClaiming', () => { strategy: 'non-default', definitions, excludedTaskTypes: [], - unusedTypes: [], taskStore: taskStoreMock.create({ taskManagerId: '' }), maxAttempts: 2, getAvailableCapacity: () => 10, @@ -134,7 +133,6 @@ describe('TaskClaiming', () => { strategy: 'default', definitions, excludedTaskTypes: [], - unusedTypes: [], taskStore: taskStoreMock.create({ taskManagerId: '' }), maxAttempts: 2, getAvailableCapacity: () => 10, diff --git a/x-pack/plugins/task_manager/server/queries/task_claiming.ts b/x-pack/plugins/task_manager/server/queries/task_claiming.ts index c9bca31755408..1b1e414903628 100644 --- a/x-pack/plugins/task_manager/server/queries/task_claiming.ts +++ b/x-pack/plugins/task_manager/server/queries/task_claiming.ts @@ -34,7 +34,6 @@ export interface TaskClaimingOpts { logger: Logger; strategy: string; definitions: TaskTypeDictionary; - unusedTypes: string[]; taskStore: TaskStore; maxAttempts: number; excludedTaskTypes: string[]; @@ -92,7 +91,6 @@ export class TaskClaiming { private readonly taskClaimingBatchesByType: TaskClaimingBatches; private readonly taskMaxAttempts: Record; private readonly excludedTaskTypes: string[]; - private readonly unusedTypes: string[]; private readonly taskClaimer: TaskClaimerFn; private readonly taskPartitioner: TaskPartitioner; @@ -111,7 +109,6 @@ export class TaskClaiming { this.taskClaimingBatchesByType = this.partitionIntoClaimingBatches(this.definitions); this.taskMaxAttempts = Object.fromEntries(this.normalizeMaxAttempts(this.definitions)); this.excludedTaskTypes = opts.excludedTaskTypes; - this.unusedTypes = opts.unusedTypes; this.taskClaimer = getTaskClaimer(this.logger, opts.strategy); this.events$ = new Subject(); this.taskPartitioner = opts.taskPartitioner; @@ -178,7 +175,6 @@ export class TaskClaiming { taskStore: this.taskStore, events$: this.events$, getCapacity: this.getAvailableCapacity, - unusedTypes: this.unusedTypes, definitions: this.definitions, taskMaxAttempts: this.taskMaxAttempts, excludedTaskTypes: this.excludedTaskTypes, diff --git a/x-pack/plugins/task_manager/server/removed_tasks/mark_removed_tasks_as_unrecognized.test.ts b/x-pack/plugins/task_manager/server/removed_tasks/mark_removed_tasks_as_unrecognized.test.ts new file mode 100644 index 0000000000000..1485216a67f33 --- /dev/null +++ b/x-pack/plugins/task_manager/server/removed_tasks/mark_removed_tasks_as_unrecognized.test.ts @@ -0,0 +1,266 @@ +/* + * 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 { mockLogger } from '../test_utils'; +import { coreMock, elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import { SCHEDULE_INTERVAL, taskRunner } from './mark_removed_tasks_as_unrecognized'; +import { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +const createTaskDoc = (id: string = '1'): SearchHit => ({ + _index: '.kibana_task_manager_9.0.0_001', + _id: `task:${id}`, + _score: 1, + _source: { + references: [], + type: 'task', + updated_at: '2024-11-06T14:17:55.935Z', + task: { + taskType: 'report', + params: '{}', + state: '{"foo":"test"}', + stateVersion: 1, + runAt: '2024-11-06T14:17:55.935Z', + enabled: true, + scheduledAt: '2024-11-06T14:17:55.935Z', + attempts: 0, + status: 'idle', + startedAt: null, + retryAt: null, + ownerId: null, + partition: 211, + }, + }, +}); + +describe('markRemovedTasksAsUnrecognizedTask', () => { + const logger = mockLogger(); + const coreSetup = coreMock.createSetup(); + const esClient = elasticsearchServiceMock.createStart(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('marks removed tasks as unrecognized', async () => { + esClient.client.asInternalUser.bulk.mockResolvedValue({ + errors: false, + took: 0, + items: [ + { + update: { + _index: '.kibana_task_manager_9.0.0_001', + _id: 'task:123', + _version: 2, + result: 'updated', + _shards: { total: 1, successful: 1, failed: 0 }, + _seq_no: 84, + _primary_term: 1, + status: 200, + }, + }, + { + update: { + _index: '.kibana_task_manager_9.0.0_001', + _id: 'task:456', + _version: 2, + result: 'updated', + _shards: { total: 1, successful: 1, failed: 0 }, + _seq_no: 84, + _primary_term: 1, + status: 200, + }, + }, + { + update: { + _index: '.kibana_task_manager_9.0.0_001', + _id: 'task:789', + _version: 2, + result: 'updated', + _shards: { total: 1, successful: 1, failed: 0 }, + _seq_no: 84, + _primary_term: 1, + status: 200, + }, + }, + ], + }); + + coreSetup.getStartServices.mockResolvedValue([ + { + ...coreMock.createStart(), + elasticsearch: esClient, + }, + {}, + coreMock.createSetup(), + ]); + // @ts-expect-error + esClient.client.asInternalUser.search.mockResponse({ + hits: { hits: [createTaskDoc('123'), createTaskDoc('456'), createTaskDoc('789')], total: 3 }, + }); + + const runner = taskRunner(logger, coreSetup.getStartServices)(); + const result = await runner.run(); + + expect(esClient.client.asInternalUser.bulk).toHaveBeenCalledWith({ + body: [ + { update: { _id: 'task:123' } }, + { doc: { task: { status: 'unrecognized' } } }, + { update: { _id: 'task:456' } }, + { doc: { task: { status: 'unrecognized' } } }, + { update: { _id: 'task:789' } }, + { doc: { task: { status: 'unrecognized' } } }, + ], + index: '.kibana_task_manager', + refresh: false, + }); + + expect(logger.debug).toHaveBeenCalledWith(`Marked 3 removed tasks as unrecognized`); + + expect(result).toEqual({ + state: {}, + schedule: { interval: SCHEDULE_INTERVAL }, + }); + }); + + it('skips update when there are no removed task types', async () => { + coreSetup.getStartServices.mockResolvedValue([ + { + ...coreMock.createStart(), + elasticsearch: esClient, + }, + {}, + coreMock.createSetup(), + ]); + // @ts-expect-error + esClient.client.asInternalUser.search.mockResponse({ + hits: { hits: [], total: 0 }, + }); + + const runner = taskRunner(logger, coreSetup.getStartServices)(); + const result = await runner.run(); + + expect(esClient.client.asInternalUser.bulk).not.toHaveBeenCalled(); + + expect(result).toEqual({ + state: {}, + schedule: { interval: SCHEDULE_INTERVAL }, + }); + }); + + it('schedules the next run even when there is an error', async () => { + coreSetup.getStartServices.mockResolvedValue([ + { + ...coreMock.createStart(), + elasticsearch: esClient, + }, + {}, + coreMock.createSetup(), + ]); + esClient.client.asInternalUser.search.mockRejectedValueOnce(new Error('foo')); + + const runner = taskRunner(logger, coreSetup.getStartServices)(); + const result = await runner.run(); + + expect(esClient.client.asInternalUser.bulk).not.toHaveBeenCalled(); + + expect(logger.error).toHaveBeenCalledWith( + 'Failed to mark removed tasks as unrecognized. Error: foo' + ); + + expect(result).toEqual({ + state: {}, + schedule: { interval: SCHEDULE_INTERVAL }, + }); + }); + + it('handles partial errors from bulk partial update', async () => { + esClient.client.asInternalUser.bulk.mockResolvedValue({ + errors: false, + took: 0, + items: [ + { + update: { + _index: '.kibana_task_manager_9.0.0_001', + _id: 'task:123', + _version: 2, + result: 'updated', + _shards: { total: 1, successful: 1, failed: 0 }, + _seq_no: 84, + _primary_term: 1, + status: 200, + }, + }, + { + update: { + _index: '.kibana_task_manager_9.0.0_001', + _id: 'task:456', + _version: 2, + result: 'updated', + _shards: { total: 1, successful: 1, failed: 0 }, + _seq_no: 84, + _primary_term: 1, + status: 200, + }, + }, + { + update: { + _index: '.kibana_task_manager_9.0.0_001', + _id: 'task:789', + _version: 2, + error: { + type: 'document_missing_exception', + reason: '[5]: document missing', + index_uuid: 'aAsFqTI0Tc2W0LCWgPNrOA', + shard: '0', + index: '.kibana_task_manager_9.0.0_001', + }, + status: 404, + }, + }, + ], + }); + + coreSetup.getStartServices.mockResolvedValue([ + { + ...coreMock.createStart(), + elasticsearch: esClient, + }, + {}, + coreMock.createSetup(), + ]); + // @ts-expect-error + esClient.client.asInternalUser.search.mockResponse({ + hits: { hits: [createTaskDoc('123'), createTaskDoc('456'), createTaskDoc('789')], total: 3 }, + }); + + const runner = taskRunner(logger, coreSetup.getStartServices)(); + const result = await runner.run(); + + expect(esClient.client.asInternalUser.bulk).toHaveBeenCalledWith({ + body: [ + { update: { _id: 'task:123' } }, + { doc: { task: { status: 'unrecognized' } } }, + { update: { _id: 'task:456' } }, + { doc: { task: { status: 'unrecognized' } } }, + { update: { _id: 'task:789' } }, + { doc: { task: { status: 'unrecognized' } } }, + ], + index: '.kibana_task_manager', + refresh: false, + }); + expect(logger.warn).toHaveBeenCalledWith( + `Error updating task task:789 to mark as unrecognized - {\"type\":\"document_missing_exception\",\"reason\":\"[5]: document missing\",\"index_uuid\":\"aAsFqTI0Tc2W0LCWgPNrOA\",\"shard\":\"0\",\"index\":\".kibana_task_manager_9.0.0_001\"}` + ); + + expect(logger.debug).toHaveBeenCalledWith(`Marked 2 removed tasks as unrecognized`); + + expect(result).toEqual({ + state: {}, + schedule: { interval: SCHEDULE_INTERVAL }, + }); + }); +}); diff --git a/x-pack/plugins/task_manager/server/removed_tasks/mark_removed_tasks_as_unrecognized.ts b/x-pack/plugins/task_manager/server/removed_tasks/mark_removed_tasks_as_unrecognized.ts new file mode 100644 index 0000000000000..e28d5221e72d5 --- /dev/null +++ b/x-pack/plugins/task_manager/server/removed_tasks/mark_removed_tasks_as_unrecognized.ts @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger } from '@kbn/logging'; +import { CoreStart } from '@kbn/core-lifecycle-server'; +import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { TaskScheduling } from '../task_scheduling'; +import { TaskTypeDictionary } from '../task_type_dictionary'; +import { ConcreteTaskInstance, TaskManagerStartContract } from '..'; +import { TaskStatus } from '../task'; +import { REMOVED_TYPES } from '../task_type_dictionary'; +import { TASK_MANAGER_INDEX } from '../constants'; + +export const TASK_ID = 'mark_removed_tasks_as_unrecognized'; +const TASK_TYPE = `task_manager:${TASK_ID}`; + +export const SCHEDULE_INTERVAL = '1h'; + +export async function scheduleMarkRemovedTasksAsUnrecognizedDefinition( + logger: Logger, + taskScheduling: TaskScheduling +) { + try { + await taskScheduling.ensureScheduled({ + id: TASK_ID, + taskType: TASK_TYPE, + schedule: { interval: SCHEDULE_INTERVAL }, + state: {}, + params: {}, + }); + } catch (e) { + logger.error(`Error scheduling ${TASK_ID} task, received ${e.message}`); + } +} + +export function registerMarkRemovedTasksAsUnrecognizedDefinition( + logger: Logger, + coreStartServices: () => Promise<[CoreStart, TaskManagerStartContract, unknown]>, + taskTypeDictionary: TaskTypeDictionary +) { + taskTypeDictionary.registerTaskDefinitions({ + [TASK_TYPE]: { + title: 'Mark removed tasks as unrecognized', + createTaskRunner: taskRunner(logger, coreStartServices), + }, + }); +} + +export function taskRunner( + logger: Logger, + coreStartServices: () => Promise<[CoreStart, TaskManagerStartContract, unknown]> +) { + return () => { + return { + async run() { + try { + const [{ elasticsearch }] = await coreStartServices(); + const esClient = elasticsearch.client.asInternalUser; + + const removedTasks = await queryForRemovedTasks(esClient); + + if (removedTasks.length > 0) { + await updateTasksToBeUnrecognized(esClient, logger, removedTasks); + } + + return { + state: {}, + schedule: { interval: SCHEDULE_INTERVAL }, + }; + } catch (e) { + logger.error(`Failed to mark removed tasks as unrecognized. Error: ${e.message}`); + return { + state: {}, + schedule: { interval: SCHEDULE_INTERVAL }, + }; + } + }, + }; + }; +} + +async function queryForRemovedTasks( + esClient: ElasticsearchClient +): Promise>> { + const result = await esClient.search({ + index: TASK_MANAGER_INDEX, + body: { + size: 100, + _source: false, + query: { + bool: { + must: [ + { + terms: { + 'task.taskType': REMOVED_TYPES, + }, + }, + ], + }, + }, + }, + }); + + return result.hits.hits; +} + +async function updateTasksToBeUnrecognized( + esClient: ElasticsearchClient, + logger: Logger, + removedTasks: Array> +) { + const bulkBody = []; + for (const task of removedTasks) { + bulkBody.push({ update: { _id: task._id } }); + bulkBody.push({ doc: { task: { status: TaskStatus.Unrecognized } } }); + } + + let removedCount = 0; + try { + const removeResults = await esClient.bulk({ + index: TASK_MANAGER_INDEX, + refresh: false, + body: bulkBody, + }); + for (const removeResult of removeResults.items) { + if (!removeResult.update || !removeResult.update._id) { + logger.warn( + `Error updating task with unknown to mark as unrecognized - malformed response` + ); + } else if (removeResult.update?.error) { + logger.warn( + `Error updating task ${ + removeResult.update._id + } to mark as unrecognized - ${JSON.stringify(removeResult.update.error)}` + ); + } else { + removedCount++; + } + } + logger.debug(`Marked ${removedCount} removed tasks as unrecognized`); + } catch (err) { + // don't worry too much about errors, we'll try again next time + logger.warn(`Error updating tasks to mark as unrecognized: ${err}`); + } +} diff --git a/x-pack/plugins/task_manager/server/task_claimers/index.ts b/x-pack/plugins/task_manager/server/task_claimers/index.ts index 178ebacf68cb9..f41c489fd7550 100644 --- a/x-pack/plugins/task_manager/server/task_claimers/index.ts +++ b/x-pack/plugins/task_manager/server/task_claimers/index.ts @@ -26,7 +26,6 @@ export interface TaskClaimerOpts { events$: Subject; taskStore: TaskStore; definitions: TaskTypeDictionary; - unusedTypes: string[]; excludedTaskTypes: string[]; taskMaxAttempts: Record; logger: Logger; diff --git a/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.test.ts b/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.test.ts index fe44ce9e94c68..07dae3c48a392 100644 --- a/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.test.ts +++ b/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.test.ts @@ -190,7 +190,6 @@ describe('TaskClaiming', () => { definitions, taskStore: store, excludedTaskTypes, - unusedTypes: unusedTaskTypes, maxAttempts: taskClaimingOpts.maxAttempts ?? 2, getAvailableCapacity: taskClaimingOpts.getAvailableCapacity ?? (() => 10), taskPartitioner, @@ -206,20 +205,17 @@ describe('TaskClaiming', () => { claimingOpts, hits = [generateFakeTasks(1)], excludedTaskTypes = [], - unusedTaskTypes = [], }: { storeOpts: Partial; taskClaimingOpts: Partial; claimingOpts: Omit; hits?: ConcreteTaskInstance[][]; excludedTaskTypes?: string[]; - unusedTaskTypes?: string[]; }) { const { taskClaiming, store } = initialiseTestClaiming({ storeOpts, taskClaimingOpts, excludedTaskTypes, - unusedTaskTypes, hits, }); @@ -355,7 +351,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -378,7 +373,7 @@ describe('TaskClaiming', () => { expect(mockApmTrans.end).toHaveBeenCalledWith('success'); expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 3; stale: 0; conflicts: 0; missing: 0; capacity reached: 3; updateErrors: 0; getErrors: 0; removed: 0;', + 'task claimer claimed: 3; stale: 0; conflicts: 0; missing: 0; capacity reached: 3; updateErrors: 0; getErrors: 0;', { tags: ['taskClaiming', 'claimAvailableTasksMget'] } ); @@ -440,312 +435,6 @@ describe('TaskClaiming', () => { expect(result.docs.length).toEqual(3); }); - test('should not claim tasks of removed type', async () => { - const store = taskStoreMock.create({ taskManagerId: 'test-test' }); - store.convertToSavedObjectIds.mockImplementation((ids) => ids.map((id) => `task:${id}`)); - - const fetchedTasks = [ - mockInstance({ id: `id-1`, taskType: 'report' }), - mockInstance({ id: `id-2`, taskType: 'report' }), - mockInstance({ id: `id-3`, taskType: 'yawn' }), - ]; - - const { versionMap, docLatestVersions } = getVersionMapsFromTasks(fetchedTasks); - store.msearch.mockResolvedValueOnce({ docs: fetchedTasks, versionMap }); - store.getDocVersions.mockResolvedValueOnce(docLatestVersions); - - store.bulkGet.mockResolvedValueOnce([fetchedTasks[2]].map(asOk)); - store.bulkPartialUpdate.mockResolvedValueOnce([fetchedTasks[2]].map(getPartialUpdateResult)); - store.bulkPartialUpdate.mockResolvedValueOnce( - [fetchedTasks[0], fetchedTasks[1]].map(getPartialUpdateResult) - ); - - const taskClaiming = new TaskClaiming({ - logger: taskManagerLogger, - strategy: CLAIM_STRATEGY_MGET, - definitions: taskDefinitions, - taskStore: store, - excludedTaskTypes: [], - unusedTypes: ['report'], - maxAttempts: 2, - getAvailableCapacity: () => 10, - taskPartitioner, - }); - - const resultOrErr = await taskClaiming.claimAvailableTasksIfCapacityIsAvailable({ - claimOwnershipUntil: new Date(), - }); - - if (!isOk(resultOrErr)) { - expect(resultOrErr).toBe(undefined); - } - - const result = unwrap(resultOrErr) as ClaimOwnershipResult; - - expect(apm.startTransaction).toHaveBeenCalledWith( - TASK_MANAGER_MARK_AS_CLAIMED, - TASK_MANAGER_TRANSACTION_TYPE - ); - expect(mockApmTrans.end).toHaveBeenCalledWith('success'); - - expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 1; stale: 0; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0; removed: 2;', - { tags: ['taskClaiming', 'claimAvailableTasksMget'] } - ); - - expect(store.msearch.mock.calls[0][0]?.[0]).toMatchObject({ - size: 40, - seq_no_primary_term: true, - }); - expect(store.getDocVersions).toHaveBeenCalledWith(['task:id-1', 'task:id-2', 'task:id-3']); - expect(store.bulkPartialUpdate).toHaveBeenCalledTimes(2); - expect(store.bulkPartialUpdate).toHaveBeenNthCalledWith(1, [ - { - id: fetchedTasks[2].id, - version: fetchedTasks[2].version, - scheduledAt: fetchedTasks[2].runAt, - attempts: 1, - ownerId: 'test-test', - retryAt: new Date('1970-01-01T00:05:30.000Z'), - status: 'running', - startedAt: new Date('1970-01-01T00:00:00.000Z'), - }, - ]); - expect(store.bulkPartialUpdate).toHaveBeenNthCalledWith(2, [ - { - id: fetchedTasks[0].id, - version: fetchedTasks[0].version, - status: 'unrecognized', - }, - { - id: fetchedTasks[1].id, - version: fetchedTasks[1].version, - status: 'unrecognized', - }, - ]); - expect(store.bulkGet).toHaveBeenCalledWith(['id-3']); - - expect(result.stats).toEqual({ - tasksClaimed: 1, - tasksConflicted: 0, - tasksErrors: 0, - tasksUpdated: 1, - tasksLeftUnclaimed: 0, - staleTasks: 0, - }); - expect(result.docs.length).toEqual(1); - }); - - test('should log warning if error updating single removed task as unrecognized', async () => { - const store = taskStoreMock.create({ taskManagerId: 'test-test' }); - store.convertToSavedObjectIds.mockImplementation((ids) => ids.map((id) => `task:${id}`)); - - const fetchedTasks = [ - mockInstance({ id: `id-1`, taskType: 'report' }), - mockInstance({ id: `id-2`, taskType: 'report' }), - mockInstance({ id: `id-3`, taskType: 'yawn' }), - ]; - - const { versionMap, docLatestVersions } = getVersionMapsFromTasks(fetchedTasks); - store.msearch.mockResolvedValueOnce({ docs: fetchedTasks, versionMap }); - store.getDocVersions.mockResolvedValueOnce(docLatestVersions); - - store.bulkGet.mockResolvedValueOnce([fetchedTasks[2]].map(asOk)); - store.bulkPartialUpdate.mockResolvedValueOnce([fetchedTasks[2]].map(getPartialUpdateResult)); - store.bulkPartialUpdate.mockResolvedValueOnce([ - asOk(fetchedTasks[0]), - asErr({ - type: 'task', - id: fetchedTasks[1].id, - status: 404, - error: { - type: 'document_missing_exception', - reason: '[5]: document missing', - index_uuid: 'aAsFqTI0Tc2W0LCWgPNrOA', - shard: '0', - index: '.kibana_task_manager_8.16.0_001', - }, - }), - ]); - - const taskClaiming = new TaskClaiming({ - logger: taskManagerLogger, - strategy: CLAIM_STRATEGY_MGET, - definitions: taskDefinitions, - taskStore: store, - excludedTaskTypes: [], - unusedTypes: ['report'], - maxAttempts: 2, - getAvailableCapacity: () => 10, - taskPartitioner, - }); - - const resultOrErr = await taskClaiming.claimAvailableTasksIfCapacityIsAvailable({ - claimOwnershipUntil: new Date(), - }); - - if (!isOk(resultOrErr)) { - expect(resultOrErr).toBe(undefined); - } - - const result = unwrap(resultOrErr) as ClaimOwnershipResult; - - expect(apm.startTransaction).toHaveBeenCalledWith( - TASK_MANAGER_MARK_AS_CLAIMED, - TASK_MANAGER_TRANSACTION_TYPE - ); - expect(mockApmTrans.end).toHaveBeenCalledWith('success'); - - expect(taskManagerLogger.warn).toHaveBeenCalledWith( - 'Error updating task id-2:task to mark as unrecognized during claim: {"type":"document_missing_exception","reason":"[5]: document missing","index_uuid":"aAsFqTI0Tc2W0LCWgPNrOA","shard":"0","index":".kibana_task_manager_8.16.0_001"}', - { tags: ['taskClaiming', 'claimAvailableTasksMget'] } - ); - expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 1; stale: 0; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0; removed: 1;', - { tags: ['taskClaiming', 'claimAvailableTasksMget'] } - ); - - expect(store.msearch.mock.calls[0][0]?.[0]).toMatchObject({ - size: 40, - seq_no_primary_term: true, - }); - expect(store.getDocVersions).toHaveBeenCalledWith(['task:id-1', 'task:id-2', 'task:id-3']); - expect(store.bulkPartialUpdate).toHaveBeenCalledTimes(2); - expect(store.bulkPartialUpdate).toHaveBeenNthCalledWith(1, [ - { - id: fetchedTasks[2].id, - version: fetchedTasks[2].version, - scheduledAt: fetchedTasks[2].runAt, - attempts: 1, - ownerId: 'test-test', - retryAt: new Date('1970-01-01T00:05:30.000Z'), - status: 'running', - startedAt: new Date('1970-01-01T00:00:00.000Z'), - }, - ]); - expect(store.bulkPartialUpdate).toHaveBeenNthCalledWith(2, [ - { - id: fetchedTasks[0].id, - version: fetchedTasks[0].version, - status: 'unrecognized', - }, - { - id: fetchedTasks[1].id, - version: fetchedTasks[1].version, - status: 'unrecognized', - }, - ]); - expect(store.bulkGet).toHaveBeenCalledWith(['id-3']); - - expect(result.stats).toEqual({ - tasksClaimed: 1, - tasksConflicted: 0, - tasksErrors: 0, - tasksUpdated: 1, - tasksLeftUnclaimed: 0, - staleTasks: 0, - }); - expect(result.docs.length).toEqual(1); - }); - - test('should log warning if error updating all removed tasks as unrecognized', async () => { - const store = taskStoreMock.create({ taskManagerId: 'test-test' }); - store.convertToSavedObjectIds.mockImplementation((ids) => ids.map((id) => `task:${id}`)); - - const fetchedTasks = [ - mockInstance({ id: `id-1`, taskType: 'report' }), - mockInstance({ id: `id-2`, taskType: 'report' }), - mockInstance({ id: `id-3`, taskType: 'yawn' }), - ]; - - const { versionMap, docLatestVersions } = getVersionMapsFromTasks(fetchedTasks); - store.msearch.mockResolvedValueOnce({ docs: fetchedTasks, versionMap }); - store.getDocVersions.mockResolvedValueOnce(docLatestVersions); - - store.bulkGet.mockResolvedValueOnce([fetchedTasks[2]].map(asOk)); - store.bulkPartialUpdate.mockResolvedValueOnce([fetchedTasks[2]].map(getPartialUpdateResult)); - store.bulkPartialUpdate.mockRejectedValueOnce(new Error('Oh no')); - - const taskClaiming = new TaskClaiming({ - logger: taskManagerLogger, - strategy: CLAIM_STRATEGY_MGET, - definitions: taskDefinitions, - taskStore: store, - excludedTaskTypes: [], - unusedTypes: ['report'], - maxAttempts: 2, - getAvailableCapacity: () => 10, - taskPartitioner, - }); - - const resultOrErr = await taskClaiming.claimAvailableTasksIfCapacityIsAvailable({ - claimOwnershipUntil: new Date(), - }); - - if (!isOk(resultOrErr)) { - expect(resultOrErr).toBe(undefined); - } - - const result = unwrap(resultOrErr) as ClaimOwnershipResult; - - expect(apm.startTransaction).toHaveBeenCalledWith( - TASK_MANAGER_MARK_AS_CLAIMED, - TASK_MANAGER_TRANSACTION_TYPE - ); - expect(mockApmTrans.end).toHaveBeenCalledWith('success'); - - expect(taskManagerLogger.warn).toHaveBeenCalledWith( - 'Error updating tasks to mark as unrecognized during claim: Error: Oh no', - { tags: ['taskClaiming', 'claimAvailableTasksMget'] } - ); - expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 1; stale: 0; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0; removed: 0;', - { tags: ['taskClaiming', 'claimAvailableTasksMget'] } - ); - - expect(store.msearch.mock.calls[0][0]?.[0]).toMatchObject({ - size: 40, - seq_no_primary_term: true, - }); - expect(store.getDocVersions).toHaveBeenCalledWith(['task:id-1', 'task:id-2', 'task:id-3']); - expect(store.bulkGet).toHaveBeenCalledWith(['id-3']); - expect(store.bulkPartialUpdate).toHaveBeenCalledTimes(2); - expect(store.bulkPartialUpdate).toHaveBeenNthCalledWith(1, [ - { - id: fetchedTasks[2].id, - version: fetchedTasks[2].version, - scheduledAt: fetchedTasks[2].runAt, - attempts: 1, - ownerId: 'test-test', - retryAt: new Date('1970-01-01T00:05:30.000Z'), - status: 'running', - startedAt: new Date('1970-01-01T00:00:00.000Z'), - }, - ]); - expect(store.bulkPartialUpdate).toHaveBeenNthCalledWith(2, [ - { - id: fetchedTasks[0].id, - version: fetchedTasks[0].version, - status: 'unrecognized', - }, - { - id: fetchedTasks[1].id, - version: fetchedTasks[1].version, - status: 'unrecognized', - }, - ]); - - expect(result.stats).toEqual({ - tasksClaimed: 1, - tasksConflicted: 0, - tasksErrors: 0, - tasksUpdated: 1, - tasksLeftUnclaimed: 0, - staleTasks: 0, - }); - expect(result.docs.length).toEqual(1); - }); - test('should handle no tasks to claim', async () => { const store = taskStoreMock.create({ taskManagerId: 'test-test' }); store.convertToSavedObjectIds.mockImplementation((ids) => ids.map((id) => `task:${id}`)); @@ -761,7 +450,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -828,7 +516,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -851,7 +538,7 @@ describe('TaskClaiming', () => { expect(mockApmTrans.end).toHaveBeenCalledWith('success'); expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 2; stale: 0; conflicts: 0; missing: 1; capacity reached: 0; updateErrors: 0; getErrors: 0; removed: 0;', + 'task claimer claimed: 2; stale: 0; conflicts: 0; missing: 1; capacity reached: 0; updateErrors: 0; getErrors: 0;', { tags: ['taskClaiming', 'claimAvailableTasksMget'] } ); @@ -922,7 +609,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -945,7 +631,7 @@ describe('TaskClaiming', () => { expect(mockApmTrans.end).toHaveBeenCalledWith('success'); expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 2; stale: 0; conflicts: 0; missing: 1; capacity reached: 0; updateErrors: 0; getErrors: 0; removed: 0;', + 'task claimer claimed: 2; stale: 0; conflicts: 0; missing: 1; capacity reached: 0; updateErrors: 0; getErrors: 0;', { tags: ['taskClaiming', 'claimAvailableTasksMget'] } ); @@ -1016,7 +702,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -1039,7 +724,7 @@ describe('TaskClaiming', () => { expect(mockApmTrans.end).toHaveBeenCalledWith('success'); expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 2; stale: 1; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0; removed: 0;', + 'task claimer claimed: 2; stale: 1; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0;', { tags: ['taskClaiming', 'claimAvailableTasksMget'] } ); @@ -1116,7 +801,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -1139,7 +823,7 @@ describe('TaskClaiming', () => { expect(mockApmTrans.end).toHaveBeenCalledWith('success'); expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 4; stale: 0; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0; removed: 0;', + 'task claimer claimed: 4; stale: 0; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0;', { tags: ['taskClaiming', 'claimAvailableTasksMget'] } ); @@ -1248,7 +932,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -1271,7 +954,7 @@ describe('TaskClaiming', () => { expect(mockApmTrans.end).toHaveBeenCalledWith('success'); expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 3; stale: 0; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 1; removed: 0;', + 'task claimer claimed: 3; stale: 0; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 1;', { tags: ['taskClaiming', 'claimAvailableTasksMget'] } ); expect(taskManagerLogger.error).toHaveBeenCalledWith( @@ -1377,7 +1060,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -1400,7 +1082,7 @@ describe('TaskClaiming', () => { expect(mockApmTrans.end).toHaveBeenCalledWith('success'); expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 3; stale: 0; conflicts: 1; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0; removed: 0;', + 'task claimer claimed: 3; stale: 0; conflicts: 1; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0;', { tags: ['taskClaiming', 'claimAvailableTasksMget'] } ); expect(taskManagerLogger.warn).toHaveBeenCalledWith( @@ -1504,7 +1186,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -1619,7 +1300,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -1642,7 +1322,7 @@ describe('TaskClaiming', () => { expect(mockApmTrans.end).toHaveBeenCalledWith('success'); expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 3; stale: 0; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 1; getErrors: 0; removed: 0;', + 'task claimer claimed: 3; stale: 0; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 1; getErrors: 0;', { tags: ['taskClaiming', 'claimAvailableTasksMget'] } ); expect(taskManagerLogger.error).toHaveBeenCalledWith( @@ -1753,7 +1433,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -1776,7 +1455,7 @@ describe('TaskClaiming', () => { expect(mockApmTrans.end).toHaveBeenCalledWith('success'); expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 3; stale: 0; conflicts: 1; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0; removed: 0;', + 'task claimer claimed: 3; stale: 0; conflicts: 1; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0;', { tags: ['taskClaiming', 'claimAvailableTasksMget'] } ); expect(taskManagerLogger.error).not.toHaveBeenCalled(); @@ -1870,7 +1549,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -2488,7 +2166,6 @@ describe('TaskClaiming', () => { strategy: CLAIM_STRATEGY_MGET, definitions, excludedTaskTypes: [], - unusedTypes: [], taskStore, maxAttempts: 2, getAvailableCapacity, diff --git a/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.ts b/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.ts index 16d9ba5c7fae7..431daab8dd2cb 100644 --- a/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.ts +++ b/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.ts @@ -57,7 +57,6 @@ interface OwnershipClaimingOpts { claimOwnershipUntil: Date; size: number; taskTypes: Set; - removedTypes: Set; getCapacity: (taskType?: string | undefined) => number; excludedTaskTypePatterns: string[]; taskStore: TaskStore; @@ -90,19 +89,16 @@ export async function claimAvailableTasksMget( async function claimAvailableTasks(opts: TaskClaimerOpts): Promise { const { getCapacity, claimOwnershipUntil, batches, events$, taskStore, taskPartitioner } = opts; - const { definitions, unusedTypes, excludedTaskTypes, taskMaxAttempts } = opts; + const { definitions, excludedTaskTypes, taskMaxAttempts } = opts; const logger = createWrappedLogger({ logger: opts.logger, tags: [claimAvailableTasksMget.name] }); const initialCapacity = getCapacity(); const stopTaskTimer = startTaskTimer(); - const removedTypes = new Set(unusedTypes); // REMOVED_TYPES - // get a list of candidate tasks to claim, with their version info const { docs, versionMap } = await searchAvailableTasks({ definitions, taskTypes: new Set(definitions.getAllTypes()), excludedTaskTypePatterns: excludedTaskTypes, - removedTypes, taskStore, events$, claimOwnershipUntil, @@ -125,18 +121,12 @@ async function claimAvailableTasks(opts: TaskClaimerOpts): Promise `task:${doc.id}`)); - // filter out stale, missing and removed tasks + // filter out stale and missing tasks const currentTasks: ConcreteTaskInstance[] = []; const staleTasks: ConcreteTaskInstance[] = []; const missingTasks: ConcreteTaskInstance[] = []; - const removedTasks: ConcreteTaskInstance[] = []; for (const searchDoc of docs) { - if (removedTypes.has(searchDoc.taskType)) { - removedTasks.push(searchDoc); - continue; - } - const searchVersion = versionMap.get(searchDoc.id); const latestVersion = docLatestVersions.get(`task:${searchDoc.id}`); if (!searchVersion || !latestVersion) { @@ -236,42 +226,8 @@ async function claimAvailableTasks(opts: TaskClaimerOpts): Promise 0) { - const tasksToRemove = Array.from(removedTasks); - const tasksToRemoveUpdates: PartialConcreteTaskInstance[] = []; - for (const task of tasksToRemove) { - tasksToRemoveUpdates.push({ - id: task.id, - status: TaskStatus.Unrecognized, - }); - } - - // don't worry too much about errors, we'll get them next time - try { - const removeResults = await taskStore.bulkPartialUpdate(tasksToRemoveUpdates); - for (const removeResult of removeResults) { - if (isOk(removeResult)) { - removedCount++; - } else { - const { id, type, error } = removeResult.error; - logger.warn( - `Error updating task ${id}:${type} to mark as unrecognized during claim: ${JSON.stringify( - error - )}` - ); - } - } - } catch (err) { - // swallow the error because this is unrelated to the claim cycle - logger.warn(`Error updating tasks to mark as unrecognized during claim: ${err}`); - } - } - // TODO: need a better way to generate stats - const message = `task claimer claimed: ${fullTasksToRun.length}; stale: ${staleTasks.length}; conflicts: ${conflicts}; missing: ${missingTasks.length}; capacity reached: ${leftOverTasks.length}; updateErrors: ${bulkUpdateErrors}; getErrors: ${bulkGetErrors}; removed: ${removedCount};`; + const message = `task claimer claimed: ${fullTasksToRun.length}; stale: ${staleTasks.length}; conflicts: ${conflicts}; missing: ${missingTasks.length}; capacity reached: ${leftOverTasks.length}; updateErrors: ${bulkUpdateErrors}; getErrors: ${bulkGetErrors};`; logger.debug(message); // build results @@ -306,7 +262,6 @@ export const NO_ASSIGNED_PARTITIONS_WARNING_INTERVAL = 60000; async function searchAvailableTasks({ definitions, taskTypes, - removedTypes, excludedTaskTypePatterns, taskStore, getCapacity, @@ -318,7 +273,6 @@ async function searchAvailableTasks({ const claimPartitions = buildClaimPartitions({ types: taskTypes, excludedTaskTypes, - removedTypes, getCapacity, definitions, }); @@ -352,10 +306,7 @@ async function searchAvailableTasks({ // Task must be enabled EnabledTask, // a task type that's not excluded (may be removed or not) - OneOfTaskTypes( - 'task.taskType', - claimPartitions.unlimitedTypes.concat(Array.from(removedTypes)) - ), + OneOfTaskTypes('task.taskType', claimPartitions.unlimitedTypes), // Either a task with idle status and runAt <= now or // status running or claiming with a retryAt <= now. shouldBeOneOf(IdleTaskWithExpiredRunAt, RunningOrClaimingTaskWithExpiredRetryAt), @@ -407,7 +358,6 @@ async function searchAvailableTasks({ } interface ClaimPartitions { - removedTypes: string[]; unlimitedTypes: string[]; limitedTypes: Map; } @@ -415,30 +365,23 @@ interface ClaimPartitions { interface BuildClaimPartitionsOpts { types: Set; excludedTaskTypes: Set; - removedTypes: Set; getCapacity: (taskType?: string) => number; definitions: TaskTypeDictionary; } function buildClaimPartitions(opts: BuildClaimPartitionsOpts): ClaimPartitions { const result: ClaimPartitions = { - removedTypes: [], unlimitedTypes: [], limitedTypes: new Map(), }; - const { types, excludedTaskTypes, removedTypes, getCapacity, definitions } = opts; + const { types, excludedTaskTypes, getCapacity, definitions } = opts; for (const type of types) { const definition = definitions.get(type); if (definition == null) continue; if (excludedTaskTypes.has(type)) continue; - if (removedTypes.has(type)) { - result.removedTypes.push(type); - continue; - } - if (definition.maxConcurrency == null) { result.unlimitedTypes.push(definition.type); continue; diff --git a/x-pack/plugins/task_manager/server/task_claimers/strategy_update_by_query.test.ts b/x-pack/plugins/task_manager/server/task_claimers/strategy_update_by_query.test.ts index 13e6faf2de0fd..623693e71c54d 100644 --- a/x-pack/plugins/task_manager/server/task_claimers/strategy_update_by_query.test.ts +++ b/x-pack/plugins/task_manager/server/task_claimers/strategy_update_by_query.test.ts @@ -99,14 +99,12 @@ describe('TaskClaiming', () => { hits = [generateFakeTasks(1)], versionConflicts = 2, excludedTaskTypes = [], - unusedTaskTypes = [], }: { storeOpts: Partial; taskClaimingOpts: Partial; hits?: ConcreteTaskInstance[][]; versionConflicts?: number; excludedTaskTypes?: string[]; - unusedTaskTypes?: string[]; }) { const definitions = storeOpts.definitions ?? taskDefinitions; const store = taskStoreMock.create({ taskManagerId: storeOpts.taskManagerId }); @@ -136,7 +134,6 @@ describe('TaskClaiming', () => { definitions, taskStore: store, excludedTaskTypes, - unusedTypes: unusedTaskTypes, maxAttempts: taskClaimingOpts.maxAttempts ?? 2, getAvailableCapacity: taskClaimingOpts.getAvailableCapacity ?? (() => 10), taskPartitioner, @@ -153,7 +150,6 @@ describe('TaskClaiming', () => { hits = [generateFakeTasks(1)], versionConflicts = 2, excludedTaskTypes = [], - unusedTaskTypes = [], }: { storeOpts: Partial; taskClaimingOpts: Partial; @@ -161,14 +157,12 @@ describe('TaskClaiming', () => { hits?: ConcreteTaskInstance[][]; versionConflicts?: number; excludedTaskTypes?: string[]; - unusedTaskTypes?: string[]; }) { const getCapacity = taskClaimingOpts.getAvailableCapacity ?? (() => 10); const { taskClaiming, store } = initialiseTestClaiming({ storeOpts, taskClaimingOpts, excludedTaskTypes, - unusedTaskTypes, hits, versionConflicts, }); @@ -471,7 +465,6 @@ if (doc['task.runAt'].size()!=0) { 'anotherLimitedToOne', 'limitedToTwo', ], - unusedTaskTypes: [], taskMaxAttempts: { unlimited: maxAttempts, }, @@ -493,7 +486,6 @@ if (doc['task.runAt'].size()!=0) { 'anotherLimitedToOne', 'limitedToTwo', ], - unusedTaskTypes: [], taskMaxAttempts: { limitedToOne: maxAttempts, }, @@ -640,7 +632,6 @@ if (doc['task.runAt'].size()!=0) { }, taskPartitioner, excludedTaskTypes: [], - unusedTypes: [], }); const resultOrErr = await taskClaiming.claimAvailableTasksIfCapacityIsAvailable({ @@ -848,129 +839,6 @@ if (doc['task.runAt'].size()!=0) { expect(firstCycle).not.toMatchObject(secondCycle); }); - test('it passes any unusedTaskTypes to script', async () => { - const maxAttempts = _.random(2, 43); - const customMaxAttempts = _.random(44, 100); - const taskManagerId = uuidv1(); - const fieldUpdates = { - ownerId: taskManagerId, - retryAt: new Date(Date.now()), - }; - const definitions = new TaskTypeDictionary(mockLogger()); - definitions.registerTaskDefinitions({ - foo: { - title: 'foo', - createTaskRunner: jest.fn(), - }, - bar: { - title: 'bar', - maxAttempts: customMaxAttempts, - createTaskRunner: jest.fn(), - }, - foobar: { - title: 'foobar', - maxAttempts: customMaxAttempts, - createTaskRunner: jest.fn(), - }, - }); - - const { - args: { - updateByQuery: [{ query, script }], - }, - } = await testClaimAvailableTasks({ - storeOpts: { - definitions, - taskManagerId, - }, - taskClaimingOpts: { - maxAttempts, - }, - claimingOpts: { - claimOwnershipUntil: new Date(), - }, - excludedTaskTypes: ['foobar'], - unusedTaskTypes: ['barfoo'], - }); - expect(query).toMatchObject({ - bool: { - must: [ - { - bool: { - must: [ - { - term: { - 'task.enabled': true, - }, - }, - ], - }, - }, - { - bool: { - should: [ - { - bool: { - must: [ - { term: { 'task.status': 'idle' } }, - { range: { 'task.runAt': { lte: 'now' } } }, - ], - }, - }, - { - bool: { - must: [ - { - bool: { - should: [ - { term: { 'task.status': 'running' } }, - { term: { 'task.status': 'claiming' } }, - ], - }, - }, - { range: { 'task.retryAt': { lte: 'now' } } }, - ], - }, - }, - ], - }, - }, - ], - filter: [ - { - bool: { - must_not: [ - { - bool: { - should: [ - { term: { 'task.status': 'running' } }, - { term: { 'task.status': 'claiming' } }, - ], - must: { range: { 'task.retryAt': { gt: 'now' } } }, - }, - }, - ], - }, - }, - ], - }, - }); - expect(script).toMatchObject({ - source: expect.any(String), - lang: 'painless', - params: { - fieldUpdates, - claimableTaskTypes: ['foo', 'bar'], - skippedTaskTypes: ['foobar'], - unusedTaskTypes: ['barfoo'], - taskMaxAttempts: { - bar: customMaxAttempts, - foo: maxAttempts, - }, - }, - }); - }); - test('it claims tasks by setting their ownerId, status and retryAt', async () => { const taskManagerId = uuidv1(); const claimOwnershipUntil = new Date(Date.now()); @@ -1356,7 +1224,6 @@ if (doc['task.runAt'].size()!=0) { strategy: 'update_by_query', definitions, excludedTaskTypes: [], - unusedTypes: [], taskStore, maxAttempts: 2, getAvailableCapacity, diff --git a/x-pack/plugins/task_manager/server/task_claimers/strategy_update_by_query.ts b/x-pack/plugins/task_manager/server/task_claimers/strategy_update_by_query.ts index 5a4bccb43b984..fdfd09e07f9c7 100644 --- a/x-pack/plugins/task_manager/server/task_claimers/strategy_update_by_query.ts +++ b/x-pack/plugins/task_manager/server/task_claimers/strategy_update_by_query.ts @@ -51,7 +51,6 @@ interface OwnershipClaimingOpts { taskStore: TaskStore; events$: Subject; definitions: TaskTypeDictionary; - unusedTypes: string[]; excludedTaskTypes: string[]; taskMaxAttempts: Record; } @@ -60,7 +59,7 @@ export async function claimAvailableTasksUpdateByQuery( opts: TaskClaimerOpts ): Promise { const { getCapacity, claimOwnershipUntil, batches, events$, taskStore } = opts; - const { definitions, unusedTypes, excludedTaskTypes, taskMaxAttempts } = opts; + const { definitions, excludedTaskTypes, taskMaxAttempts } = opts; const initialCapacity = getCapacity(); let accumulatedResult = getEmptyClaimOwnershipResult(); @@ -83,7 +82,6 @@ export async function claimAvailableTasksUpdateByQuery( taskTypes: isLimited(batch) ? new Set([batch.tasksTypes]) : batch.tasksTypes, taskStore, definitions, - unusedTypes, excludedTaskTypes, taskMaxAttempts, }); @@ -137,7 +135,6 @@ async function markAvailableTasksAsClaimed({ claimOwnershipUntil, size, taskTypes, - unusedTypes, taskMaxAttempts, }: OwnershipClaimingOpts): Promise { const { taskTypesToSkip = [], taskTypesToClaim = [] } = groupBy( @@ -164,7 +161,6 @@ async function markAvailableTasksAsClaimed({ }, claimableTaskTypes: taskTypesToClaim, skippedTaskTypes: taskTypesToSkip, - unusedTaskTypes: unusedTypes, taskMaxAttempts: pick(taskMaxAttempts, taskTypesToClaim), }); diff --git a/x-pack/plugins/task_manager/tsconfig.json b/x-pack/plugins/task_manager/tsconfig.json index 55ad764c5bdcd..b11eaaf44a905 100644 --- a/x-pack/plugins/task_manager/tsconfig.json +++ b/x-pack/plugins/task_manager/tsconfig.json @@ -27,7 +27,8 @@ "@kbn/logging", "@kbn/core-lifecycle-server", "@kbn/cloud-plugin", - "@kbn/core-saved-objects-base-server-internal" + "@kbn/core-saved-objects-base-server-internal", + "@kbn/core-elasticsearch-server", ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 0008cac9a55dd..f03c509001511 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -862,11 +862,7 @@ "controls.controlGroup.manageControl.dataSource.dataViewTitle": "Vue de données", "controls.controlGroup.manageControl.dataSource.fieldListErrorTitle": "Erreur lors du chargement de la liste des champs", "controls.controlGroup.manageControl.dataSource.fieldTitle": "Champ", - "controls.controlGroup.manageControl.dataSource.formGroupDescription": "Sélectionnez la vue de données et le champ pour lesquels vous voulez créer un contrôle.", - "controls.controlGroup.manageControl.dataSource.formGroupTitle": "Source de données", "controls.controlGroup.manageControl.dataSource.selectDataViewMessage": "Veuillez sélectionner une vue de données", - "controls.controlGroup.manageControl.displaySettings.formGroupDescription": "Changez la manière dont le contrôle apparaît sur votre tableau de bord.", - "controls.controlGroup.manageControl.displaySettings.formGroupTitle": "Paramètres d'affichage", "controls.controlGroup.manageControl.displaySettings.growSwitchTitle": "Augmenter la largeur en fonction de l'espace disponible", "controls.controlGroup.manageControl.displaySettings.titleInputTitle": "Étiquette", "controls.controlGroup.manageControl.displaySettings.widthInputTitle": "Largeur minimale", @@ -36088,32 +36084,31 @@ "xpack.searchIndices.server.createIndex.errorMessage": "Échec de la création de l'index en raison d'une exception. {errorMessage}", "xpack.searchIndices.server.deleteDocument.errorMessage": "Impossible de supprimer le document", "xpack.searchIndices.settingsTabLabel": "Paramètres", - "xpack.searchIndices.startPage.codeView.apiKeyDescription": "Assurez-vous de le conserver dans un endroit sûr. Vous ne pourrez pas le récupérer plus tard.", - "xpack.searchIndices.startPage.codeView.apiKeyTitle": "Copier votre clé d'API", - "xpack.searchIndices.startPage.codeView.explicitGenerate.apiKeyDescription": "Créez une clé d'API pour vous connecter à Elasticsearch.", - "xpack.searchIndices.startPage.codeView.explicitGenerate.apiKeyTitle": "Créer une clé d'API", - "xpack.searchIndices.startPage.createIndex.action.text": "Créer mon index", - "xpack.searchIndices.startPage.createIndex.apiKeyCreation.description": "Nous allons créer une clé d'API pour cet index", - "xpack.searchIndices.startPage.createIndex.description": "Un index stocke vos données et définit le schéma, ou les mappings de champs, pour vos recherches", - "xpack.searchIndices.startPage.createIndex.fileUpload.link": "Charger un fichier", - "xpack.searchIndices.startPage.createIndex.fileUpload.text": "Vous disposez déjà de données ? {link}", - "xpack.searchIndices.startPage.createIndex.name.helpText": "Les noms d'index doivent être en minuscules et ne peuvent contenir que des tirets et des chiffres", - "xpack.searchIndices.startPage.createIndex.name.label": "Nommer votre index", - "xpack.searchIndices.startPage.createIndex.name.placeholder": "Définir un nom pour votre index", - "xpack.searchIndices.startPage.createIndex.permissionTooltip": "Vous ne disposez pas d'autorisation pour créer un index.", + "xpack.searchIndices.shared.codeView.apiKeyDescription": "Assurez-vous de le conserver dans un endroit sûr. Vous ne pourrez pas le récupérer plus tard.", + "xpack.searchIndices.shared.codeView.apiKeyTitle": "Copier votre clé d'API", + "xpack.searchIndices.shared.codeView.explicitGenerate.apiKeyDescription": "Créez une clé d'API pour vous connecter à Elasticsearch.", + "xpack.searchIndices.shared.codeView.explicitGenerate.apiKeyTitle": "Créer une clé d'API", + "xpack.searchIndices.shared.createIndex.action.text": "Créer mon index", + "xpack.searchIndices.shared.createIndex.apiKeyCreation.description": "Nous allons créer une clé d'API pour cet index", + "xpack.searchIndices.shared.createIndex.description": "Un index stocke vos données et définit le schéma, ou les mappings de champs, pour vos recherches", + "xpack.searchIndices.shared.createIndex.fileUpload.link": "Charger un fichier", + "xpack.searchIndices.shared.createIndex.fileUpload.text": "Vous disposez déjà de données ? {link}", + "xpack.searchIndices.shared.createIndex.name.helpText": "Les noms d'index doivent être en minuscules et ne peuvent contenir que des tirets et des chiffres", + "xpack.searchIndices.shared.createIndex.name.label": "Nommer votre index", + "xpack.searchIndices.shared.createIndex.name.placeholder": "Définir un nom pour votre index", + "xpack.searchIndices.shared.createIndex.observabilityCallout.logs.button": "Collectez et analysez les logs", + "xpack.searchIndices.shared.createIndex.observabilityCallout.logs.subTitle": "Explorer Logstash et Beats", + "xpack.searchIndices.shared.createIndex.observabilityCallout.o11yTrial.button": "Démarrer un essai d'Observability", + "xpack.searchIndices.shared.createIndex.observabilityCallout.o11yTrial.subTitle": "Puissant monitoring des performances", + "xpack.searchIndices.shared.createIndex.observabilityCallout.title": "Vous cherchez à stocker vos logs ou vos données d'indicateurs ?", + "xpack.searchIndices.shared.createIndex.pageTitle": "Elasticsearch", + "xpack.searchIndices.shared.createIndex.permissionTooltip": "Vous ne disposez pas d'autorisation pour créer un index.", + "xpack.searchIndices.shared.createIndex.viewSelect.code": "Code", + "xpack.searchIndices.shared.createIndex.viewSelect.legend": "Créer une sélection de vue d'index", + "xpack.searchIndices.shared.createIndex.viewSelect.ui": "Interface utilisateur", + "xpack.searchIndices.shared.statusFetchError.title": "Erreur lors du chargement des index", + "xpack.searchIndices.shared.statusFetchError.unknownError": "Erreur inconnue lors de la récupération des index.", "xpack.searchIndices.startPage.createIndex.title": "Créer votre premier index", - "xpack.searchIndices.startPage.createIndex.viewSelec.legend": "Créer une sélection de vue d'index", - "xpack.searchIndices.startPage.createIndex.viewSelect.code": "Code", - "xpack.searchIndices.startPage.createIndex.viewSelect.ui": "Interface utilisateur", - "xpack.searchIndices.startPage.observabilityCallout.logs.button": "Collectez et analysez les logs", - "xpack.searchIndices.startPage.observabilityCallout.logs.subTitle": "Explorer Logstash et Beats", - "xpack.searchIndices.startPage.observabilityCallout.o11yTrial.button": "Démarrer un essai d'Observability", - "xpack.searchIndices.startPage.observabilityCallout.o11yTrial.subTitle": "Puissant monitoring des performances", - "xpack.searchIndices.startPage.observabilityCallout.title": "Vous cherchez à stocker vos logs ou vos données d'indicateurs ?", - "xpack.searchIndices.startPage.pageDescription": "Vectorisez, recherchez et visualisez vos données", - "xpack.searchIndices.startPage.pageTitle": "Elasticsearch", - "xpack.searchIndices.startPage.statusFetchError.title": "Erreur lors du chargement des index", - "xpack.searchIndices.startPage.statusFetchError.unknownError": "Erreur inconnue lors de la récupération des index.", "xpack.searchInferenceEndpoints.actions.copyID": "Copier l'identifiant du point de terminaison d'inférence {inferenceId}", "xpack.searchInferenceEndpoints.actions.copyIDSuccess": "Identifiant du point de terminaison d'inférence {inferenceId} copié", "xpack.searchInferenceEndpoints.actions.deleteEndpoint": "Supprimer le point de terminaison d'inférence {selectedEndpointName}", @@ -43280,7 +43275,6 @@ "xpack.serverlessSearch.nav.content.indices": "Gestion des index", "xpack.serverlessSearch.nav.devTools": "Outils de développement", "xpack.serverlessSearch.nav.gettingStarted": "Commencer", - "xpack.serverlessSearch.nav.home": "Accueil", "xpack.serverlessSearch.nav.mngt": "Gestion", "xpack.serverlessSearch.nav.performance": "Performances", "xpack.serverlessSearch.nav.projectSettings": "Paramètres de projet", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1d90ec5e84fd9..0b5587b2bd916 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -864,11 +864,7 @@ "controls.controlGroup.manageControl.dataSource.dataViewTitle": "データビュー", "controls.controlGroup.manageControl.dataSource.fieldListErrorTitle": "フィールドリストの読み込みエラー", "controls.controlGroup.manageControl.dataSource.fieldTitle": "フィールド", - "controls.controlGroup.manageControl.dataSource.formGroupDescription": "コントロールを作成するデータビューとフィールドを選択します。", - "controls.controlGroup.manageControl.dataSource.formGroupTitle": "データソース", "controls.controlGroup.manageControl.dataSource.selectDataViewMessage": "データビューを選択してください", - "controls.controlGroup.manageControl.displaySettings.formGroupDescription": "ダッシュボードにコントロールを表示する方法を変更します。", - "controls.controlGroup.manageControl.displaySettings.formGroupTitle": "表示設定", "controls.controlGroup.manageControl.displaySettings.growSwitchTitle": "空きスペースに合わせて幅を拡大", "controls.controlGroup.manageControl.displaySettings.titleInputTitle": "ラベル", "controls.controlGroup.manageControl.displaySettings.widthInputTitle": "最小幅", @@ -36056,32 +36052,31 @@ "xpack.searchIndices.server.createIndex.errorMessage": "例外が発生したため、インデックスを作成できませんでした。{errorMessage}", "xpack.searchIndices.server.deleteDocument.errorMessage": "ドキュメントを削除できませんでした", "xpack.searchIndices.settingsTabLabel": "設定", - "xpack.searchIndices.startPage.codeView.apiKeyDescription": "必ず安全に保管してください。後から取得することはできません。", - "xpack.searchIndices.startPage.codeView.apiKeyTitle": "APIキーをコピー", - "xpack.searchIndices.startPage.codeView.explicitGenerate.apiKeyDescription": "Elasticsearchに接続するためのAPIキーを作成します。", - "xpack.searchIndices.startPage.codeView.explicitGenerate.apiKeyTitle": "APIキーを作成する", - "xpack.searchIndices.startPage.createIndex.action.text": "インデックスを作成", - "xpack.searchIndices.startPage.createIndex.apiKeyCreation.description": "このインデックスのAPIキーを作成します", - "xpack.searchIndices.startPage.createIndex.description": "インデックスはデータを格納し、検索のためのスキーマ、つまりフィールドマッピングを定義します。", - "xpack.searchIndices.startPage.createIndex.fileUpload.link": "ファイルをアップロード", - "xpack.searchIndices.startPage.createIndex.fileUpload.text": "すでに一部のデータがありますか?{link}", - "xpack.searchIndices.startPage.createIndex.name.helpText": "インデックス名は小文字で、ハイフンと数字のみを使用する必要があります。", - "xpack.searchIndices.startPage.createIndex.name.label": "インデックスの名前を指定", - "xpack.searchIndices.startPage.createIndex.name.placeholder": "インデックスの名前を入力", - "xpack.searchIndices.startPage.createIndex.permissionTooltip": "APIキーを作成する権限がありません。", + "xpack.searchIndices.shared.codeView.apiKeyDescription": "必ず安全に保管してください。後から取得することはできません。", + "xpack.searchIndices.shared.codeView.apiKeyTitle": "APIキーをコピー", + "xpack.searchIndices.shared.codeView.explicitGenerate.apiKeyDescription": "Elasticsearchに接続するためのAPIキーを作成します。", + "xpack.searchIndices.shared.codeView.explicitGenerate.apiKeyTitle": "APIキーを作成する", + "xpack.searchIndices.shared.createIndex.action.text": "インデックスを作成", + "xpack.searchIndices.shared.createIndex.apiKeyCreation.description": "このインデックスのAPIキーを作成します", + "xpack.searchIndices.shared.createIndex.description": "インデックスはデータを格納し、検索のためのスキーマ、つまりフィールドマッピングを定義します。", + "xpack.searchIndices.shared.createIndex.fileUpload.link": "ファイルをアップロード", + "xpack.searchIndices.shared.createIndex.fileUpload.text": "すでに一部のデータがありますか?{link}", + "xpack.searchIndices.shared.createIndex.name.helpText": "インデックス名は小文字で、ハイフンと数字のみを使用する必要があります。", + "xpack.searchIndices.shared.createIndex.name.label": "インデックスの名前を指定", + "xpack.searchIndices.shared.createIndex.name.placeholder": "インデックスの名前を入力", + "xpack.searchIndices.shared.createIndex.observabilityCallout.logs.button": "ログを収集して分析", + "xpack.searchIndices.shared.createIndex.observabilityCallout.logs.subTitle": "LogstashとBeatsを探索", + "xpack.searchIndices.shared.createIndex.observabilityCallout.o11yTrial.button": "オブザーバビリティの試用を開始", + "xpack.searchIndices.shared.createIndex.observabilityCallout.o11yTrial.subTitle": "強力なパフォーマンス監視", + "xpack.searchIndices.shared.createIndex.observabilityCallout.title": "ログとメトリックデータを格納する方法をお探しですか?", + "xpack.searchIndices.shared.createIndex.pageTitle": "Elasticsearch", + "xpack.searchIndices.shared.createIndex.permissionTooltip": "APIキーを作成する権限がありません。", + "xpack.searchIndices.shared.createIndex.viewSelect.code": "コード", + "xpack.searchIndices.shared.createIndex.viewSelect.legend": "インデックスビュー選択を作成", + "xpack.searchIndices.shared.createIndex.viewSelect.ui": "UI", + "xpack.searchIndices.shared.statusFetchError.title": "インデックスの読み込み中にエラーが発生", + "xpack.searchIndices.shared.statusFetchError.unknownError": "インデックスの取得中の不明なエラー", "xpack.searchIndices.startPage.createIndex.title": "最初のインデックスを作成", - "xpack.searchIndices.startPage.createIndex.viewSelec.legend": "インデックスビュー選択を作成", - "xpack.searchIndices.startPage.createIndex.viewSelect.code": "コード", - "xpack.searchIndices.startPage.createIndex.viewSelect.ui": "UI", - "xpack.searchIndices.startPage.observabilityCallout.logs.button": "ログを収集して分析", - "xpack.searchIndices.startPage.observabilityCallout.logs.subTitle": "LogstashとBeatsを探索", - "xpack.searchIndices.startPage.observabilityCallout.o11yTrial.button": "オブザーバビリティの試用を開始", - "xpack.searchIndices.startPage.observabilityCallout.o11yTrial.subTitle": "強力なパフォーマンス監視", - "xpack.searchIndices.startPage.observabilityCallout.title": "ログとメトリックデータを格納する方法をお探しですか?", - "xpack.searchIndices.startPage.pageDescription": "データをベクトル化、検索、可視化", - "xpack.searchIndices.startPage.pageTitle": "Elasticsearch", - "xpack.searchIndices.startPage.statusFetchError.title": "インデックスの読み込み中にエラーが発生", - "xpack.searchIndices.startPage.statusFetchError.unknownError": "インデックスの取得中の不明なエラー", "xpack.searchInferenceEndpoints.actions.copyID": "推論エンドポイント ID {inferenceId}をコピー", "xpack.searchInferenceEndpoints.actions.copyIDSuccess": "推論エンドポイント ID {inferenceId}がコピーされました", "xpack.searchInferenceEndpoints.actions.deleteEndpoint": "推論エンドポイント{selectedEndpointName}を削除", @@ -43245,7 +43240,6 @@ "xpack.serverlessSearch.nav.content.indices": "インデックス管理", "xpack.serverlessSearch.nav.devTools": "開発ツール", "xpack.serverlessSearch.nav.gettingStarted": "はじめに", - "xpack.serverlessSearch.nav.home": "ホーム", "xpack.serverlessSearch.nav.mngt": "管理", "xpack.serverlessSearch.nav.performance": "パフォーマンス", "xpack.serverlessSearch.nav.projectSettings": "プロジェクト設定", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 30eb1f0c0fc2a..4d4db174396e2 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -856,11 +856,7 @@ "controls.controlGroup.manageControl.dataSource.dataViewTitle": "数据视图", "controls.controlGroup.manageControl.dataSource.fieldListErrorTitle": "加载字段列表时出错", "controls.controlGroup.manageControl.dataSource.fieldTitle": "字段", - "controls.controlGroup.manageControl.dataSource.formGroupDescription": "选择要为其创建控件的数据视图和字段。", - "controls.controlGroup.manageControl.dataSource.formGroupTitle": "数据源", "controls.controlGroup.manageControl.dataSource.selectDataViewMessage": "请选择数据视图", - "controls.controlGroup.manageControl.displaySettings.formGroupDescription": "更改控件在仪表板上的显示方式。", - "controls.controlGroup.manageControl.displaySettings.formGroupTitle": "显示设置", "controls.controlGroup.manageControl.displaySettings.growSwitchTitle": "扩大宽度以适应可用空间", "controls.controlGroup.manageControl.displaySettings.titleInputTitle": "标签", "controls.controlGroup.manageControl.displaySettings.widthInputTitle": "最小宽度", @@ -35493,32 +35489,30 @@ "xpack.searchIndices.server.createIndex.errorMessage": "由于出现异常,无法创建索引。{errorMessage}", "xpack.searchIndices.server.deleteDocument.errorMessage": "无法删除文档", "xpack.searchIndices.settingsTabLabel": "设置", - "xpack.searchIndices.startPage.codeView.apiKeyDescription": "请确保将其存放在某个安全位置。稍后您将无法对其进行检索。", - "xpack.searchIndices.startPage.codeView.apiKeyTitle": "复制您的 API 密钥", - "xpack.searchIndices.startPage.codeView.explicitGenerate.apiKeyDescription": "创建 API 密钥以连接到 Elasticsearch。", - "xpack.searchIndices.startPage.codeView.explicitGenerate.apiKeyTitle": "创建 API 密钥", - "xpack.searchIndices.startPage.createIndex.action.text": "创建我的索引", - "xpack.searchIndices.startPage.createIndex.apiKeyCreation.description": "我们将为此索引创建 API 密钥", - "xpack.searchIndices.startPage.createIndex.description": "索引存储您的数据并为您的搜索定义架构或字段映射", - "xpack.searchIndices.startPage.createIndex.fileUpload.link": "上传文件", - "xpack.searchIndices.startPage.createIndex.fileUpload.text": "已具有某些数据?{link}", - "xpack.searchIndices.startPage.createIndex.name.helpText": "索引名称必须为小写,并且只能包含连字符和数字", - "xpack.searchIndices.startPage.createIndex.name.label": "命名您的索引", - "xpack.searchIndices.startPage.createIndex.name.placeholder": "输入索引的名称", - "xpack.searchIndices.startPage.createIndex.permissionTooltip": "您无权创建索引。", + "xpack.searchIndices.shared.codeView.apiKeyDescription": "请确保将其存放在某个安全位置。稍后您将无法对其进行检索。", + "xpack.searchIndices.shared.codeView.apiKeyTitle": "复制您的 API 密钥", + "xpack.searchIndices.shared.codeView.explicitGenerate.apiKeyDescription": "创建 API 密钥以连接到 Elasticsearch。", + "xpack.searchIndices.shared.codeView.explicitGenerate.apiKeyTitle": "创建 API 密钥", + "xpack.searchIndices.shared.createIndex.action.text": "创建我的索引", + "xpack.searchIndices.shared.createIndex.apiKeyCreation.description": "我们将为此索引创建 API 密钥", + "xpack.searchIndices.shared.createIndex.description": "索引存储您的数据并为您的搜索定义架构或字段映射", + "xpack.searchIndices.shared.createIndex.fileUpload.link": "上传文件", + "xpack.searchIndices.shared.createIndex.fileUpload.text": "已具有某些数据?{link}", + "xpack.searchIndices.shared.createIndex.name.helpText": "索引名称必须为小写,并且只能包含连字符和数字", + "xpack.searchIndices.shared.createIndex.name.label": "命名您的索引", + "xpack.searchIndices.shared.createIndex.name.placeholder": "输入索引的名称", + "xpack.searchIndices.shared.createIndex.observabilityCallout.logs.button": "收集和分析日志", + "xpack.searchIndices.shared.createIndex.observabilityCallout.logs.subTitle": "浏览 Logstash 和 Beats", + "xpack.searchIndices.shared.createIndex.observabilityCallout.o11yTrial.button": "开始 Observability 试用", + "xpack.searchIndices.shared.createIndex.observabilityCallout.o11yTrial.subTitle": "强大的性能监测", + "xpack.searchIndices.shared.createIndex.observabilityCallout.title": "计划存储您的日志或指标数据?", + "xpack.searchIndices.shared.createIndex.pageTitle": "Elasticsearch", + "xpack.searchIndices.shared.createIndex.permissionTooltip": "您无权创建索引。", + "xpack.searchIndices.shared.createIndex.viewSelect.code": "Code", + "xpack.searchIndices.shared.createIndex.viewSelect.ui": "UI", + "xpack.searchIndices.shared.statusFetchError.title": "加载索引时出错", + "xpack.searchIndices.shared.statusFetchError.unknownError": "提取索引时出现未知错误。", "xpack.searchIndices.startPage.createIndex.title": "创建您的首个索引", - "xpack.searchIndices.startPage.createIndex.viewSelec.legend": "创建索引视图选择", - "xpack.searchIndices.startPage.createIndex.viewSelect.code": "Code", - "xpack.searchIndices.startPage.createIndex.viewSelect.ui": "UI", - "xpack.searchIndices.startPage.observabilityCallout.logs.button": "收集和分析日志", - "xpack.searchIndices.startPage.observabilityCallout.logs.subTitle": "浏览 Logstash 和 Beats", - "xpack.searchIndices.startPage.observabilityCallout.o11yTrial.button": "开始 Observability 试用", - "xpack.searchIndices.startPage.observabilityCallout.o11yTrial.subTitle": "强大的性能监测", - "xpack.searchIndices.startPage.observabilityCallout.title": "计划存储您的日志或指标数据?", - "xpack.searchIndices.startPage.pageDescription": "向量化、搜索和可视化您的数据", - "xpack.searchIndices.startPage.pageTitle": "Elasticsearch", - "xpack.searchIndices.startPage.statusFetchError.title": "加载索引时出错", - "xpack.searchIndices.startPage.statusFetchError.unknownError": "提取索引时出现未知错误。", "xpack.searchInferenceEndpoints.actions.copyID": "复制推理终端 ID {inferenceId}", "xpack.searchInferenceEndpoints.actions.copyIDSuccess": "已复制推理终端 ID {inferenceId}", "xpack.searchInferenceEndpoints.actions.deleteEndpoint": "删除推理终端 {selectedEndpointName}", @@ -42590,7 +42584,6 @@ "xpack.serverlessSearch.nav.content.indices": "索引管理", "xpack.serverlessSearch.nav.devTools": "开发工具", "xpack.serverlessSearch.nav.gettingStarted": "入门", - "xpack.serverlessSearch.nav.home": "主页", "xpack.serverlessSearch.nav.mngt": "管理", "xpack.serverlessSearch.nav.performance": "性能", "xpack.serverlessSearch.nav.projectSettings": "项目设置", diff --git a/x-pack/test/alerting_api_integration/common/plugins/task_manager_fixture/server/plugin.ts b/x-pack/test/alerting_api_integration/common/plugins/task_manager_fixture/server/plugin.ts index 6e4eba065ec70..f6e3383270436 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/task_manager_fixture/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/task_manager_fixture/server/plugin.ts @@ -95,6 +95,34 @@ export class SampleTaskManagerFixturePlugin return res.ok({ body: {} }); } ); + + router.post( + { + path: '/api/alerting_tasks/run_mark_tasks_as_unrecognized', + validate: { + body: schema.object({}), + }, + }, + async ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> => { + try { + const taskManager = await this.taskManagerStart; + await taskManager.ensureScheduled({ + id: 'mark_removed_tasks_as_unrecognized', + taskType: 'task_manager:mark_removed_tasks_as_unrecognized', + schedule: { interval: '1h' }, + state: {}, + params: {}, + }); + return res.ok({ body: await taskManager.runSoon('mark_removed_tasks_as_unrecognized') }); + } catch (err) { + return res.ok({ body: { id: 'mark_removed_tasks_as_unrecognized', error: `${err}` } }); + } + } + ); } public start(core: CoreStart, { taskManager }: SampleTaskManagerFixtureStartDeps) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/scheduled_task_id.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/scheduled_task_id.ts index 0086dd2679c74..f05075be810a1 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/scheduled_task_id.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/scheduled_task_id.ts @@ -138,6 +138,12 @@ export default function createScheduledTaskIdTests({ getService }: FtrProviderCo // When we enable the rule, the unrecognized task should be removed and a new // task created in its place + await supertestWithoutAuth + .post('/api/alerting_tasks/run_mark_tasks_as_unrecognized') + .set('kbn-xsrf', 'foo') + .send({}) + .expect(200); + // scheduled task should exist and be unrecognized await retry.try(async () => { const taskRecordLoaded = await getScheduledTask(RULE_ID); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/constants/archiver.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/constants/archiver.ts new file mode 100644 index 0000000000000..6afc2e9eca63b --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/constants/archiver.ts @@ -0,0 +1,32 @@ +/* + * 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. + */ + +type ArchiveName = + | '8.0.0' + | 'apm_8.0.0' + | 'apm_mappings_only_8.0.0' + | 'infra_metrics_and_apm' + | 'metrics_8.0.0' + | 'ml_8.0.0' + | 'observability_overview' + | 'rum_8.0.0' + | 'rum_test_data'; + +export const ARCHIVER_ROUTES: { [key in ArchiveName]: string } = { + '8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/8.0.0', + 'apm_8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_8.0.0', + 'apm_mappings_only_8.0.0': + 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_mappings_only_8.0.0', + infra_metrics_and_apm: + 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/infra_metrics_and_apm', + 'metrics_8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/metrics_8.0.0', + 'ml_8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/ml_8.0.0', + observability_overview: + 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/observability_overview', + 'rum_8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/rum_8.0.0', + rum_test_data: 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/rum_test_data', +}; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/failed_transactions.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/failed_transactions.spec.ts new file mode 100644 index 0000000000000..549f48009197f --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/failed_transactions.spec.ts @@ -0,0 +1,236 @@ +/* + * 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 { orderBy } from 'lodash'; +import expect from '@kbn/expect'; +import type { FailedTransactionsCorrelationsResponse } from '@kbn/apm-plugin/common/correlations/failed_transactions_correlations/types'; +import { EVENT_OUTCOME } from '@kbn/apm-plugin/common/es_fields/apm'; +import { EventOutcome } from '@kbn/apm-plugin/common/event_outcome'; +import { LatencyDistributionChartType } from '@kbn/apm-plugin/common/latency_distribution_chart_types'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { ARCHIVER_ROUTES } from '../constants/archiver'; + +// These tests go through the full sequence of queries required +// to get the final results for a failed transactions correlation analysis. +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const esArchiver = getService('esArchiver'); + // This matches the parameters used for the other tab's queries in `../correlations/*`. + const getOptions = () => ({ + environment: 'ENVIRONMENT_ALL', + start: '2020', + end: '2021', + kuery: '', + }); + + describe('failed transactions', () => { + describe('without data', () => { + it('handles the empty state', async () => { + const overallDistributionResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', + params: { + body: { + ...getOptions(), + percentileThreshold: 95, + chartType: LatencyDistributionChartType.failedTransactionsCorrelations, + }, + }, + }); + + expect(overallDistributionResponse.status).to.eql( + 200, + `Expected status to be '200', got '${overallDistributionResponse.status}'` + ); + + const errorDistributionResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', + params: { + body: { + ...getOptions(), + percentileThreshold: 95, + termFilters: [{ fieldName: EVENT_OUTCOME, fieldValue: EventOutcome.failure }], + chartType: LatencyDistributionChartType.failedTransactionsCorrelations, + }, + }, + }); + + expect(errorDistributionResponse.status).to.eql( + 200, + `Expected status to be '200', got '${errorDistributionResponse.status}'` + ); + + const fieldCandidatesResponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/correlations/field_candidates/transactions', + params: { + query: getOptions(), + }, + }); + + expect(fieldCandidatesResponse.status).to.eql( + 200, + `Expected status to be '200', got '${fieldCandidatesResponse.status}'` + ); + + const failedTransactionsCorrelationsResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/correlations/p_values/transactions', + params: { + body: { + ...getOptions(), + fieldCandidates: fieldCandidatesResponse.body?.fieldCandidates, + }, + }, + }); + + expect(failedTransactionsCorrelationsResponse.status).to.eql( + 200, + `Expected status to be '200', got '${failedTransactionsCorrelationsResponse.status}'` + ); + + const finalRawResponse: FailedTransactionsCorrelationsResponse = { + ccsWarning: failedTransactionsCorrelationsResponse.body?.ccsWarning, + percentileThresholdValue: overallDistributionResponse.body?.percentileThresholdValue, + overallHistogram: overallDistributionResponse.body?.overallHistogram, + failedTransactionsCorrelations: + failedTransactionsCorrelationsResponse.body?.failedTransactionsCorrelations, + }; + + expect(finalRawResponse?.failedTransactionsCorrelations?.length).to.eql( + 0, + `Expected 0 identified correlations, got ${finalRawResponse?.failedTransactionsCorrelations?.length}.` + ); + }); + }); + + describe('with data', () => { + before(async () => { + await esArchiver.load(ARCHIVER_ROUTES['8.0.0']); + }); + after(async () => { + await esArchiver.unload(ARCHIVER_ROUTES['8.0.0']); + }); + + it('runs queries and returns results', async () => { + const overallDistributionResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', + params: { + body: { + ...getOptions(), + percentileThreshold: 95, + chartType: LatencyDistributionChartType.failedTransactionsCorrelations, + }, + }, + }); + + expect(overallDistributionResponse.status).to.eql( + 200, + `Expected status to be '200', got '${overallDistributionResponse.status}'` + ); + + const errorDistributionResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', + params: { + body: { + ...getOptions(), + percentileThreshold: 95, + termFilters: [{ fieldName: EVENT_OUTCOME, fieldValue: EventOutcome.failure }], + chartType: LatencyDistributionChartType.failedTransactionsCorrelations, + }, + }, + }); + + expect(errorDistributionResponse.status).to.eql( + 200, + `Expected status to be '200', got '${errorDistributionResponse.status}'` + ); + + const fieldCandidatesResponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/correlations/field_candidates/transactions', + params: { + query: getOptions(), + }, + }); + + expect(fieldCandidatesResponse.status).to.eql( + 200, + `Expected status to be '200', got '${fieldCandidatesResponse.status}'` + ); + + const fieldCandidates = fieldCandidatesResponse.body?.fieldCandidates.filter( + (t) => !(t === EVENT_OUTCOME) + ); + + // Identified 80 fieldCandidates. + expect(fieldCandidates.length).to.eql( + 80, + `Expected field candidates length to be '80', got '${fieldCandidates.length}'` + ); + + const failedTransactionsCorrelationsResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/correlations/p_values/transactions', + params: { + body: { + ...getOptions(), + fieldCandidates, + }, + }, + }); + + expect(failedTransactionsCorrelationsResponse.status).to.eql( + 200, + `Expected status to be '200', got '${failedTransactionsCorrelationsResponse.status}'` + ); + + const fieldsToSample = new Set(); + if ( + failedTransactionsCorrelationsResponse.body?.failedTransactionsCorrelations.length > 0 + ) { + failedTransactionsCorrelationsResponse.body?.failedTransactionsCorrelations.forEach( + (d) => { + fieldsToSample.add(d.fieldName); + } + ); + } + + const finalRawResponse: FailedTransactionsCorrelationsResponse = { + ccsWarning: failedTransactionsCorrelationsResponse.body?.ccsWarning, + percentileThresholdValue: overallDistributionResponse.body?.percentileThresholdValue, + overallHistogram: overallDistributionResponse.body?.overallHistogram, + errorHistogram: errorDistributionResponse.body?.overallHistogram, + failedTransactionsCorrelations: + failedTransactionsCorrelationsResponse.body?.failedTransactionsCorrelations, + }; + + expect(finalRawResponse?.percentileThresholdValue).to.be(1309695.875); + expect(finalRawResponse?.errorHistogram?.length).to.be(101); + expect(finalRawResponse?.overallHistogram?.length).to.be(101); + + expect(finalRawResponse?.failedTransactionsCorrelations?.length).to.eql( + 29, + `Expected 29 identified correlations, got ${finalRawResponse?.failedTransactionsCorrelations?.length}.` + ); + + const sortedCorrelations = orderBy( + finalRawResponse?.failedTransactionsCorrelations, + ['score', 'fieldName', 'fieldValue'], + ['desc', 'asc', 'asc'] + ); + const correlation = sortedCorrelations?.[0]; + + expect(typeof correlation).to.be('object'); + expect(correlation?.doc_count).to.be(31); + expect(correlation?.score).to.be(83.70467673605746); + expect(correlation?.bg_count).to.be(31); + expect(correlation?.fieldName).to.be('transaction.result'); + expect(correlation?.fieldValue).to.be('HTTP 5xx'); + expect(typeof correlation?.pValue).to.be('number'); + expect(typeof correlation?.normalizedScore).to.be('number'); + expect(typeof correlation?.failurePercentage).to.be('number'); + expect(typeof correlation?.successPercentage).to.be('number'); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/field_candidates.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/field_candidates.spec.ts new file mode 100644 index 0000000000000..8db9a7df05fd3 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/field_candidates.spec.ts @@ -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 expect from '@kbn/expect'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { ARCHIVER_ROUTES } from '../constants/archiver'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const esArchiver = getService('esArchiver'); + + const endpoint = 'GET /internal/apm/correlations/field_candidates/transactions'; + + const getOptions = () => ({ + params: { + query: { + environment: 'ENVIRONMENT_ALL', + start: '2020', + end: '2021', + kuery: '', + }, + }, + }); + + describe('field candidates', () => { + describe('without data', () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); + + expect(response.status).to.be(200); + // If the source indices are empty, there will be no field candidates + // because of the `include_empty_fields: false` option in the query. + expect(response.body?.fieldCandidates.length).to.be(0); + }); + }); + + describe('with data and default args', () => { + before(async () => { + await esArchiver.load(ARCHIVER_ROUTES['8.0.0']); + }); + after(async () => { + await esArchiver.unload(ARCHIVER_ROUTES['8.0.0']); + }); + + it('returns field candidates', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); + + expect(response.status).to.eql(200); + expect(response.body?.fieldCandidates.length).to.be(81); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/correlations/field_value_pairs.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/field_value_pairs.spec.ts similarity index 59% rename from x-pack/test/apm_api_integration/tests/correlations/field_value_pairs.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/field_value_pairs.spec.ts index 4765e83342e52..9fcd438421b6a 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/field_value_pairs.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/field_value_pairs.spec.ts @@ -6,11 +6,12 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { ARCHIVER_ROUTES } from '../constants/archiver'; -export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiClient = getService('apmApiClient'); - const registry = getService('registry'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const esArchiver = getService('esArchiver'); const endpoint = 'POST /internal/apm/correlations/field_value_pairs/transactions'; @@ -41,22 +42,27 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, }); - registry.when('field value pairs without data', { config: 'trial', archives: [] }, () => { - it('handles the empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint, - ...getOptions(), - }); + describe('field value pairs', () => { + describe('without data', () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); - expect(response.status).to.be(200); - expect(response.body?.fieldValuePairs.length).to.be(0); + expect(response.status).to.be(200); + expect(response.body?.fieldValuePairs.length).to.be(0); + }); }); - }); - registry.when( - 'field value pairs with data and default args', - { config: 'trial', archives: ['8.0.0'] }, - () => { + describe('with data and default args', () => { + before(async () => { + await esArchiver.load(ARCHIVER_ROUTES['8.0.0']); + }); + after(async () => { + await esArchiver.unload(ARCHIVER_ROUTES['8.0.0']); + }); + it('returns field value pairs', async () => { const response = await apmApiClient.readUser({ endpoint, @@ -66,6 +72,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.eql(200); expect(response.body?.fieldValuePairs.length).to.be(124); }); - } - ); + }); + }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/index.ts new file mode 100644 index 0000000000000..660556edb8d79 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('correlations', () => { + loadTestFile(require.resolve('./failed_transactions.spec.ts')); + loadTestFile(require.resolve('./field_candidates.spec.ts')); + loadTestFile(require.resolve('./field_value_pairs.spec.ts')); + loadTestFile(require.resolve('./latency.spec.ts')); + loadTestFile(require.resolve('./p_values.spec.ts')); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/latency.spec.ts similarity index 93% rename from x-pack/test/apm_api_integration/tests/correlations/latency.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/latency.spec.ts index 5326136976428..e0080806f6a0e 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/latency.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/latency.spec.ts @@ -14,13 +14,14 @@ import type { LatencyCorrelationsResponse, } from '@kbn/apm-plugin/common/correlations/latency_correlations/types'; import { LatencyDistributionChartType } from '@kbn/apm-plugin/common/latency_distribution_chart_types'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { ARCHIVER_ROUTES } from '../constants/archiver'; // These tests go through the full sequence of queries required // to get the final results for a latency correlation analysis. -export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiClient = getService('apmApiClient'); - const registry = getService('registry'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const esArchiver = getService('esArchiver'); // This matches the parameters used for the other tab's queries in `../correlations/*`. const getOptions = () => ({ @@ -30,10 +31,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { kuery: '', }); - registry.when( - 'correlations latency overall without data', - { config: 'trial', archives: [] }, - () => { + describe('latency', () => { + describe('overall without data', () => { it('handles the empty state', async () => { const overallDistributionResponse = await apmApiClient.readUser({ endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', @@ -104,13 +103,16 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(finalRawResponse?.overallHistogram).to.be(undefined); expect(finalRawResponse?.latencyCorrelations?.length).to.be(0); }); - } - ); + }); + + describe('with data and opbeans-node args', () => { + before(async () => { + await esArchiver.load(ARCHIVER_ROUTES['8.0.0']); + }); + after(async () => { + await esArchiver.unload(ARCHIVER_ROUTES['8.0.0']); + }); - registry.when( - 'correlations latency with data and opbeans-node args', - { config: 'trial', archives: ['8.0.0'] }, - () => { // putting this into a single `it` because the responses depend on each other it('runs queries and returns results', async () => { const overallDistributionResponse = await apmApiClient.readUser({ @@ -250,6 +252,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(correlation?.ksTest).to.be(1.9848961005439386e-12); expect(correlation?.histogram?.length).to.be(101); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/correlations/p_values.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/p_values.spec.ts similarity index 58% rename from x-pack/test/apm_api_integration/tests/correlations/p_values.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/p_values.spec.ts index 42a9fdadbb480..ba6e3384cec93 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/p_values.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/p_values.spec.ts @@ -6,11 +6,12 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { ARCHIVER_ROUTES } from '../constants/archiver'; -export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiClient = getService('apmApiClient'); - const registry = getService('registry'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const esArchiver = getService('esArchiver'); const endpoint = 'POST /internal/apm/correlations/p_values/transactions'; @@ -41,22 +42,27 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, }); - registry.when('p values without data', { config: 'trial', archives: [] }, () => { - it('handles the empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint, - ...getOptions(), - }); + describe('p values', () => { + describe('without data', () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); - expect(response.status).to.be(200); - expect(response.body?.failedTransactionsCorrelations.length).to.be(0); + expect(response.status).to.be(200); + expect(response.body?.failedTransactionsCorrelations.length).to.be(0); + }); }); - }); - registry.when( - 'p values with data and default args', - { config: 'trial', archives: ['8.0.0'] }, - () => { + describe('with data and default args', () => { + before(async () => { + await esArchiver.load(ARCHIVER_ROUTES['8.0.0']); + }); + after(async () => { + await esArchiver.unload(ARCHIVER_ROUTES['8.0.0']); + }); + it('returns p values', async () => { const response = await apmApiClient.readUser({ endpoint, @@ -66,6 +72,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.eql(200); expect(response.body?.failedTransactionsCorrelations.length).to.be(15); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/correlations/significant_correlations.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/significant_correlations.spec.ts similarity index 71% rename from x-pack/test/apm_api_integration/tests/correlations/significant_correlations.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/significant_correlations.spec.ts index d4450c192a029..e1f968d178868 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/significant_correlations.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/significant_correlations.spec.ts @@ -6,11 +6,12 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { ARCHIVER_ROUTES } from '../constants/archiver'; -export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiClient = getService('apmApiClient'); - const registry = getService('registry'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const esArchiver = getService('esArchiver'); const endpoint = 'POST /internal/apm/correlations/significant_correlations/transactions'; @@ -65,22 +66,27 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, }); - registry.when('significant correlations without data', { config: 'trial', archives: [] }, () => { - it('handles the empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint, - ...getOptions(), - }); + describe('significant correlations', () => { + describe('without data', () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); - expect(response.status).to.be(200); - expect(response.body?.latencyCorrelations.length).to.be(0); + expect(response.status).to.be(200); + expect(response.body?.latencyCorrelations.length).to.be(0); + }); }); - }); - registry.when( - 'significant correlations with data and default args', - { config: 'trial', archives: ['8.0.0'] }, - () => { + describe('with data and default args', () => { + before(async () => { + await esArchiver.load(ARCHIVER_ROUTES['8.0.0']); + }); + after(async () => { + await esArchiver.unload(ARCHIVER_ROUTES['8.0.0']); + }); + it('returns significant correlations', async () => { const response = await apmApiClient.readUser({ endpoint, @@ -90,6 +96,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.eql(200); expect(response.body?.latencyCorrelations.length).to.be(7); }); - } - ); + }); + }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/index.ts new file mode 100644 index 0000000000000..d7a36e3e447b7 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('entities', () => { + loadTestFile(require.resolve('./service_logs_error_rate_timeseries.spec.ts')); + loadTestFile(require.resolve('./service_logs_rate_timeseries.spec.ts')); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/entities/logs/service_logs_error_rate_timeseries.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/service_logs_error_rate_timeseries.spec.ts similarity index 85% rename from x-pack/test/apm_api_integration/tests/entities/logs/service_logs_error_rate_timeseries.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/service_logs_error_rate_timeseries.spec.ts index 282039b8957c9..f6e167db0318e 100644 --- a/x-pack/test/apm_api_integration/tests/entities/logs/service_logs_error_rate_timeseries.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/service_logs_error_rate_timeseries.spec.ts @@ -9,12 +9,12 @@ import { log, timerange } from '@kbn/apm-synthtrace-client'; import { first, last } from 'lodash'; import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; import { APIClientRequestParamsOf } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { LogsSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const logSynthtrace = getService('logSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const serviceName = 'synth-go'; const start = new Date('2024-01-01T00:00:00.000Z').getTime(); @@ -45,25 +45,26 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); return response; } + describe('logs error rate timeseries', () => { + describe('when data is not loaded', () => { + it('handles empty state', async () => { + const response = await getLogsErrorRateTimeseries(); + expect(response.status).to.be(200); + expect(response.body.currentPeriod).to.empty(); + }); + }); - registry.when( - 'Logs error rate timeseries when data is not loaded', - { config: 'basic', archives: [] }, - () => { - describe('Logs error rate api', () => { - it('handles the empty state', async () => { - const response = await getLogsErrorRateTimeseries(); - expect(response.status).to.be(200); - expect(response.body.currentPeriod).to.empty(); - }); + describe('when data loaded', () => { + let logSynthtrace: LogsSynthtraceEsClient; + + before(async () => { + logSynthtrace = await synthtrace.createLogsSynthtraceEsClient(); + }); + + after(async () => { + await logSynthtrace.clean(); }); - } - ); - registry.when( - 'Logs error rate timeseries when data loaded', - { config: 'basic', archives: [] }, - () => { describe('Logs without log level field', () => { before(async () => { return logSynthtrace.index([ @@ -170,6 +171,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/service_logs_rate_timeseries.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/service_logs_rate_timeseries.spec.ts new file mode 100644 index 0000000000000..fb10925b9906d --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/service_logs_rate_timeseries.spec.ts @@ -0,0 +1,180 @@ +/* + * 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 { log, timerange } from '@kbn/apm-synthtrace-client'; +import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; +import { APIClientRequestParamsOf } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import { first, last } from 'lodash'; +import { LogsSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const serviceName = 'synth-go'; + const start = new Date('2024-01-01T00:00:00.000Z').getTime(); + const end = new Date('2024-01-01T00:15:00.000Z').getTime() - 1; + + const hostName = 'synth-host'; + + async function getLogsRateTimeseries( + overrides?: RecursivePartial< + APIClientRequestParamsOf<'GET /internal/apm/entities/services/{serviceName}/logs_rate_timeseries'>['params'] + > + ) { + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/entities/services/{serviceName}/logs_rate_timeseries', + params: { + path: { + serviceName: 'synth-go', + ...overrides?.path, + }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', + ...overrides?.query, + }, + }, + }); + return response; + } + describe('logs rate timeseries', () => { + describe('when data is not loaded', () => { + it('handles empty state', async () => { + const response = await getLogsRateTimeseries(); + expect(response.status).to.be(200); + expect(response.body.currentPeriod).to.empty(); + }); + }); + + describe('when data loaded', () => { + let logSynthtrace: LogsSynthtraceEsClient; + + before(async () => { + logSynthtrace = await synthtrace.createLogsSynthtraceEsClient(); + }); + + after(async () => { + await logSynthtrace.clean(); + }); + + describe('Logs without log level field', () => { + before(async () => { + return logSynthtrace.index([ + timerange(start, end) + .interval('1m') + .rate(1) + .generator((timestamp) => + log.create().message('This is a log message').timestamp(timestamp).defaults({ + 'log.file.path': '/my-service.log', + 'service.name': serviceName, + 'host.name': hostName, + }) + ), + ]); + }); + after(async () => { + await logSynthtrace.clean(); + }); + + it('returns {} if log level is not available ', async () => { + const response = await getLogsRateTimeseries(); + expect(response.status).to.be(200); + }); + }); + + describe('Logs with log.level=error', () => { + before(async () => { + return logSynthtrace.index([ + timerange(start, end) + .interval('1m') + .rate(1) + .generator((timestamp) => + log + .create() + .message('This is a log message') + .logLevel('error') + .timestamp(timestamp) + .defaults({ + 'log.file.path': '/my-service.log', + 'service.name': serviceName, + 'host.name': hostName, + 'service.environment': 'test', + }) + ), + timerange(start, end) + .interval('2m') + .rate(1) + .generator((timestamp) => + log + .create() + .message('This is an error log message') + .logLevel('error') + .timestamp(timestamp) + .defaults({ + 'log.file.path': '/my-service.log', + 'service.name': 'my-service', + 'host.name': hostName, + 'service.environment': 'production', + }) + ), + timerange(start, end) + .interval('5m') + .rate(1) + .generator((timestamp) => + log + .create() + .message('This is an info message') + .logLevel('info') + .timestamp(timestamp) + .defaults({ + 'log.file.path': '/my-service.log', + 'service.name': 'my-service', + 'host.name': hostName, + 'service.environment': 'production', + }) + ), + ]); + }); + after(async () => { + await logSynthtrace.clean(); + }); + + it('returns log rate timeseries', async () => { + const response = await getLogsRateTimeseries(); + expect(response.status).to.be(200); + expect( + response.body.currentPeriod[serviceName].every(({ y }) => y === 0.06666666666666667) + ).to.be(true); + }); + + it('handles environment filter', async () => { + const response = await getLogsRateTimeseries({ query: { environment: 'foo' } }); + expect(response.status).to.be(200); + expect(response.body.currentPeriod).to.empty(); + }); + + describe('when my-service is selected', () => { + it('returns some data', async () => { + const response = await getLogsRateTimeseries({ + path: { serviceName: 'my-service' }, + }); + + expect(response.status).to.be(200); + expect(first(response.body.currentPeriod?.['my-service'])?.y).to.be( + 0.18181818181818182 + ); + expect(last(response.body.currentPeriod?.['my-service'])?.y).to.be(0.09090909090909091); + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts index 3e490a621d3f9..f1e8fc381a072 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts @@ -15,6 +15,8 @@ export default function apmApiIntegrationTests({ loadTestFile(require.resolve('./mobile')); loadTestFile(require.resolve('./custom_dashboards')); loadTestFile(require.resolve('./dependencies')); + loadTestFile(require.resolve('./correlations')); + loadTestFile(require.resolve('./entities')); loadTestFile(require.resolve('./cold_start')); }); } diff --git a/x-pack/test/apm_api_integration/common/fixtures/es_archiver/8.0.0/mappings.json b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/8.0.0/mappings.json index a64e037343bb3..c79a4c6b52309 100644 --- a/x-pack/test/apm_api_integration/common/fixtures/es_archiver/8.0.0/mappings.json +++ b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/8.0.0/mappings.json @@ -3786,10 +3786,6 @@ "index": { "auto_expand_replicas": "0-1", "codec": "best_compression", - "lifecycle": { - "name": "apm-rollover-30-days", - "rollover_alias": "apm-8.0.0-error" - }, "mapping": { "total_fields": { "limit": "2000" @@ -4243,8 +4239,7 @@ "transaction.message.queue.name", "fields.*" ] - }, - "refresh_interval": "1ms" + } } } } @@ -8183,10 +8178,6 @@ "index": { "auto_expand_replicas": "0-1", "codec": "best_compression", - "lifecycle": { - "name": "apm-rollover-30-days", - "rollover_alias": "apm-8.0.0-metric" - }, "mapping": { "total_fields": { "limit": "2000" @@ -8640,8 +8631,7 @@ "transaction.message.queue.name", "fields.*" ] - }, - "refresh_interval": "1ms" + } } } } @@ -12871,8 +12861,7 @@ "transaction.message.queue.name", "fields.*" ] - }, - "refresh_interval": "1ms" + } } } } @@ -16653,10 +16642,6 @@ "index": { "auto_expand_replicas": "0-1", "codec": "best_compression", - "lifecycle": { - "name": "apm-rollover-30-days", - "rollover_alias": "apm-8.0.0-profile" - }, "mapping": { "total_fields": { "limit": "2000" @@ -17110,8 +17095,7 @@ "transaction.message.queue.name", "fields.*" ] - }, - "refresh_interval": "1ms" + } } } } @@ -20899,10 +20883,6 @@ "index": { "auto_expand_replicas": "0-1", "codec": "best_compression", - "lifecycle": { - "name": "apm-rollover-30-days", - "rollover_alias": "apm-8.0.0-span" - }, "mapping": { "total_fields": { "limit": "2000" @@ -21356,8 +21336,7 @@ "transaction.message.queue.name", "fields.*" ] - }, - "refresh_interval": "1ms" + } } } } @@ -25242,10 +25221,6 @@ "index": { "auto_expand_replicas": "0-1", "codec": "best_compression", - "lifecycle": { - "name": "apm-rollover-30-days", - "rollover_alias": "apm-8.0.0-transaction" - }, "mapping": { "total_fields": { "limit": "2000" @@ -25699,8 +25674,7 @@ "transaction.message.queue.name", "fields.*" ] - }, - "refresh_interval": "1ms" + } } } } diff --git a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.spec.ts b/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.spec.ts deleted file mode 100644 index 13754f6c7eb5a..0000000000000 --- a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.spec.ts +++ /dev/null @@ -1,223 +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 { orderBy } from 'lodash'; -import expect from '@kbn/expect'; -import type { FailedTransactionsCorrelationsResponse } from '@kbn/apm-plugin/common/correlations/failed_transactions_correlations/types'; -import { EVENT_OUTCOME } from '@kbn/apm-plugin/common/es_fields/apm'; -import { EventOutcome } from '@kbn/apm-plugin/common/event_outcome'; -import { LatencyDistributionChartType } from '@kbn/apm-plugin/common/latency_distribution_chart_types'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -// These tests go through the full sequence of queries required -// to get the final results for a failed transactions correlation analysis. -export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiClient = getService('apmApiClient'); - const registry = getService('registry'); - - // This matches the parameters used for the other tab's queries in `../correlations/*`. - const getOptions = () => ({ - environment: 'ENVIRONMENT_ALL', - start: '2020', - end: '2021', - kuery: '', - }); - - registry.when('failed transactions without data', { config: 'trial', archives: [] }, () => { - it('handles the empty state', async () => { - const overallDistributionResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', - params: { - body: { - ...getOptions(), - percentileThreshold: 95, - chartType: LatencyDistributionChartType.failedTransactionsCorrelations, - }, - }, - }); - - expect(overallDistributionResponse.status).to.eql( - 200, - `Expected status to be '200', got '${overallDistributionResponse.status}'` - ); - - const errorDistributionResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', - params: { - body: { - ...getOptions(), - percentileThreshold: 95, - termFilters: [{ fieldName: EVENT_OUTCOME, fieldValue: EventOutcome.failure }], - chartType: LatencyDistributionChartType.failedTransactionsCorrelations, - }, - }, - }); - - expect(errorDistributionResponse.status).to.eql( - 200, - `Expected status to be '200', got '${errorDistributionResponse.status}'` - ); - - const fieldCandidatesResponse = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/correlations/field_candidates/transactions', - params: { - query: getOptions(), - }, - }); - - expect(fieldCandidatesResponse.status).to.eql( - 200, - `Expected status to be '200', got '${fieldCandidatesResponse.status}'` - ); - - const failedTransactionsCorrelationsResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/correlations/p_values/transactions', - params: { - body: { - ...getOptions(), - fieldCandidates: fieldCandidatesResponse.body?.fieldCandidates, - }, - }, - }); - - expect(failedTransactionsCorrelationsResponse.status).to.eql( - 200, - `Expected status to be '200', got '${failedTransactionsCorrelationsResponse.status}'` - ); - - const finalRawResponse: FailedTransactionsCorrelationsResponse = { - ccsWarning: failedTransactionsCorrelationsResponse.body?.ccsWarning, - percentileThresholdValue: overallDistributionResponse.body?.percentileThresholdValue, - overallHistogram: overallDistributionResponse.body?.overallHistogram, - failedTransactionsCorrelations: - failedTransactionsCorrelationsResponse.body?.failedTransactionsCorrelations, - }; - - expect(finalRawResponse?.failedTransactionsCorrelations?.length).to.eql( - 0, - `Expected 0 identified correlations, got ${finalRawResponse?.failedTransactionsCorrelations?.length}.` - ); - }); - }); - - registry.when('failed transactions with data', { config: 'trial', archives: ['8.0.0'] }, () => { - it('runs queries and returns results', async () => { - const overallDistributionResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', - params: { - body: { - ...getOptions(), - percentileThreshold: 95, - chartType: LatencyDistributionChartType.failedTransactionsCorrelations, - }, - }, - }); - - expect(overallDistributionResponse.status).to.eql( - 200, - `Expected status to be '200', got '${overallDistributionResponse.status}'` - ); - - const errorDistributionResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', - params: { - body: { - ...getOptions(), - percentileThreshold: 95, - termFilters: [{ fieldName: EVENT_OUTCOME, fieldValue: EventOutcome.failure }], - chartType: LatencyDistributionChartType.failedTransactionsCorrelations, - }, - }, - }); - - expect(errorDistributionResponse.status).to.eql( - 200, - `Expected status to be '200', got '${errorDistributionResponse.status}'` - ); - - const fieldCandidatesResponse = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/correlations/field_candidates/transactions', - params: { - query: getOptions(), - }, - }); - - expect(fieldCandidatesResponse.status).to.eql( - 200, - `Expected status to be '200', got '${fieldCandidatesResponse.status}'` - ); - - const fieldCandidates = fieldCandidatesResponse.body?.fieldCandidates.filter( - (t) => !(t === EVENT_OUTCOME) - ); - - // Identified 80 fieldCandidates. - expect(fieldCandidates.length).to.eql( - 80, - `Expected field candidates length to be '80', got '${fieldCandidates.length}'` - ); - - const failedTransactionsCorrelationsResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/correlations/p_values/transactions', - params: { - body: { - ...getOptions(), - fieldCandidates, - }, - }, - }); - - expect(failedTransactionsCorrelationsResponse.status).to.eql( - 200, - `Expected status to be '200', got '${failedTransactionsCorrelationsResponse.status}'` - ); - - const fieldsToSample = new Set(); - if (failedTransactionsCorrelationsResponse.body?.failedTransactionsCorrelations.length > 0) { - failedTransactionsCorrelationsResponse.body?.failedTransactionsCorrelations.forEach((d) => { - fieldsToSample.add(d.fieldName); - }); - } - - const finalRawResponse: FailedTransactionsCorrelationsResponse = { - ccsWarning: failedTransactionsCorrelationsResponse.body?.ccsWarning, - percentileThresholdValue: overallDistributionResponse.body?.percentileThresholdValue, - overallHistogram: overallDistributionResponse.body?.overallHistogram, - errorHistogram: errorDistributionResponse.body?.overallHistogram, - failedTransactionsCorrelations: - failedTransactionsCorrelationsResponse.body?.failedTransactionsCorrelations, - }; - - expect(finalRawResponse?.percentileThresholdValue).to.be(1309695.875); - expect(finalRawResponse?.errorHistogram?.length).to.be(101); - expect(finalRawResponse?.overallHistogram?.length).to.be(101); - - expect(finalRawResponse?.failedTransactionsCorrelations?.length).to.eql( - 29, - `Expected 29 identified correlations, got ${finalRawResponse?.failedTransactionsCorrelations?.length}.` - ); - - const sortedCorrelations = orderBy( - finalRawResponse?.failedTransactionsCorrelations, - ['score', 'fieldName', 'fieldValue'], - ['desc', 'asc', 'asc'] - ); - const correlation = sortedCorrelations?.[0]; - - expect(typeof correlation).to.be('object'); - expect(correlation?.doc_count).to.be(31); - expect(correlation?.score).to.be(83.70467673605746); - expect(correlation?.bg_count).to.be(31); - expect(correlation?.fieldName).to.be('transaction.result'); - expect(correlation?.fieldValue).to.be('HTTP 5xx'); - expect(typeof correlation?.pValue).to.be('number'); - expect(typeof correlation?.normalizedScore).to.be('number'); - expect(typeof correlation?.failurePercentage).to.be('number'); - expect(typeof correlation?.successPercentage).to.be('number'); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/correlations/field_candidates.spec.ts b/x-pack/test/apm_api_integration/tests/correlations/field_candidates.spec.ts deleted file mode 100644 index 4a5472cf5cb23..0000000000000 --- a/x-pack/test/apm_api_integration/tests/correlations/field_candidates.spec.ts +++ /dev/null @@ -1,57 +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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiClient = getService('apmApiClient'); - const registry = getService('registry'); - - const endpoint = 'GET /internal/apm/correlations/field_candidates/transactions'; - - const getOptions = () => ({ - params: { - query: { - environment: 'ENVIRONMENT_ALL', - start: '2020', - end: '2021', - kuery: '', - }, - }, - }); - - registry.when('field candidates without data', { config: 'trial', archives: [] }, () => { - it('handles the empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint, - ...getOptions(), - }); - - expect(response.status).to.be(200); - // If the source indices are empty, there will be no field candidates - // because of the `include_empty_fields: false` option in the query. - expect(response.body?.fieldCandidates.length).to.be(0); - }); - }); - - registry.when( - 'field candidates with data and default args', - { config: 'trial', archives: ['8.0.0'] }, - () => { - it('returns field candidates', async () => { - const response = await apmApiClient.readUser({ - endpoint, - ...getOptions(), - }); - - expect(response.status).to.eql(200); - expect(response.body?.fieldCandidates.length).to.be(81); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/entities/logs/service_logs_rate_timeseries.spec.ts b/x-pack/test/apm_api_integration/tests/entities/logs/service_logs_rate_timeseries.spec.ts deleted file mode 100644 index d4717b25bba93..0000000000000 --- a/x-pack/test/apm_api_integration/tests/entities/logs/service_logs_rate_timeseries.spec.ts +++ /dev/null @@ -1,173 +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 expect from '@kbn/expect'; -import { log, timerange } from '@kbn/apm-synthtrace-client'; -import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; -import { APIClientRequestParamsOf } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { first, last } from 'lodash'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const logSynthtrace = getService('logSynthtraceEsClient'); - - const serviceName = 'synth-go'; - const start = new Date('2024-01-01T00:00:00.000Z').getTime(); - const end = new Date('2024-01-01T00:15:00.000Z').getTime() - 1; - - const hostName = 'synth-host'; - - async function getLogsRateTimeseries( - overrides?: RecursivePartial< - APIClientRequestParamsOf<'GET /internal/apm/entities/services/{serviceName}/logs_rate_timeseries'>['params'] - > - ) { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/entities/services/{serviceName}/logs_rate_timeseries', - params: { - path: { - serviceName: 'synth-go', - ...overrides?.path, - }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - environment: 'ENVIRONMENT_ALL', - kuery: '', - ...overrides?.query, - }, - }, - }); - return response; - } - - registry.when( - 'Logs rate timeseries when data is not loaded', - { config: 'basic', archives: [] }, - () => { - describe('Logs rate api', () => { - it('handles the empty state', async () => { - const response = await getLogsRateTimeseries(); - expect(response.status).to.be(200); - expect(response.body.currentPeriod).to.empty(); - }); - }); - } - ); - - registry.when('Logs rate timeseries when data loaded', { config: 'basic', archives: [] }, () => { - describe('Logs without log level field', () => { - before(async () => { - return logSynthtrace.index([ - timerange(start, end) - .interval('1m') - .rate(1) - .generator((timestamp) => - log.create().message('This is a log message').timestamp(timestamp).defaults({ - 'log.file.path': '/my-service.log', - 'service.name': serviceName, - 'host.name': hostName, - }) - ), - ]); - }); - after(async () => { - await logSynthtrace.clean(); - }); - - it('returns {} if log level is not available ', async () => { - const response = await getLogsRateTimeseries(); - expect(response.status).to.be(200); - }); - }); - - describe('Logs with log.level=error', () => { - before(async () => { - return logSynthtrace.index([ - timerange(start, end) - .interval('1m') - .rate(1) - .generator((timestamp) => - log - .create() - .message('This is a log message') - .logLevel('error') - .timestamp(timestamp) - .defaults({ - 'log.file.path': '/my-service.log', - 'service.name': serviceName, - 'host.name': hostName, - 'service.environment': 'test', - }) - ), - timerange(start, end) - .interval('2m') - .rate(1) - .generator((timestamp) => - log - .create() - .message('This is an error log message') - .logLevel('error') - .timestamp(timestamp) - .defaults({ - 'log.file.path': '/my-service.log', - 'service.name': 'my-service', - 'host.name': hostName, - 'service.environment': 'production', - }) - ), - timerange(start, end) - .interval('5m') - .rate(1) - .generator((timestamp) => - log - .create() - .message('This is an info message') - .logLevel('info') - .timestamp(timestamp) - .defaults({ - 'log.file.path': '/my-service.log', - 'service.name': 'my-service', - 'host.name': hostName, - 'service.environment': 'production', - }) - ), - ]); - }); - after(async () => { - await logSynthtrace.clean(); - }); - - it('returns log rate timeseries', async () => { - const response = await getLogsRateTimeseries(); - expect(response.status).to.be(200); - expect( - response.body.currentPeriod[serviceName].every(({ y }) => y === 0.06666666666666667) - ).to.be(true); - }); - - it('handles environment filter', async () => { - const response = await getLogsRateTimeseries({ query: { environment: 'foo' } }); - expect(response.status).to.be(200); - expect(response.body.currentPeriod).to.empty(); - }); - - describe('when my-service is selected', () => { - it('returns some data', async () => { - const response = await getLogsRateTimeseries({ - path: { serviceName: 'my-service' }, - }); - - expect(response.status).to.be(200); - expect(first(response.body.currentPeriod?.['my-service'])?.y).to.be(0.18181818181818182); - expect(last(response.body.currentPeriod?.['my-service'])?.y).to.be(0.09090909090909091); - }); - }); - }); - }); -} diff --git a/x-pack/test/cloud_security_posture_api/es_archives/logs_gcp_audit/data.json b/x-pack/test/cloud_security_posture_api/es_archives/logs_gcp_audit/data.json index 9f536d0bb6dc9..37f7ebdff5fb1 100644 --- a/x-pack/test/cloud_security_posture_api/es_archives/logs_gcp_audit/data.json +++ b/x-pack/test/cloud_security_posture_api/es_archives/logs_gcp_audit/data.json @@ -497,3 +497,123 @@ } } } + +{ + "type": "doc", + "value": { + "data_stream": "logs-gcp.audit-default", + "id": "5", + "index": ".ds-logs-gcp.audit-default-2024.10.07-000001", + "source": { + "@timestamp": "2024-09-01T12:34:56.789Z", + "actor": { + "entity": { + "id": "admin5@example.com" + } + }, + "client": { + "user": { + "email": "admin5@example.com" + } + }, + "cloud": { + "project": { + "id": "your-project-id" + }, + "provider": "gcp" + }, + "ecs": { + "version": "8.11.0" + }, + "event": { + "action": "google.iam.admin.v1.ListRoles", + "agent_id_status": "missing", + "category": [ + "session", + "network", + "configuration" + ], + "id": "without target", + "ingested": "2024-10-07T17:47:35Z", + "kind": "event", + "outcome": "success", + "provider": "activity", + "type": [ + "end", + "access", + "allowed" + ] + }, + "gcp": { + "audit": { + "authorization_info": [ + { + "granted": true, + "permission": "iam.roles.create", + "resource": "projects/your-project-id" + } + ], + "logentry_operation": { + "id": "operation-0987654321" + }, + "request": { + "@type": "type.googleapis.com/google.iam.admin.v1.CreateRoleRequest", + "parent": "projects/your-project-id", + "role": { + "description": "A custom role with specific permissions", + "includedPermissions": [ + "resourcemanager.projects.get", + "resourcemanager.projects.list" + ], + "name": "projects/your-project-id/roles/customRole", + "title": "Custom Role" + }, + "roleId": "customRole" + }, + "resource_name": "projects/your-project-id/roles/customRole", + "response": { + "@type": "type.googleapis.com/google.iam.admin.v1.Role", + "description": "A custom role with specific permissions", + "includedPermissions": [ + "resourcemanager.projects.get", + "resourcemanager.projects.list" + ], + "name": "projects/your-project-id/roles/customRole", + "stage": "GA", + "title": "Custom Role" + }, + "type": "type.googleapis.com/google.cloud.audit.AuditLog" + } + }, + "log": { + "level": "NOTICE", + "logger": "projects/your-project-id/logs/cloudaudit.googleapis.com%2Factivity" + }, + "related": { + "ip": [ + "10.0.0.1" + ], + "user": [ + "admin3@example.com" + ] + }, + "service": { + "name": "iam.googleapis.com" + }, + "source": { + "ip": "10.0.0.1" + }, + "tags": [ + "_geoip_database_unavailable_GeoLite2-City.mmdb", + "_geoip_database_unavailable_GeoLite2-ASN.mmdb" + ], + "user_agent": { + "device": { + "name": "Other" + }, + "name": "Other", + "original": "google-cloud-sdk/324.0.0" + } + } + } +} diff --git a/x-pack/test/cloud_security_posture_api/routes/graph.ts b/x-pack/test/cloud_security_posture_api/routes/graph.ts index bd2f71ef3b9b2..8043e6e22feb6 100644 --- a/x-pack/test/cloud_security_posture_api/routes/graph.ts +++ b/x-pack/test/cloud_security_posture_api/routes/graph.ts @@ -11,6 +11,8 @@ import { } from '@kbn/core-http-common'; import expect from '@kbn/expect'; import type { Agent } from 'supertest'; +import { ApiMessageCode } from '@kbn/cloud-security-posture-common/types/graph/latest'; +import type { GraphRequest } from '@kbn/cloud-security-posture-common/types/graph/latest'; import { FtrProviderContext } from '../ftr_provider_context'; import { result } from '../utils'; import { CspSecurityCommonProvider } from './helper/user_roles_utilites'; @@ -19,12 +21,13 @@ import { CspSecurityCommonProvider } from './helper/user_roles_utilites'; export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; + const logger = getService('log'); const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const supertestWithoutAuth = getService('supertestWithoutAuth'); const cspSecurity = CspSecurityCommonProvider(providerContext); - const postGraph = (agent: Agent, body: any, auth?: { user: string; pass: string }) => { + const postGraph = (agent: Agent, body: GraphRequest, auth?: { user: string; pass: string }) => { const req = agent .post('/internal/cloud_security_posture/graph') .set(ELASTIC_HTTP_VERSION_HEADER, '1') @@ -45,7 +48,6 @@ export default function (providerContext: FtrProviderContext) { supertestWithoutAuth, { query: { - actorIds: [], eventIds: [], start: 'now-1d/d', end: 'now/d', @@ -55,19 +57,7 @@ export default function (providerContext: FtrProviderContext) { user: 'role_security_no_read_user', pass: cspSecurity.getPasswordForUser('role_security_no_read_user'), } - ).expect(result(403)); - }); - }); - - describe('Validation', () => { - it('should return 400 when missing `actorIds` field', async () => { - await postGraph(supertest, { - query: { - eventIds: [], - start: 'now-1d/d', - end: 'now/d', - }, - }).expect(result(400)); + ).expect(result(403, logger)); }); }); @@ -84,10 +74,54 @@ export default function (providerContext: FtrProviderContext) { ); }); - it('should return an empty graph', async () => { + describe('Validation', () => { + it('should return 400 when missing `eventIds` field', async () => { + await postGraph(supertest, { + // @ts-expect-error ignore error for testing + query: { + start: 'now-1d/d', + end: 'now/d', + }, + }).expect(result(400, logger)); + }); + + it('should return 400 when missing `esQuery` field is not of type bool', async () => { + await postGraph(supertest, { + query: { + eventIds: [], + start: 'now-1d/d', + end: 'now/d', + esQuery: { + // @ts-expect-error ignore error for testing + match_all: {}, + }, + }, + }).expect(result(400, logger)); + }); + + it('should return 400 with unsupported `esQuery`', async () => { + await postGraph(supertest, { + query: { + eventIds: [], + start: 'now-1d/d', + end: 'now/d', + esQuery: { + bool: { + filter: [ + { + unsupported: 'unsupported', + }, + ], + }, + }, + }, + }).expect(result(400, logger)); + }); + }); + + it('should return an empty graph / should return 200 when missing `esQuery` field', async () => { const response = await postGraph(supertest, { query: { - actorIds: [], eventIds: [], start: 'now-1d/d', end: 'now/d', @@ -96,20 +130,32 @@ export default function (providerContext: FtrProviderContext) { expect(response.body).to.have.property('nodes').length(0); expect(response.body).to.have.property('edges').length(0); + expect(response.body).not.to.have.property('messages'); }); it('should return a graph with nodes and edges by actor', async () => { const response = await postGraph(supertest, { query: { - actorIds: ['admin@example.com'], eventIds: [], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', + esQuery: { + bool: { + filter: [ + { + match_phrase: { + 'actor.entity.id': 'admin@example.com', + }, + }, + ], + }, + }, }, }).expect(result(200)); expect(response.body).to.have.property('nodes').length(3); expect(response.body).to.have.property('edges').length(2); + expect(response.body).not.to.have.property('messages'); response.body.nodes.forEach((node: any) => { expect(node).to.have.property('color'); @@ -131,7 +177,6 @@ export default function (providerContext: FtrProviderContext) { it('should return a graph with nodes and edges by alert', async () => { const response = await postGraph(supertest, { query: { - actorIds: [], eventIds: ['kabcd1234efgh5678'], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', @@ -140,6 +185,7 @@ export default function (providerContext: FtrProviderContext) { expect(response.body).to.have.property('nodes').length(3); expect(response.body).to.have.property('edges').length(2); + expect(response.body).not.to.have.property('messages'); response.body.nodes.forEach((node: any) => { expect(node).to.have.property('color'); @@ -161,7 +207,6 @@ export default function (providerContext: FtrProviderContext) { it('color of alert of failed event should be danger', async () => { const response = await postGraph(supertest, { query: { - actorIds: [], eventIds: ['failed-event'], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', @@ -170,6 +215,7 @@ export default function (providerContext: FtrProviderContext) { expect(response.body).to.have.property('nodes').length(3); expect(response.body).to.have.property('edges').length(2); + expect(response.body).not.to.have.property('messages'); response.body.nodes.forEach((node: any) => { expect(node).to.have.property('color'); @@ -191,15 +237,26 @@ export default function (providerContext: FtrProviderContext) { it('color of event of failed event should be warning', async () => { const response = await postGraph(supertest, { query: { - actorIds: ['admin2@example.com'], eventIds: [], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', + esQuery: { + bool: { + filter: [ + { + match_phrase: { + 'actor.entity.id': 'admin2@example.com', + }, + }, + ], + }, + }, }, }).expect(result(200)); expect(response.body).to.have.property('nodes').length(3); expect(response.body).to.have.property('edges').length(2); + expect(response.body).not.to.have.property('messages'); response.body.nodes.forEach((node: any) => { expect(node).to.have.property('color'); @@ -219,18 +276,29 @@ export default function (providerContext: FtrProviderContext) { }); }); - it('2 grouped of events, 1 failed, 1 success', async () => { + it('2 grouped events, 1 failed, 1 success', async () => { const response = await postGraph(supertest, { query: { - actorIds: ['admin3@example.com'], eventIds: [], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', + esQuery: { + bool: { + filter: [ + { + match_phrase: { + 'actor.entity.id': 'admin3@example.com', + }, + }, + ], + }, + }, }, }).expect(result(200)); expect(response.body).to.have.property('nodes').length(5); expect(response.body).to.have.property('edges').length(6); + expect(response.body).not.to.have.property('messages'); expect(response.body.nodes[0].shape).equal('group', 'Groups should be the first nodes'); @@ -247,11 +315,167 @@ export default function (providerContext: FtrProviderContext) { response.body.edges.forEach((edge: any) => { expect(edge).to.have.property('color'); expect(edge.color).equal( - edge.id.includes('outcome(failed)') ? 'warning' : 'primary', + edge.id.includes('outcome(failed)') || + (edge.id.includes('grp(') && !edge.id.includes('outcome(success)')) + ? 'warning' + : 'primary', + `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` + ); + }); + }); + + it('should support more than 1 eventIds', async () => { + const response = await postGraph(supertest, { + query: { + eventIds: ['kabcd1234efgh5678', 'failed-event'], + start: '2024-09-01T00:00:00Z', + end: '2024-09-02T00:00:00Z', + }, + }).expect(result(200)); + + expect(response.body).to.have.property('nodes').length(5); + expect(response.body).to.have.property('edges').length(4); + expect(response.body).not.to.have.property('messages'); + + response.body.nodes.forEach((node: any) => { + expect(node).to.have.property('color'); + expect(node.color).equal( + 'danger', + `node color mismatched [node: ${node.id}] [actual: ${node.color}]` + ); + }); + + response.body.edges.forEach((edge: any) => { + expect(edge).to.have.property('color'); + expect(edge.color).equal( + 'danger', + `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` + ); + }); + }); + + it('should return a graph with nodes and edges by alert and actor', async () => { + const response = await postGraph(supertest, { + query: { + eventIds: ['kabcd1234efgh5678'], + start: '2024-09-01T00:00:00Z', + end: '2024-09-02T00:00:00Z', + esQuery: { + bool: { + filter: [ + { + match_phrase: { + 'actor.entity.id': 'admin2@example.com', + }, + }, + ], + }, + }, + }, + }).expect(result(200)); + + expect(response.body).to.have.property('nodes').length(5); + expect(response.body).to.have.property('edges').length(4); + expect(response.body).not.to.have.property('messages'); + + response.body.nodes.forEach((node: any, idx: number) => { + expect(node).to.have.property('color'); + expect(node.color).equal( + idx <= 2 // First 3 nodes are expected to be colored as danger (ORDER MATTERS, alerts are expected to be first) + ? 'danger' + : node.shape === 'label' && node.id.includes('outcome(failed)') + ? 'warning' + : 'primary', + `node color mismatched [node: ${node.id}] [actual: ${node.color}]` + ); + }); + + response.body.edges.forEach((edge: any, idx: number) => { + expect(edge).to.have.property('color'); + expect(edge.color).equal( + idx <= 1 ? 'danger' : 'warning', `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` ); }); }); + + it('Should filter unknown targets', async () => { + const response = await postGraph(supertest, { + query: { + eventIds: [], + start: '2024-09-01T00:00:00Z', + end: '2024-09-02T00:00:00Z', + esQuery: { + bool: { + filter: [ + { + match_phrase: { + 'actor.entity.id': 'admin5@example.com', + }, + }, + ], + }, + }, + }, + }).expect(result(200)); + + expect(response.body).to.have.property('nodes').length(0); + expect(response.body).to.have.property('edges').length(0); + expect(response.body).not.to.have.property('messages'); + }); + + it('Should return unknown targets', async () => { + const response = await postGraph(supertest, { + showUnknownTarget: true, + query: { + eventIds: [], + start: '2024-09-01T00:00:00Z', + end: '2024-09-02T00:00:00Z', + esQuery: { + bool: { + filter: [ + { + match_phrase: { + 'actor.entity.id': 'admin5@example.com', + }, + }, + ], + }, + }, + }, + }).expect(result(200)); + + expect(response.body).to.have.property('nodes').length(3); + expect(response.body).to.have.property('edges').length(2); + expect(response.body).not.to.have.property('messages'); + }); + + it('Should limit number of nodes', async () => { + const response = await postGraph(supertest, { + nodesLimit: 1, + query: { + eventIds: [], + start: '2024-09-01T00:00:00Z', + end: '2024-09-02T00:00:00Z', + esQuery: { + bool: { + filter: [ + { + exists: { + field: 'actor.entity.id', + }, + }, + ], + }, + }, + }, + }).expect(result(200)); + + expect(response.body).to.have.property('nodes').length(3); // Minimal number of nodes in a single relationship + expect(response.body).to.have.property('edges').length(2); + expect(response.body).to.have.property('messages').length(1); + expect(response.body.messages[0]).equal(ApiMessageCode.ReachedNodesLimit); + }); }); }); } diff --git a/x-pack/test/cloud_security_posture_api/utils.ts b/x-pack/test/cloud_security_posture_api/utils.ts index e64c583af3868..210a081b91473 100644 --- a/x-pack/test/cloud_security_posture_api/utils.ts +++ b/x-pack/test/cloud_security_posture_api/utils.ts @@ -36,22 +36,23 @@ export const waitForPluginInitialized = ({ logger.debug('CSP plugin is initialized'); }); -export function result(status: number): CallbackHandler { +export function result(status: number, logger?: ToolingLog): CallbackHandler { return (err: any, res: Response) => { if ((res?.status || err.status) !== status) { - const e = new Error( + throw new Error( `Expected ${status} ,got ${res?.status || err.status} resp: ${ res?.body ? JSON.stringify(res.body) : err.text }` ); - throw e; + } else if (err) { + logger?.warning(`Error result ${err.text}`); } }; } export class EsIndexDataProvider { private es: EsClient; - private index: string; + private readonly index: string; constructor(es: EsClient, index: string) { this.es = es; diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_with_streaming.ts b/x-pack/test/fleet_api_integration/apis/epm/install_with_streaming.ts index 152e3dfd4c69d..4fa0e485be2b5 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_with_streaming.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_with_streaming.ts @@ -36,7 +36,8 @@ export default function (providerContext: FtrProviderContext) { return res?._source?.['epm-packages'] as Installation; }; - describe('Installs a package using stream-based approach', () => { + // Failing: See https://github.com/elastic/kibana/issues/199701 + describe.skip('Installs a package using stream-based approach', () => { skipIfNoDockerRegistry(providerContext); before(async () => { diff --git a/x-pack/test/functional/page_objects/index_management_page.ts b/x-pack/test/functional/page_objects/index_management_page.ts index f257f76cbfc5b..8053293f98633 100644 --- a/x-pack/test/functional/page_objects/index_management_page.ts +++ b/x-pack/test/functional/page_objects/index_management_page.ts @@ -162,13 +162,13 @@ export function IndexManagementPageProvider({ getService }: FtrProviderContext) }, async clickCreateIndexButton() { await testSubjects.click('createIndexButton'); - await testSubjects.existOrFail('createIndexSaveButton'); }, async setCreateIndexName(value: string) { await testSubjects.existOrFail('createIndexNameFieldText'); await testSubjects.setValue('createIndexNameFieldText', value); }, async clickCreateIndexSaveButton() { + await testSubjects.existOrFail('createIndexSaveButton'); await testSubjects.click('createIndexSaveButton'); // Wait for modal to close await testSubjects.missingOrFail('createIndexSaveButton', { diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts index 51b0842e60bc8..c5927d894911c 100644 --- a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts @@ -114,6 +114,34 @@ export function initRoutes( } ); + router.post( + { + path: `/api/sample_tasks/run_mark_removed_tasks_as_unrecognized`, + validate: { + body: schema.object({}), + }, + }, + async function ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + try { + const taskManager = await taskManagerStart; + await taskManager.ensureScheduled({ + id: 'mark_removed_tasks_as_unrecognized', + taskType: 'task_manager:mark_removed_tasks_as_unrecognized', + schedule: { interval: '1h' }, + state: {}, + params: {}, + }); + return res.ok({ body: await taskManager.runSoon('mark_removed_tasks_as_unrecognized') }); + } catch (err) { + return res.ok({ body: { id: 'mark_removed_tasks_as_unrecognized', error: `${err}` } }); + } + } + ); + router.post( { path: `/api/sample_tasks/bulk_enable`, diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts index 091e0fe01e415..c8056c2ee205e 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts @@ -168,6 +168,7 @@ export default function ({ getService }: FtrProviderContext) { 'security:telemetry-timelines', 'session_cleanup', 'task_manager:delete_inactive_background_task_nodes', + 'task_manager:mark_removed_tasks_as_unrecognized', ]); }); }); diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management_removed_types.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management_removed_types.ts index aae90a52572c7..a7447353e805a 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management_removed_types.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management_removed_types.ts @@ -92,6 +92,12 @@ export default function ({ getService }: FtrProviderContext) { let scheduledTaskRuns = 0; let scheduledTaskInstanceRunAt = scheduledTask.runAt; + await request + .post('/api/sample_tasks/run_mark_removed_tasks_as_unrecognized') + .set('kbn-xsrf', 'xxx') + .send({}) + .expect(200); + await retry.try(async () => { const tasks = (await currentTasks()).docs; expect(tasks.length).to.eql(3); diff --git a/x-pack/test/task_manager_claimer_update_by_query/plugins/sample_task_plugin_mget/server/init_routes.ts b/x-pack/test/task_manager_claimer_update_by_query/plugins/sample_task_plugin_mget/server/init_routes.ts index f1e697399fe09..acdbae0b00337 100644 --- a/x-pack/test/task_manager_claimer_update_by_query/plugins/sample_task_plugin_mget/server/init_routes.ts +++ b/x-pack/test/task_manager_claimer_update_by_query/plugins/sample_task_plugin_mget/server/init_routes.ts @@ -117,6 +117,34 @@ export function initRoutes( } ); + router.post( + { + path: `/api/sample_tasks/run_mark_removed_tasks_as_unrecognized`, + validate: { + body: schema.object({}), + }, + }, + async function ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + try { + const taskManager = await taskManagerStart; + await taskManager.ensureScheduled({ + id: 'mark_removed_tasks_as_unrecognized', + taskType: 'task_manager:mark_removed_tasks_as_unrecognized', + schedule: { interval: '1h' }, + state: {}, + params: {}, + }); + return res.ok({ body: await taskManager.runSoon('mark_removed_tasks_as_unrecognized') }); + } catch (err) { + return res.ok({ body: { id: 'mark_removed_tasks_as_unrecognized', error: `${err}` } }); + } + } + ); + router.post( { path: `/api/sample_tasks/bulk_enable`, diff --git a/x-pack/test/task_manager_claimer_update_by_query/test_suites/task_manager/task_management_removed_types.ts b/x-pack/test/task_manager_claimer_update_by_query/test_suites/task_manager/task_management_removed_types.ts index aae90a52572c7..a7447353e805a 100644 --- a/x-pack/test/task_manager_claimer_update_by_query/test_suites/task_manager/task_management_removed_types.ts +++ b/x-pack/test/task_manager_claimer_update_by_query/test_suites/task_manager/task_management_removed_types.ts @@ -92,6 +92,12 @@ export default function ({ getService }: FtrProviderContext) { let scheduledTaskRuns = 0; let scheduledTaskInstanceRunAt = scheduledTask.runAt; + await request + .post('/api/sample_tasks/run_mark_removed_tasks_as_unrecognized') + .set('kbn-xsrf', 'xxx') + .send({}) + .expect(200); + await retry.try(async () => { const tasks = (await currentTasks()).docs; expect(tasks.length).to.eql(3); diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/graph.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/graph.ts index 741d25291e8fa..aaccdd0e9a41c 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/graph.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/graph.ts @@ -12,6 +12,7 @@ import { } from '@kbn/core-http-common'; import { result } from '@kbn/test-suites-xpack/cloud_security_posture_api/utils'; import type { Agent } from 'supertest'; +import type { GraphRequest } from '@kbn/cloud-security-posture-common/types/graph/v1'; import type { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { @@ -19,7 +20,7 @@ export default function ({ getService }: FtrProviderContext) { const roleScopedSupertest = getService('roleScopedSupertest'); let supertestViewer: Pick; - const postGraph = (agent: Pick, body: any) => { + const postGraph = (agent: Pick, body: GraphRequest) => { const req = agent .post('/internal/cloud_security_posture/graph') .set(ELASTIC_HTTP_VERSION_HEADER, '1') @@ -48,7 +49,6 @@ export default function ({ getService }: FtrProviderContext) { it('should return an empty graph', async () => { const response = await postGraph(supertestViewer, { query: { - actorIds: [], eventIds: [], start: 'now-1d/d', end: 'now/d', @@ -57,20 +57,26 @@ export default function ({ getService }: FtrProviderContext) { expect(response.body).to.have.property('nodes').length(0); expect(response.body).to.have.property('edges').length(0); + expect(response.body).not.to.have.property('messages'); }); it('should return a graph with nodes and edges by actor', async () => { const response = await postGraph(supertestViewer, { query: { - actorIds: ['admin@example.com'], eventIds: [], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', + esQuery: { + bool: { + filter: [{ match_phrase: { 'actor.entity.id': 'admin@example.com' } }], + }, + }, }, }).expect(result(200)); expect(response.body).to.have.property('nodes').length(3); expect(response.body).to.have.property('edges').length(2); + expect(response.body).not.to.have.property('messages'); response.body.nodes.forEach((node: any) => { expect(node).to.have.property('color'); diff --git a/x-pack/test_serverless/functional/page_objects/index.ts b/x-pack/test_serverless/functional/page_objects/index.ts index 0c6b776433b0c..2c894b28b065f 100644 --- a/x-pack/test_serverless/functional/page_objects/index.ts +++ b/x-pack/test_serverless/functional/page_objects/index.ts @@ -24,6 +24,7 @@ import { SvlSearchHomePageProvider } from './svl_search_homepage'; import { SvlSearchIndexDetailPageProvider } from './svl_search_index_detail_page'; import { SvlSearchElasticsearchStartPageProvider } from './svl_search_elasticsearch_start_page'; import { SvlApiKeysProvider } from './svl_api_keys'; +import { SvlSearchCreateIndexPageProvider } from './svl_search_create_index_page'; export const pageObjects = { ...xpackFunctionalPageObjects, @@ -45,4 +46,5 @@ export const pageObjects = { svlSearchIndexDetailPage: SvlSearchIndexDetailPageProvider, svlSearchElasticsearchStartPage: SvlSearchElasticsearchStartPageProvider, svlApiKeys: SvlApiKeysProvider, + svlSearchCreateIndexPage: SvlSearchCreateIndexPageProvider, }; diff --git a/x-pack/test_serverless/functional/page_objects/svl_search_create_index_page.ts b/x-pack/test_serverless/functional/page_objects/svl_search_create_index_page.ts new file mode 100644 index 0000000000000..3d0d48f98a6a5 --- /dev/null +++ b/x-pack/test_serverless/functional/page_objects/svl_search_create_index_page.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export function SvlSearchCreateIndexPageProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const browser = getService('browser'); + const retry = getService('retry'); + + return { + async expectToBeOnCreateIndexPage() { + expect(await browser.getCurrentUrl()).contain('/app/elasticsearch/indices/create'); + await testSubjects.existOrFail('elasticsearchCreateIndexPage', { timeout: 2000 }); + }, + async expectToBeOnIndexDetailsPage() { + await retry.tryForTime(60 * 1000, async () => { + expect(await browser.getCurrentUrl()).contain('/app/elasticsearch/indices/index_details'); + }); + }, + async expectToBeOnIndexListPage() { + await retry.tryForTime(60 * 1000, async () => { + expect(await browser.getCurrentUrl()).contain( + '/app/management/data/index_management/indices' + ); + }); + }, + async expectToBeOnMLFileUploadPage() { + await retry.tryForTime(60 * 1000, async () => { + expect(await browser.getCurrentUrl()).contain('/app/ml/filedatavisualizer'); + }); + }, + async expectIndexNameToExist() { + await testSubjects.existOrFail('indexNameField'); + }, + async setIndexNameValue(value: string) { + await testSubjects.existOrFail('indexNameField'); + await testSubjects.setValue('indexNameField', value); + }, + async expectCloseCreateIndexButtonExists() { + await testSubjects.existOrFail('closeCreateIndex'); + }, + async clickCloseCreateIndexButton() { + await testSubjects.existOrFail('closeCreateIndex'); + await testSubjects.click('closeCreateIndex'); + }, + async expectCreateIndexButtonToExist() { + await testSubjects.existOrFail('createIndexBtn'); + }, + async expectCreateIndexButtonToBeEnabled() { + await testSubjects.existOrFail('createIndexBtn'); + expect(await testSubjects.isEnabled('createIndexBtn')).equal(true); + }, + async expectCreateIndexButtonToBeDisabled() { + await testSubjects.existOrFail('createIndexBtn'); + expect(await testSubjects.isEnabled('createIndexBtn')).equal(false); + }, + async clickCreateIndexButton() { + await testSubjects.existOrFail('createIndexBtn'); + expect(await testSubjects.isEnabled('createIndexBtn')).equal(true); + await testSubjects.click('createIndexBtn'); + }, + async expectCreateIndexCodeView() { + await testSubjects.existOrFail('createIndexCodeView'); + }, + async expectCreateIndexUIView() { + await testSubjects.existOrFail('createIndexUIView'); + }, + async clickUIViewButton() { + await testSubjects.existOrFail('createIndexUIViewBtn'); + await testSubjects.click('createIndexUIViewBtn'); + }, + async clickCodeViewButton() { + await testSubjects.existOrFail('createIndexCodeViewBtn'); + await testSubjects.click('createIndexCodeViewBtn'); + }, + async clickFileUploadLink() { + await testSubjects.existOrFail('uploadFileLink'); + await testSubjects.click('uploadFileLink'); + }, + async expectAPIKeyVisibleInCodeBlock(apiKey: string) { + await testSubjects.existOrFail('createIndex-code-block'); + await retry.try(async () => { + expect(await testSubjects.getVisibleText('createIndex-code-block')).to.contain(apiKey); + }); + }, + + async expectAPIKeyPreGenerated() { + await testSubjects.existOrFail('apiKeyHasBeenGenerated'); + }, + + async expectAPIKeyNotPreGenerated() { + await testSubjects.existOrFail('apiKeyHasNotBeenGenerated'); + }, + + async expectAPIKeyFormNotAvailable() { + await testSubjects.missingOrFail('apiKeyHasNotBeenGenerated'); + await testSubjects.missingOrFail('apiKeyHasBeenGenerated'); + }, + }; +} diff --git a/x-pack/test_serverless/functional/page_objects/svl_search_elasticsearch_start_page.ts b/x-pack/test_serverless/functional/page_objects/svl_search_elasticsearch_start_page.ts index 3a3c9700cf2aa..aadb41d6f432a 100644 --- a/x-pack/test_serverless/functional/page_objects/svl_search_elasticsearch_start_page.ts +++ b/x-pack/test_serverless/functional/page_objects/svl_search_elasticsearch_start_page.ts @@ -42,6 +42,20 @@ export function SvlSearchElasticsearchStartPageProvider({ getService }: FtrProvi await testSubjects.existOrFail('indexNameField'); await testSubjects.setValue('indexNameField', value); }, + async expectCloseCreateIndexButtonExists() { + await testSubjects.existOrFail('closeCreateIndex'); + }, + async clickCloseCreateIndexButton() { + await testSubjects.existOrFail('closeCreateIndex'); + await testSubjects.click('closeCreateIndex'); + }, + async expectSkipButtonExists() { + await testSubjects.existOrFail('createIndexSkipBtn'); + }, + async clickSkipButton() { + await testSubjects.existOrFail('createIndexSkipBtn'); + await testSubjects.click('createIndexSkipBtn'); + }, async expectCreateIndexButtonToExist() { await testSubjects.existOrFail('createIndexBtn'); }, diff --git a/x-pack/test_serverless/functional/test_suites/common/management/index_management/index_detail.ts b/x-pack/test_serverless/functional/test_suites/common/management/index_management/index_detail.ts index f8734e610a61c..be3b683d9903a 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/index_management/index_detail.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/index_management/index_detail.ts @@ -14,6 +14,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const security = getService('security'); const testIndexName = `index-ftr-test-${Math.random()}`; describe('Index Details ', function () { + this.tags(['skipSvlSearch']); before(async () => { await security.testUser.setRoles(['index_management_user']); await pageObjects.svlCommonPage.loginAsAdmin(); @@ -34,7 +35,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.indexManagement.expectIndexToExist(testIndexName); }); describe('can view index details', function () { - this.tags(['skipSvlSearch']); it('index with no documents', async () => { await pageObjects.indexManagement.indexDetailsPage.openIndexDetailsPage(0); await pageObjects.indexManagement.indexDetailsPage.expectIndexDetailsPageIsLoaded(); diff --git a/x-pack/test_serverless/functional/test_suites/common/management/index_management/indices.ts b/x-pack/test_serverless/functional/test_suites/common/management/index_management/indices.ts index e98fcc09e97d1..0d8f091123627 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/index_management/indices.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/index_management/indices.ts @@ -17,6 +17,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const es = getService('es'); describe('Indices', function () { + this.tags(['skipSvlSearch']); before(async () => { await security.testUser.setRoles(['index_management_user']); await pageObjects.svlCommonPage.loginAsAdmin(); @@ -53,7 +54,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { after(async () => { await esDeleteAllIndices(testIndexName); }); - this.tags('skipSvlSearch'); it('navigates to overview', async () => { await pageObjects.indexManagement.changeManageIndexTab('showOverviewIndexMenuButton'); await pageObjects.indexManagement.indexDetailsPage.expectIndexDetailsPageIsLoaded(); diff --git a/x-pack/test_serverless/functional/test_suites/search/elasticsearch_start.ts b/x-pack/test_serverless/functional/test_suites/search/elasticsearch_start.ts index 129f769283b34..39228137cf7d7 100644 --- a/x-pack/test_serverless/functional/test_suites/search/elasticsearch_start.ts +++ b/x-pack/test_serverless/functional/test_suites/search/elasticsearch_start.ts @@ -156,6 +156,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await pageObjects.svlSearchElasticsearchStartPage.expectAnalyzeLogsLink(); await pageObjects.svlSearchElasticsearchStartPage.expectO11yTrialLink(); }); + + it('should have close button', async () => { + await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage(); + await pageObjects.svlSearchElasticsearchStartPage.expectCloseCreateIndexButtonExists(); + await pageObjects.svlSearchElasticsearchStartPage.clickCloseCreateIndexButton(); + await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnIndexListPage(); + }); + it('should have skip button', async () => { + await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage(); + await pageObjects.svlSearchElasticsearchStartPage.expectSkipButtonExists(); + await pageObjects.svlSearchElasticsearchStartPage.clickSkipButton(); + await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnIndexListPage(); + }); }); describe('viewer', function () { before(async () => { diff --git a/x-pack/test_serverless/functional/test_suites/search/index.ts b/x-pack/test_serverless/functional/test_suites/search/index.ts index 903f98c63b776..99190ae0cc072 100644 --- a/x-pack/test_serverless/functional/test_suites/search/index.ts +++ b/x-pack/test_serverless/functional/test_suites/search/index.ts @@ -15,6 +15,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./elasticsearch_start.ts')); loadTestFile(require.resolve('./search_index_detail.ts')); loadTestFile(require.resolve('./getting_started')); + loadTestFile(require.resolve('./index_management')); loadTestFile(require.resolve('./connectors/connectors_overview')); loadTestFile(require.resolve('./default_dataview')); loadTestFile(require.resolve('./pipelines')); diff --git a/x-pack/test_serverless/functional/test_suites/search/index_management.ts b/x-pack/test_serverless/functional/test_suites/search/index_management.ts index ed7f09eecb0e8..08b093f660640 100644 --- a/x-pack/test_serverless/functional/test_suites/search/index_management.ts +++ b/x-pack/test_serverless/functional/test_suites/search/index_management.ts @@ -5,6 +5,7 @@ * 2.0. */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; import { testHasEmbeddedConsole } from './embedded_console'; @@ -16,11 +17,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'common', 'header', 'indexManagement', + 'svlSearchCreateIndexPage', ]); + const browser = getService('browser'); const security = getService('security'); + const es = getService('es'); const esDeleteAllIndices = getService('esDeleteAllIndices'); const testIndexName = `test-index-ftr-${Math.random()}`; + const testAPIIndexName = `test-api-index-ftr-${Math.random()}`; describe('index management', function () { before(async () => { await security.testUser.setRoles(['index_management_user']); @@ -32,23 +37,81 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await pageObjects.header.waitUntilLoadingHasFinished(); }); after(async () => { - await esDeleteAllIndices(testIndexName); + await esDeleteAllIndices([testIndexName, testAPIIndexName]); + }); + + it('renders the indices tab', async () => { + const url = await browser.getCurrentUrl(); + expect(url).to.contain(`/indices`); }); it('has embedded dev console', async () => { await testHasEmbeddedConsole(pageObjects); }); - it('can create an index', async () => { - await pageObjects.indexManagement.clickCreateIndexButton(); - await pageObjects.indexManagement.setCreateIndexName(testIndexName); - await pageObjects.indexManagement.clickCreateIndexSaveButton(); - await pageObjects.indexManagement.expectIndexToExist(testIndexName); + describe('create index', function () { + beforeEach(async () => { + await pageObjects.common.navigateToApp('indexManagement'); + // Navigate to the indices tab + await pageObjects.indexManagement.changeTabs('indicesTab'); + await pageObjects.header.waitUntilLoadingHasFinished(); + }); + it('can create an index', async () => { + await pageObjects.indexManagement.clickCreateIndexButton(); + await pageObjects.svlSearchCreateIndexPage.expectToBeOnCreateIndexPage(); + await pageObjects.svlSearchCreateIndexPage.expectCreateIndexUIView(); + await pageObjects.svlSearchCreateIndexPage.expectCreateIndexButtonToBeEnabled(); + await pageObjects.svlSearchCreateIndexPage.setIndexNameValue(testIndexName); + await pageObjects.svlSearchCreateIndexPage.clickCreateIndexButton(); + await pageObjects.svlSearchCreateIndexPage.expectToBeOnIndexDetailsPage(); + await pageObjects.common.navigateToApp('indexManagement'); + await pageObjects.indexManagement.changeTabs('indicesTab'); + await pageObjects.indexManagement.expectIndexToExist(testIndexName); + }); + it('should redirect to index details when index is created via API and on the code view', async () => { + await pageObjects.indexManagement.clickCreateIndexButton(); + + await pageObjects.svlSearchCreateIndexPage.expectToBeOnCreateIndexPage(); + await pageObjects.svlSearchCreateIndexPage.expectCreateIndexUIView(); + await pageObjects.svlSearchCreateIndexPage.clickCodeViewButton(); + await pageObjects.svlSearchCreateIndexPage.expectCreateIndexCodeView(); + await es.indices.create({ index: testAPIIndexName }); + await pageObjects.svlSearchCreateIndexPage.expectToBeOnIndexDetailsPage(); + }); + it('should have file upload link', async () => { + await pageObjects.indexManagement.clickCreateIndexButton(); + + await pageObjects.svlSearchCreateIndexPage.expectToBeOnCreateIndexPage(); + await pageObjects.svlSearchCreateIndexPage.clickFileUploadLink(); + await pageObjects.svlSearchCreateIndexPage.expectToBeOnMLFileUploadPage(); + }); + it('should support closing create index page', async () => { + await pageObjects.indexManagement.clickCreateIndexButton(); + + await pageObjects.svlSearchCreateIndexPage.expectCloseCreateIndexButtonExists(); + await pageObjects.svlSearchCreateIndexPage.clickCloseCreateIndexButton(); + await pageObjects.svlSearchCreateIndexPage.expectToBeOnIndexListPage(); + }); + it('should have the embedded console', async () => { + await pageObjects.indexManagement.clickCreateIndexButton(); + + await testHasEmbeddedConsole(pageObjects); + }); }); - it('can view index details - index with no documents', async () => { - await pageObjects.indexManagement.indexDetailsPage.openIndexDetailsPage(0); - await pageObjects.indexManagement.indexDetailsPage.expectIndexDetailsPageIsLoaded(); + describe('manage index', function () { + beforeEach(async () => { + await pageObjects.common.navigateToApp('indexManagement'); + // Navigate to the indices tab + await pageObjects.indexManagement.changeTabs('indicesTab'); + await pageObjects.header.waitUntilLoadingHasFinished(); + await pageObjects.indexManagement.manageIndex(testIndexName); + await pageObjects.indexManagement.manageIndexContextMenuExists(); + }); + it('can delete index', async () => { + await pageObjects.indexManagement.confirmDeleteModalIsVisible(); + await pageObjects.indexManagement.expectIndexIsDeleted(testIndexName); + }); }); }); } diff --git a/x-pack/test_serverless/functional/test_suites/search/navigation.ts b/x-pack/test_serverless/functional/test_suites/search/navigation.ts index a6da7b1467e9a..97952d68f8fd1 100644 --- a/x-pack/test_serverless/functional/test_suites/search/navigation.ts +++ b/x-pack/test_serverless/functional/test_suites/search/navigation.ts @@ -36,10 +36,11 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { // check side nav links await solutionNavigation.sidenav.expectSectionExists('search_project_nav'); await solutionNavigation.sidenav.expectLinkActive({ - deepLinkId: 'elasticsearchStart', + deepLinkId: 'management:index_management', }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Indices' }); await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ - deepLinkId: 'elasticsearchStart', + text: 'Create your first index', }); await testSubjects.existOrFail(`elasticsearchStartPage`); @@ -53,6 +54,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { }); await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Data' }); await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Index Management' }); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Indices' }); // > Connectors await solutionNavigation.sidenav.clickLink({ @@ -176,9 +178,9 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { // navigate back to serverless search overview await svlCommonNavigation.clickLogo(); await svlCommonNavigation.sidenav.expectLinkActive({ - deepLinkId: 'elasticsearchStart', + deepLinkId: 'management:index_management', }); - await svlCommonNavigation.breadcrumbs.expectBreadcrumbExists({ text: `Home` }); + await svlCommonNavigation.breadcrumbs.expectBreadcrumbExists({ text: `Indices` }); await testSubjects.existOrFail(`elasticsearchStartPage`); await expectNoPageReload(); @@ -236,7 +238,6 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { it('renders expected side navigation items', async () => { await solutionNavigation.sidenav.openSection('project_settings_project_nav'); // Verify all expected top-level links exist - await solutionNavigation.sidenav.expectLinkExists({ text: 'Home' }); await solutionNavigation.sidenav.expectLinkExists({ text: 'Data' }); await solutionNavigation.sidenav.expectLinkExists({ text: 'Index Management' }); await solutionNavigation.sidenav.expectLinkExists({ text: 'Connectors' }); @@ -261,7 +262,6 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await solutionNavigation.sidenav.openSection('project_settings_project_nav'); await solutionNavigation.sidenav.expectOnlyDefinedLinks([ 'search_project_nav', - 'home', 'data', 'management:index_management', 'serverlessConnectors', diff --git a/x-pack/test_serverless/functional/test_suites/search/search_playground/playground_overview.ts b/x-pack/test_serverless/functional/test_suites/search/search_playground/playground_overview.ts index e1570d2191378..946afe08a0b73 100644 --- a/x-pack/test_serverless/functional/test_suites/search/search_playground/playground_overview.ts +++ b/x-pack/test_serverless/functional/test_suites/search/search_playground/playground_overview.ts @@ -69,7 +69,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await pageObjects.searchPlayground.PlaygroundStartChatPage.expectPlaygroundHeaderComponentsToExist(); await pageObjects.searchPlayground.PlaygroundStartChatPage.expectPlaygroundHeaderComponentsToDisabled(); await pageObjects.searchPlayground.PlaygroundStartChatPage.expectPlaygroundStartChatPageComponentsToExist(); - await pageObjects.searchPlayground.PlaygroundStartChatPage.expectPlaygroundStartChatPageIndexCalloutExists(); + await pageObjects.searchPlayground.PlaygroundStartChatPage.expectPlaygroundStartChatPageIndexButtonExists(); }); describe('with gen ai connectors', () => { @@ -106,7 +106,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); describe('without any indices', () => { - it('hide no index callout when index added', async () => { + it('hide no create index button when index added', async () => { await createIndex(); await pageObjects.searchPlayground.PlaygroundStartChatPage.expectOpenFlyoutAndSelectIndex(); }); diff --git a/yarn.lock b/yarn.lock index 5faf426cf4d25..47ee0df930999 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1721,12 +1721,12 @@ "@elastic/transport" "^8.3.1" tslib "^2.4.0" -"@elastic/elasticsearch@^8.15.0": - version "8.15.0" - resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-8.15.0.tgz#cb29b3ae33203c545d435cf3dc4b557c8b4961d5" - integrity sha512-mG90EMdTDoT6GFSdqpUAhWK9LGuiJo6tOWqs0Usd/t15mPQDj7ZqHXfCBqNkASZpwPZpbAYVjd57S6nbUBINCg== +"@elastic/elasticsearch@^8.15.1": + version "8.15.1" + resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-8.15.1.tgz#ca294ba11ed1514bf87d4a2e253b11f6cefd8552" + integrity sha512-L3YzSaxrasMMGtcxnktiUDjS5f177L0zpHsBH+jL0LgPhdMk9xN/VKrAaYzvri86IlV5IbveA0ANV6o/BDUmhQ== dependencies: - "@elastic/transport" "^8.7.0" + "@elastic/transport" "^8.8.1" tslib "^2.4.0" "@elastic/ems-client@8.5.3": @@ -1906,10 +1906,10 @@ undici "^5.28.3" yaml "^2.2.2" -"@elastic/transport@^8.3.1", "@elastic/transport@^8.7.0": - version "8.7.0" - resolved "https://registry.yarnpkg.com/@elastic/transport/-/transport-8.7.0.tgz#006987fc5583f61c266e0b1003371e82efc7a6b5" - integrity sha512-IqXT7a8DZPJtqP2qmX1I2QKmxYyN27kvSW4g6pInESE1SuGwZDp2FxHJ6W2kwmYOJwQdAt+2aWwzXO5jHo9l4A== +"@elastic/transport@^8.3.1", "@elastic/transport@^8.8.1": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@elastic/transport/-/transport-8.8.1.tgz#d64244907bccdad5626c860b492faeef12194b1f" + integrity sha512-4RQIiChwNIx3B0O+2JdmTq/Qobj6+1g2RQnSv1gt4V2SVfAYjGwOKu0ZMKEHQOXYNG6+j/Chero2G9k3/wXLEw== dependencies: "@opentelemetry/api" "1.x" debug "^4.3.4" @@ -8483,12 +8483,12 @@ require-from-string "^2.0.2" uri-js-replace "^1.0.1" -"@redocly/cli@^1.25.10": - version "1.25.10" - resolved "https://registry.yarnpkg.com/@redocly/cli/-/cli-1.25.10.tgz#647e33e4171d74a4f879304ba87366ac650ed83d" - integrity sha512-zoRMvSYOLzurcb3be5HLLlc5dLGICyHY8mueCbdE2DmLbFERhJJ5iiABKvNRJSr03AR6X569f4mraBJpAsGJnQ== +"@redocly/cli@^1.25.11": + version "1.25.11" + resolved "https://registry.yarnpkg.com/@redocly/cli/-/cli-1.25.11.tgz#8ec17a6535aebfd166e8cab8ffcc9d768af1b014" + integrity sha512-dttBsmLnnbTlJCTa+s7Sy+qtXDq692n7Ru3nUUIHp9XdCbhXIHWhpc8uAl+GmR4MGbVe8ohATl3J+zX3aFy82A== dependencies: - "@redocly/openapi-core" "1.25.10" + "@redocly/openapi-core" "1.25.11" abort-controller "^3.0.0" chokidar "^3.5.1" colorette "^1.2.0" @@ -8513,10 +8513,10 @@ resolved "https://registry.yarnpkg.com/@redocly/config/-/config-0.16.0.tgz#4b7700a5cb6e04bc6d6fdb94b871c9e260a1fba6" integrity sha512-t9jnODbUcuANRSl/K4L9nb12V+U5acIHnVSl26NWrtSdDZVtoqUXk2yGFPZzohYf62cCfEQUT8ouJ3bhPfpnJg== -"@redocly/openapi-core@1.25.10", "@redocly/openapi-core@^1.4.0": - version "1.25.10" - resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.25.10.tgz#6ca3f1ad1b826e3680f91752abf11aa40856f6b8" - integrity sha512-wcGnSonJZvjpPaJJs+qh0ADYy0aCbaNhCXhJVES9RlknMc7V9nbqLQ67lkwaXhpp/fskm9GJWL/U9Xyiuclbqw== +"@redocly/openapi-core@1.25.11", "@redocly/openapi-core@^1.4.0": + version "1.25.11" + resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.25.11.tgz#93f168284986da6809363b001e9aa7c2104c2fc0" + integrity sha512-bH+a8izQz4fnKROKoX3bEU8sQ9rjvEIZOqU6qTmxlhOJ0NsKa5e+LmU18SV0oFeg5YhWQhhEDihXkvKJ1wMMNQ== dependencies: "@redocly/ajv" "^8.11.2" "@redocly/config" "^0.16.0"