From 430c02f67effb6316d45d3e8e8d4465fab076544 Mon Sep 17 00:00:00 2001 From: Tiago Vila Verde Date: Wed, 10 Jan 2024 11:53:12 +0200 Subject: [PATCH 01/26] [Security Solution][Entity Analytics] Asset Criticality UI for new entity flyouts (#174438) This PR adds the Asset Criticality selector to both the new User and Host expandable flyouts User flyout: Screenshot 2024-01-08 at 12 17 57 Host flyout: Screenshot 2024-01-08 at 12 18 09 Part of [#8148](https://github.com/elastic/security-team/issues/8148) and the parent epic [#4208](https://github.com/elastic/security-team/issues/4208) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../asset_criticality_selector.tsx | 22 ++++++++++++++----- .../host_right/content.stories.tsx | 3 +++ .../entity_details/host_right/content.tsx | 21 +++++++++--------- .../entity_details/host_right/index.tsx | 1 + .../user_right/content.stories.tsx | 5 +++++ .../entity_details/user_right/content.tsx | 7 +++++- .../entity_details/user_right/index.tsx | 1 + .../side_panel/host_details/index.tsx | 2 ++ .../user_details/user_details_flyout.tsx | 3 ++- 9 files changed, 48 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.tsx index f2a4c4c3a3288..d3506456e5035 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.tsx @@ -22,13 +22,16 @@ import { EuiModalHeaderTitle, EuiSuperSelect, EuiText, + EuiTitle, EuiHorizontalRule, + useEuiTheme, } from '@elastic/eui'; import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; import { CRITICALITY_LEVEL_DESCRIPTION, CRITICALITY_LEVEL_TITLE, @@ -45,6 +48,7 @@ interface Props { export const AssetCriticalitySelector: React.FC = ({ entity }) => { const modal = useCriticalityModal(); const criticality = useAssetCriticalityData(entity, modal); + const { euiTheme } = useEuiTheme(); if (criticality.privileges.isLoading || !criticality.privileges.data?.has_all_required) { return null; @@ -52,15 +56,23 @@ export const AssetCriticalitySelector: React.FC = ({ entity }) => { return ( <> - + +

+ +

+
} + buttonProps={{ + css: css` + color: ${euiTheme.colors.primary}; + `, + }} data-test-subj="asset-criticality-selector" > {criticality.query.isLoading || criticality.mutation.isLoading ? ( diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.stories.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.stories.tsx index 9bea5cb2a4ac2..84553f673022f 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.stories.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.stories.tsx @@ -40,6 +40,7 @@ storiesOf('Components/HostPanelContent', module) scopeId={'test-scopeId'} isDraggable={false} openDetailsPanel={() => {}} + hostName={'test-host-name'} /> )) .add('no observed data', () => ( @@ -62,6 +63,7 @@ storiesOf('Components/HostPanelContent', module) scopeId={'test-scopeId'} isDraggable={false} openDetailsPanel={() => {}} + hostName={'test-host-name'} /> )) .add('loading', () => ( @@ -84,5 +86,6 @@ storiesOf('Components/HostPanelContent', module) scopeId={'test-scopeId'} isDraggable={false} openDetailsPanel={() => {}} + hostName={'test-host-name'} /> )); diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx index eb7d5f3fda26d..88da19ffdabff 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import { EuiHorizontalRule } from '@elastic/eui'; - import React from 'react'; +import { EuiHorizontalRule } from '@elastic/eui'; +import { AssetCriticalitySelector } from '../../../entity_analytics/components/asset_criticality/asset_criticality_selector'; import { RiskSummary } from '../../../entity_analytics/components/risk_summary_flyout/risk_summary'; import type { RiskScoreState } from '../../../entity_analytics/api/hooks/use_risk_score'; import type { RiskScoreEntity, HostItem } from '../../../../common/search_strategy'; @@ -25,9 +25,11 @@ interface HostPanelContentProps { scopeId: string; isDraggable: boolean; openDetailsPanel: (tab: EntityDetailsLeftPanelTab) => void; + hostName: string; } export const HostPanelContent = ({ + hostName, observedHost, riskScoreState, contextID, @@ -41,16 +43,15 @@ export const HostPanelContent = ({ {riskScoreState.isModuleEnabled && riskScoreState.data?.length !== 0 && ( <> - { - - } - + + )} + {}} + userName={'test-user-name'} /> )) .add('integration disabled', () => ( @@ -59,6 +60,7 @@ storiesOf('Components/UserPanelContent', module) scopeId={'test-scopeId'} isDraggable={false} openDetailsPanel={() => {}} + userName={'test-user-name'} /> )) .add('no managed data', () => ( @@ -74,6 +76,7 @@ storiesOf('Components/UserPanelContent', module) scopeId={'test-scopeId'} isDraggable={false} openDetailsPanel={() => {}} + userName={'test-user-name'} /> )) .add('no observed data', () => ( @@ -109,6 +112,7 @@ storiesOf('Components/UserPanelContent', module) scopeId={'test-scopeId'} isDraggable={false} openDetailsPanel={() => {}} + userName={'test-user-name'} /> )) .add('loading', () => ( @@ -148,5 +152,6 @@ storiesOf('Components/UserPanelContent', module) scopeId={'test-scopeId'} isDraggable={false} openDetailsPanel={() => {}} + userName={'test-user-name'} /> )); diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx index f1f7916d3907c..89844f3faece7 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx @@ -8,6 +8,8 @@ import { EuiHorizontalRule } from '@elastic/eui'; import React from 'react'; +import { AssetCriticalitySelector } from '../../../entity_analytics/components/asset_criticality/asset_criticality_selector'; + import { OBSERVED_USER_QUERY_ID } from '../../../explore/users/containers/users/observed_details'; import { RiskSummary } from '../../../entity_analytics/components/risk_summary_flyout/risk_summary'; import type { RiskScoreState } from '../../../entity_analytics/api/hooks/use_risk_score'; @@ -22,6 +24,7 @@ import { useObservedUserItems } from './hooks/use_observed_user_items'; import type { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; interface UserPanelContentProps { + userName: string; observedUser: ObservedEntityData; managedUser: ManagedUserData; riskScoreState: RiskScoreState; @@ -32,6 +35,7 @@ interface UserPanelContentProps { } export const UserPanelContent = ({ + userName, observedUser, managedUser, riskScoreState, @@ -51,9 +55,10 @@ export const UserPanelContent = ({ queryId={USER_PANEL_RISK_SCORE_QUERY_ID} openDetailsPanel={openDetailsPanel} /> - + )} + = React.memo( + diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/user_details_flyout.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/user_details_flyout.tsx index e2963c2e3ca3c..1366e3474b5ea 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/user_details_flyout.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/user_details_flyout.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiFlyoutHeader, EuiFlyoutBody, EuiSpacer } from '@elastic/eui'; +import { EuiFlyoutHeader, EuiFlyoutBody, EuiSpacer, EuiHorizontalRule } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; import { AssetCriticalitySelector } from '../../../../entity_analytics/components/asset_criticality/asset_criticality_selector'; @@ -44,6 +44,7 @@ export const UserDetailsFlyout = ({ + From 411f9d31f1e8520caaf3dbfe90b08c7137bf2e97 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Wed, 10 Jan 2024 12:10:34 +0200 Subject: [PATCH 02/26] [ES|QL] Remove fieldlist from the state (#174340) ## Summary We don't need to save the fieldList to the state. I save it to the cache and use it from there where needed or retrieve the information from the state allColumns (which already contains this information). This will make the SOs lighter. This should be bwc, I tested it and everything seems to work as expected. I will try to do something similar for the allColumns but it might be trickier. Will do this on a followup PR ### Checklist - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../public/__mocks__/suggestions.ts | 2 - .../chart/utils/get_lens_attributes.test.ts | 1 - .../datasources/text_based/datapanel.test.tsx | 8 +- .../datasources/text_based/datapanel.tsx | 8 +- .../text_based/dnd/get_drop_props.test.tsx | 2 - .../datasources/text_based/dnd/mocks.tsx | 1 - .../text_based/dnd/on_drop.test.ts | 8 +- .../datasources/text_based/fieldlist_cache.ts | 19 +++ .../datasources/text_based/remove_column.ts | 2 +- .../text_based/text_based_languages.test.ts | 126 +++++++----------- .../text_based/text_based_languages.tsx | 31 +++-- .../public/datasources/text_based/types.ts | 1 - .../datasources/text_based/utils.test.ts | 48 ------- .../public/datasources/text_based/utils.ts | 5 +- .../lens/public/lens_suggestions_api.test.ts | 2 +- .../lens/public/lens_suggestions_api.ts | 1 - 16 files changed, 105 insertions(+), 160 deletions(-) create mode 100644 x-pack/plugins/lens/public/datasources/text_based/fieldlist_cache.ts diff --git a/src/plugins/unified_histogram/public/__mocks__/suggestions.ts b/src/plugins/unified_histogram/public/__mocks__/suggestions.ts index bed2eee388cde..1de961c55c020 100644 --- a/src/plugins/unified_histogram/public/__mocks__/suggestions.ts +++ b/src/plugins/unified_histogram/public/__mocks__/suggestions.ts @@ -76,7 +76,6 @@ export const currentSuggestionMock = { timeField: 'timestamp', }, }, - fieldList: [], indexPatternRefs: [], initialContext: { dataViewSpec: { @@ -215,7 +214,6 @@ export const allSuggestionsMock = [ timeField: 'timestamp', }, }, - fieldList: [], indexPatternRefs: [], initialContext: { dataViewSpec: { diff --git a/src/plugins/unified_histogram/public/chart/utils/get_lens_attributes.test.ts b/src/plugins/unified_histogram/public/chart/utils/get_lens_attributes.test.ts index 53ec7350401b7..998cd17968049 100644 --- a/src/plugins/unified_histogram/public/chart/utils/get_lens_attributes.test.ts +++ b/src/plugins/unified_histogram/public/chart/utils/get_lens_attributes.test.ts @@ -565,7 +565,6 @@ describe('getLensAttributes', () => { "state": Object { "datasourceStates": Object { "textBased": Object { - "fieldList": Array [], "indexPatternRefs": Array [], "initialContext": Object { "contextualFields": Array [ diff --git a/x-pack/plugins/lens/public/datasources/text_based/datapanel.test.tsx b/x-pack/plugins/lens/public/datasources/text_based/datapanel.test.tsx index 001e2d5b99af8..ca151b9ad21a6 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/datasources/text_based/datapanel.test.tsx @@ -30,6 +30,7 @@ import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; import { createIndexPatternServiceMock } from '../../mocks/data_views_service_mock'; import { createMockFramePublicAPI } from '../../mocks'; import { DataViewsState } from '../../state_management'; +import { addColumnsToCache } from './fieldlist_cache'; const fieldsFromQuery = [ { @@ -106,7 +107,7 @@ const initialState: TextBasedPrivateState = { index: '1', columns: [], allColumns: [], - query: { sql: 'SELECT * FROM foo' }, + query: { esql: 'SELECT * FROM foo' }, }, }, indexPatternRefs: [ @@ -114,9 +115,10 @@ const initialState: TextBasedPrivateState = { { id: '2', title: 'my-fake-restricted-pattern' }, { id: '3', title: 'my-compatible-pattern' }, ], - fieldList: fieldsFromQuery, }; +addColumnsToCache('SELECT * FROM my-fake-index-pattern', fieldsFromQuery); + function getFrameAPIMock({ indexPatterns, ...rest @@ -189,7 +191,7 @@ describe('TextBased Query Languages Data Panel', () => { fromDate: 'now-7d', toDate: 'now', }, - query: { sql: 'SELECT * FROM my-fake-index-pattern' } as unknown as Query, + query: { esql: 'SELECT * FROM my-fake-index-pattern' } as unknown as Query, filters: [], showNoDataPopover: jest.fn(), dropOntoWorkspace: jest.fn(), diff --git a/x-pack/plugins/lens/public/datasources/text_based/datapanel.tsx b/x-pack/plugins/lens/public/datasources/text_based/datapanel.tsx index 113125484cddf..cca962c1884b2 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/datapanel.tsx +++ b/x-pack/plugins/lens/public/datasources/text_based/datapanel.tsx @@ -12,7 +12,7 @@ import { isEqual } from 'lodash'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; -import { isOfAggregateQueryType } from '@kbn/es-query'; +import { isOfAggregateQueryType, getAggregateQueryMode } from '@kbn/es-query'; import { DatatableColumn, ExpressionsStart } from '@kbn/expressions-plugin/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { @@ -28,6 +28,7 @@ import type { DatasourceDataPanelProps } from '../../types'; import type { TextBasedPrivateState } from './types'; import { getStateFromAggregateQuery } from './utils'; import { FieldItem } from '../common/field_item'; +import { getColumnsFromCache } from './fieldlist_cache'; const getCustomFieldType: GetCustomFieldType = (field) => field?.meta.type; @@ -67,15 +68,14 @@ export function TextBasedDataPanel({ expressions, frameDataViews ); - setDataHasLoaded(true); setState(stateFromQuery); } } fetchData(); }, [data, dataViews, expressions, prevQuery, query, setState, state, frame.dataViews]); - - const { fieldList } = state; + const language = isOfAggregateQueryType(query) ? getAggregateQueryMode(query) : null; + const fieldList = language ? getColumnsFromCache(query[language]) : []; const onSelectedFieldFilter = useCallback( (field: DatatableColumn): boolean => { diff --git a/x-pack/plugins/lens/public/datasources/text_based/dnd/get_drop_props.test.tsx b/x-pack/plugins/lens/public/datasources/text_based/dnd/get_drop_props.test.tsx index ca9d48c17cbb8..9b368050b0567 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/dnd/get_drop_props.test.tsx +++ b/x-pack/plugins/lens/public/datasources/text_based/dnd/get_drop_props.test.tsx @@ -27,7 +27,6 @@ const defaultProps = { allColumns: [...fieldList, column1, column2, column3], }, }, - fieldList, }, source: numericDraggedColumn, target: { @@ -86,7 +85,6 @@ describe('Text-based: getDropProps', () => { allColumns: [...fieldListNonNumericOnly, column1, column2, column3], }, }, - fieldList: fieldListNonNumericOnly, }, source: notNumericDraggedField, } as unknown as DatasourceDimensionDropHandlerProps; diff --git a/x-pack/plugins/lens/public/datasources/text_based/dnd/mocks.tsx b/x-pack/plugins/lens/public/datasources/text_based/dnd/mocks.tsx index 90a37acab1043..c657290b00759 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/dnd/mocks.tsx +++ b/x-pack/plugins/lens/public/datasources/text_based/dnd/mocks.tsx @@ -159,7 +159,6 @@ export const defaultProps = { errors: [], }, }, - fieldList, indexPatternRefs: [], }, source: numericDraggedColumn, diff --git a/x-pack/plugins/lens/public/datasources/text_based/dnd/on_drop.test.ts b/x-pack/plugins/lens/public/datasources/text_based/dnd/on_drop.test.ts index 9bcb4f6545cde..bbcca24f3a943 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/dnd/on_drop.test.ts +++ b/x-pack/plugins/lens/public/datasources/text_based/dnd/on_drop.test.ts @@ -69,7 +69,13 @@ describe('onDrop', () => { layers: { first: expect.objectContaining({ columns: expectedColumns, - allColumns: [...fieldList, ...expectedColumns], + allColumns: [ + ...fieldList, + column1, + column2, + column3, + { ...column1, columnId: 'newId' }, + ], }), }, }) diff --git a/x-pack/plugins/lens/public/datasources/text_based/fieldlist_cache.ts b/x-pack/plugins/lens/public/datasources/text_based/fieldlist_cache.ts new file mode 100644 index 0000000000000..eaf2c201835ab --- /dev/null +++ b/x-pack/plugins/lens/public/datasources/text_based/fieldlist_cache.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { DatatableColumn } from '@kbn/expressions-plugin/public'; + +const cachedColumns = new Map(); + +export const addColumnsToCache = (query: string, list: DatatableColumn[]) => { + const trimmedQuery = query.replaceAll('\n', '').trim(); + cachedColumns.set(trimmedQuery, list); +}; + +export const getColumnsFromCache = (query: string) => { + const trimmedQuery = query.replaceAll('\n', '').trim(); + return cachedColumns.get(trimmedQuery); +}; diff --git a/x-pack/plugins/lens/public/datasources/text_based/remove_column.ts b/x-pack/plugins/lens/public/datasources/text_based/remove_column.ts index bd10678d9d160..2228492dc2b88 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/remove_column.ts +++ b/x-pack/plugins/lens/public/datasources/text_based/remove_column.ts @@ -20,7 +20,7 @@ export const removeColumn: Datasource['removeColumn'] = ( [layerId]: { ...prevState.layers[layerId], columns: prevState.layers[layerId].columns.filter((col) => col.columnId !== columnId), - allColumns: prevState.layers[layerId].allColumns.filter((col) => col.columnId !== columnId), + allColumns: prevState.layers[layerId].allColumns, }, }, }; diff --git a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.test.ts b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.test.ts index 604e45f72ec84..032c99a6b5780 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.test.ts +++ b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.test.ts @@ -119,15 +119,6 @@ describe('Textbased Data Source', () => { query: { esql: 'FROM foo' }, }, }, - fieldList: [ - { - id: 'col1', - name: 'Test 1', - meta: { - type: 'number', - }, - }, - ], } as unknown as TextBasedPrivateState; }); @@ -308,7 +299,6 @@ describe('Textbased Data Source', () => { describe('#createEmptyLayer', () => { it('creates state with empty layers', () => { expect(TextBasedDatasource.createEmptyLayer('index-pattern-id')).toEqual({ - fieldList: [], layers: {}, indexPatternRefs: [], }); @@ -402,7 +392,6 @@ describe('Textbased Data Source', () => { expect(suggestions[0].state).toEqual({ ...state, initialContext: undefined, - fieldList: textBasedQueryColumns, indexPatternRefs: [ { id: '1', @@ -554,7 +543,6 @@ describe('Textbased Data Source', () => { expect(suggestions[0].state).toEqual({ ...state, initialContext: undefined, - fieldList: textBasedQueryColumns, indexPatternRefs: [ { id: '1', @@ -637,49 +625,49 @@ describe('Textbased Data Source', () => { describe('#suggestsLimitedColumns', () => { it('should return true if query returns big number of columns', () => { - const fieldList = [ - { - id: 'a', - name: 'Test 1', - meta: { - type: 'number', - }, - }, - { - id: 'b', - name: 'Test 2', - meta: { - type: 'number', - }, - }, - { - id: 'c', - name: 'Test 3', - meta: { - type: 'date', - }, - }, - { - id: 'd', - name: 'Test 4', - meta: { - type: 'string', - }, - }, - { - id: 'e', - name: 'Test 5', - meta: { - type: 'string', - }, - }, - ]; const state = { - fieldList, + totalFields: 5, layers: { a: { query: { esql: 'from foo' }, index: 'foo', + allColumns: [ + { + id: 'a', + name: 'Test 1', + meta: { + type: 'number', + }, + }, + { + id: 'b', + name: 'Test 2', + meta: { + type: 'number', + }, + }, + { + id: 'c', + name: 'Test 3', + meta: { + type: 'date', + }, + }, + { + id: 'd', + name: 'Test 4', + meta: { + type: 'string', + }, + }, + { + id: 'e', + name: 'Test 5', + meta: { + type: 'string', + }, + }, + ], }, }, } as unknown as TextBasedPrivateState; @@ -1009,22 +997,6 @@ describe('Textbased Data Source', () => { index: 'foo', }, }, - fieldList: [ - { - id: 'col1', - name: 'Test 1', - meta: { - type: 'number', - }, - }, - { - id: 'col2', - name: 'Test 2', - meta: { - type: 'number', - }, - }, - ], } as unknown as TextBasedPrivateState; publicAPI = TextBasedDatasource.getPublicAPI({ @@ -1039,25 +1011,17 @@ describe('Textbased Data Source', () => { }); it('should return only the columns that exist on the query', () => { - const state = { - ...baseState, - fieldList: [ - { - id: 'col2', - name: 'Test 2', - meta: { - type: 'number', - }, - }, - ], - } as unknown as TextBasedPrivateState; - publicAPI = TextBasedDatasource.getPublicAPI({ - state, + state: baseState, layerId: 'a', indexPatterns, }); - expect(publicAPI.getTableSpec()).toEqual([]); + expect(publicAPI.getTableSpec()).toEqual([ + { + columnId: 'col1', + fields: ['Test 1'], + }, + ]); }); }); diff --git a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx index bc46f0b4076d9..d9be929567bae 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx +++ b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx @@ -46,6 +46,7 @@ import { getUniqueLabelGenerator, nonNullable } from '../../utils'; import { onDrop, getDropProps } from './dnd'; import { removeColumn } from './remove_column'; import { canColumnBeUsedBeInMetricDimension, MAX_NUM_OF_COLUMNS } from './utils'; +import { getColumnsFromCache, addColumnsToCache } from './fieldlist_cache'; function getLayerReferenceName(layerId: string) { return `textBasedLanguages-datasource-layer-${layerId}`; @@ -141,12 +142,14 @@ export function getTextBasedDatasource({ }; }); + const language = getAggregateQueryMode(context.query); + addColumnsToCache(context.query[language], textBasedQueryColumns); + const index = context.dataViewSpec.id ?? context.dataViewSpec.title; const query = context.query; const updatedState = { ...state, initialContext: undefined, - fieldList: textBasedQueryColumns, ...(context.dataViewSpec.id ? { indexPatternRefs: [ @@ -292,7 +295,6 @@ export function getTextBasedDatasource({ return { indexPatternRefs: [], layers: {}, - fieldList: [], }; }, @@ -316,7 +318,6 @@ export function getTextBasedDatasource({ newState: { ...state, layers: newLayers, - fieldList: state.fieldList, }, }; }, @@ -341,8 +342,9 @@ export function getTextBasedDatasource({ // at this case we don't suggest all columns in a table but the first // MAX_NUM_OF_COLUMNS suggestsLimitedColumns(state: TextBasedPrivateState) { - const fieldsList = state?.fieldList ?? []; - return fieldsList.length >= MAX_NUM_OF_COLUMNS; + const layers = Object.values(state.layers); + const allColumns = layers[0].allColumns; + return allColumns.length >= MAX_NUM_OF_COLUMNS; }, isTimeBased: (state, indexPatterns) => { if (!state) return false; @@ -421,8 +423,14 @@ export function getTextBasedDatasource({ }, DimensionEditorComponent: (props: DatasourceDimensionEditorProps) => { - const fields = props.state.fieldList; const allColumns = props.state.layers[props.layerId]?.allColumns; + const fields = allColumns.map((col) => { + return { + id: col.columnId, + name: col.fieldName, + meta: col?.meta ?? { type: 'number' }, + }; + }); const selectedField = allColumns?.find((column) => column.columnId === props.columnId); const hasNumberTypeColumns = allColumns?.some((c) => c?.meta?.type === 'number'); @@ -539,12 +547,8 @@ export function getTextBasedDatasource({ datasourceId: 'textBased', getTableSpec: () => { - const columns = state.layers[layerId]?.columns.filter((c) => { - const columnExists = state?.fieldList?.some((f) => f.name === c?.fieldName); - if (columnExists) return c; - }); return ( - columns.map((column) => ({ + state.layers[layerId]?.columns.map((column) => ({ columnId: column.columnId, fields: [column.fieldName], })) || [] @@ -592,7 +596,10 @@ export function getTextBasedDatasource({ }; }, getDatasourceSuggestionsForField(state, draggedField) { - const field = state.fieldList?.find((f) => f.id === (draggedField as TextBasedField).id); + const layers = Object.values(state.layers); + const query = layers?.[0]?.query; + const fieldList = query ? getColumnsFromCache(query[getAggregateQueryMode(query)]) : []; + const field = fieldList?.find((f) => f.id === (draggedField as TextBasedField).id); if (!field) return []; return Object.entries(state.layers)?.map(([id, layer]) => { const newId = generateId(); diff --git a/x-pack/plugins/lens/public/datasources/text_based/types.ts b/x-pack/plugins/lens/public/datasources/text_based/types.ts index 67c652450366c..8cbf2cdf0d1a1 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/types.ts +++ b/x-pack/plugins/lens/public/datasources/text_based/types.ts @@ -34,7 +34,6 @@ export interface TextBasedLayer { export interface TextBasedPersistedState { layers: Record; initialContext?: VisualizeFieldContext | VisualizeEditorContext; - fieldList?: DatatableColumn[]; } export type TextBasedPrivateState = TextBasedPersistedState & { diff --git a/x-pack/plugins/lens/public/datasources/text_based/utils.test.ts b/x-pack/plugins/lens/public/datasources/text_based/utils.test.ts index 3a01a7ba9efea..2c82eb9450f7d 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/utils.test.ts +++ b/x-pack/plugins/lens/public/datasources/text_based/utils.test.ts @@ -188,7 +188,6 @@ describe('Text based languages utils', () => { }, }, indexPatternRefs: [], - fieldList: [], initialContext: { textBasedColumns: textBasedQueryColumns, query: { sql: 'SELECT * FROM "foo"' }, @@ -247,29 +246,6 @@ describe('Text based languages utils', () => { name: 'Foo', }, }, - fieldList: [ - { - name: 'timestamp', - id: 'timestamp', - meta: { - type: 'date', - }, - }, - { - name: 'bytes', - id: 'bytes', - meta: { - type: 'number', - }, - }, - { - name: 'memory', - id: 'memory', - meta: { - type: 'number', - }, - }, - ], indexPatternRefs: [ { id: '3', @@ -340,7 +316,6 @@ describe('Text based languages utils', () => { }, }, indexPatternRefs: [], - fieldList: [], initialContext: { textBasedColumns: textBasedQueryColumns, query: { sql: 'SELECT * FROM "foo"' }, @@ -404,29 +379,6 @@ describe('Text based languages utils', () => { name: 'Foo', }, }, - fieldList: [ - { - name: 'timestamp', - id: 'timestamp', - meta: { - type: 'date', - }, - }, - { - name: 'bytes', - id: 'bytes', - meta: { - type: 'number', - }, - }, - { - name: 'memory', - id: 'memory', - meta: { - type: 'number', - }, - }, - ], indexPatternRefs: [ { id: '3', diff --git a/x-pack/plugins/lens/public/datasources/text_based/utils.ts b/x-pack/plugins/lens/public/datasources/text_based/utils.ts index ecf4fbcd12ff2..5486f210bf981 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/utils.ts +++ b/x-pack/plugins/lens/public/datasources/text_based/utils.ts @@ -12,6 +12,7 @@ import { type AggregateQuery, getIndexPatternFromSQLQuery, getIndexPatternFromESQLQuery, + getAggregateQueryMode, } from '@kbn/es-query'; import type { DatatableColumn } from '@kbn/expressions-plugin/public'; import { generateId } from '../../id_generator'; @@ -19,6 +20,7 @@ import { fetchDataFromAggregateQuery } from './fetch_data_from_aggregate_query'; import type { IndexPatternRef, TextBasedPrivateState, TextBasedLayerColumn } from './types'; import type { DataViewsState } from '../../state_management'; +import { addColumnsToCache } from './fieldlist_cache'; export const MAX_NUM_OF_COLUMNS = 5; @@ -109,6 +111,8 @@ export async function getStateFromAggregateQuery( timeFieldName = dataView.timeFieldName; const table = await fetchDataFromAggregateQuery(query, dataView, data, expressions); columnsFromQuery = table?.columns ?? []; + const language = getAggregateQueryMode(query); + addColumnsToCache(query[language], columnsFromQuery); allColumns = getAllColumns(state.layers[newLayerId].allColumns, columnsFromQuery); } catch (e) { errors.push(e); @@ -129,7 +133,6 @@ export async function getStateFromAggregateQuery( return { ...tempState, - fieldList: columnsFromQuery ?? [], indexPatternRefs, initialContext: context, }; diff --git a/x-pack/plugins/lens/public/lens_suggestions_api.test.ts b/x-pack/plugins/lens/public/lens_suggestions_api.test.ts index 82271e56aa98a..dc03340bd6698 100644 --- a/x-pack/plugins/lens/public/lens_suggestions_api.test.ts +++ b/x-pack/plugins/lens/public/lens_suggestions_api.test.ts @@ -70,7 +70,7 @@ describe('suggestionsApi', () => { }; const suggestions = suggestionsApi({ context, dataView, datasourceMap, visualizationMap }); expect(datasourceMap.textBased.getDatasourceSuggestionsForVisualizeField).toHaveBeenCalledWith( - { layers: {}, fieldList: [], indexPatternRefs: [], initialContext: context }, + { layers: {}, indexPatternRefs: [], initialContext: context }, 'index1', '', { index1: { id: 'index1' } } diff --git a/x-pack/plugins/lens/public/lens_suggestions_api.ts b/x-pack/plugins/lens/public/lens_suggestions_api.ts index cddcf5ade4cf3..a6b48ac84e8bd 100644 --- a/x-pack/plugins/lens/public/lens_suggestions_api.ts +++ b/x-pack/plugins/lens/public/lens_suggestions_api.ts @@ -37,7 +37,6 @@ export const suggestionsApi = ({ isLoading: false, state: { layers: {}, - fieldList: [], indexPatternRefs: [], initialContext: context, }, From e75b71f66d9c103a99b8bb8bb84929a07acdc003 Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Wed, 10 Jan 2024 11:26:01 +0100 Subject: [PATCH 03/26] Add telemetry to risk inputs on entity flyout (#174504) ## Summary Add EBT events for risk summary inside the entity flyout. * User collapses or expands the Risk Summary view * User launches the expanded flyout to view risk inputs (Risk inputs or Expand flyout) * User adds risk inputs to the timeline (These are implicitly tracked from the table, so please double check) Sample of sent events: ``` "Toggle Risk Summary Clicked" event_type: "Toggle Risk Summary Clicked" properties: {entity: 'host', action: 'hide'} event_type: "Toggle Risk Summary Clicked" properties: {entity: 'host', action: 'show'} event_type: "Risk Inputs Expanded Flyout Opened" properties: {entity: 'host'} event_type: "Add Risk Input To Timeline Clicked" properties: {quantity: 1} event_type: "Add Risk Input To Timeline Clicked" properties: {quantity: 5} event_type: "Toggle Risk Summary Clicked" properties: {entity: 'user', action: 'hide'} event_type: "Toggle Risk Summary Clicked" properties: {entity: 'user', action: 'show'} event_type: "Risk Inputs Expanded Flyout Opened" properties: {entity: 'user'} ``` ### How to test it? * Open kibana and generate alerts * Enable the risk engine (make sure it generates data) * Open the alerts page and click on host|user name * Interact with the risk score component and open the expandable flyout * The telemetry events should be logged on the console ### Dashboard https://telemetry-v2-staging.elastic.dev/s/securitysolution/app/r/s/Qt437 ![Screenshot 2024-01-09 at 11 33 03](https://github.com/elastic/kibana/assets/1490444/5a903c3d-458c-479f-a6ee-6ce593107d3d) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/common/lib/telemetry/constants.ts | 3 ++ .../events/entity_analytics/index.ts | 46 +++++++++++++++++++ .../events/entity_analytics/types.ts | 27 ++++++++++- .../lib/telemetry/events/telemetry_events.ts | 6 +++ .../lib/telemetry/telemetry_client.mock.ts | 3 ++ .../common/lib/telemetry/telemetry_client.ts | 13 ++++++ .../public/common/lib/telemetry/types.ts | 11 +++++ .../hooks/use_risk_input_actions.ts | 17 ++++++- .../risk_summary_flyout/risk_summary.tsx | 17 ++++++- .../entity_details/host_right/index.tsx | 8 +++- .../entity_details/user_right/index.tsx | 8 +++- 11 files changed, 153 insertions(+), 6 deletions(-) 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 3ce4c5c6b47eb..d0bad4b00a263 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 @@ -49,6 +49,9 @@ export enum TelemetryEventTypes { EntityAlertsClicked = 'Entity Alerts Clicked', EntityRiskFiltered = 'Entity Risk Filtered', 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', 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 73cec55dabfc1..78968de060186 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 @@ -53,3 +53,49 @@ export const entityRiskFilteredEvent: TelemetryEvent = { }, }, }; + +export const toggleRiskSummaryClickedEvent: TelemetryEvent = { + eventType: TelemetryEventTypes.ToggleRiskSummaryClicked, + schema: { + entity: { + type: 'keyword', + _meta: { + description: 'Entity name (host|user)', + optional: false, + }, + }, + action: { + type: 'keyword', + _meta: { + description: 'It defines if the section is opening or closing (show|hide)', + optional: false, + }, + }, + }, +}; + +export const RiskInputsExpandedFlyoutOpenedEvent: TelemetryEvent = { + eventType: TelemetryEventTypes.RiskInputsExpandedFlyoutOpened, + schema: { + entity: { + type: 'keyword', + _meta: { + description: 'Entity name (host|user)', + optional: false, + }, + }, + }, +}; + +export const addRiskInputToTimelineClickedEvent: TelemetryEvent = { + eventType: TelemetryEventTypes.AddRiskInputToTimelineClicked, + schema: { + quantity: { + type: 'integer', + _meta: { + description: 'Quantity of alerts added to timeline', + optional: false, + }, + }, + }, +}; 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 dd0aeb4ce3384..7da80b09cf602 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 @@ -19,10 +19,23 @@ export interface ReportEntityRiskFilteredParams extends EntityParam { selectedSeverity: RiskSeverity; } +export interface ReportToggleRiskSummaryClickedParams extends EntityParam { + action: 'show' | 'hide'; +} + +export type ReportRiskInputsExpandedFlyoutOpenedParams = EntityParam; + +export interface ReportAddRiskInputToTimelineClickedParams { + quantity: number; +} + export type ReportEntityAnalyticsTelemetryEventParams = | ReportEntityDetailsClickedParams | ReportEntityAlertsClickedParams - | ReportEntityRiskFilteredParams; + | ReportEntityRiskFilteredParams + | ReportToggleRiskSummaryClickedParams + | ReportRiskInputsExpandedFlyoutOpenedParams + | ReportAddRiskInputToTimelineClickedParams; export type EntityAnalyticsTelemetryEvent = | { @@ -36,4 +49,16 @@ export type EntityAnalyticsTelemetryEvent = | { eventType: TelemetryEventTypes.EntityRiskFiltered; schema: RootSchema; + } + | { + eventType: TelemetryEventTypes.AddRiskInputToTimelineClicked; + schema: RootSchema; + } + | { + eventType: TelemetryEventTypes.ToggleRiskSummaryClicked; + schema: RootSchema; + } + | { + eventType: TelemetryEventTypes.RiskInputsExpandedFlyoutOpened; + 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 1a330db7b82c2..21a4be1b56207 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 @@ -15,6 +15,9 @@ import { entityAlertsClickedEvent, entityClickedEvent, entityRiskFilteredEvent, + addRiskInputToTimelineClickedEvent, + RiskInputsExpandedFlyoutOpenedEvent, + toggleRiskSummaryClickedEvent, } from './entity_analytics'; import { assistantInvokedEvent, @@ -143,6 +146,9 @@ export const telemetryEvents = [ entityClickedEvent, entityAlertsClickedEvent, entityRiskFilteredEvent, + toggleRiskSummaryClickedEvent, + RiskInputsExpandedFlyoutOpenedEvent, + addRiskInputToTimelineClickedEvent, mlJobUpdateEvent, cellActionClickedEvent, anomaliesCountClickedEvent, 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 index e797c3a49a8e9..b4a5c9683127e 100644 --- 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 @@ -24,4 +24,7 @@ export const createTelemetryClientMock = (): jest.Mocked = reportDataQualityIndexChecked: jest.fn(), reportDataQualityCheckAllCompleted: jest.fn(), reportBreadcrumbClicked: jest.fn(), + reportToggleRiskSummaryClicked: jest.fn(), + reportRiskInputsExpandedFlyoutOpened: jest.fn(), + reportAddRiskInputToTimelineClicked: 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 index 04d0ebeaa5ae6..af49eb3843664 100644 --- 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 @@ -24,8 +24,11 @@ import type { ReportAssistantMessageSentParams, ReportAssistantQuickPromptParams, ReportAssistantSettingToggledParams, + ReportRiskInputsExpandedFlyoutOpenedParams, + ReportToggleRiskSummaryClickedParams, } from './types'; import { TelemetryEventTypes } from './constants'; +import type { ReportAddRiskInputToTimelineClickedParams } from './events/entity_analytics/types'; /** * Client which aggregate all the available telemetry tracking functions @@ -107,6 +110,16 @@ export class TelemetryClient implements TelemetryClientStart { 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); }; 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 c98e0eafcd434..cfc80c05e8c70 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 @@ -23,10 +23,13 @@ import type { } from './events/data_quality/types'; import type { EntityAnalyticsTelemetryEvent, + ReportAddRiskInputToTimelineClickedParams, ReportEntityAlertsClickedParams, ReportEntityAnalyticsTelemetryEventParams, ReportEntityDetailsClickedParams, ReportEntityRiskFilteredParams, + ReportRiskInputsExpandedFlyoutOpenedParams, + ReportToggleRiskSummaryClickedParams, } from './events/entity_analytics/types'; import type { AssistantTelemetryEvent, @@ -44,6 +47,9 @@ export type { ReportEntityAlertsClickedParams, ReportEntityDetailsClickedParams, ReportEntityRiskFilteredParams, + ReportRiskInputsExpandedFlyoutOpenedParams, + ReportToggleRiskSummaryClickedParams, + ReportAddRiskInputToTimelineClickedParams, } from './events/entity_analytics/types'; export interface TelemetryServiceSetupParams { @@ -95,10 +101,15 @@ export interface TelemetryClientStart { 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; reportCellActionClicked(params: ReportCellActionClickedParams): void; 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 a5c89b864b84d..1201275f985eb 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 @@ -30,7 +30,7 @@ export const useRiskInputActions = (alerts: AlertRawData[], closePopover: () => tableId: TableId.riskInputs, }); - const { cases: casesService } = useKibana().services; + const { cases: casesService, telemetry } = useKibana().services; const createCaseFlyout = casesService?.hooks.useCasesAddToNewCaseFlyout({ onSuccess: noop }); const selectCaseModal = casesService?.hooks.useCasesAddToExistingCaseModal(); @@ -58,7 +58,12 @@ export const useRiskInputActions = (alerts: AlertRawData[], closePopover: () => closePopover(); createCaseFlyout.open({ attachments: caseAttachments }); }, + addToNewTimeline: () => { + telemetry.reportAddRiskInputToTimelineClicked({ + quantity: alerts.length, + }); + closePopover(); timelineAction.onClick( alerts.map((alert: AlertRawData) => { @@ -79,6 +84,14 @@ export const useRiskInputActions = (alerts: AlertRawData[], closePopover: () => ); }, }), - [alerts, caseAttachments, closePopover, createCaseFlyout, selectCaseModal, timelineAction] + [ + alerts, + caseAttachments, + closePopover, + createCaseFlyout, + selectCaseModal, + telemetry, + timelineAction, + ] ); }; 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 8a15ec3cd435c..96a3ccf2c4884 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 @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import type { EuiBasicTableColumn } from '@elastic/eui'; import { useEuiTheme, @@ -21,6 +21,7 @@ import { css } from '@emotion/react'; import { FormattedMessage } from '@kbn/i18n-react'; import { euiThemeVars } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../../common/lib/kibana/kibana_react'; import { EntityDetailsLeftPanelTab } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; import type { HostRiskScore, @@ -73,6 +74,7 @@ const RiskSummaryComponent = ({ queryId, openDetailsPanel, }: RiskSummaryProps) => { + const { telemetry } = useKibana().services; const { data } = riskScoreData; const riskData = data && data.length > 0 ? data[0] : undefined; const entityData = getEntityData(riskData); @@ -138,8 +140,21 @@ const RiskSummaryComponent = ({ [alertsData?.length] ); + const onToggle = useCallback( + (isOpen) => { + const entity = isUserRiskData(riskData) ? 'user' : 'host'; + + telemetry.reportToggleRiskSummaryClicked({ + entity, + action: isOpen ? 'show' : 'hide', + }); + }, + [riskData, telemetry] + ); + return ( { + const { telemetry } = useKibana().services; const { openLeftPanel } = useExpandableFlyoutContext(); const { to, from, isInitializing, setQuery, deleteQuery } = useGlobalTime(); const hostNameFilterQuery = useMemo( @@ -77,6 +79,10 @@ export const HostPanel = ({ contextID, scopeId, hostName, isDraggable }: HostPan const openTabPanel = useCallback( (tab?: EntityDetailsLeftPanelTab) => { + telemetry.reportRiskInputsExpandedFlyoutOpened({ + entity: 'host', + }); + openLeftPanel({ id: HostDetailsPanelKey, params: { @@ -86,7 +92,7 @@ export const HostPanel = ({ contextID, scopeId, hostName, isDraggable }: HostPan }, }); }, - [openLeftPanel, hostName, isRiskScoreExist] + [telemetry, openLeftPanel, hostName, isRiskScoreExist] ); const openDefaultPanel = useCallback(() => openTabPanel(), [openTabPanel]); 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 d722b3b134893..fc5a616862448 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 @@ -8,6 +8,7 @@ import React, { useCallback, useMemo } from 'react'; import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { useKibana } from '../../../common/lib/kibana/kibana_react'; import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score'; import { ManagedUserDatasetKey } from '../../../../common/search_strategy/security_solution/users/managed_details'; import { useManagedUser } from '../../../timelines/components/side_panel/new_user_detail/hooks/use_managed_user'; @@ -46,6 +47,7 @@ const FIRST_RECORD_PAGINATION = { }; export const UserPanel = ({ contextID, scopeId, userName, isDraggable }: UserPanelProps) => { + const { telemetry } = useKibana().services; const userNameFilterQuery = useMemo( () => (userName ? buildUserNamesFilter([userName]) : undefined), [userName] @@ -80,6 +82,10 @@ export const UserPanel = ({ contextID, scopeId, userName, isDraggable }: UserPan const { openLeftPanel } = useExpandableFlyoutContext(); const openPanelTab = useCallback( (tab?: EntityDetailsLeftPanelTab) => { + telemetry.reportRiskInputsExpandedFlyoutOpened({ + entity: 'user', + }); + openLeftPanel({ id: UserDetailsPanelKey, params: { @@ -92,7 +98,7 @@ export const UserPanel = ({ contextID, scopeId, userName, isDraggable }: UserPan path: tab ? { tab } : undefined, }); }, - [email, openLeftPanel, userName, userRiskData] + [telemetry, email, openLeftPanel, userName, userRiskData] ); const openPanelFirstTab = useCallback(() => openPanelTab(), [openPanelTab]); From 47839966a0a0bc3c01d0d312f0e60219736ada34 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Wed, 10 Jan 2024 11:27:50 +0100 Subject: [PATCH 04/26] Update common test details in serverless test readme (#174165) ## Summary This PR updates the details around common tests in the serverless test README file. --- x-pack/test_serverless/README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/x-pack/test_serverless/README.md b/x-pack/test_serverless/README.md index 6d7c6e350b9b2..f90f89a8b3b46 100644 --- a/x-pack/test_serverless/README.md +++ b/x-pack/test_serverless/README.md @@ -48,10 +48,11 @@ x-pack/test_serverless/ ### Common tests As outlined above, tests in the `common` API integration and functional test suites are -covering functionality that's shared across serverless projects. As a result, these tests -are automatically included in all project specific test configurations and don't have a -dedicated configuration file. We always run in the context of one of the serverless projects -and invoke the corresponding set of tests, which then also includes the `common` tests. +covering functionality that's shared across serverless projects. That's why these tests +don't have a dedicated config file and instead need to be included in project specific +configurations. + +**If you add a new `api_integration` or `functional` `common` sub-directory, remember to add it to the corresponding `common_configs` of all projects (`x-pack/test_serverless/[api_integration|functional]/test_suites/[observability|search|security]/common_configs`).** In case a common test needs to be skipped for one of the projects, there are the following suite tags available to do so: `skipSvlOblt`, `skipSvlSearch`, `skipSvlSec`, which can be @@ -71,6 +72,8 @@ specific test directory and not to `common` with two skips. Note, that `common` tests are invoked three times in a full test run: once per project to make sure the covered shared functionality works correctly in every project. So when writing tests there, be mindful about the test run time. +See also the README files for [Serverless Common API Integration Tests](https://github.com/elastic/kibana/blob/main/x-pack/test_serverless/api_integration/test_suites/common/README.md) and [Serverless Common Functional Tests](https://github.com/elastic/kibana/blob/main/x-pack/test_serverless/functional/test_suites/common/README.md). + ### Shared services and page objects Test services and page objects from `x-pack/test/[api_integration|functional]` From 8fde8e505bdca01b82e95451171906db85d2d04d Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 10 Jan 2024 12:28:16 +0200 Subject: [PATCH 05/26] [Cases] Fixes edit tags flyout flaky tests (#174503) ## Summary Fixes: https://github.com/elastic/kibana/issues/174177, https://github.com/elastic/kibana/issues/174176 Successfully run 3x100 times: https://buildkite.com/elastic/kibana-pull-request/builds?branch=cnasikas%3Afix_tags_flyout_flaky_test ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../assignees/edit_assignees_flyout.test.tsx | 2 +- .../actions/tags/edit_tags_flyout.test.tsx | 74 +++++++++---------- 2 files changed, 35 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/cases/public/components/actions/assignees/edit_assignees_flyout.test.tsx b/x-pack/plugins/cases/public/components/actions/assignees/edit_assignees_flyout.test.tsx index 15ab25f9db223..fec9999cbbcfc 100644 --- a/x-pack/plugins/cases/public/components/actions/assignees/edit_assignees_flyout.test.tsx +++ b/x-pack/plugins/cases/public/components/actions/assignees/edit_assignees_flyout.test.tsx @@ -35,7 +35,7 @@ describe('EditAssigneesFlyout', () => { jest.clearAllMocks(); appMock = createAppMockRenderer(); - useBulkGetUserProfilesMock.mockReturnValue({ data: userProfilesMap }); + useBulkGetUserProfilesMock.mockReturnValue({ data: userProfilesMap, isLoading: false }); useSuggestUserProfilesMock.mockReturnValue({ data: userProfiles, isLoading: false }); }); diff --git a/x-pack/plugins/cases/public/components/actions/tags/edit_tags_flyout.test.tsx b/x-pack/plugins/cases/public/components/actions/tags/edit_tags_flyout.test.tsx index a167d88f1bf62..d72d89bd909be 100644 --- a/x-pack/plugins/cases/public/components/actions/tags/edit_tags_flyout.test.tsx +++ b/x-pack/plugins/cases/public/components/actions/tags/edit_tags_flyout.test.tsx @@ -7,18 +7,19 @@ import React from 'react'; import userEvent from '@testing-library/user-event'; +import { waitFor, screen } from '@testing-library/react'; + import type { AppMockRenderer } from '../../../common/mock'; import { createAppMockRenderer } from '../../../common/mock'; -import { basicCase } from '../../../containers/mock'; -import { waitForComponentToUpdate } from '../../../common/test_utils'; +import { basicCase, tags } from '../../../containers/mock'; +import { useGetTags } from '../../../containers/use_get_tags'; import { EditTagsFlyout } from './edit_tags_flyout'; -import { waitFor } from '@testing-library/react'; -jest.mock('../../../containers/api'); +jest.mock('../../../containers/use_get_tags'); + +const useGetTagsMock = useGetTags as jest.Mock; -// Failing: See https://github.com/elastic/kibana/issues/174176 -// Failing: See https://github.com/elastic/kibana/issues/174177 -describe.skip('EditTagsFlyout', () => { +describe('EditTagsFlyout', () => { let appMock: AppMockRenderer; /** @@ -32,64 +33,57 @@ describe.skip('EditTagsFlyout', () => { onSaveTags: jest.fn(), }; + useGetTagsMock.mockReturnValue({ isLoading: false, data: tags }); + beforeEach(() => { - appMock = createAppMockRenderer(); jest.clearAllMocks(); + appMock = createAppMockRenderer(); }); it('renders correctly', async () => { - const result = appMock.render(); - - expect(result.getByTestId('cases-edit-tags-flyout')).toBeInTheDocument(); - expect(result.getByTestId('cases-edit-tags-flyout-title')).toBeInTheDocument(); - expect(result.getByTestId('cases-edit-tags-flyout-cancel')).toBeInTheDocument(); - expect(result.getByTestId('cases-edit-tags-flyout-submit')).toBeInTheDocument(); + appMock.render(); - await waitForComponentToUpdate(); + expect(await screen.findByTestId('cases-edit-tags-flyout')).toBeInTheDocument(); + expect(await screen.findByTestId('cases-edit-tags-flyout-title')).toBeInTheDocument(); + expect(await screen.findByTestId('cases-edit-tags-flyout-cancel')).toBeInTheDocument(); + expect(await screen.findByTestId('cases-edit-tags-flyout-submit')).toBeInTheDocument(); }); it('calls onClose when pressing the cancel button', async () => { - const result = appMock.render(); + appMock.render(); - userEvent.click(result.getByTestId('cases-edit-tags-flyout-cancel')); - expect(props.onClose).toHaveBeenCalled(); + userEvent.click(await screen.findByTestId('cases-edit-tags-flyout-cancel')); - await waitForComponentToUpdate(); + await waitFor(() => { + expect(props.onClose).toHaveBeenCalled(); + }); }); it('calls onSaveTags when pressing the save selection button', async () => { - const result = appMock.render(); - - await waitForComponentToUpdate(); + appMock.render(); - await waitFor(() => { - expect(result.getByText('coke')).toBeInTheDocument(); - }); + expect(await screen.findByText('coke')).toBeInTheDocument(); - userEvent.click(result.getByText('coke')); - userEvent.click(result.getByTestId('cases-edit-tags-flyout-submit')); + userEvent.click(await screen.findByText('coke')); + userEvent.click(await screen.findByTestId('cases-edit-tags-flyout-submit')); - expect(props.onSaveTags).toHaveBeenCalledWith({ - selectedItems: ['pepsi'], - unSelectedItems: ['coke'], + await waitFor(() => { + expect(props.onSaveTags).toHaveBeenCalledWith({ + selectedItems: ['pepsi'], + unSelectedItems: ['coke'], + }); }); }); it('shows the case title when selecting one case', async () => { - const result = appMock.render(); - - expect(result.getByText(basicCase.title)).toBeInTheDocument(); + appMock.render(); - await waitForComponentToUpdate(); + expect(await screen.findByText(basicCase.title)).toBeInTheDocument(); }); it('shows the number of total selected cases in the title when selecting multiple cases', async () => { - const result = appMock.render( - - ); - - expect(result.getByText('Selected cases: 2')).toBeInTheDocument(); + appMock.render(); - await waitForComponentToUpdate(); + expect(await screen.findByText('Selected cases: 2')).toBeInTheDocument(); }); }); From d99d6eca4e301b83aae8b453902e9a61d065d3c8 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Wed, 10 Jan 2024 12:04:45 +0100 Subject: [PATCH 06/26] remove unused constants referencing `theme:darkMode` (#174502) ## Summary Part of https://github.com/elastic/kibana/issues/173529 Cleanup unused constants referencing the darkMode uisetting ID to avoid the risk of developer using them. --- x-pack/plugins/osquery/common/constants.ts | 1 - x-pack/plugins/osquery/common/index.ts | 2 +- x-pack/plugins/synthetics/common/constants/capabilities.ts | 1 - x-pack/plugins/uptime/common/constants/capabilities.ts | 1 - 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/x-pack/plugins/osquery/common/constants.ts b/x-pack/plugins/osquery/common/constants.ts index 5887d783d4ce4..966d00f708be1 100644 --- a/x-pack/plugins/osquery/common/constants.ts +++ b/x-pack/plugins/osquery/common/constants.ts @@ -6,7 +6,6 @@ */ export const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000; -export const DEFAULT_DARK_MODE = 'theme:darkMode'; export const OSQUERY_INTEGRATION_NAME = 'osquery_manager'; export const BASE_PATH = '/app/osquery'; diff --git a/x-pack/plugins/osquery/common/index.ts b/x-pack/plugins/osquery/common/index.ts index bec9e75f07ef4..a0f314bf273d6 100644 --- a/x-pack/plugins/osquery/common/index.ts +++ b/x-pack/plugins/osquery/common/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -export { DEFAULT_DARK_MODE, OSQUERY_INTEGRATION_NAME, BASE_PATH } from './constants'; +export { OSQUERY_INTEGRATION_NAME, BASE_PATH } from './constants'; export const PLUGIN_ID = 'osquery'; export const PLUGIN_NAME = 'Osquery'; diff --git a/x-pack/plugins/synthetics/common/constants/capabilities.ts b/x-pack/plugins/synthetics/common/constants/capabilities.ts index 2768c3bf439d6..b182345f1a32f 100644 --- a/x-pack/plugins/synthetics/common/constants/capabilities.ts +++ b/x-pack/plugins/synthetics/common/constants/capabilities.ts @@ -8,4 +8,3 @@ export const INTEGRATED_SOLUTIONS = ['apm', 'infrastructure', 'logs']; export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges'; -export const DEFAULT_DARK_MODE = 'theme:darkMode'; diff --git a/x-pack/plugins/uptime/common/constants/capabilities.ts b/x-pack/plugins/uptime/common/constants/capabilities.ts index 2768c3bf439d6..b182345f1a32f 100644 --- a/x-pack/plugins/uptime/common/constants/capabilities.ts +++ b/x-pack/plugins/uptime/common/constants/capabilities.ts @@ -8,4 +8,3 @@ export const INTEGRATED_SOLUTIONS = ['apm', 'infrastructure', 'logs']; export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges'; -export const DEFAULT_DARK_MODE = 'theme:darkMode'; From 1470a360dbe15a06e9ac306094b30522f9638f8a Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Wed, 10 Jan 2024 14:11:56 +0200 Subject: [PATCH 07/26] [Lens][ES|QL] Editing listens the advanced setting (#174569) ## Summary Create ES|QL panels already listen to this setting. I added the check to editing too image image --- x-pack/plugins/lens/public/embeddable/embeddable.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index ff1af09019c1b..0a4c47d82a601 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -1507,6 +1507,10 @@ export class Embeddable } public getIsEditable() { + // for ES|QL, editing is allowed only if the advanced setting is on + if (Boolean(this.isTextBasedLanguage()) && !this.deps.uiSettings.get('discover:enableESQL')) { + return false; + } return ( this.deps.capabilities.canSaveVisualizations || (!this.inputIsRefType(this.getInput()) && From 664c1a0a08003f82cb88c9b4a51f194fbb42cd84 Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Wed, 10 Jan 2024 13:20:53 +0100 Subject: [PATCH 08/26] Search use actual es endpoint in guides (#174518) ## Summary Use the actual configured Elasticsearch endpoint in the search plugin, outside of cloud where we still use the cloud configured endpoint. --- .../__mocks__/kea_logic/kibana_logic.mock.ts | 1 + .../elasticsearch_guide.tsx | 5 +- .../public/applications/index.tsx | 5 +- .../shared/api_key/api_key_panel.tsx | 5 +- .../getting_started/languages/constants.ts | 1 - .../shared/kibana/kibana_logic.ts | 5 ++ .../shared/layout/endpoints_header_action.tsx | 5 +- .../test_helpers/test_utils.test_helper.tsx | 3 + .../enterprise_search/public/plugin.ts | 22 ++++++- .../__mocks__/routerDependencies.mock.ts | 2 + .../server/lib/check_access.test.ts | 3 + .../lib/enterprise_search_config_api.test.ts | 3 + .../enterprise_search/server/plugin.ts | 34 +++++++++-- .../routes/enterprise_search/config_data.ts | 20 ++++++- .../server/services/global_config_service.ts | 57 +++++++++++++++++++ 15 files changed, 152 insertions(+), 19 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/server/services/global_config_service.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts index 0c092b9d806b3..4388276117fc7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts @@ -34,6 +34,7 @@ export const mockKibanaValues = { }, config: { host: 'http://localhost:3002' }, data: dataPluginMock.createStartContract(), + esConfig: { elasticsearch_host: 'https://your_deployment_url' }, guidedOnboarding: {}, history: mockHistory, isCloud: false, diff --git a/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx index dd82686a31405..f89547b00c3cf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx @@ -13,22 +13,23 @@ import { EuiHorizontalRule, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { LanguageDefinitionSnippetArguments } from '@kbn/search-api-panels'; -import { ELASTICSEARCH_URL_PLACEHOLDER } from '@kbn/search-api-panels/constants'; import { FetchApiKeysAPILogic } from '../../../enterprise_search_overview/api/fetch_api_keys_logic'; import { CreateApiKeyFlyout } from '../../../shared/api_key/create_api_key_flyout'; import { useCloudDetails } from '../../../shared/cloud_details/cloud_details'; import { GettingStarted } from '../../../shared/getting_started/getting_started'; +import { KibanaLogic } from '../../../shared/kibana'; import { EnterpriseSearchElasticsearchPageTemplate } from '../layout'; export const ElasticsearchGuide = () => { const cloudContext = useCloudDetails(); const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); + const { esConfig } = useValues(KibanaLogic); const codeArgs: LanguageDefinitionSnippetArguments = { apiKey: '', cloudId: cloudContext.cloudId, - url: cloudContext.elasticsearchUrl || ELASTICSEARCH_URL_PLACEHOLDER, + url: esConfig.elasticsearch_host, }; const { makeRequest } = useActions(FetchApiKeysAPILogic); const { data } = useValues(FetchApiKeysAPILogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 55362dbfa8430..74c81d30a0825 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -22,7 +22,7 @@ import { Router } from '@kbn/shared-ux-router'; import { DEFAULT_PRODUCT_FEATURES } from '../../common/constants'; import { ClientConfigType, InitialAppData, ProductAccess } from '../../common/types'; -import { PluginsStart, ClientData } from '../plugin'; +import { PluginsStart, ClientData, ESConfig } from '../plugin'; import { externalUrl } from './shared/enterprise_search_url'; import { mountFlashMessagesLogic, Toasts } from './shared/flash_messages'; @@ -50,7 +50,7 @@ export const renderApp = ( params: AppMountParameters; plugins: PluginsStart; }, - { config, data }: { config: ClientConfigType; data: ClientData } + { config, data, esConfig }: { config: ClientConfigType; data: ClientData; esConfig: ESConfig } ) => { const { access, @@ -106,6 +106,7 @@ export const renderApp = ( charts, cloud, config, + esConfig, data: plugins.data, guidedOnboarding, history, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/api_key/api_key_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/api_key/api_key_panel.tsx index 5556b284d8d4a..c5d1034f488d8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/api_key/api_key_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/api_key/api_key_panel.tsx @@ -25,7 +25,6 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { ELASTICSEARCH_URL_PLACEHOLDER } from '@kbn/search-api-panels/constants'; import { FetchApiKeysAPILogic } from '../../enterprise_search_overview/api/fetch_api_keys_logic'; import { KibanaLogic } from '../kibana'; @@ -37,16 +36,16 @@ const COPIED_LABEL = i18n.translate('xpack.enterpriseSearch.overview.apiKey.copi }); export const ApiKeyPanel: React.FC = () => { - const { cloud, navigateToUrl } = useValues(KibanaLogic); + const { cloud, esConfig, navigateToUrl } = useValues(KibanaLogic); const { makeRequest } = useActions(FetchApiKeysAPILogic); const { data } = useValues(FetchApiKeysAPILogic); const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); + const elasticsearchEndpoint = esConfig.elasticsearch_host; useEffect(() => makeRequest({}), []); const apiKeys = data?.api_keys || []; const cloudId = cloud?.cloudId; - const elasticsearchEndpoint = cloud?.elasticsearchUrl || ELASTICSEARCH_URL_PLACEHOLDER; return ( <> diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/getting_started/languages/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/getting_started/languages/constants.ts index b0b122fa01b5c..4e7036600eaa9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/getting_started/languages/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/getting_started/languages/constants.ts @@ -6,5 +6,4 @@ */ export const API_KEY_PLACEHOLDER = 'your_api_key'; -export const ELASTICSEARCH_URL_PLACEHOLDER = 'https://your_deployment_url'; export const INDEX_NAME_PLACEHOLDER = 'index_name'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts index cf7e92dbd3475..201e91b439bc0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts @@ -19,13 +19,16 @@ import { IUiSettingsClient, } from '@kbn/core/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; + import { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public'; import { LensPublicStart } from '@kbn/lens-plugin/public'; import { MlPluginStart } from '@kbn/ml-plugin/public'; +import { ELASTICSEARCH_URL_PLACEHOLDER } from '@kbn/search-api-panels/constants'; import { AuthenticatedUser, SecurityPluginStart } from '@kbn/security-plugin/public'; import { SharePluginStart } from '@kbn/share-plugin/public'; import { ClientConfigType, ProductAccess, ProductFeatures } from '../../../../common/types'; +import { ESConfig } from '../../../plugin'; import { HttpLogic } from '../http'; import { createHref, CreateHrefOptions } from '../react_router_helpers'; @@ -40,6 +43,7 @@ export interface KibanaLogicProps { cloud?: CloudSetup; config: ClientConfigType; data: DataPublicPluginStart; + esConfig: ESConfig; guidedOnboarding?: GuidedOnboardingPluginStart; history: ScopedHistory; isSidebarEnabled: boolean; @@ -75,6 +79,7 @@ export const KibanaLogic = kea>({ cloud: [props.cloud || {}, {}], config: [props.config || {}, {}], data: [props.data, {}], + esConfig: [props.esConfig || { elasticsearch_host: ELASTICSEARCH_URL_PLACEHOLDER }, {}], guidedOnboarding: [props.guidedOnboarding, {}], history: [props.history, {}], isSidebarEnabled: [props.isSidebarEnabled, {}], diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/endpoints_header_action.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/endpoints_header_action.tsx index bbb27c342c992..35f1d28378c74 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/endpoints_header_action.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/endpoints_header_action.tsx @@ -33,7 +33,6 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { ELASTICSEARCH_URL_PLACEHOLDER } from '@kbn/search-api-panels/constants'; import { FetchApiKeysAPILogic } from '../../enterprise_search_overview/api/fetch_api_keys_logic'; import { CreateApiKeyFlyout } from '../api_key/create_api_key_flyout'; @@ -43,7 +42,7 @@ import { EndpointIcon } from './endpoint_icon'; export const EndpointsHeaderAction: React.FC = ({ children }) => { const [isPopoverOpen, setPopoverOpen] = useState(false); - const { cloud, navigateToUrl } = useValues(KibanaLogic); + const { cloud, esConfig, navigateToUrl } = useValues(KibanaLogic); const { makeRequest } = useActions(FetchApiKeysAPILogic); const { data } = useValues(FetchApiKeysAPILogic); const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); @@ -57,7 +56,7 @@ export const EndpointsHeaderAction: React.FC = ({ children }) => { const apiKeys = data?.api_keys || []; const cloudId = cloud?.cloudId; - const elasticsearchEndpoint = cloud?.elasticsearchUrl || ELASTICSEARCH_URL_PLACEHOLDER; + const elasticsearchEndpoint = esConfig.elasticsearch_host; const button = ( setPopoverOpen(!isPopoverOpen)}> diff --git a/x-pack/plugins/enterprise_search/public/applications/test_helpers/test_utils.test_helper.tsx b/x-pack/plugins/enterprise_search/public/applications/test_helpers/test_utils.test_helper.tsx index b26052f72e80c..cab4a7c04b368 100644 --- a/x-pack/plugins/enterprise_search/public/applications/test_helpers/test_utils.test_helper.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/test_helpers/test_utils.test_helper.tsx @@ -48,6 +48,9 @@ export const mockKibanaProps: KibanaLogicProps = { }, }, data: dataPluginMock.createStartContract(), + esConfig: { + elasticsearch_host: 'https://your_deployment_url', + }, guidedOnboarding: {}, history: mockHistory, isSidebarEnabled: true, diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index ce116ee52c2a6..e7b9b862f3d5d 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -23,6 +23,7 @@ import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import { LensPublicStart } from '@kbn/lens-plugin/public'; import { LicensingPluginStart } from '@kbn/licensing-plugin/public'; import { MlPluginStart } from '@kbn/ml-plugin/public'; +import { ELASTICSEARCH_URL_PLACEHOLDER } from '@kbn/search-api-panels/constants'; import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public'; import { SharePluginStart } from '@kbn/share-plugin/public'; @@ -69,17 +70,29 @@ export interface PluginsStart { ml: MlPluginStart; } +export interface ESConfig { + elasticsearch_host: string; +} + export class EnterpriseSearchPlugin implements Plugin { private config: ClientConfigType; + private esConfig: ESConfig; constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.get(); + this.esConfig = { elasticsearch_host: ELASTICSEARCH_URL_PLACEHOLDER }; } private data: ClientData = {} as ClientData; private async getInitialData(http: HttpSetup) { - if (!this.config.host && this.config.canDeployEntSearch) return; // No API to call + try { + this.esConfig = await http.get('/internal/enterprise_search/es_config'); + } catch { + this.esConfig = { elasticsearch_host: ELASTICSEARCH_URL_PLACEHOLDER }; + } + + if (!this.config.host) return; // No API to call if (this.hasInitialized) return; // We've already made an initial call try { @@ -113,7 +126,12 @@ export class EnterpriseSearchPlugin implements Plugin { private getPluginData() { // Small helper for grouping plugin data related args together - return { config: this.config, data: this.data, isSidebarEnabled: this.isSidebarEnabled }; + return { + config: this.config, + data: this.data, + esConfig: this.esConfig, + isSidebarEnabled: this.isSidebarEnabled, + }; } private hasInitialized: boolean = false; diff --git a/x-pack/plugins/enterprise_search/server/__mocks__/routerDependencies.mock.ts b/x-pack/plugins/enterprise_search/server/__mocks__/routerDependencies.mock.ts index 623b851b9ab7c..dba33ade88324 100644 --- a/x-pack/plugins/enterprise_search/server/__mocks__/routerDependencies.mock.ts +++ b/x-pack/plugins/enterprise_search/server/__mocks__/routerDependencies.mock.ts @@ -10,6 +10,7 @@ import { loggingSystemMock } from '@kbn/core/server/mocks'; import { mlPluginServerMock } from '@kbn/ml-plugin/server/mocks'; import { ConfigType } from '..'; +import { GlobalConfigService } from '../services/global_config_service'; export const mockLogger = loggingSystemMock.createLogger().get(); @@ -36,6 +37,7 @@ export const mockConfig = { export const mockDependencies = { // Mock router should be handled on a per-test basis config: mockConfig, + globalConfigService: new GlobalConfigService(), log: mockLogger, enterpriseSearchRequestHandler: mockRequestHandler as any, ml: mockMl, diff --git a/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts b/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts index e19cdc7153215..ab4b27ed1f1c2 100644 --- a/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts @@ -7,6 +7,8 @@ import { spacesMock } from '@kbn/spaces-plugin/server/mocks'; +import { GlobalConfigService } from '../services/global_config_service'; + import { checkAccess } from './check_access'; jest.mock('./enterprise_search_config_api', () => ({ @@ -51,6 +53,7 @@ describe('checkAccess', () => { canDeployEntSearch: true, host: 'http://localhost:3002', }, + globalConfigService: new GlobalConfigService(), security: mockSecurity, spaces: mockSpaces, } as any; diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts index ae6f7b4607653..fb17854f6f674 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts @@ -19,6 +19,8 @@ jest.mock('@kbn/repo-info', () => ({ import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import { GlobalConfigService } from '../services/global_config_service'; + import { callEnterpriseSearchConfigAPI, warnMismatchedVersions, @@ -37,6 +39,7 @@ describe('callEnterpriseSearchConfigAPI', () => { }; const mockDependencies = { config: mockConfig, + globalConfigService: new GlobalConfigService(), request: mockRequest, log: loggingSystemMock.create().get(), } as any; diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 1132de4fbcccf..7a764e9ef6fb5 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -77,6 +77,7 @@ import { appSearchTelemetryType } from './saved_objects/app_search/telemetry'; import { enterpriseSearchTelemetryType } from './saved_objects/enterprise_search/telemetry'; import { workplaceSearchTelemetryType } from './saved_objects/workplace_search/telemetry'; +import { GlobalConfigService } from './services/global_config_service'; import { uiSettings as enterpriseSearchUISettings } from './ui_settings'; import { getSearchResultProvider } from './utils/search_result_provider'; @@ -104,9 +105,8 @@ export interface PluginsStart { export interface RouteDependencies { config: ConfigType; enterpriseSearchRequestHandler: IEnterpriseSearchRequestHandler; - getSavedObjectsService?(): SavedObjectsServiceStart; - + globalConfigService: GlobalConfigService; log: Logger; ml?: MlPluginSetup; router: IRouter; @@ -115,6 +115,7 @@ export interface RouteDependencies { export class EnterpriseSearchPlugin implements Plugin { private readonly config: ConfigType; private readonly logger: Logger; + private readonly globalConfigService: GlobalConfigService; /** * Exposed services @@ -122,11 +123,19 @@ export class EnterpriseSearchPlugin implements Plugin { constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.get(); + this.globalConfigService = new GlobalConfigService(); this.logger = initializerContext.logger.get(); } public setup( - { capabilities, http, savedObjects, getStartServices, uiSettings }: CoreSetup, + { + capabilities, + elasticsearch, + http, + savedObjects, + getStartServices, + uiSettings, + }: CoreSetup, { usageCollection, security, @@ -139,6 +148,7 @@ export class EnterpriseSearchPlugin implements Plugin { cloud, }: PluginsSetup ) { + this.globalConfigService.setup(elasticsearch.legacy.config$, cloud); const config = this.config; const log = this.logger; const PLUGIN_IDS = [ @@ -185,7 +195,14 @@ export class EnterpriseSearchPlugin implements Plugin { async (request: KibanaRequest) => { const [, { spaces }] = await getStartServices(); - const dependencies = { config, security, spaces, request, log, ml }; + const dependencies = { + config, + security, + spaces, + request, + log, + ml, + }; const { hasAppSearchAccess, hasWorkplaceSearchAccess } = await checkAccess(dependencies); const showEnterpriseSearch = @@ -228,7 +245,14 @@ export class EnterpriseSearchPlugin implements Plugin { */ const router = http.createRouter(); const enterpriseSearchRequestHandler = new EnterpriseSearchRequestHandler({ config, log }); - const dependencies = { router, config, log, enterpriseSearchRequestHandler, ml }; + const dependencies = { + router, + config, + globalConfigService: this.globalConfigService, + log, + enterpriseSearchRequestHandler, + ml, + }; registerConfigDataRoute(dependencies); if (config.canDeployEntSearch) registerAppSearchRoutes(dependencies); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts index e65941cb7f20e..7983c2c88b5d9 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts @@ -18,7 +18,12 @@ const errorMessage = i18n.translate( } ); -export function registerConfigDataRoute({ router, config, log }: RouteDependencies) { +export function registerConfigDataRoute({ + router, + config, + log, + globalConfigService, +}: RouteDependencies) { router.get( { path: '/internal/enterprise_search/config_data', @@ -45,4 +50,17 @@ export function registerConfigDataRoute({ router, config, log }: RouteDependenci } }) ); + + router.get( + { + path: '/internal/enterprise_search/es_config', + validate: false, + }, + elasticsearchErrorHandler(log, async (context, request, response) => { + return response.ok({ + body: { elasticsearch_host: globalConfigService.elasticsearchUrl }, + headers: { 'content-type': 'application/json' }, + }); + }) + ); } diff --git a/x-pack/plugins/enterprise_search/server/services/global_config_service.ts b/x-pack/plugins/enterprise_search/server/services/global_config_service.ts new file mode 100644 index 0000000000000..ecb2edac98644 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/services/global_config_service.ts @@ -0,0 +1,57 @@ +/* + * 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 { Observable, Subscription } from 'rxjs'; + +import { CloudSetup } from '@kbn/cloud-plugin/server'; +import { ElasticsearchConfig } from '@kbn/core/server'; + +export class GlobalConfigService { + /** + * + */ + + private cloudUrl?: string; + + /** + * An observable that emits elasticsearch config. + */ + private config$?: Observable; + + /** + * A reference to the subscription to the elasticsearch observable + */ + private configSub?: Subscription; + + public get elasticsearchUrl(): string { + return this.cloudUrl + ? this.cloudUrl + : this.globalConfigElasticsearchUrl || 'https://your_deployment_url'; + } + + /** + * The elasticsearch config value at a given point in time. + */ + private globalConfigElasticsearchUrl?: string; + + setup(config$: Observable, cloud: CloudSetup) { + this.cloudUrl = cloud.elasticsearchUrl; + this.config$ = config$; + this.configSub = this.config$.subscribe((config) => { + const rawHost = config.hosts[0]; + // strip username, password, URL params and other potentially sensitive info from hosts URL + const hostUrl = new URL(rawHost); + this.globalConfigElasticsearchUrl = `${hostUrl.origin}${hostUrl.pathname}`; + }); + } + + stop() { + if (this.configSub) { + this.configSub.unsubscribe(); + } + } +} From 53ffb143ed1e105f448632c27c1d7b858e13386e Mon Sep 17 00:00:00 2001 From: Nikita Indik Date: Wed, 10 Jan 2024 14:23:31 +0100 Subject: [PATCH 09/26] [Security Solution] Unskip bulk actions Cypress tests (#174365) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary **Resolves: https://github.com/elastic/kibana/issues/171101** 200 runs of bulk_edit_rules_actions.cy.ts in ESS env: [*Buildkite 4776*](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/4776) 200 runs of bulk_edit_rules_actions.cy.ts in Serverless env: [*Buildkite 4777*](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/4777) All tests were green except a couple tests that stopped abruptly mid-way because of CI runner timeouts. So there weren't failed tests. Two issues were causing fails: 1. `"hunter_no_actions"` role that was used in "User with no privileges can't add rule actions" test doesn't exist in Serverless env. Changed it to `"t1_analyst"` – it exists in both ESS and Serverless and doesn't give permission to edit rules. 2. Race condition caused by disabled auto-refresh - In the `beforeAll` hook the auto-refresh is disabled for the Rule Management page. - Then `excessivelyInstallAllPrebuiltRules` is called, which installs all 1000+ prebuilt rules (only in Serverless, because ESS has 0 prebuilt rules available to install). - While the installation is in progress the Rule Management page loads and displays 9 rules. - Then the test selects all rules (9) and executes a bulk update on them. - Once the bulk action succeeds, the user sees a toast with "1000+ actions have been updated" while the test expects "9 actions have been updated" because of disabled auto-refresh. - I decided to skip installing all the Elastic prebuilt rules because the operation is very heavy and we check that bulk actions work for prebuilt rules anyways since two test prebuilt rules are created and installed in beforeAll. --- .../bulk_edit_rules_actions.cy.ts | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_actions.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_actions.cy.ts index f6df3e59bab93..e4b39d8178751 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_actions.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_actions.cy.ts @@ -16,7 +16,7 @@ import { createRuleAssetSavedObject } from '../../../../../helpers/rules'; import { RULES_BULK_EDIT_ACTIONS_INFO, RULES_BULK_EDIT_ACTIONS_WARNING, - ADD_RULE_ACTIONS_MENU_ITEM, + BULK_ACTIONS_BTN, } from '../../../../../screens/rules_bulk_actions'; import { actionFormSelector } from '../../../../../screens/common/rule_actions'; @@ -47,7 +47,6 @@ import { submitBulkEditForm, checkOverwriteRuleActionsCheckbox, openBulkEditRuleActionsForm, - openBulkActionsMenu, } from '../../../../../tasks/rules_bulk_actions'; import { login } from '../../../../../tasks/login'; import { visitRulesManagementTable } from '../../../../../tasks/rules_management'; @@ -65,7 +64,6 @@ import { } from '../../../../../objects/rule'; import { createAndInstallMockedPrebuiltRules, - excessivelyInstallAllPrebuiltRules, preventPrebuiltRulesPackageInstallation, } from '../../../../../tasks/api_calls/prebuilt_rules'; @@ -74,10 +72,9 @@ const ruleNameToAssert = 'Custom rule name with actions'; const expectedExistingSlackMessage = 'Existing slack action'; const expectedSlackMessage = 'Slack action test message'; -// TODO: Fix and unskip in Serverless https://github.com/elastic/kibana/issues/171101 describe( 'Detection rules, bulk edit of rule actions', - { tags: ['@ess', '@serverless', '@brokenInServerless', '@brokenInServerlessQA'] }, + { tags: ['@ess', '@serverless', '@brokenInServerlessQA'] }, () => { beforeEach(() => { login(); @@ -148,7 +145,7 @@ describe( context('Restricted action privileges', () => { it("User with no privileges can't add rule actions", () => { - login(ROLES.hunter_no_actions); + login(ROLES.t1_analyst); visitRulesManagementTable(); expectManagementTableRules([ @@ -164,11 +161,7 @@ describe( ]); waitForCallOutToBeShown(MISSING_PRIVILEGES_CALLOUT, 'primary'); - selectAllRules(); - - openBulkActionsMenu(); - - cy.get(ADD_RULE_ACTIONS_MENU_ITEM).should('be.disabled'); + cy.get(BULK_ACTIONS_BTN).should('not.exist'); }); }); @@ -197,8 +190,6 @@ describe( throttleUnit: 'd', }; - excessivelyInstallAllPrebuiltRules(); - getRulesManagementTableRows().then((rows) => { // select both custom and prebuilt rules selectAllRules(); @@ -227,8 +218,6 @@ describe( }); it('Overwrite rule actions in rules', () => { - excessivelyInstallAllPrebuiltRules(); - getRulesManagementTableRows().then((rows) => { // select both custom and prebuilt rules selectAllRules(); From e2692bf6b327e30770d8d6df747d19739fdccfc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Wed, 10 Jan 2024 14:25:33 +0100 Subject: [PATCH 10/26] [Index Management] New empty state for indices list (#174292) ## Summary This PR adds a new empty component when the indices list doesn't have any indices to display. There are 2 empty prompts: the first is displayed when the table is loaded and by default there are no indices that can be shown (for example because the "include hidden indices" toggle is disabled by default). The headers of the table are still displayed so that the user can toggle additional indices to be displayed, like the hidden or rollup indices. The second empty prompt is displayed when the user searched for indices by typing in the search bar and the search filtered out all indices. This PR also adds an extension service function to change the first empty prompt to a custom component. ### Screenshots #### No indices yet Screenshot 2024-01-04 at 18 23 45 #### No indices found Screenshot 2024-01-04 at 18 26 25 --------- Co-authored-by: Nathan Reese Co-authored-by: amyjtechwriter <61687663+amyjtechwriter@users.noreply.github.com> --- .../client_integration/helpers/index.ts | 2 +- .../helpers/test_subjects.ts | 6 +- .../client_integration/home/home.test.ts | 16 +-- .../home/indices_tab.helpers.ts | 12 +- ...dices_tab.test.ts => indices_tab.test.tsx} | 95 +++++++++++++--- .../components/no_match/no_match.tsx | 87 ++++++++++++-- .../create_index/create_index_modal.tsx | 4 +- .../index_list/index_table/index_table.js | 106 +++++++++++------- .../services/extensions_service.mock.ts | 1 + .../public/services/extensions_service.ts | 22 ++++ 10 files changed, 262 insertions(+), 89 deletions(-) rename x-pack/plugins/index_management/__jest__/client_integration/home/{indices_tab.test.ts => indices_tab.test.tsx} (85%) diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts index 1fad428342482..897a120242033 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts @@ -8,7 +8,7 @@ import './mocks'; export type { TestBed } from '@kbn/test-jest-helpers'; -export { nextTick, getRandomString, findTestSubject } from '@kbn/test-jest-helpers'; +export { getRandomString, findTestSubject } from '@kbn/test-jest-helpers'; export { setupEnvironment, diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts index 52ac8313ef4d0..eaea7f04568d4 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts @@ -109,4 +109,8 @@ export type TestSubjects = | 'dataRetentionEnabledField.input' | 'enrichPoliciesInsuficientPrivileges' | 'dataRetentionDetail' - | 'createIndexSaveButton'; + | 'createIndexSaveButton' + | 'createIndexMessage' + | 'indicesSearch' + | 'noIndicesMessage' + | 'clearIndicesSearch'; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts index 49216eb285498..ef3bfabf7b9c9 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; -import { setupEnvironment, nextTick } from '../helpers'; +import { setupEnvironment } from '../helpers'; import { HomeTestBed, setup } from './home.helpers'; describe('', () => { @@ -18,14 +18,10 @@ describe('', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadIndicesResponse([]); - testBed = await setup(httpSetup); - await act(async () => { - const { component } = testBed; - - await nextTick(); - component.update(); + testBed = await setup(httpSetup); }); + testBed.component.update(); }); test('should set the correct app title', () => { @@ -69,13 +65,11 @@ describe('', () => { httpRequestsMockHelpers.setLoadTemplatesResponse({ templates: [], legacyTemplates: [] }); - actions.selectHomeTab('templatesTab'); - await act(async () => { - await nextTick(); - component.update(); + actions.selectHomeTab('templatesTab'); }); + component.update(); expect(exists('indicesList')).toBe(false); expect(exists('templateList')).toBe(true); }); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts index 91a74ff0f558b..cd250756d3dd2 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts @@ -39,12 +39,12 @@ export interface IndicesTestBed extends TestBed { clickIncludeHiddenIndicesToggle: () => void; clickDataStreamAt: (index: number) => Promise; dataStreamLinkExistsAt: (index: number) => boolean; - clickManageContextMenuButton: () => void; - clickContextMenuOption: (optionDataTestSubject: string) => void; - clickModalConfirm: () => void; - clickCreateIndexButton: () => void; - clickCreateIndexCancelButton: () => void; - clickCreateIndexSaveButton: () => void; + clickManageContextMenuButton: () => Promise; + clickContextMenuOption: (optionDataTestSubject: string) => Promise; + clickModalConfirm: () => Promise; + clickCreateIndexButton: () => Promise; + clickCreateIndexCancelButton: () => Promise; + clickCreateIndexSaveButton: () => Promise; }; findDataStreamDetailPanel: () => ReactWrapper; findDataStreamDetailPanelTitle: () => string; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.tsx similarity index 85% rename from x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts rename to x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.tsx index f7990a3288f04..b1b3c3f2014a0 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.tsx @@ -5,10 +5,28 @@ * 2.0. */ +/* + * Mocking EuiSearchBar because its onChange is not firing during tests + */ +import { EuiSearchBoxProps } from '@elastic/eui/src/components/search_bar/search_box'; + +jest.mock('@elastic/eui/lib/components/search_bar/search_box', () => { + return { + EuiSearchBox: (props: EuiSearchBoxProps) => ( + ) => { + props.onSearch(event.target.value); + }} + /> + ), + }; +}); +import React from 'react'; import { act } from 'react-dom/test-utils'; import { API_BASE_PATH, INTERNAL_API_BASE_PATH } from '../../../common'; -import { setupEnvironment, nextTick } from '../helpers'; +import { setupEnvironment } from '../helpers'; import { IndicesTestBed, setup } from './indices_tab.helpers'; import { createDataStreamPayload, createNonDataStreamIndex } from './data_streams_tab.helpers'; @@ -97,16 +115,12 @@ describe('', () => { createDataStreamPayload({ name: 'dataStream1' }) ); - testBed = await setup(httpSetup, { - history: createMemoryHistory(), - }); - await act(async () => { - const { component } = testBed; - - await nextTick(); - component.update(); + testBed = await setup(httpSetup, { + history: createMemoryHistory(), + }); }); + testBed.component.update(); }); test('navigates to the data stream in the Data Streams tab', async () => { @@ -168,6 +182,55 @@ describe('', () => { expect(testBed.actions.findIndexDetailsPageTitle()).toContain(indexName); }); + describe('empty list component', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadIndicesResponse([]); + await act(async () => { + testBed = await setup(httpSetup); + }); + testBed.component.update(); + }); + + test('renders the default empty list content', () => { + expect(testBed.exists('createIndexMessage')).toBe(true); + }); + + it('displays an empty list content if set via extensions service', async () => { + httpRequestsMockHelpers.setLoadIndicesResponse([]); + await act(async () => { + testBed = await setup(httpSetup, { + services: { + extensionsService: { + _emptyListContent: { + renderContent: () => { + return
Empty list content
; + }, + }, + }, + }, + }); + }); + testBed.component.update(); + + expect(testBed.component.text()).toContain('Empty list content'); + }); + + it('renders "no indices found" prompt for search', async () => { + const { find, component, exists } = testBed; + await act(async () => { + find('indicesSearch').simulate('change', { target: { value: 'non-existing-index' } }); + }); + component.update(); + + expect(exists('noIndicesMessage')).toBe(true); + + find('clearIndicesSearch').simulate('click'); + component.update(); + + expect(exists('noIndicesMessage')).toBe(false); + }); + }); + describe('index actions', () => { const indexNameA = 'testIndexA'; const indexNameB = 'testIndexB'; @@ -416,16 +479,12 @@ describe('', () => { }, ]); - testBed = await setup(httpSetup, { - history: createMemoryHistory(), - }); - await act(async () => { - const { component } = testBed; - - await nextTick(); - component.update(); + testBed = await setup(httpSetup, { + history: createMemoryHistory(), + }); }); + testBed.component.update(); }); test('shows the create index button', async () => { @@ -441,7 +500,7 @@ describe('', () => { expect(exists('createIndexNameFieldText')).toBe(true); - await await actions.clickCreateIndexCancelButton(); + await actions.clickCreateIndexCancelButton(); expect(exists('createIndexNameFieldText')).toBe(false); }); 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 e972a0ff6948d..6f7c79724763e 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 @@ -6,13 +6,86 @@ */ import React from 'react'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { ExtensionsService } from '../../../services'; +import { CreateIndexButton } from '../../sections/home/index_list/create_index/create_index_button'; -export const NoMatch = () => ( -
- void; + filter: string; + resetFilter: () => void; + extensionsService: ExtensionsService; +}) => { + if (filter) { + return ( + + + + } + body={ +

+ +

+ } + actions={ + + + + } + /> + ); + } + + if (extensionsService.emptyListContent) { + return extensionsService.emptyListContent.renderContent({ + createIndexButton: , + }); + } + + return ( + + + + } + body={ +

+ +

+ } + actions={} /> -
-); + ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/create_index/create_index_modal.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/create_index/create_index_modal.tsx index c54aaf0b12374..dc74454b62a5c 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/create_index/create_index_modal.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/create_index/create_index_modal.tsx @@ -51,14 +51,14 @@ export const CreateIndexModal = ({ closeModal, loadIndices }: CreateIndexModalPr const { error } = await createIndex(indexName); setIsSaving(false); if (!error) { - loadIndices(); - closeModal(); notificationService.showSuccessToast( i18n.translate('xpack.idxMgmt.createIndex.successfullyCreatedIndexMessage', { defaultMessage: 'Successfully created index: {indexName}', values: { indexName }, }) ); + closeModal(); + loadIndices(); return; } setCreateError(error.message); diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js index 7a15abef3a16f..000c34988310b 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js @@ -258,14 +258,13 @@ export class IndexTable extends Component { return indexOfUnselectedItem === -1; }; - buildHeader(config) { + buildHeader(headers) { const { sortField, isSortAscending } = this.props; - const headers = getHeaders({ showIndexStats: config.enableIndexStats }); return Object.entries(headers).map(([fieldName, label]) => { const isSorted = sortField === fieldName; // we only want to make index name column 25% width when there are more columns displayed const widthClassName = - fieldName === 'name' && config.enableIndexStats ? 'indTable__header__width' : ''; + fieldName === 'name' && Object.keys(headers).length > 2 ? 'indTable__header__width' : ''; return ( {({ services, config }) => { const { extensionsService } = services; - + const headers = getHeaders({ showIndexStats: config.enableIndexStats }); + const columnsCount = Object.keys(headers).length + 1; return ( @@ -593,6 +601,7 @@ export class IndexTable extends Component { defaultMessage: 'Search', } ), + 'data-test-subj': 'indicesSearch', }} aria-label={i18n.translate( 'xpack.idxMgmt.indexTable.systemIndicesSearchIndicesAriaLabel', @@ -631,43 +640,54 @@ export class IndexTable extends Component { - {indices.length > 0 ? ( -
- - - - - - - - - - - - {this.buildHeader(config)} - - - {this.buildRows(services, config)} - -
- ) : ( - - )} +
+ + + + + + + + + + + + {this.buildHeader(headers)} + + + + {indices.length > 0 ? ( + this.buildRows(services, config) + ) : ( + + + filterChanged('')} + extensionsService={extensionsService} + /> + + + )} + + +
diff --git a/x-pack/plugins/index_management/public/services/extensions_service.mock.ts b/x-pack/plugins/index_management/public/services/extensions_service.mock.ts index 8f4968ad35e41..616e81af9a7af 100644 --- a/x-pack/plugins/index_management/public/services/extensions_service.mock.ts +++ b/x-pack/plugins/index_management/public/services/extensions_service.mock.ts @@ -16,6 +16,7 @@ const createServiceMock = (): ExtensionsSetupMock => ({ addBanner: jest.fn(), addFilter: jest.fn(), addToggle: jest.fn(), + setEmptyListContent: jest.fn(), addIndexDetailsTab: jest.fn(), setIndexOverviewContent: jest.fn(), setIndexMappingsContent: jest.fn(), diff --git a/x-pack/plugins/index_management/public/services/extensions_service.ts b/x-pack/plugins/index_management/public/services/extensions_service.ts index 1eb68e9a0b746..955b92ab160e5 100644 --- a/x-pack/plugins/index_management/public/services/extensions_service.ts +++ b/x-pack/plugins/index_management/public/services/extensions_service.ts @@ -27,6 +27,12 @@ export interface IndexBadge { color: EuiBadgeProps['color']; } +export interface EmptyListContent { + renderContent: (args: { + createIndexButton: ReturnType; + }) => ReturnType; +} + export interface ExtensionsSetup { // adds an option to the "manage index" menu addAction(action: any): void; @@ -38,6 +44,8 @@ export interface ExtensionsSetup { addBadge(badge: IndexBadge): void; // adds a toggle to the indices list addToggle(toggle: any): void; + // set the content to render when the indices list is empty + setEmptyListContent(content: EmptyListContent): void; // adds a tab to the index details page addIndexDetailsTab(tab: IndexDetailsTab): void; // sets content to render instead of the code block on the overview tab of the index page @@ -63,6 +71,7 @@ export class ExtensionsService { }, ]; private _toggles: any[] = []; + private _emptyListContent: EmptyListContent | null = null; private _indexDetailsTabs: IndexDetailsTab[] = []; private _indexOverviewContent: IndexContent | null = null; private _indexMappingsContent: IndexContent | null = null; @@ -75,6 +84,7 @@ export class ExtensionsService { addBanner: this.addBanner.bind(this), addFilter: this.addFilter.bind(this), addToggle: this.addToggle.bind(this), + setEmptyListContent: this.setEmptyListContent.bind(this), addIndexDetailsTab: this.addIndexDetailsTab.bind(this), setIndexOverviewContent: this.setIndexOverviewContent.bind(this), setIndexMappingsContent: this.setIndexMappingsContent.bind(this), @@ -103,6 +113,14 @@ export class ExtensionsService { this._toggles.push(toggle); } + private setEmptyListContent(content: EmptyListContent) { + if (this._emptyListContent) { + throw new Error(`The empty list content has already been set.`); + } else { + this._emptyListContent = content; + } + } + private addIndexDetailsTab(tab: IndexDetailsTab) { this._indexDetailsTabs.push(tab); } @@ -143,6 +161,10 @@ export class ExtensionsService { return this._toggles; } + public get emptyListContent() { + return this._emptyListContent; + } + public get indexDetailsTabs() { return this._indexDetailsTabs; } From f9c83448c94713a227aa62aae223c90db852e6fa Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 10 Jan 2024 14:53:43 +0100 Subject: [PATCH 11/26] [Obs AI Assistant] Move esql docs to dir that does not get ... (#174583) any folder called `docs` is automatically deleted when creating a distributable. Rename it to `esql_docs` so this doesn't happen. We need tests, but will tackle that separately. Not sure how to write tests for the distributable. --- .../server/functions/esql/{docs => esql_docs}/esql-abs.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-acos.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-asin.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-atan.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-atan2.txt | 0 .../functions/esql/{docs => esql_docs}/esql-auto_bucket.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-avg.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-case.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-ceil.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-coalesce.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-concat.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-cos.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-cosh.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-count.txt | 0 .../functions/esql/{docs => esql_docs}/esql-count_distinct.txt | 0 .../functions/esql/{docs => esql_docs}/esql-date_extract.txt | 0 .../functions/esql/{docs => esql_docs}/esql-date_format.txt | 0 .../functions/esql/{docs => esql_docs}/esql-date_parse.txt | 0 .../functions/esql/{docs => esql_docs}/esql-date_trunc.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-dissect.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-drop.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-e.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-enrich.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-eval.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-floor.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-from.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-greatest.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-grok.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-keep.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-least.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-left.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-length.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-limit.txt | 0 .../functions/esql/{docs => esql_docs}/esql-limitations.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-log10.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-ltrim.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-max.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-median.txt | 0 .../esql/{docs => esql_docs}/esql-median_absolute_deviation.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-min.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-mv_avg.txt | 0 .../functions/esql/{docs => esql_docs}/esql-mv_concat.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-mv_count.txt | 0 .../functions/esql/{docs => esql_docs}/esql-mv_dedupe.txt | 0 .../functions/esql/{docs => esql_docs}/esql-mv_expand.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-mv_max.txt | 0 .../functions/esql/{docs => esql_docs}/esql-mv_median.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-mv_min.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-mv_sum.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-now.txt | 0 .../functions/esql/{docs => esql_docs}/esql-numeric-fields.txt | 0 .../functions/esql/{docs => esql_docs}/esql-operators.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-overview.txt | 0 .../functions/esql/{docs => esql_docs}/esql-percentile.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-pi.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-pow.txt | 0 .../esql/{docs => esql_docs}/esql-processing-commands.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-rename.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-replace.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-right.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-round.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-row.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-rtrim.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-show.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-sin.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-sinh.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-sort.txt | 0 .../functions/esql/{docs => esql_docs}/esql-source-commands.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-split.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-sqrt.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-stats.txt | 0 .../functions/esql/{docs => esql_docs}/esql-substring.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-sum.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-syntax.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-tan.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-tanh.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-tau.txt | 0 .../functions/esql/{docs => esql_docs}/esql-to_boolean.txt | 0 .../esql/{docs => esql_docs}/esql-to_cartesianpoint.txt | 0 .../functions/esql/{docs => esql_docs}/esql-to_datetime.txt | 0 .../functions/esql/{docs => esql_docs}/esql-to_degrees.txt | 0 .../functions/esql/{docs => esql_docs}/esql-to_double.txt | 0 .../functions/esql/{docs => esql_docs}/esql-to_geopoint.txt | 0 .../functions/esql/{docs => esql_docs}/esql-to_integer.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-to_ip.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-to_long.txt | 0 .../functions/esql/{docs => esql_docs}/esql-to_radians.txt | 0 .../functions/esql/{docs => esql_docs}/esql-to_string.txt | 0 .../esql/{docs => esql_docs}/esql-to_unsigned_long.txt | 0 .../functions/esql/{docs => esql_docs}/esql-to_version.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-trim.txt | 0 .../server/functions/esql/{docs => esql_docs}/esql-where.txt | 0 .../observability_ai_assistant/server/functions/esql/index.ts | 2 +- 93 files changed, 1 insertion(+), 1 deletion(-) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-abs.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-acos.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-asin.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-atan.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-atan2.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-auto_bucket.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-avg.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-case.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-ceil.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-coalesce.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-concat.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-cos.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-cosh.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-count.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-count_distinct.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-date_extract.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-date_format.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-date_parse.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-date_trunc.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-dissect.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-drop.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-e.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-enrich.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-eval.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-floor.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-from.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-greatest.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-grok.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-keep.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-least.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-left.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-length.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-limit.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-limitations.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-log10.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-ltrim.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-max.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-median.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-median_absolute_deviation.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-min.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-mv_avg.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-mv_concat.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-mv_count.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-mv_dedupe.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-mv_expand.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-mv_max.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-mv_median.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-mv_min.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-mv_sum.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-now.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-numeric-fields.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-operators.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-overview.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-percentile.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-pi.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-pow.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-processing-commands.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-rename.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-replace.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-right.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-round.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-row.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-rtrim.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-show.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-sin.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-sinh.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-sort.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-source-commands.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-split.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-sqrt.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-stats.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-substring.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-sum.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-syntax.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-tan.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-tanh.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-tau.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-to_boolean.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-to_cartesianpoint.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-to_datetime.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-to_degrees.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-to_double.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-to_geopoint.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-to_integer.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-to_ip.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-to_long.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-to_radians.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-to_string.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-to_unsigned_long.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-to_version.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-trim.txt (100%) rename x-pack/plugins/observability_ai_assistant/server/functions/esql/{docs => esql_docs}/esql-where.txt (100%) diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-abs.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-abs.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-abs.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-abs.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-acos.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-acos.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-acos.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-acos.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-asin.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-asin.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-asin.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-asin.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-atan.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-atan.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-atan.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-atan.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-atan2.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-atan2.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-atan2.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-atan2.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-auto_bucket.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-auto_bucket.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-auto_bucket.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-auto_bucket.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-avg.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-avg.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-avg.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-avg.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-case.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-case.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-case.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-case.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-ceil.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-ceil.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-ceil.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-ceil.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-coalesce.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-coalesce.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-coalesce.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-coalesce.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-concat.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-concat.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-concat.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-concat.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-cos.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-cos.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-cos.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-cos.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-cosh.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-cosh.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-cosh.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-cosh.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-count.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-count.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-count.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-count.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-count_distinct.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-count_distinct.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-count_distinct.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-count_distinct.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-date_extract.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-date_extract.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-date_extract.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-date_extract.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-date_format.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-date_format.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-date_format.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-date_format.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-date_parse.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-date_parse.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-date_parse.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-date_parse.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-date_trunc.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-date_trunc.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-date_trunc.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-date_trunc.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-dissect.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-dissect.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-dissect.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-dissect.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-drop.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-drop.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-drop.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-drop.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-e.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-e.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-e.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-e.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-enrich.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-enrich.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-enrich.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-enrich.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-eval.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-eval.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-eval.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-eval.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-floor.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-floor.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-floor.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-floor.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-from.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-from.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-from.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-from.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-greatest.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-greatest.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-greatest.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-greatest.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-grok.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-grok.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-grok.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-grok.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-keep.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-keep.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-keep.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-keep.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-least.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-least.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-least.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-least.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-left.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-left.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-left.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-left.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-length.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-length.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-length.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-length.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-limit.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-limit.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-limit.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-limit.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-limitations.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-limitations.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-limitations.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-limitations.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-log10.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-log10.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-log10.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-log10.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-ltrim.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-ltrim.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-ltrim.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-ltrim.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-max.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-max.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-max.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-max.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-median.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-median.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-median.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-median.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-median_absolute_deviation.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-median_absolute_deviation.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-median_absolute_deviation.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-median_absolute_deviation.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-min.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-min.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-min.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-min.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-mv_avg.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_avg.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-mv_avg.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_avg.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-mv_concat.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_concat.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-mv_concat.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_concat.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-mv_count.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_count.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-mv_count.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_count.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-mv_dedupe.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_dedupe.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-mv_dedupe.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_dedupe.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-mv_expand.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_expand.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-mv_expand.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_expand.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-mv_max.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_max.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-mv_max.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_max.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-mv_median.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_median.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-mv_median.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_median.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-mv_min.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_min.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-mv_min.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_min.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-mv_sum.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_sum.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-mv_sum.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_sum.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-now.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-now.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-now.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-now.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-numeric-fields.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-numeric-fields.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-numeric-fields.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-numeric-fields.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-operators.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-operators.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-operators.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-operators.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-overview.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-overview.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-overview.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-overview.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-percentile.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-percentile.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-percentile.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-percentile.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-pi.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-pi.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-pi.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-pi.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-pow.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-pow.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-pow.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-pow.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-processing-commands.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-processing-commands.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-processing-commands.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-processing-commands.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-rename.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-rename.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-rename.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-rename.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-replace.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-replace.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-replace.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-replace.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-right.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-right.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-right.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-right.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-round.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-round.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-round.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-round.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-row.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-row.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-row.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-row.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-rtrim.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-rtrim.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-rtrim.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-rtrim.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-show.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-show.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-show.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-show.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-sin.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-sin.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-sin.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-sin.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-sinh.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-sinh.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-sinh.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-sinh.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-sort.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-sort.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-sort.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-sort.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-source-commands.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-source-commands.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-source-commands.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-source-commands.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-split.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-split.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-split.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-split.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-sqrt.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-sqrt.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-sqrt.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-sqrt.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-stats.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-stats.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-stats.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-stats.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-substring.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-substring.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-substring.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-substring.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-sum.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-sum.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-sum.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-sum.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-syntax.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-syntax.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-syntax.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-syntax.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-tan.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-tan.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-tan.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-tan.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-tanh.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-tanh.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-tanh.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-tanh.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-tau.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-tau.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-tau.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-tau.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_boolean.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_boolean.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_boolean.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_boolean.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_cartesianpoint.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_cartesianpoint.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_cartesianpoint.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_cartesianpoint.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_datetime.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_datetime.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_datetime.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_datetime.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_degrees.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_degrees.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_degrees.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_degrees.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_double.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_double.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_double.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_double.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_geopoint.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_geopoint.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_geopoint.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_geopoint.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_integer.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_integer.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_integer.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_integer.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_ip.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_ip.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_ip.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_ip.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_long.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_long.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_long.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_long.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_radians.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_radians.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_radians.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_radians.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_string.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_string.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_string.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_string.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_unsigned_long.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_unsigned_long.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_unsigned_long.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_unsigned_long.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_version.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_version.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-to_version.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_version.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-trim.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-trim.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-trim.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-trim.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-where.txt b/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-where.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/docs/esql-where.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-where.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/index.ts b/x-pack/plugins/observability_ai_assistant/server/functions/esql/index.ts index bbadaa82a8fb3..5aea8913bf115 100644 --- a/x-pack/plugins/observability_ai_assistant/server/functions/esql/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/functions/esql/index.ts @@ -30,7 +30,7 @@ const loadSystemMessage = once(async () => { }); const loadEsqlDocs = once(async () => { - const dir = Path.join(__dirname, './docs'); + const dir = Path.join(__dirname, './esql_docs'); const files = (await readdir(dir)).filter((file) => Path.extname(file) === '.txt'); if (!files.length) { From bc85d80b101d00b885a4f66cd2f156422371d7ba Mon Sep 17 00:00:00 2001 From: Kevin Lacabane Date: Wed, 10 Jan 2024 15:56:55 +0100 Subject: [PATCH 12/26] [stack monitoring docs] how to reach elastic-package service from agent (#174602) ## Summary Added section describing how to reach service started by `elastic-package service` from elastic-agent. Also fixed invalid link --- x-pack/plugins/monitoring/dev_docs/how_to/work_with_packages.md | 2 ++ .../monitoring/dev_docs/reference/data_collection_modes.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/monitoring/dev_docs/how_to/work_with_packages.md b/x-pack/plugins/monitoring/dev_docs/how_to/work_with_packages.md index f1c5eff8d07b8..4ab2349c332da 100644 --- a/x-pack/plugins/monitoring/dev_docs/how_to/work_with_packages.md +++ b/x-pack/plugins/monitoring/dev_docs/how_to/work_with_packages.md @@ -61,6 +61,8 @@ A package can define the services it needs to monitor for development and automa This can be done by creating a `docker-compose` file under the package `_dev/deploy` directory, then running `elastic-package service up -v` in the package folder. An example is the [elasticsearch package](https://github.com/elastic/integrations/tree/main/packages/elasticsearch/_dev/deploy/docker) that starts a service which generates every types of logs with the help of a script executing queries. +**Note** that the container started with `elastic-package service up` will run in its own network and elastic-agent running with `elastic-package stack up` needs to specify the full docker service name to reach it. For example, if you want to collect metrics from the [elasticsearch instance](https://github.com/elastic/integrations/blob/main/packages/elasticsearch/_dev/deploy/docker/docker-compose.yml#L17) started with `elastic-package service up` you can configure elasticsearch integration to reach it at `http://elastic-package-service-elasticsearch-1:9200` (this may vary depending on OS/docker version). Alternatively you can reach the service on localhost via the forwarded port `http://host.docker.internal:9201`. + ### Collecting logs To collect logs elastic-agent needs access to the raw files. Let's see how that works taking `elasticsearch` package as an example. diff --git a/x-pack/plugins/monitoring/dev_docs/reference/data_collection_modes.md b/x-pack/plugins/monitoring/dev_docs/reference/data_collection_modes.md index b34bc550a0a8f..ea585fda0306e 100644 --- a/x-pack/plugins/monitoring/dev_docs/reference/data_collection_modes.md +++ b/x-pack/plugins/monitoring/dev_docs/reference/data_collection_modes.md @@ -145,7 +145,7 @@ Beats also doesn't have filebeat module or recommended configuration, but the lo ### Package-driven collection -See [working with packages](../howto/work_with_packages.md) for details on how to develop and test Stack Monitoring packages. +See [working with packages](../how_to/work_with_packages.md) for details on how to develop and test Stack Monitoring packages. When using package-driven collection, each component in your Elastic stack is given a corresponding fleet package (also known as "integration"). From d54898dba68a39b3ab014aaeaa432cfc9961fb3d Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Wed, 10 Jan 2024 08:24:08 -0700 Subject: [PATCH 13/26] Move EuiSuperDatePicker harness to test-eui-helpers (#174543) ## Summary Contributes a new helper to our shared EUI RTL helpers package. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../kbn-test-eui-helpers/src/rtl_helpers.tsx | 74 ++++++++++++++++++- .../group_preview.test.tsx | 51 ++----------- .../event_annotation_listing/tsconfig.json | 3 +- 3 files changed, 81 insertions(+), 47 deletions(-) diff --git a/packages/kbn-test-eui-helpers/src/rtl_helpers.tsx b/packages/kbn-test-eui-helpers/src/rtl_helpers.tsx index e5a44615e980d..fc469d9cb234f 100644 --- a/packages/kbn-test-eui-helpers/src/rtl_helpers.tsx +++ b/packages/kbn-test-eui-helpers/src/rtl_helpers.tsx @@ -5,10 +5,80 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - -import { screen, within } from '@testing-library/react'; +import moment from 'moment'; +import userEvent from '@testing-library/user-event'; +import { screen, within, fireEvent } from '@testing-library/react'; export const getButtonGroupInputValue = (testId: string) => () => { const buttonGroup = screen.getByTestId(testId); return within(buttonGroup).getByRole('button', { pressed: true }); }; + +export class EuiSuperDatePickerTestHarness { + // From https://github.com/elastic/eui/blob/6a30eba7c2a154691c96a1d17c8b2f3506d351a3/src/components/date_picker/super_date_picker/super_date_picker.tsx#L222 + private static readonly dateFormat = 'MMM D, YYYY @ HH:mm:ss.SSS'; + + /** + * This method returns the currently selected commonly-used range as a string + * + * The empty string is returned if a commonly-used range is not currently selected + */ + public static get currentCommonlyUsedRange() { + return screen.queryByTestId('superDatePickerShowDatesButton')?.textContent ?? ''; + } + + /** + * This method returns the currently selected range as a pair of strings + */ + public static get currentRange() { + if (screen.queryByTestId('superDatePickerShowDatesButton')) { + // showing a commonly-used range + return { from: '', to: '' }; + } + + return { + from: screen.getByTestId('superDatePickerstartDatePopoverButton').textContent, + to: screen.getByTestId('superDatePickerendDatePopoverButton').textContent, + }; + } + + /** + * This method runs an assertion against the currently selected range using + * UNIX timestamps. + * + * NOTE: it does not (yet) support commonly-used (textual) ranges like "Last 15 minutes" + */ + public static assertCurrentRange(range: { from: number; to: number }, expect: jest.Expect) { + expect(EuiSuperDatePickerTestHarness.currentRange).toEqual({ + from: moment(range.from).format(EuiSuperDatePickerTestHarness.dateFormat), + to: moment(range.to).format(EuiSuperDatePickerTestHarness.dateFormat), + }); + } + + /** + * Opens the popover for the date picker + */ + static togglePopover() { + userEvent.click(screen.getByRole('button', { name: 'Date quick select' })); + } + + /** + * Selects a commonly-used range from the date picker (opens the popover if it's not already open) + */ + static async selectCommonlyUsedRange(label: string) { + if (!screen.queryByText('Commonly used')) this.togglePopover(); + + // Using fireEvent here because userEvent erroneously claims that + // pointer-events is set to 'none'. + // + // I have verified that this fixed on the latest version of the @testing-library/user-event package + fireEvent.click(await screen.findByText(label)); + } + + /** + * Activates the refresh button + */ + static refresh() { + userEvent.click(screen.getByRole('button', { name: 'Refresh' })); + } +} diff --git a/src/plugins/event_annotation_listing/public/components/group_editor_flyout/group_preview.test.tsx b/src/plugins/event_annotation_listing/public/components/group_editor_flyout/group_preview.test.tsx index 386976ac571b6..935aad45570ac 100644 --- a/src/plugins/event_annotation_listing/public/components/group_editor_flyout/group_preview.test.tsx +++ b/src/plugins/event_annotation_listing/public/components/group_editor_flyout/group_preview.test.tsx @@ -21,51 +21,14 @@ import { TypedLensByValueInput, } from '@kbn/lens-plugin/public'; import { Datatable } from '@kbn/expressions-plugin/common'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; import userEvent from '@testing-library/user-event'; import { I18nProvider } from '@kbn/i18n-react'; import { GroupPreview } from './group_preview'; import { LensByValueInput } from '@kbn/lens-plugin/public/embeddable'; import { DATA_LAYER_ID, DATE_HISTOGRAM_COLUMN_ID, getCurrentTimeField } from './lens_attributes'; -import moment from 'moment'; - -class EuiSuperDatePickerTestHarness { - public static get currentCommonlyUsedRange() { - return screen.queryByTestId('superDatePickerShowDatesButton')?.textContent ?? ''; - } - - // TODO - add assertion with date formatting - public static get currentRange() { - if (screen.queryByTestId('superDatePickerShowDatesButton')) { - // showing a commonly-used range - return { from: '', to: '' }; - } - - return { - from: screen.getByTestId('superDatePickerstartDatePopoverButton').textContent, - to: screen.getByTestId('superDatePickerendDatePopoverButton').textContent, - }; - } - - static togglePopover() { - userEvent.click(screen.getByRole('button', { name: 'Date quick select' })); - } - - static async selectCommonlyUsedRange(label: string) { - if (!screen.queryByText('Commonly used')) this.togglePopover(); - - // Using fireEvent here because userEvent erroneously claims that - // pointer-events is set to 'none'. - // - // I have verified that this fixed on the latest version of the @testing-library/user-event package - fireEvent.click(await screen.findByText(label)); - } - - static refresh() { - userEvent.click(screen.getByRole('button', { name: 'Refresh' })); - } -} +import { EuiSuperDatePickerTestHarness } from '@kbn/test-eui-helpers'; describe('group editor preview', () => { const annotation = getDefaultManualAnnotation('my-id', 'some-timestamp'); @@ -186,11 +149,11 @@ describe('group editor preview', () => { // from chart brush userEvent.click(screen.getByTestId('brushEnd')); - const format = 'MMM D, YYYY @ HH:mm:ss.SSS'; // from https://github.com/elastic/eui/blob/6a30eba7c2a154691c96a1d17c8b2f3506d351a3/src/components/date_picker/super_date_picker/super_date_picker.tsx#L222; - expect(EuiSuperDatePickerTestHarness.currentRange).toEqual({ - from: moment(BRUSH_RANGE[0]).format(format), - to: moment(BRUSH_RANGE[1]).format(format), - }); + EuiSuperDatePickerTestHarness.assertCurrentRange( + { from: BRUSH_RANGE[0], to: BRUSH_RANGE[1] }, + expect + ); + expect(getEmbeddableTimeRange()).toEqual({ from: new Date(BRUSH_RANGE[0]).toISOString(), to: new Date(BRUSH_RANGE[1]).toISOString(), diff --git a/src/plugins/event_annotation_listing/tsconfig.json b/src/plugins/event_annotation_listing/tsconfig.json index 8c9efd4559400..e3c77073de168 100644 --- a/src/plugins/event_annotation_listing/tsconfig.json +++ b/src/plugins/event_annotation_listing/tsconfig.json @@ -41,7 +41,8 @@ "@kbn/core-notifications-browser-mocks", "@kbn/core-notifications-browser", "@kbn/core-saved-objects-api-browser", - "@kbn/content-management-table-list-view-common" + "@kbn/content-management-table-list-view-common", + "@kbn/test-eui-helpers" ], "exclude": [ "target/**/*", From 30ce43bea3424fe420186910794c1fe915581c63 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 10 Jan 2024 17:26:22 +0200 Subject: [PATCH 14/26] [Cases] Fix create case submit button flaky test (#174570) ## Summary Fixes: https://github.com/elastic/kibana/issues/174376 Successful runs: https://buildkite.com/elastic/kibana-pull-request/builds?branch=cnasikas%3Afix_174376 ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../components/create/submit_button.test.tsx | 80 +++++++------------ 1 file changed, 27 insertions(+), 53 deletions(-) diff --git a/x-pack/plugins/cases/public/components/create/submit_button.test.tsx b/x-pack/plugins/cases/public/components/create/submit_button.test.tsx index dc6e7f62746e8..a68b05bde5895 100644 --- a/x-pack/plugins/cases/public/components/create/submit_button.test.tsx +++ b/x-pack/plugins/cases/public/components/create/submit_button.test.tsx @@ -6,81 +6,55 @@ */ import React from 'react'; -import { mount } from 'enzyme'; -import { waitFor } from '@testing-library/react'; +import { waitFor, screen } from '@testing-library/react'; -import { useForm, Form } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { SubmitCaseButton } from './submit_button'; -import type { FormProps } from './schema'; -import { schema } from './schema'; +import type { AppMockRenderer } from '../../common/mock'; +import { createAppMockRenderer } from '../../common/mock'; +import { FormTestComponent } from '../../common/test_utils'; +import userEvent from '@testing-library/user-event'; -// FLAKY: https://github.com/elastic/kibana/issues/174376 -describe.skip('SubmitCaseButton', () => { +describe('SubmitCaseButton', () => { + let appMockRender: AppMockRenderer; const onSubmit = jest.fn(); - const MockHookWrapperComponent: React.FC = ({ children }) => { - const { form } = useForm({ - defaultValue: { title: 'My title' }, - schema: { - title: schema.title, - }, - onSubmit, - }); - - return
{children}
; - }; - beforeEach(() => { jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); }); - it('it renders', async () => { - const wrapper = mount( - + it('renders', async () => { + appMockRender.render( + - + ); - expect(wrapper.find(`[data-test-subj="create-case-submit"]`).exists()).toBeTruthy(); + expect(await screen.findByTestId('create-case-submit')).toBeInTheDocument(); }); - it('it submits', async () => { - const wrapper = mount( - + it('submits', async () => { + appMockRender.render( + - + ); - wrapper.find(`button[data-test-subj="create-case-submit"]`).first().simulate('click'); - await waitFor(() => expect(onSubmit).toBeCalled()); - }); - it('it disables when submitting', async () => { - const wrapper = mount( - - - - ); + userEvent.click(await screen.findByTestId('create-case-submit')); - wrapper.find(`button[data-test-subj="create-case-submit"]`).first().simulate('click'); - await waitFor(() => - expect( - wrapper.find(`[data-test-subj="create-case-submit"]`).first().prop('isDisabled') - ).toBeTruthy() - ); + await waitFor(() => expect(onSubmit).toBeCalled()); }); - it('it is loading when submitting', async () => { - const wrapper = mount( - + it('disables when submitting', async () => { + appMockRender.render( + - + ); - wrapper.find(`button[data-test-subj="create-case-submit"]`).first().simulate('click'); - await waitFor(() => - expect( - wrapper.find(`[data-test-subj="create-case-submit"]`).first().prop('isLoading') - ).toBeTruthy() - ); + const button = await screen.findByTestId('create-case-submit'); + userEvent.click(button); + + await waitFor(() => expect(button).toBeDisabled()); }); }); From 2f2083b71893cfbcc9370ea2e162ea5c384dcdc0 Mon Sep 17 00:00:00 2001 From: Abdul Wahab Zahid Date: Wed, 10 Jan 2024 16:33:01 +0100 Subject: [PATCH 15/26] [Infra UI] Fix `Open in Logs` links in Infra and APM when in Serverless (#172137) Fixes https://github.com/elastic/kibana/issues/171082 ## Summary The PR wraps the [LogsLocator](https://github.com/elastic/kibana/blob/f59ac2916d02d643310dd44a8f80b7a9cc61f608/x-pack/plugins/infra/common/locators/logs_locator.ts#L18C27-L18C27) and [NodeLogsLocator](https://github.com/elastic/kibana/blob/f59ac2916d02d643310dd44a8f80b7a9cc61f608/x-pack/plugins/infra/common/locators/node_logs_locator.ts#L16) of **infra** plugin inside corresponding locators in **logs_shared** plugin while including the fallback logic to navigate to Logs Explorer when Steam UI isn't available. Previously, it was assumed that Steam UI will always be available as long as Infra UI is available, but **infra** plugin introduced a new feature flag `logsUIEnabled` which when `false` won't enable the `/stream/` route in Serverless. The added locators in **logs_shared** will now check whether locators redirecting to `/steam/` are available, otherwise they'll redirect to Logs Explorer, thus the new locators are abstracting this decision in their definition. This abstraction was already being done in **apm** plugin, which has also been refactored to use the newly added **logs_shared** locators. Links in Serverless: https://github.com/elastic/kibana/assets/2748376/16e5747a-546e-44b3-87e3-95428945cf63 --- .../instance_actions_menu/index.tsx | 8 +- .../instance_actions_menu/menu_sections.ts | 25 +++--- .../shared/links/observability_logs_link.ts | 89 ------------------- .../transaction_action_menu/sections.test.ts | 31 +++---- .../transaction_action_menu/sections.ts | 55 +++++------- .../transaction_action_menu.test.tsx | 33 ++++--- .../transaction_action_menu.tsx | 15 +--- .../apm_plugin/mock_apm_plugin_context.tsx | 12 ++- .../plugins/infra/common/locators/helpers.ts | 11 --- x-pack/plugins/infra/common/locators/index.ts | 8 +- .../infra/common/locators/locators.test.ts | 29 +++--- .../infra/common/locators/logs_locator.ts | 12 +-- .../common/locators/node_logs_locator.ts | 20 +++-- .../log_threshold/log_threshold_rule_type.tsx | 8 +- .../asset_details/tabs/logs/logs.tsx | 21 ++--- .../public/pages/link_to/redirect_to_logs.tsx | 9 +- .../pages/link_to/redirect_to_node_logs.tsx | 13 +-- .../tabs/logs/logs_link_to_stream.tsx | 7 +- .../components/waffle/node_context_menu.tsx | 8 +- x-pack/plugins/infra/public/plugin.ts | 16 ++-- x-pack/plugins/logs_shared/common/index.ts | 12 ++- .../common/locators/get_logs_locators.ts | 25 ++++++ .../logs_shared/common/locators/helpers.ts | 43 +++++++++ .../logs_shared/common/locators/index.ts | 10 +++ .../logs_shared/common/locators/infra.ts | 10 +++ .../common/locators/logs_locator.ts | 45 +++++++--- .../common/locators/node_logs_locator.ts | 43 +++++++-- .../common/locators/trace_logs_locator.ts | 47 ++++++++++ .../logs_shared/common/locators/types.ts | 39 +++++--- x-pack/plugins/logs_shared/kibana.jsonc | 3 +- x-pack/plugins/logs_shared/public/plugin.ts | 33 ++++++- x-pack/plugins/logs_shared/public/types.ts | 16 ++-- x-pack/plugins/logs_shared/tsconfig.json | 4 +- 33 files changed, 441 insertions(+), 319 deletions(-) delete mode 100644 x-pack/plugins/apm/public/components/shared/links/observability_logs_link.ts create mode 100644 x-pack/plugins/logs_shared/common/locators/get_logs_locators.ts create mode 100644 x-pack/plugins/logs_shared/common/locators/helpers.ts create mode 100644 x-pack/plugins/logs_shared/common/locators/infra.ts create mode 100644 x-pack/plugins/logs_shared/common/locators/trace_logs_locator.ts diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/index.tsx index 450c5ec061971..ff88e61fd5132 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/index.tsx @@ -19,10 +19,7 @@ import { AllDatasetsLocatorParams, ALL_DATASETS_LOCATOR_ID, } from '@kbn/deeplinks-observability/locators'; -import { - NODE_LOGS_LOCATOR_ID, - NodeLogsLocatorParams, -} from '@kbn/logs-shared-plugin/common'; +import { getLogsLocatorsFromUrlService } from '@kbn/logs-shared-plugin/common'; import { isJavaAgentName } from '../../../../../../common/agent_name'; import { SERVICE_NODE_NAME } from '../../../../../../common/es_fields/apm'; import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context'; @@ -63,8 +60,7 @@ export function InstanceActionsMenu({ const allDatasetsLocator = share.url.locators.get( ALL_DATASETS_LOCATOR_ID )!; - const nodeLogsLocator = - share.url.locators.get(NODE_LOGS_LOCATOR_ID)!; + const { nodeLogsLocator } = getLogsLocatorsFromUrlService(share.url); if (isPending(status)) { return ( diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/menu_sections.ts b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/menu_sections.ts index 8401cc6bbc744..3f258ea089a15 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/menu_sections.ts +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/instance_actions_menu/menu_sections.ts @@ -8,10 +8,10 @@ import { i18n } from '@kbn/i18n'; import { IBasePath } from '@kbn/core/public'; import moment from 'moment'; -import type { LocatorPublic } from '@kbn/share-plugin/public'; import { AllDatasetsLocatorParams } from '@kbn/deeplinks-observability/locators'; +import type { LocatorPublic } from '@kbn/share-plugin/public'; import { NodeLogsLocatorParams } from '@kbn/logs-shared-plugin/common'; -import { getNodeLogsHref } from '../../../../shared/links/observability_logs_link'; +import { findInventoryFields } from '@kbn/metrics-data-access-plugin/common'; import { APIReturnType } from '../../../../../services/rest/create_call_apm_api'; import { getInfraHref } from '../../../../shared/links/infra_link'; import { @@ -58,20 +58,17 @@ export function getMenuSections({ : undefined; const infraMetricsQuery = getInfraMetricsQuery(instanceDetails['@timestamp']); - const podLogsHref = getNodeLogsHref( - 'pod', - podId!, + const podLogsHref = nodeLogsLocator.getRedirectUrl({ + nodeField: findInventoryFields('pod').id, + nodeId: podId!, time, - allDatasetsLocator, - nodeLogsLocator - ); - const containerLogsHref = getNodeLogsHref( - 'container', - containerId!, + }); + + const containerLogsHref = nodeLogsLocator.getRedirectUrl({ + nodeField: findInventoryFields('container').id, + nodeId: containerId!, time, - allDatasetsLocator, - nodeLogsLocator - ); + }); const podActions: Action[] = [ { diff --git a/x-pack/plugins/apm/public/components/shared/links/observability_logs_link.ts b/x-pack/plugins/apm/public/components/shared/links/observability_logs_link.ts deleted file mode 100644 index 72ae29960942e..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/links/observability_logs_link.ts +++ /dev/null @@ -1,89 +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 { - LogsLocatorParams, - NodeLogsLocatorParams, -} from '@kbn/logs-shared-plugin/common'; -import { AllDatasetsLocatorParams } from '@kbn/deeplinks-observability/locators'; -import { LocatorPublic } from '@kbn/share-plugin/common'; -import moment from 'moment'; -import { DurationInputObject } from 'moment'; - -type NodeType = 'host' | 'pod' | 'container'; - -const NodeTypeMapping: Record = { - host: 'host.name', - container: 'container.id', - pod: 'kubernetes.pod.uid', -}; - -export const getNodeLogsHref = ( - nodeType: NodeType, - id: string, - time: number | undefined, - allDatasetsLocator: LocatorPublic, - infraNodeLocator?: LocatorPublic -): string => { - if (infraNodeLocator) - return infraNodeLocator?.getRedirectUrl({ - nodeId: id!, - nodeType, - time, - }); - - return allDatasetsLocator.getRedirectUrl({ - query: getNodeQuery(nodeType, id), - ...(time - ? { - timeRange: { - from: getTimeRangeStartFromTime(time), - to: getTimeRangeEndFromTime(time), - }, - } - : {}), - }); -}; - -export const getTraceLogsHref = ( - traceId: string, - time: number | undefined, - allDatasetsLocator: LocatorPublic, - infraLogsLocator: LocatorPublic -): string => { - const query = `trace.id:"${traceId}" OR (not trace.id:* AND "${traceId}")`; - - if (infraLogsLocator) - return infraLogsLocator.getRedirectUrl({ - filter: query, - time, - }); - - return allDatasetsLocator.getRedirectUrl({ - query: { language: 'kuery', query }, - ...(time - ? { - timeRange: { - from: getTimeRangeStartFromTime(time), - to: getTimeRangeEndFromTime(time), - }, - } - : {}), - }); -}; - -const getNodeQuery = (type: NodeType, id: string) => { - return { language: 'kuery', query: `${NodeTypeMapping[type]}: ${id}` }; -}; - -const defaultTimeRangeFromPositionOffset: DurationInputObject = { hours: 1 }; - -const getTimeRangeStartFromTime = (time: number): string => - moment(time).subtract(defaultTimeRangeFromPositionOffset).toISOString(); - -const getTimeRangeEndFromTime = (time: number): string => - moment(time).add(defaultTimeRangeFromPositionOffset).toISOString(); diff --git a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections.test.ts b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections.test.ts index 7d7a720f27cfc..dd1cfa389453f 100644 --- a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections.test.ts +++ b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections.test.ts @@ -7,11 +7,6 @@ import { createMemoryHistory } from 'history'; import { IBasePath } from '@kbn/core/public'; -import { LocatorPublic } from '@kbn/share-plugin/common'; -import { - LogsLocatorParams, - NodeLogsLocatorParams, -} from '@kbn/logs-shared-plugin/common'; import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { getSections } from './sections'; import { @@ -19,7 +14,7 @@ import { ApmRouter, } from '../../routing/apm_route_config'; import { - infraLocatorsMock, + logsLocatorsMock, observabilityLogExplorerLocatorsMock, } from '../../../context/apm_plugin/mock_apm_plugin_context'; @@ -30,11 +25,11 @@ const apmRouter = { } as ApmRouter; const { allDatasetsLocator } = observabilityLogExplorerLocatorsMock; -const { nodeLogsLocator, logsLocator } = infraLocatorsMock; +const { nodeLogsLocator, traceLogsLocator } = logsLocatorsMock; -const expectInfraLocatorsToBeCalled = () => { +const expectLogsLocatorsToBeCalled = () => { expect(nodeLogsLocator.getRedirectUrl).toBeCalledTimes(3); - expect(logsLocator.getRedirectUrl).toBeCalledTimes(1); + expect(traceLogsLocator.getRedirectUrl).toBeCalledTimes(1); }; describe('Transaction action menu', () => { @@ -70,9 +65,7 @@ describe('Transaction action menu', () => { location, apmRouter, allDatasetsLocator, - logsLocator: logsLocator as unknown as LocatorPublic, - nodeLogsLocator: - nodeLogsLocator as unknown as LocatorPublic, + logsLocators: logsLocatorsMock, infraLinksAvailable: false, rangeFrom: 'now-24h', rangeTo: 'now', @@ -121,7 +114,7 @@ describe('Transaction action menu', () => { }, ], ]); - expectInfraLocatorsToBeCalled(); + expectLogsLocatorsToBeCalled(); }); it('shows pod and required sections only', () => { @@ -138,10 +131,8 @@ describe('Transaction action menu', () => { basePath, location, apmRouter, - logsLocator: logsLocator as unknown as LocatorPublic, - nodeLogsLocator: - nodeLogsLocator as unknown as LocatorPublic, allDatasetsLocator, + logsLocators: logsLocatorsMock, infraLinksAvailable: true, rangeFrom: 'now-24h', rangeTo: 'now', @@ -209,7 +200,7 @@ describe('Transaction action menu', () => { }, ], ]); - expectInfraLocatorsToBeCalled(); + expectLogsLocatorsToBeCalled(); }); it('shows host and required sections only', () => { @@ -226,10 +217,8 @@ describe('Transaction action menu', () => { basePath, location, apmRouter, - logsLocator: logsLocator as unknown as LocatorPublic, - nodeLogsLocator: - nodeLogsLocator as unknown as LocatorPublic, allDatasetsLocator, + logsLocators: logsLocatorsMock, infraLinksAvailable: true, rangeFrom: 'now-24h', rangeTo: 'now', @@ -296,6 +285,6 @@ describe('Transaction action menu', () => { }, ], ]); - expectInfraLocatorsToBeCalled(); + expectLogsLocatorsToBeCalled(); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections.ts b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections.ts index 09f742ad1254e..398a657d06714 100644 --- a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections.ts +++ b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections.ts @@ -11,10 +11,8 @@ import { IBasePath } from '@kbn/core/public'; import { isEmpty, pickBy } from 'lodash'; import moment from 'moment'; import url from 'url'; -import { - LogsLocatorParams, - NodeLogsLocatorParams, -} from '@kbn/logs-shared-plugin/common'; +import type { getLogsLocatorsFromUrlService } from '@kbn/logs-shared-plugin/common'; +import { findInventoryFields } from '@kbn/metrics-data-access-plugin/common'; import { LocatorPublic } from '@kbn/share-plugin/common'; import { AllDatasetsLocatorParams } from '@kbn/deeplinks-observability/locators'; import type { ProfilingLocators } from '@kbn/observability-shared-plugin/public'; @@ -27,10 +25,6 @@ import { fromQuery } from '../links/url_helpers'; import { SectionRecord, getNonEmptySections, Action } from './sections_helper'; import { HOST_NAME, TRACE_ID } from '../../../../common/es_fields/apm'; import { ApmRouter } from '../../routing/apm_route_config'; -import { - getNodeLogsHref, - getTraceLogsHref, -} from '../links/observability_logs_link'; function getInfraMetricsQuery(transaction: Transaction) { const timestamp = new Date(transaction['@timestamp']).getTime(); @@ -53,8 +47,7 @@ export const getSections = ({ rangeTo, environment, allDatasetsLocator, - logsLocator, - nodeLogsLocator, + logsLocators, dataViewId, }: { transaction?: Transaction; @@ -67,8 +60,7 @@ export const getSections = ({ rangeTo: string; environment: Environment; allDatasetsLocator: LocatorPublic; - logsLocator: LocatorPublic; - nodeLogsLocator: LocatorPublic; + logsLocators: ReturnType; dataViewId?: string; }) => { if (!transaction) return []; @@ -95,33 +87,26 @@ export const getSections = ({ }); // Logs hrefs - const podLogsHref = getNodeLogsHref( - 'pod', - podId!, + const podLogsHref = logsLocators.nodeLogsLocator.getRedirectUrl({ + nodeField: findInventoryFields('pod').id, + nodeId: podId!, time, - allDatasetsLocator, - nodeLogsLocator - ); - const containerLogsHref = getNodeLogsHref( - 'container', - containerId!, + }); + const containerLogsHref = logsLocators.nodeLogsLocator.getRedirectUrl({ + nodeField: findInventoryFields('container').id, + nodeId: containerId!, time, - allDatasetsLocator, - nodeLogsLocator - ); - const hostLogsHref = getNodeLogsHref( - 'host', - hostName!, + }); + const hostLogsHref = logsLocators.nodeLogsLocator.getRedirectUrl({ + nodeField: findInventoryFields('host').id, + nodeId: hostName!, time, - allDatasetsLocator, - nodeLogsLocator - ); - const traceLogsHref = getTraceLogsHref( - transaction.trace.id!, + }); + + const traceLogsHref = logsLocators.traceLogsLocator.getRedirectUrl({ + traceId: transaction.trace.id!, time, - allDatasetsLocator, - logsLocator - ); + }); const podActions: Action[] = [ { diff --git a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/transaction_action_menu.test.tsx b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/transaction_action_menu.test.tsx index be78efeb870ee..ce9d81a52eb32 100644 --- a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/transaction_action_menu.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/transaction_action_menu.test.tsx @@ -13,13 +13,14 @@ import { License } from '@kbn/licensing-plugin/common/license'; import { LOGS_LOCATOR_ID, NODE_LOGS_LOCATOR_ID, + TRACE_LOGS_LOCATOR_ID, } from '@kbn/logs-shared-plugin/common'; import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { mockApmPluginContextValue, MockApmPluginContextWrapper, - infraLocatorsMock, + logsLocatorsMock, } from '../../../context/apm_plugin/mock_apm_plugin_context'; import { LicenseContext } from '../../../context/license/license_context'; import * as hooks from '../../../hooks/use_fetcher'; @@ -43,11 +44,15 @@ const apmContextMock = { locators: { get: (id: string) => { if (id === LOGS_LOCATOR_ID) { - return infraLocatorsMock.logsLocator; + return logsLocatorsMock.logsLocator; } if (id === NODE_LOGS_LOCATOR_ID) { - return infraLocatorsMock.nodeLogsLocator; + return logsLocatorsMock.nodeLogsLocator; + } + + if (id === TRACE_LOGS_LOCATOR_ID) { + return logsLocatorsMock.traceLogsLocator; } }, }, @@ -102,9 +107,9 @@ const renderTransaction = async (transaction: Record) => { return rendered; }; -const expectInfraLocatorsToBeCalled = () => { - expect(infraLocatorsMock.nodeLogsLocator.getRedirectUrl).toBeCalled(); - expect(infraLocatorsMock.logsLocator.getRedirectUrl).toBeCalled(); +const expectLogsLocatorsToBeCalled = () => { + expect(logsLocatorsMock.nodeLogsLocator.getRedirectUrl).toBeCalled(); + expect(logsLocatorsMock.traceLogsLocator.getRedirectUrl).toBeCalled(); }; let useAdHocApmDataViewSpy: jest.SpyInstance; @@ -144,10 +149,10 @@ describe('TransactionActionMenu ', () => { expect(findByText('View transaction in Discover')).not.toBeNull(); }); - it('should call infra locators getRedirectUrl function', async () => { + it('should call logs locators getRedirectUrl function', async () => { await renderTransaction(Transactions.transactionWithMinimalData); - expectInfraLocatorsToBeCalled(); + expectLogsLocatorsToBeCalled(); }); describe('when there is no pod id', () => { @@ -169,10 +174,10 @@ describe('TransactionActionMenu ', () => { }); describe('when there is a pod id', () => { - it('should call infra locators getRedirectUrl function', async () => { + it('should call logs locators getRedirectUrl function', async () => { await renderTransaction(Transactions.transactionWithKubernetesData); - expectInfraLocatorsToBeCalled(); + expectLogsLocatorsToBeCalled(); }); it('renders the pod metrics link', async () => { @@ -206,11 +211,11 @@ describe('TransactionActionMenu ', () => { }); }); - describe('should call infra locators getRedirectUrl function', () => { + describe('should call logs locators getRedirectUrl function', () => { it('renders the Container logs link', async () => { await renderTransaction(Transactions.transactionWithContainerData); - expectInfraLocatorsToBeCalled(); + expectLogsLocatorsToBeCalled(); }); it('renders the Container metrics link', async () => { @@ -245,10 +250,10 @@ describe('TransactionActionMenu ', () => { }); describe('when there is a hostname', () => { - it('should call infra locators getRedirectUrl function', async () => { + it('should call logs locators getRedirectUrl function', async () => { await renderTransaction(Transactions.transactionWithHostData); - expectInfraLocatorsToBeCalled(); + expectLogsLocatorsToBeCalled(); }); it('renders the Host metrics link', async () => { diff --git a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/transaction_action_menu.tsx b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/transaction_action_menu.tsx index 21cfea70c4e31..fe3cd90222ef4 100644 --- a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/transaction_action_menu.tsx +++ b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/transaction_action_menu.tsx @@ -25,13 +25,8 @@ import { AllDatasetsLocatorParams, ALL_DATASETS_LOCATOR_ID, } from '@kbn/deeplinks-observability/locators'; -import { - LOGS_LOCATOR_ID, - LogsLocatorParams, - NODE_LOGS_LOCATOR_ID, - NodeLogsLocatorParams, -} from '@kbn/logs-shared-plugin/common'; import type { ProfilingLocators } from '@kbn/observability-shared-plugin/public'; +import { getLogsLocatorsFromUrlService } from '@kbn/logs-shared-plugin/common'; import { useAnyOfApmParams } from '../../../hooks/use_apm_params'; import { ApmFeatureFlagName } from '../../../../common/apm_feature_flags'; import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; @@ -144,10 +139,7 @@ function ActionMenuSections({ const allDatasetsLocator = share.url.locators.get( ALL_DATASETS_LOCATOR_ID )!; - const logsLocator = - share.url.locators.get(LOGS_LOCATOR_ID)!; - const nodeLogsLocator = - share.url.locators.get(NODE_LOGS_LOCATOR_ID)!; + const logsLocators = getLogsLocatorsFromUrlService(share.url); const infraLinksAvailable = useApmFeatureFlag( ApmFeatureFlagName.InfraUiAvailable @@ -173,8 +165,7 @@ function ActionMenuSections({ rangeTo, environment, allDatasetsLocator, - logsLocator, - nodeLogsLocator, + logsLocators, dataViewId: dataView?.id, }); diff --git a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx index 61710babd1dac..f6f45a273de45 100644 --- a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx @@ -13,6 +13,11 @@ import { merge } from 'lodash'; import { coreMock } from '@kbn/core/public/mocks'; import { UrlService } from '@kbn/share-plugin/common/url_service'; import { createObservabilityRuleTypeRegistryMock } from '@kbn/observability-plugin/public'; +import { + LogsLocatorParams, + NodeLogsLocatorParams, + TraceLogsLocatorParams, +} from '@kbn/logs-shared-plugin/common'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; import { MlLocatorDefinition } from '@kbn/ml-plugin/public'; import { enableComparisonByDefault } from '@kbn/observability-plugin/public'; @@ -131,9 +136,10 @@ export const observabilityLogExplorerLocatorsMock = { singleDatasetLocator: sharePluginMock.createLocator(), }; -export const infraLocatorsMock = { - nodeLogsLocator: sharePluginMock.createLocator(), - logsLocator: sharePluginMock.createLocator(), +export const logsLocatorsMock = { + logsLocator: sharePluginMock.createLocator(), + nodeLogsLocator: sharePluginMock.createLocator(), + traceLogsLocator: sharePluginMock.createLocator(), }; const mockCorePlugins = { diff --git a/x-pack/plugins/infra/common/locators/helpers.ts b/x-pack/plugins/infra/common/locators/helpers.ts index 582499407bb40..d067ea15e7ebe 100644 --- a/x-pack/plugins/infra/common/locators/helpers.ts +++ b/x-pack/plugins/infra/common/locators/helpers.ts @@ -13,10 +13,8 @@ import { LogViewReference, ResolvedLogView, LogsLocatorParams, - NodeLogsLocatorParams, } from '@kbn/logs-shared-plugin/common'; import { flowRight } from 'lodash'; -import { findInventoryFields } from '@kbn/metrics-data-access-plugin/common'; import type { InfraClientCoreSetup } from '../../public/types'; import { MESSAGE_FIELD, TIMESTAMP_FIELD } from '../constants'; import type { TimeRange } from '../time'; @@ -33,15 +31,6 @@ interface LocationToDiscoverParams { logView?: LogViewReference; } -export const createNodeLogsQuery = (params: NodeLogsLocatorParams) => { - const { nodeType, nodeId, filter } = params; - - const nodeFilter = `${findInventoryFields(nodeType).id}: ${nodeId}`; - const query = filter ? `(${nodeFilter}) and (${filter})` : nodeFilter; - - return query; -}; - export const createSearchString = ({ time, timeRange, diff --git a/x-pack/plugins/infra/common/locators/index.ts b/x-pack/plugins/infra/common/locators/index.ts index d84c42a6dc21e..914334d2df97c 100644 --- a/x-pack/plugins/infra/common/locators/index.ts +++ b/x-pack/plugins/infra/common/locators/index.ts @@ -5,13 +5,13 @@ * 2.0. */ -import type { LogsLocator } from './logs_locator'; -import type { NodeLogsLocator } from './node_logs_locator'; +import type { InfraLogsLocator } from './logs_locator'; +import type { InfraNodeLogsLocator } from './node_logs_locator'; export * from './logs_locator'; export * from './node_logs_locator'; export interface InfraLocators { - logsLocator: LogsLocator; - nodeLogsLocator: NodeLogsLocator; + logsLocator?: InfraLogsLocator; + nodeLogsLocator?: InfraNodeLogsLocator; } diff --git a/x-pack/plugins/infra/common/locators/locators.test.ts b/x-pack/plugins/infra/common/locators/locators.test.ts index 607fd41b1bab6..7996380e3268b 100644 --- a/x-pack/plugins/infra/common/locators/locators.test.ts +++ b/x-pack/plugins/infra/common/locators/locators.test.ts @@ -6,8 +6,8 @@ */ import { v4 as uuidv4 } from 'uuid'; -import { LogsLocatorDefinition, LogsLocatorDependencies } from './logs_locator'; -import { NodeLogsLocatorDefinition } from './node_logs_locator'; +import { InfraLogsLocatorDefinition, InfraLogsLocatorDependencies } from './logs_locator'; +import { InfraNodeLogsLocatorDefinition } from './node_logs_locator'; import { coreMock } from '@kbn/core/public/mocks'; import { findInventoryFields } from '@kbn/metrics-data-access-plugin/common'; import moment from 'moment'; @@ -19,11 +19,11 @@ import { } from '@kbn/logs-shared-plugin/common'; const setupLogsLocator = async () => { - const deps: LogsLocatorDependencies = { + const deps: InfraLogsLocatorDependencies = { core: coreMock.createSetup(), }; - const logsLocator = new LogsLocatorDefinition(deps); - const nodeLogsLocator = new NodeLogsLocatorDefinition(deps); + const logsLocator = new InfraLogsLocatorDefinition(deps); + const nodeLogsLocator = new InfraNodeLogsLocatorDefinition(deps); return { logsLocator, @@ -33,8 +33,9 @@ const setupLogsLocator = async () => { describe('Infra Locators', () => { const APP_ID = 'logs'; - const nodeType = 'host'; const FILTER_QUERY = 'trace.id:1234'; + const nodeType = 'host'; + const nodeField = findInventoryFields(nodeType).id; const nodeId = uuidv4(); const time = 1550671089404; const from = 1676815089000; @@ -124,7 +125,7 @@ describe('Infra Locators', () => { it('should create a link to Node Logs with no state', async () => { const params: NodeLogsLocatorParams = { nodeId, - nodeType, + nodeField, time, }; const { nodeLogsLocator } = await setupLogsLocator(); @@ -139,7 +140,7 @@ describe('Infra Locators', () => { it('should allow specifying specific logPosition', async () => { const params: NodeLogsLocatorParams = { nodeId, - nodeType, + nodeField, time, }; const { nodeLogsLocator } = await setupLogsLocator(); @@ -152,7 +153,7 @@ describe('Infra Locators', () => { it('should allow specifying specific filter', async () => { const params: NodeLogsLocatorParams = { nodeId, - nodeType, + nodeField, time, filter: FILTER_QUERY, }; @@ -166,7 +167,7 @@ describe('Infra Locators', () => { it('should allow specifying specific view id', async () => { const params: NodeLogsLocatorParams = { nodeId, - nodeType, + nodeField, time, logView: { ...DEFAULT_LOG_VIEW, logViewId: 'test' }, }; @@ -180,7 +181,7 @@ describe('Infra Locators', () => { it('should allow specifying specific time range', async () => { const params: NodeLogsLocatorParams = { nodeId, - nodeType, + nodeField, time, from, to, @@ -196,7 +197,7 @@ describe('Infra Locators', () => { it('should return correct structured url', async () => { const params: NodeLogsLocatorParams = { nodeId, - nodeType, + nodeField, time, logView: DEFAULT_LOG_VIEW, filter: FILTER_QUERY, @@ -237,7 +238,7 @@ const constructLogPosition = (time: number = 1550671089404) => { }; const constructLogFilter = ({ - nodeType, + nodeField, nodeId, filter, timeRange, @@ -246,7 +247,7 @@ const constructLogFilter = ({ let finalFilter = filter || ''; if (nodeId) { - const nodeFilter = `${findInventoryFields(nodeType!).id}: ${nodeId}`; + const nodeFilter = `${nodeField}: ${nodeId}`; finalFilter = filter ? `(${nodeFilter}) and (${filter})` : nodeFilter; } diff --git a/x-pack/plugins/infra/common/locators/logs_locator.ts b/x-pack/plugins/infra/common/locators/logs_locator.ts index e481c0d53d390..952a6b4704aea 100644 --- a/x-pack/plugins/infra/common/locators/logs_locator.ts +++ b/x-pack/plugins/infra/common/locators/logs_locator.ts @@ -6,19 +6,19 @@ */ import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public'; -import { LOGS_LOCATOR_ID, LogsLocatorParams } from '@kbn/logs-shared-plugin/common'; +import { INFRA_LOGS_LOCATOR_ID, LogsLocatorParams } from '@kbn/logs-shared-plugin/common'; import type { InfraClientCoreSetup } from '../../public/types'; -export type LogsLocator = LocatorPublic; +export type InfraLogsLocator = LocatorPublic; -export interface LogsLocatorDependencies { +export interface InfraLogsLocatorDependencies { core: InfraClientCoreSetup; } -export class LogsLocatorDefinition implements LocatorDefinition { - public readonly id = LOGS_LOCATOR_ID; +export class InfraLogsLocatorDefinition implements LocatorDefinition { + public readonly id = INFRA_LOGS_LOCATOR_ID; - constructor(protected readonly deps: LogsLocatorDependencies) {} + constructor(protected readonly deps: InfraLogsLocatorDependencies) {} public readonly getLocation = async (params: LogsLocatorParams) => { const { createSearchString } = await import('./helpers'); diff --git a/x-pack/plugins/infra/common/locators/node_logs_locator.ts b/x-pack/plugins/infra/common/locators/node_logs_locator.ts index c8c53ec69292e..d5bfe4d7ac936 100644 --- a/x-pack/plugins/infra/common/locators/node_logs_locator.ts +++ b/x-pack/plugins/infra/common/locators/node_logs_locator.ts @@ -6,20 +6,24 @@ */ import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public'; -import { NODE_LOGS_LOCATOR_ID, NodeLogsLocatorParams } from '@kbn/logs-shared-plugin/common'; -import type { LogsLocatorDependencies } from './logs_locator'; +import { + INFRA_NODE_LOGS_LOCATOR_ID, + NodeLogsLocatorParams, + createNodeLogsQuery, +} from '@kbn/logs-shared-plugin/common'; +import type { InfraLogsLocatorDependencies } from './logs_locator'; -export type NodeLogsLocator = LocatorPublic; +export type InfraNodeLogsLocator = LocatorPublic; -export type NodeLogsLocatorDependencies = LogsLocatorDependencies; +export type InfraNodeLogsLocatorDependencies = InfraLogsLocatorDependencies; -export class NodeLogsLocatorDefinition implements LocatorDefinition { - public readonly id = NODE_LOGS_LOCATOR_ID; +export class InfraNodeLogsLocatorDefinition implements LocatorDefinition { + public readonly id = INFRA_NODE_LOGS_LOCATOR_ID; - constructor(protected readonly deps: NodeLogsLocatorDependencies) {} + constructor(protected readonly deps: InfraNodeLogsLocatorDependencies) {} public readonly getLocation = async (params: NodeLogsLocatorParams) => { - const { createNodeLogsQuery, createSearchString } = await import('./helpers'); + const { createSearchString } = await import('./helpers'); const query = createNodeLogsQuery(params); diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.tsx index 1f2e8731a85df..f87558de360b7 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.tsx @@ -6,9 +6,9 @@ */ import { i18n } from '@kbn/i18n'; +import { UrlService } from '@kbn/share-plugin/common/url_service'; +import { getLogsLocatorsFromUrlService } from '@kbn/logs-shared-plugin/common'; import { ObservabilityRuleTypeModel } from '@kbn/observability-plugin/public'; -import type { LocatorPublic } from '@kbn/share-plugin/public'; -import type { LogsLocatorParams } from '@kbn/logs-shared-plugin/common'; import { LOG_DOCUMENT_COUNT_RULE_TYPE_ID, PartialRuleParams, @@ -44,7 +44,7 @@ const logThresholdDefaultRecoveryMessage = i18n.translate( export function createLogThresholdRuleType( core: InfraClientCoreSetup, - logsLocator: LocatorPublic + urlService: UrlService ): ObservabilityRuleTypeModel { const ruleParamsExpression = createLazyComponentWithKibanaContext( core, @@ -56,6 +56,8 @@ export function createLogThresholdRuleType( () => import('./components/alert_details_app_section') ); + const { logsLocator } = getLogsLocatorsFromUrlService(urlService); + return { id: LOG_DOCUMENT_COUNT_RULE_TYPE_ID, description: i18n.translate('xpack.infra.logs.alertFlyout.alertDescription', { diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx index 1999c6604f553..cec2bb6f5e3e5 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx @@ -12,7 +12,11 @@ import { i18n } from '@kbn/i18n'; import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; import { LogStream } from '@kbn/logs-shared-plugin/public'; -import { DEFAULT_LOG_VIEW, LogViewReference } from '@kbn/logs-shared-plugin/common'; +import { + DEFAULT_LOG_VIEW, + getLogsLocatorsFromUrlService, + LogViewReference, +} from '@kbn/logs-shared-plugin/common'; import { findInventoryFields } from '@kbn/metrics-data-access-plugin/common'; import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; import { InfraLoadingPanel } from '../../../loading'; @@ -34,7 +38,7 @@ export const Logs = () => { const { loading: logViewLoading, reference: logViewReference } = logs ?? {}; const { services } = useKibanaContextForPlugin(); - const { locators } = services; + const { nodeLogsLocator } = getLogsLocatorsFromUrlService(services.share.url); const [textQuery, setTextQuery] = useState(urlState?.logsSearch ?? ''); const [textQueryDebounced, setTextQueryDebounced] = useState(urlState?.logsSearch ?? ''); @@ -77,21 +81,14 @@ export const Logs = () => { ); const logsUrl = useMemo(() => { - return locators.nodeLogsLocator.getRedirectUrl({ - nodeType: asset.type, + return nodeLogsLocator.getRedirectUrl({ + nodeField: findInventoryFields(asset.type).id, nodeId: asset.name, time: state.startTimestamp, filter: textQueryDebounced, logView, }); - }, [ - locators.nodeLogsLocator, - asset.name, - asset.type, - state.startTimestamp, - textQueryDebounced, - logView, - ]); + }, [nodeLogsLocator, asset.name, asset.type, state.startTimestamp, textQueryDebounced, logView]); return ( diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.tsx index 663df4c0f4d1a..16f13171f7106 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.tsx @@ -7,7 +7,7 @@ import { useEffect } from 'react'; import { useLocation, useParams } from 'react-router-dom'; -import { DEFAULT_LOG_VIEW } from '@kbn/logs-shared-plugin/common'; +import { DEFAULT_LOG_VIEW, getLogsLocatorsFromUrlService } from '@kbn/logs-shared-plugin/common'; import { getFilterFromLocation, getTimeFromLocation } from './query_params'; import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; @@ -16,14 +16,15 @@ export const RedirectToLogs = () => { const location = useLocation(); const { - services: { locators }, + services: { share }, } = useKibanaContextForPlugin(); + const { logsLocator } = getLogsLocatorsFromUrlService(share.url); const filter = getFilterFromLocation(location); const time = getTimeFromLocation(location); useEffect(() => { - locators.logsLocator.navigate( + logsLocator.navigate( { time, filter, @@ -31,7 +32,7 @@ export const RedirectToLogs = () => { }, { replace: true } ); - }, [filter, locators.logsLocator, logViewId, time]); + }, [filter, logsLocator, logViewId, time]); return null; }; diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx index 0eade78931ed0..0be958882cedb 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx @@ -7,8 +7,8 @@ import { useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { DEFAULT_LOG_VIEW } from '@kbn/logs-shared-plugin/common'; -import { InventoryItemType } from '@kbn/metrics-data-access-plugin/common'; +import { DEFAULT_LOG_VIEW, getLogsLocatorsFromUrlService } from '@kbn/logs-shared-plugin/common'; +import { findInventoryFields, InventoryItemType } from '@kbn/metrics-data-access-plugin/common'; import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; import { getFilterFromLocation, getTimeFromLocation } from './query_params'; @@ -26,24 +26,25 @@ export const RedirectToNodeLogs = ({ location, }: RedirectToNodeLogsType) => { const { - services: { locators }, + services: { share }, } = useKibanaContextForPlugin(); + const { nodeLogsLocator } = getLogsLocatorsFromUrlService(share.url); const filter = getFilterFromLocation(location); const time = getTimeFromLocation(location); useEffect(() => { - locators.nodeLogsLocator.navigate( + nodeLogsLocator.navigate( { + nodeField: findInventoryFields(nodeType).id, nodeId, - nodeType, time, filter, logView: { type: 'log-view-reference', logViewId }, }, { replace: true } ); - }, [filter, locators.nodeLogsLocator, logViewId, nodeId, nodeType, time]); + }, [filter, nodeLogsLocator, logViewId, nodeId, nodeType, time]); return null; }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_link_to_stream.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_link_to_stream.tsx index cd2537418e46c..d7fbd4dbf1be9 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_link_to_stream.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_link_to_stream.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; import { EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { LogViewReference } from '@kbn/logs-shared-plugin/common'; +import { getLogsLocatorsFromUrlService, LogViewReference } from '@kbn/logs-shared-plugin/common'; import { useKibanaContextForPlugin } from '../../../../../../hooks/use_kibana'; interface LogsLinkToStreamProps { @@ -20,12 +20,13 @@ interface LogsLinkToStreamProps { export const LogsLinkToStream = ({ startTime, endTime, query, logView }: LogsLinkToStreamProps) => { const { services } = useKibanaContextForPlugin(); - const { locators } = services; + const { share } = services; + const { logsLocator } = getLogsLocatorsFromUrlService(share.url); return ( = withTheme const inventoryModel = findInventoryModel(nodeType); const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000; const { services } = useKibanaContextForPlugin(); - const { application, share, locators } = services; + const { application, share } = services; + const { nodeLogsLocator } = getLogsLocatorsFromUrlService(share.url); const uiCapabilities = application?.capabilities; // Due to the changing nature of the fields between APM and this UI, // We need to have some exceptions until 7.0 & ECS is finalized. Reference @@ -109,8 +111,8 @@ export const NodeContextMenu: React.FC = withTheme defaultMessage: '{inventoryName} logs', values: { inventoryName: inventoryModel.singularDisplayName }, }), - href: locators.nodeLogsLocator.getRedirectUrl({ - nodeType, + href: nodeLogsLocator.getRedirectUrl({ + nodeField: findInventoryFields(nodeType).id, nodeId: node.id, time: currentTime, }), diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 7d717cf9057e4..f89d99a43a57b 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -27,8 +27,8 @@ import { LOG_STREAM_EMBEDDABLE } from './components/log_stream/log_stream_embedd import { LogStreamEmbeddableFactoryDefinition } from './components/log_stream/log_stream_embeddable_factory'; import { type InfraLocators, - LogsLocatorDefinition, - NodeLogsLocatorDefinition, + InfraLogsLocatorDefinition, + InfraNodeLogsLocatorDefinition, } from '../common/locators'; import { createMetricsFetchData, createMetricsHasData } from './metrics_overview_fetchers'; import { registerFeatures } from './register_feature'; @@ -179,13 +179,15 @@ export class Plugin implements InfraClientPluginClass { ); // Register Locators - const logsLocator = pluginsSetup.share.url.locators.create(new LogsLocatorDefinition({ core })); - const nodeLogsLocator = pluginsSetup.share.url.locators.create( - new NodeLogsLocatorDefinition({ core }) - ); + const logsLocator = this.config.featureFlags.logsUIEnabled + ? pluginsSetup.share.url.locators.create(new InfraLogsLocatorDefinition({ core })) + : undefined; + const nodeLogsLocator = this.config.featureFlags.logsUIEnabled + ? pluginsSetup.share.url.locators.create(new InfraNodeLogsLocatorDefinition({ core })) + : undefined; pluginsSetup.observability.observabilityRuleTypeRegistry.register( - createLogThresholdRuleType(core, logsLocator) + createLogThresholdRuleType(core, pluginsSetup.share.url) ); if (this.config.featureFlags.logsUIEnabled) { diff --git a/x-pack/plugins/logs_shared/common/index.ts b/x-pack/plugins/logs_shared/common/index.ts index 99fd7c1166863..f6b1e9ea27e43 100644 --- a/x-pack/plugins/logs_shared/common/index.ts +++ b/x-pack/plugins/logs_shared/common/index.ts @@ -60,5 +60,13 @@ export { } from './http_api'; // Locators -export { LOGS_LOCATOR_ID, NODE_LOGS_LOCATOR_ID } from './locators'; -export type { LogsLocatorParams, NodeLogsLocatorParams } from './locators'; +export { + LOGS_LOCATOR_ID, + TRACE_LOGS_LOCATOR_ID, + NODE_LOGS_LOCATOR_ID, + INFRA_LOGS_LOCATOR_ID, + INFRA_NODE_LOGS_LOCATOR_ID, + getLogsLocatorsFromUrlService, +} from './locators'; +export type { LogsLocatorParams, NodeLogsLocatorParams, TraceLogsLocatorParams } from './locators'; +export { createNodeLogsQuery } from './locators/helpers'; diff --git a/x-pack/plugins/logs_shared/common/locators/get_logs_locators.ts b/x-pack/plugins/logs_shared/common/locators/get_logs_locators.ts new file mode 100644 index 0000000000000..5c403c2bcb5b0 --- /dev/null +++ b/x-pack/plugins/logs_shared/common/locators/get_logs_locators.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UrlService } from '@kbn/share-plugin/common/url_service'; + +import { LogsLocatorParams, NodeLogsLocatorParams, TraceLogsLocatorParams } from './types'; +import { LOGS_LOCATOR_ID } from './logs_locator'; +import { NODE_LOGS_LOCATOR_ID } from './node_logs_locator'; +import { TRACE_LOGS_LOCATOR_ID } from './trace_logs_locator'; + +export const getLogsLocatorsFromUrlService = (urlService: UrlService) => { + const logsLocator = urlService.locators.get(LOGS_LOCATOR_ID)!; + const nodeLogsLocator = urlService.locators.get(NODE_LOGS_LOCATOR_ID)!; + const traceLogsLocator = urlService.locators.get(TRACE_LOGS_LOCATOR_ID)!; + + return { + logsLocator, + traceLogsLocator, + nodeLogsLocator, + }; +}; diff --git a/x-pack/plugins/logs_shared/common/locators/helpers.ts b/x-pack/plugins/logs_shared/common/locators/helpers.ts new file mode 100644 index 0000000000000..ae25c8abb3c18 --- /dev/null +++ b/x-pack/plugins/logs_shared/common/locators/helpers.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment, { DurationInputObject } from 'moment'; +import { LogsLocatorParams, NodeLogsLocatorParams, TraceLogsLocatorParams } from './types'; + +export const getLogsQuery = (params: LogsLocatorParams) => { + const { filter } = params; + + return filter ? { language: 'kuery', query: filter } : undefined; +}; + +export const createNodeLogsQuery = (params: NodeLogsLocatorParams) => { + const { nodeField, nodeId, filter } = params; + + const nodeFilter = `${nodeField}: ${nodeId}`; + return filter ? `(${nodeFilter}) and (${filter})` : nodeFilter; +}; + +export const getNodeQuery = (params: NodeLogsLocatorParams) => { + return { language: 'kuery', query: createNodeLogsQuery(params) }; +}; + +export const getTraceQuery = (params: TraceLogsLocatorParams) => { + const { traceId, filter } = params; + + const traceFilter = `trace.id:"${traceId}" OR (not trace.id:* AND "${traceId}")`; + const query = filter ? `(${traceFilter}) and (${filter})` : traceFilter; + + return { language: 'kuery', query }; +}; + +const defaultTimeRangeFromPositionOffset: DurationInputObject = { hours: 1 }; + +export const getTimeRangeStartFromTime = (time: number): string => + moment(time).subtract(defaultTimeRangeFromPositionOffset).toISOString(); + +export const getTimeRangeEndFromTime = (time: number): string => + moment(time).add(defaultTimeRangeFromPositionOffset).toISOString(); diff --git a/x-pack/plugins/logs_shared/common/locators/index.ts b/x-pack/plugins/logs_shared/common/locators/index.ts index d680977f29f89..2cbe5cc2d6ba3 100644 --- a/x-pack/plugins/logs_shared/common/locators/index.ts +++ b/x-pack/plugins/logs_shared/common/locators/index.ts @@ -6,4 +6,14 @@ */ export * from './logs_locator'; +export * from './trace_logs_locator'; export * from './node_logs_locator'; +export * from './infra'; +export * from './get_logs_locators'; + +export type { + LogsSharedLocators, + LogsLocatorParams, + NodeLogsLocatorParams, + TraceLogsLocatorParams, +} from './types'; diff --git a/x-pack/plugins/logs_shared/common/locators/infra.ts b/x-pack/plugins/logs_shared/common/locators/infra.ts new file mode 100644 index 0000000000000..c9351c375d03f --- /dev/null +++ b/x-pack/plugins/logs_shared/common/locators/infra.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const INFRA_LOGS_LOCATOR_ID = 'INFRA_LOGS_LOCATOR'; + +export const INFRA_NODE_LOGS_LOCATOR_ID = 'INFRA_NODE_LOGS_LOCATOR'; diff --git a/x-pack/plugins/logs_shared/common/locators/logs_locator.ts b/x-pack/plugins/logs_shared/common/locators/logs_locator.ts index bfd53276ab3e9..2ebb343e10bbc 100644 --- a/x-pack/plugins/logs_shared/common/locators/logs_locator.ts +++ b/x-pack/plugins/logs_shared/common/locators/logs_locator.ts @@ -5,19 +5,40 @@ * 2.0. */ -import { SerializableRecord } from '@kbn/utility-types'; -import type { TimeRange } from './time_range'; -import type { LogViewReference } from '../log_views/types'; +import { ALL_DATASETS_LOCATOR_ID, AllDatasetsLocatorParams } from '@kbn/deeplinks-observability'; +import { LocatorDefinition } from '@kbn/share-plugin/common'; +import { LocatorClient } from '@kbn/share-plugin/common/url_service'; + +import { INFRA_LOGS_LOCATOR_ID } from './infra'; +import { LogsLocatorParams } from './types'; +import { getLogsQuery, getTimeRangeEndFromTime, getTimeRangeStartFromTime } from './helpers'; export const LOGS_LOCATOR_ID = 'LOGS_LOCATOR'; -export interface LogsLocatorParams extends SerializableRecord { - /** Defines log position */ - time?: number; - /** - * Optionally set the time range in the time picker. - */ - timeRange?: TimeRange; - filter?: string; - logView?: LogViewReference; +export class LogsLocatorDefinition implements LocatorDefinition { + public readonly id = LOGS_LOCATOR_ID; + + constructor(private readonly locators: LocatorClient) {} + + public readonly getLocation = async (params: LogsLocatorParams) => { + const infraLogsLocator = this.locators.get(INFRA_LOGS_LOCATOR_ID); + if (infraLogsLocator) { + return infraLogsLocator.getLocation(params); + } + + const allDatasetsLocator = + this.locators.get(ALL_DATASETS_LOCATOR_ID)!; + const { time } = params; + return allDatasetsLocator.getLocation({ + query: getLogsQuery(params), + ...(time + ? { + timeRange: { + from: getTimeRangeStartFromTime(time), + to: getTimeRangeEndFromTime(time), + }, + } + : {}), + }); + }; } diff --git a/x-pack/plugins/logs_shared/common/locators/node_logs_locator.ts b/x-pack/plugins/logs_shared/common/locators/node_logs_locator.ts index 188c4d1e5a966..e5288630334b8 100644 --- a/x-pack/plugins/logs_shared/common/locators/node_logs_locator.ts +++ b/x-pack/plugins/logs_shared/common/locators/node_logs_locator.ts @@ -5,12 +5,45 @@ * 2.0. */ -import type { InventoryItemType } from './types'; -import type { LogsLocatorParams } from './logs_locator'; +import { + AllDatasetsLocatorParams, + ALL_DATASETS_LOCATOR_ID, +} from '@kbn/deeplinks-observability/locators'; +import { LocatorClient, LocatorDefinition } from '@kbn/share-plugin/common/url_service'; + +import { NodeLogsLocatorParams } from './types'; +import { INFRA_NODE_LOGS_LOCATOR_ID } from './infra'; +import { getNodeQuery, getTimeRangeStartFromTime, getTimeRangeEndFromTime } from './helpers'; export const NODE_LOGS_LOCATOR_ID = 'NODE_LOGS_LOCATOR'; -export interface NodeLogsLocatorParams extends LogsLocatorParams { - nodeId: string; - nodeType: InventoryItemType; +export class NodeLogsLocatorDefinition implements LocatorDefinition { + public readonly id = NODE_LOGS_LOCATOR_ID; + + constructor(private readonly locators: LocatorClient) {} + + public readonly getLocation = async (params: NodeLogsLocatorParams) => { + const infraNodeLogsLocator = this.locators.get( + INFRA_NODE_LOGS_LOCATOR_ID + ); + + if (infraNodeLogsLocator) { + return infraNodeLogsLocator.getLocation(params); + } + + const allDatasetsLocator = + this.locators.get(ALL_DATASETS_LOCATOR_ID)!; + const { time } = params; + return allDatasetsLocator.getLocation({ + query: getNodeQuery(params), + ...(time + ? { + timeRange: { + from: getTimeRangeStartFromTime(time), + to: getTimeRangeEndFromTime(time), + }, + } + : {}), + }); + }; } diff --git a/x-pack/plugins/logs_shared/common/locators/trace_logs_locator.ts b/x-pack/plugins/logs_shared/common/locators/trace_logs_locator.ts new file mode 100644 index 0000000000000..a62155aaaf4d1 --- /dev/null +++ b/x-pack/plugins/logs_shared/common/locators/trace_logs_locator.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ALL_DATASETS_LOCATOR_ID, AllDatasetsLocatorParams } from '@kbn/deeplinks-observability'; +import { LocatorDefinition } from '@kbn/share-plugin/common'; +import { LocatorClient } from '@kbn/share-plugin/common/url_service'; +import { INFRA_LOGS_LOCATOR_ID } from './infra'; +import { LogsLocatorParams, TraceLogsLocatorParams } from './types'; + +import { getTraceQuery, getTimeRangeEndFromTime, getTimeRangeStartFromTime } from './helpers'; + +export const TRACE_LOGS_LOCATOR_ID = 'TRACE_LOGS_LOCATOR'; + +export class TraceLogsLocatorDefinition implements LocatorDefinition { + public readonly id = TRACE_LOGS_LOCATOR_ID; + + constructor(private readonly locators: LocatorClient) {} + + public readonly getLocation = async (params: TraceLogsLocatorParams) => { + const infraLogsLocator = this.locators.get(INFRA_LOGS_LOCATOR_ID); + if (infraLogsLocator) { + return infraLogsLocator.getLocation({ + ...params, + filter: getTraceQuery(params).query, + }); + } + + const { time } = params; + const allDatasetsLocator = + this.locators.get(ALL_DATASETS_LOCATOR_ID)!; + return allDatasetsLocator.getLocation({ + query: getTraceQuery(params), + ...(time + ? { + timeRange: { + from: getTimeRangeStartFromTime(time), + to: getTimeRangeEndFromTime(time), + }, + } + : {}), + }); + }; +} diff --git a/x-pack/plugins/logs_shared/common/locators/types.ts b/x-pack/plugins/logs_shared/common/locators/types.ts index af6ec963098a0..07c50590b3efb 100644 --- a/x-pack/plugins/logs_shared/common/locators/types.ts +++ b/x-pack/plugins/logs_shared/common/locators/types.ts @@ -5,16 +5,33 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { SerializableRecord } from '@kbn/utility-types'; +import { LocatorPublic } from '@kbn/share-plugin/common'; +import { LogViewReference } from '../log_views/types'; +import { TimeRange } from './time_range'; -export const ItemTypeRT = rt.keyof({ - host: null, - pod: null, - container: null, - awsEC2: null, - awsS3: null, - awsSQS: null, - awsRDS: null, -}); +export interface LogsLocatorParams extends SerializableRecord { + /** Defines log position */ + time?: number; + /** + * Optionally set the time range in the time picker. + */ + timeRange?: TimeRange; + filter?: string; + logView?: LogViewReference; +} -export type InventoryItemType = rt.TypeOf; +export interface TraceLogsLocatorParams extends LogsLocatorParams { + traceId: string; +} + +export interface NodeLogsLocatorParams extends LogsLocatorParams { + nodeField: string; + nodeId: string; +} + +export interface LogsSharedLocators { + logsLocator: LocatorPublic; + nodeLogsLocator: LocatorPublic; + traceLogsLocator: LocatorPublic; +} diff --git a/x-pack/plugins/logs_shared/kibana.jsonc b/x-pack/plugins/logs_shared/kibana.jsonc index b78503b140a71..fc8dcf0e64d96 100644 --- a/x-pack/plugins/logs_shared/kibana.jsonc +++ b/x-pack/plugins/logs_shared/kibana.jsonc @@ -13,7 +13,8 @@ "dataViews", "usageCollection", "observabilityShared", - "observabilityAIAssistant" + "observabilityAIAssistant", + "share" ], "requiredBundles": ["kibanaUtils", "kibanaReact"], "extraPublicDirs": ["common"] diff --git a/x-pack/plugins/logs_shared/public/plugin.ts b/x-pack/plugins/logs_shared/public/plugin.ts index 092e95570db7f..1d6063c34eed3 100644 --- a/x-pack/plugins/logs_shared/public/plugin.ts +++ b/x-pack/plugins/logs_shared/public/plugin.ts @@ -6,9 +6,19 @@ */ import { CoreStart } from '@kbn/core/public'; +import { + LogsLocatorDefinition, + NodeLogsLocatorDefinition, + TraceLogsLocatorDefinition, +} from '../common/locators'; import { createLogAIAssistant } from './components/log_ai_assistant'; import { LogViewsService } from './services/log_views'; -import { LogsSharedClientPluginClass, LogsSharedClientStartDeps } from './types'; +import { + LogsSharedClientCoreSetup, + LogsSharedClientPluginClass, + LogsSharedClientSetupDeps, + LogsSharedClientStartDeps, +} from './types'; export class LogsSharedPlugin implements LogsSharedClientPluginClass { private logViews: LogViewsService; @@ -17,10 +27,27 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass { this.logViews = new LogViewsService(); } - public setup() { + public setup(_: LogsSharedClientCoreSetup, pluginsSetup: LogsSharedClientSetupDeps) { const logViews = this.logViews.setup(); - return { logViews }; + const logsLocator = pluginsSetup.share.url.locators.create( + new LogsLocatorDefinition(pluginsSetup.share.url.locators) + ); + const nodeLogsLocator = pluginsSetup.share.url.locators.create( + new NodeLogsLocatorDefinition(pluginsSetup.share.url.locators) + ); + + const traceLogsLocator = pluginsSetup.share.url.locators.create( + new TraceLogsLocatorDefinition(pluginsSetup.share.url.locators) + ); + + const locators = { + logsLocator, + nodeLogsLocator, + traceLogsLocator, + }; + + return { logViews, locators }; } public start(core: CoreStart, plugins: LogsSharedClientStartDeps) { diff --git a/x-pack/plugins/logs_shared/public/types.ts b/x-pack/plugins/logs_shared/public/types.ts index 2a2e9c1cf742d..da5d2ec49e622 100644 --- a/x-pack/plugins/logs_shared/public/types.ts +++ b/x-pack/plugins/logs_shared/public/types.ts @@ -5,18 +5,14 @@ * 2.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 { CoreSetup, CoreStart, Plugin as PluginClass } from '@kbn/core/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { ObservabilityAIAssistantPluginStart } from '@kbn/observability-ai-assistant-plugin/public'; +import { SharePluginSetup } from '@kbn/share-plugin/public'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; + +import { LogsSharedLocators } from '../common/locators'; import type { LogAIAssistantProps } from './components/log_ai_assistant/log_ai_assistant'; // import type { OsqueryPluginStart } from '../../osquery/public'; import { LogViewsServiceSetup, LogViewsServiceStart } from './services/log_views'; @@ -24,6 +20,7 @@ import { LogViewsServiceSetup, LogViewsServiceStart } from './services/log_views // Our own setup and start contract values export interface LogsSharedClientSetupExports { logViews: LogViewsServiceSetup; + locators: LogsSharedLocators; } export interface LogsSharedClientStartExports { @@ -31,8 +28,9 @@ export interface LogsSharedClientStartExports { LogAIAssistant: (props: Omit) => JSX.Element; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface LogsSharedClientSetupDeps {} +export interface LogsSharedClientSetupDeps { + share: SharePluginSetup; +} export interface LogsSharedClientStartDeps { data: DataPublicPluginStart; diff --git a/x-pack/plugins/logs_shared/tsconfig.json b/x-pack/plugins/logs_shared/tsconfig.json index b7c17b46c9a62..8d06a68a70873 100644 --- a/x-pack/plugins/logs_shared/tsconfig.json +++ b/x-pack/plugins/logs_shared/tsconfig.json @@ -26,6 +26,8 @@ "@kbn/datemath", "@kbn/core-http-browser", "@kbn/ui-actions-plugin", - "@kbn/observability-ai-assistant-plugin" + "@kbn/observability-ai-assistant-plugin", + "@kbn/deeplinks-observability", + "@kbn/share-plugin" ] } From f6060852109410ba028e21d679206147cbd1a011 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Wed, 10 Jan 2024 10:34:00 -0500 Subject: [PATCH 16/26] [SO] remove some plugin dependencies (#174282) ## Summary Removes some `savedObject` plugin dependencies. ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- examples/portable_dashboards_example/kibana.jsonc | 1 - src/plugins/controls/kibana.jsonc | 1 - src/plugins/dashboard/public/plugin.tsx | 2 -- src/plugins/visualizations/kibana.jsonc | 4 ++-- x-pack/plugins/maps/kibana.jsonc | 4 ++-- 5 files changed, 4 insertions(+), 8 deletions(-) diff --git a/examples/portable_dashboards_example/kibana.jsonc b/examples/portable_dashboards_example/kibana.jsonc index e4016f2b1c01e..d6fd1a82e1df5 100644 --- a/examples/portable_dashboards_example/kibana.jsonc +++ b/examples/portable_dashboards_example/kibana.jsonc @@ -16,7 +16,6 @@ "dashboard", "embeddable", "navigation", - "savedObjects", "unifiedSearch", "developerExamples", "embeddableExamples" diff --git a/src/plugins/controls/kibana.jsonc b/src/plugins/controls/kibana.jsonc index 0defa22bd351f..14718f533a8f6 100644 --- a/src/plugins/controls/kibana.jsonc +++ b/src/plugins/controls/kibana.jsonc @@ -9,7 +9,6 @@ "browser": true, "requiredPlugins": [ "presentationUtil", - "savedObjects", "kibanaReact", "expressions", "embeddable", diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 4c75362485b6a..8fb1260eb327d 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -34,7 +34,6 @@ import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/common'; import { createKbnUrlTracker } from '@kbn/kibana-utils-plugin/public'; -import type { SavedObjectsStart } from '@kbn/saved-objects-plugin/public'; import type { VisualizationsStart } from '@kbn/visualizations-plugin/public'; import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; @@ -96,7 +95,6 @@ export interface DashboardStartDependencies { inspector: InspectorStartContract; navigation: NavigationPublicPluginStart; presentationUtil: PresentationUtilPluginStart; - savedObjects: SavedObjectsStart; contentManagement: ContentManagementPublicStart; savedObjectsManagement: SavedObjectsManagementPluginStart; savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart; diff --git a/src/plugins/visualizations/kibana.jsonc b/src/plugins/visualizations/kibana.jsonc index 69caa82b50030..9d1c6c1da0e58 100644 --- a/src/plugins/visualizations/kibana.jsonc +++ b/src/plugins/visualizations/kibana.jsonc @@ -17,7 +17,6 @@ "navigation", "embeddable", "inspector", - "savedObjects", "screenshotMode", "presentationUtil", "dataViews", @@ -40,7 +39,8 @@ "requiredBundles": [ "kibanaUtils", "kibanaReact", - "charts" + "charts", + "savedObjects", ], "extraPublicDirs": [ "common/constants", diff --git a/x-pack/plugins/maps/kibana.jsonc b/x-pack/plugins/maps/kibana.jsonc index b6bf08329fb44..fb0472100e1c2 100644 --- a/x-pack/plugins/maps/kibana.jsonc +++ b/x-pack/plugins/maps/kibana.jsonc @@ -27,7 +27,6 @@ "dashboard", "embeddable", "mapsEms", - "savedObjects", "share", "presentationUtil", "contentManagement" @@ -50,7 +49,8 @@ "usageCollection", "unifiedSearch", "fieldFormats", - "textBasedLanguages" + "textBasedLanguages", + "savedObjects", ], "extraPublicDirs": [ "common" From 92d6d0eb520ab23740d981a272781c3e514f6ede Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Wed, 10 Jan 2024 10:46:46 -0500 Subject: [PATCH 17/26] [Markdown] Expand Height to Fill Vertical Space (#174276) Makes markdown expand to fill height. originally authored by @ananta in https://github.com/elastic/kibana/pull/172314 --- src/plugins/vis_type_markdown/public/markdown_vis.scss | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/plugins/vis_type_markdown/public/markdown_vis.scss b/src/plugins/vis_type_markdown/public/markdown_vis.scss index 97cfc4b151c77..923888db5652f 100644 --- a/src/plugins/vis_type_markdown/public/markdown_vis.scss +++ b/src/plugins/vis_type_markdown/public/markdown_vis.scss @@ -16,7 +16,10 @@ flex-grow: 1; } - .mkdEditor { - height: 100%; + .mkdEditor, + .euiFormControlLayout__childrenWrapper, + .euiFormControlLayout--euiTextArea, + .visEditor--markdown__textarea { + height: 100% } } From 143c521b3f4fc46cef96ad32c738fcf73c0c3ff4 Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Wed, 10 Jan 2024 17:07:05 +0100 Subject: [PATCH 18/26] [Search] Fix endpoints header persisting beyond search pages (#174627) ## Summary Unmounting the header action was failing in some cases and throwing console errors in all cases. This caused the header component to persist onto pages where it shouldn't be. Instead of explicitly unmounting, which should be done by the parent component of our non-root element, we render an empty element on unmount, which removes the header action in all unmount cases. --- x-pack/plugins/enterprise_search/public/applications/index.tsx | 2 +- .../public/applications/shared/layout/page_template.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 74c81d30a0825..7820ab340b0f9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -199,5 +199,5 @@ export const renderHeaderActions = ( , kibanaHeaderEl ); - return () => ReactDOM.unmountComponentAtNode(kibanaHeaderEl); + return () => ReactDOM.render(<>, kibanaHeaderEl); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/page_template.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/page_template.tsx index 2553a12dc17c5..f0cff6a29880e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/page_template.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/page_template.tsx @@ -76,7 +76,7 @@ export const EnterpriseSearchPageTemplateWrapper: React.FC = renderHeaderActions(EndpointsHeaderAction); } return () => { - renderHeaderActions(); + renderHeaderActions(undefined); }; }, []); return ( From fd0e46293ea2bcd3d8e876a896bfe4398acbfa3e Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Wed, 10 Jan 2024 11:16:36 -0500 Subject: [PATCH 19/26] [Response Ops][Alerting] Using `alertsClient` for transform health and anomaly detection jobs health rule types to write default alerts-as-data docs (#174537) Towards https://github.com/elastic/response-ops-team/issues/164 Resolves https://github.com/elastic/kibana/issues/171792 ## Summary * Switches these rule types to use `alertsClient` from alerting framework in favor of the deprecated `alertFactory` * Defines the `default` alert config for these rule types so framework level fields will be written out into the `.alerts-default.alerts-default` index with no rule type specific fields. Example alert doc for transform health rule: ``` { "kibana.alert.reason": "Transform test_transform_01 is not started.", "kibana.alert.rule.category": "Transform health", "kibana.alert.rule.consumer": "alerts", "kibana.alert.rule.execution.uuid": "1dd66818-962e-4fef-8ce2-5a1eab2813a2", "kibana.alert.rule.name": "Test all transforms", "kibana.alert.rule.parameters": { "includeTransforms": [ "*" ], "excludeTransforms": null, "testsConfig": null }, "kibana.alert.rule.producer": "stackAlerts", "kibana.alert.rule.revision": 0, "kibana.alert.rule.rule_type_id": "transform_health", "kibana.alert.rule.tags": [], "kibana.alert.rule.uuid": "7fb57af0-56b5-4c63-9457-add79e9c3e37", "kibana.space_ids": [ "space1" ], "@timestamp": "2024-01-10T14:43:00.974Z", "event.action": "open", "event.kind": "signal", "kibana.alert.action_group": "transform_issue", "kibana.alert.flapping": false, "kibana.alert.flapping_history": [ true ], "kibana.alert.instance.id": "Transform is not started", "kibana.alert.maintenance_window_ids": [], "kibana.alert.status": "active", "kibana.alert.uuid": "25f7b99d-e4ab-4b97-89e4-1a537692ffa5", "kibana.alert.workflow_status": "open", "kibana.alert.duration.us": 0, "kibana.alert.start": "2024-01-10T14:43:00.974Z", "kibana.alert.time_range": { "gte": "2024-01-10T14:43:00.974Z" }, "kibana.version": "8.13.0", "tags": [] } ``` --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../alert_as_data_fields.test.ts.snap | 12 ++- .../register_jobs_monitoring_rule_type.ts | 43 ++++++++-- .../register_transform_health_rule_type.ts | 39 +++++++-- x-pack/plugins/transform/tsconfig.json | 4 +- .../transform_health/index.ts | 2 +- .../transform_health/{alert.ts => rule.ts} | 85 +++++++++++++------ 6 files changed, 138 insertions(+), 47 deletions(-) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/transform_health/{alert.ts => rule.ts} (67%) diff --git a/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap b/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap index 3f3785b89f619..95f1fca184d56 100644 --- a/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap +++ b/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap @@ -8973,7 +8973,11 @@ Object { } `; -exports[`Alert as data fields checks detect AAD fields changes for: transform_health 1`] = `undefined`; +exports[`Alert as data fields checks detect AAD fields changes for: transform_health 1`] = ` +Object { + "fieldMap": Object {}, +} +`; exports[`Alert as data fields checks detect AAD fields changes for: xpack.ml.anomaly_detection_alert 1`] = ` Object { @@ -9087,7 +9091,11 @@ Object { } `; -exports[`Alert as data fields checks detect AAD fields changes for: xpack.ml.anomaly_detection_jobs_health 1`] = `undefined`; +exports[`Alert as data fields checks detect AAD fields changes for: xpack.ml.anomaly_detection_jobs_health 1`] = ` +Object { + "fieldMap": Object {}, +} +`; exports[`Alert as data fields checks detect AAD fields changes for: xpack.synthetics.alerts.monitorStatus 1`] = ` Object { diff --git a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts index 2b53fb7a4c7c4..014c3dd365b6d 100644 --- a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts +++ b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts @@ -16,9 +16,13 @@ import type { ActionGroup, AlertInstanceContext, AlertInstanceState, + RecoveredActionGroupId, RuleTypeState, } from '@kbn/alerting-plugin/common'; +import { AlertsClientError, DEFAULT_AAD_CONFIG } from '@kbn/alerting-plugin/server'; import type { RuleExecutorOptions } from '@kbn/alerting-plugin/server'; +import type { DefaultAlert } from '@kbn/alerts-as-data-utils'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; import { ML_ALERT_TYPES } from '../../../common/constants/alerts'; import { PLUGIN_ID } from '../../../common/constants/app'; import { MINIMUM_FULL_LICENSE } from '../../../common/license'; @@ -90,7 +94,8 @@ export type JobsHealthExecutorOptions = RuleExecutorOptions< Record, Record, AnomalyDetectionJobsHealthAlertContext, - AnomalyDetectionJobRealtimeIssue + AnomalyDetectionJobRealtimeIssue, + DefaultAlert >; export function registerJobsMonitoringRuleType({ @@ -104,7 +109,9 @@ export function registerJobsMonitoringRuleType({ RuleTypeState, AlertInstanceState, AnomalyDetectionJobsHealthAlertContext, - AnomalyDetectionJobRealtimeIssue + AnomalyDetectionJobRealtimeIssue, + RecoveredActionGroupId, + DefaultAlert >({ id: ML_ALERT_TYPES.AD_JOBS_HEALTH, name: i18n.translate('xpack.ml.jobsHealthAlertingRule.name', { @@ -142,12 +149,19 @@ export function registerJobsMonitoringRuleType({ minimumLicenseRequired: MINIMUM_FULL_LICENSE, isExportable: true, doesSetRecoveryContext: true, + alerts: DEFAULT_AAD_CONFIG, async executor(options) { const { services, rule: { name }, } = options; + const { alertsClient } = services; + + if (!alertsClient) { + throw new AlertsClientError(); + } + const fakeRequest = {} as KibanaRequest; const { getTestsResults } = mlServicesProviders.jobsHealthServiceProvider( services.savedObjectsClient, @@ -165,19 +179,30 @@ export function registerJobsMonitoringRuleType({ .join(', ')}` ); - unhealthyTests.forEach(({ name: alertInstanceName, context }) => { - const alertInstance = services.alertFactory.create(alertInstanceName); - alertInstance.scheduleActions(ANOMALY_DETECTION_JOB_REALTIME_ISSUE, context); + unhealthyTests.forEach(({ name: alertName, context }) => { + alertsClient.report({ + id: alertName, + actionGroup: ANOMALY_DETECTION_JOB_REALTIME_ISSUE, + context, + payload: { + [ALERT_REASON]: context.message, + }, + }); }); } // Set context for recovered alerts - const { getRecoveredAlerts } = services.alertFactory.done(); - for (const recoveredAlert of getRecoveredAlerts()) { - const recoveredAlertId = recoveredAlert.getId(); + for (const recoveredAlert of alertsClient.getRecoveredAlerts()) { + const recoveredAlertId = recoveredAlert.alert.getId(); const testResult = executionResult.find((v) => v.name === recoveredAlertId); if (testResult) { - recoveredAlert.setContext(testResult.context); + alertsClient.setAlertData({ + id: recoveredAlertId, + context: testResult.context, + payload: { + [ALERT_REASON]: testResult.context.message, + }, + }); } } diff --git a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts index 3eaa1db46018d..c6450fc243dc2 100644 --- a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts +++ b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts @@ -11,11 +11,14 @@ import type { ActionGroup, AlertInstanceContext, AlertInstanceState, + RecoveredActionGroupId, RuleTypeState, } from '@kbn/alerting-plugin/common'; -import type { RuleType } from '@kbn/alerting-plugin/server'; +import { AlertsClientError, DEFAULT_AAD_CONFIG, RuleType } from '@kbn/alerting-plugin/server'; import type { PluginSetupContract as AlertingSetup } from '@kbn/alerting-plugin/server'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/server'; +import type { DefaultAlert } from '@kbn/alerts-as-data-utils'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; import { PLUGIN, type TransformHealth, TRANSFORM_RULE_TYPE } from '../../../../common/constants'; import { transformHealthRuleParams, TransformHealthRuleParams } from './schema'; import { transformHealthServiceProvider } from './transform_health_service'; @@ -73,7 +76,9 @@ export function getTransformHealthRuleType( RuleTypeState, AlertInstanceState, TransformHealthAlertContext, - TransformIssue + TransformIssue, + RecoveredActionGroupId, + DefaultAlert > { return { id: TRANSFORM_RULE_TYPE.TRANSFORM_HEALTH, @@ -110,12 +115,17 @@ export function getTransformHealthRuleType( minimumLicenseRequired: PLUGIN.MINIMUM_LICENSE_REQUIRED, isExportable: true, doesSetRecoveryContext: true, + alerts: DEFAULT_AAD_CONFIG, async executor(options) { const { - services: { scopedClusterClient, alertFactory, uiSettingsClient }, + services: { scopedClusterClient, alertsClient, uiSettingsClient }, params, } = options; + if (!alertsClient) { + throw new AlertsClientError(); + } + const fieldFormatsRegistry = await getFieldFormatsStart().fieldFormatServiceFactory( uiSettingsClient ); @@ -131,18 +141,29 @@ export function getTransformHealthRuleType( if (unhealthyTests.length > 0) { unhealthyTests.forEach(({ name: alertInstanceName, context }) => { - const alertInstance = alertFactory.create(alertInstanceName); - alertInstance.scheduleActions(TRANSFORM_ISSUE, context); + alertsClient.report({ + id: alertInstanceName, + actionGroup: TRANSFORM_ISSUE, + context, + payload: { + [ALERT_REASON]: context.message, + }, + }); }); } // Set context for recovered alerts - const { getRecoveredAlerts } = alertFactory.done(); - for (const recoveredAlert of getRecoveredAlerts()) { - const recoveredAlertId = recoveredAlert.getId(); + for (const recoveredAlert of alertsClient.getRecoveredAlerts()) { + const recoveredAlertId = recoveredAlert.alert.getId(); const testResult = executionResult.find((v) => v.name === recoveredAlertId); if (testResult) { - recoveredAlert.setContext(testResult.context); + alertsClient.setAlertData({ + id: recoveredAlertId, + context: testResult.context, + payload: { + [ALERT_REASON]: testResult.context.message, + }, + }); } } diff --git a/x-pack/plugins/transform/tsconfig.json b/x-pack/plugins/transform/tsconfig.json index 41f6fa29b8aa0..faa4e2e67dd53 100644 --- a/x-pack/plugins/transform/tsconfig.json +++ b/x-pack/plugins/transform/tsconfig.json @@ -72,7 +72,9 @@ "@kbn/data-view-editor-plugin", "@kbn/ml-data-view-utils", "@kbn/ml-creation-wizard-utils", - "@kbn/code-editor" + "@kbn/alerts-as-data-utils", + "@kbn/code-editor", + "@kbn/rule-data-utils" ], "exclude": [ "target/**/*", diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/transform_health/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/transform_health/index.ts index adc93332a0f56..3cb706576efc3 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/transform_health/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/transform_health/index.ts @@ -10,6 +10,6 @@ import { FtrProviderContext } from '../../../../../../common/ftr_provider_contex // eslint-disable-next-line import/no-default-export export default function alertingTests({ loadTestFile }: FtrProviderContext) { describe('transform_health', function () { - loadTestFile(require.resolve('./alert')); + loadTestFile(require.resolve('./rule')); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/transform_health/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/transform_health/rule.ts similarity index 67% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/transform_health/alert.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/transform_health/rule.ts index 046ad43ef1514..24efdd2613591 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/transform_health/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/transform_health/rule.ts @@ -8,19 +8,30 @@ import expect from '@kbn/expect'; import { PutTransformsRequestSchema } from '@kbn/transform-plugin/common/api_schemas/transforms'; import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; +import { + ALERT_ACTION_GROUP, + ALERT_INSTANCE_ID, + ALERT_REASON, + ALERT_RULE_CATEGORY, + ALERT_RULE_NAME, + ALERT_RULE_TYPE_ID, + ALERT_RULE_UUID, + ALERT_STATUS, + EVENT_ACTION, +} from '@kbn/rule-data-utils'; import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; import { getUrlPrefix, ObjectRemover } from '../../../../../../common/lib'; import { Spaces } from '../../../../../scenarios'; -const ACTION_TYPE_ID = '.index'; -const ALERT_TYPE_ID = 'transform_health'; +const CONNECTOR_TYPE_ID = '.index'; +const RULE_TYPE_ID = 'transform_health'; const ES_TEST_INDEX_SOURCE = 'transform-alert:transform-health'; const ES_TEST_INDEX_REFERENCE = '-na-'; const ES_TEST_OUTPUT_INDEX_NAME = `${ES_TEST_INDEX_NAME}-ts-output`; -const ALERT_INTERVAL_SECONDS = 3; +const RULE_INTERVAL_SECONDS = 3; -interface CreateAlertParams { +interface CreateRuleParams { name: string; includeTransforms: string[]; excludeTransforms?: string[] | null; @@ -52,7 +63,7 @@ export function generateTransformConfig(transformId: string): PutTransformsReque } // eslint-disable-next-line import/no-default-export -export default function alertTests({ getService }: FtrProviderContext) { +export default function ruleTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const retry = getService('retry'); @@ -62,10 +73,15 @@ export default function alertTests({ getService }: FtrProviderContext) { const esTestIndexTool = new ESTestIndexTool(es, retry); const esTestIndexToolOutput = new ESTestIndexTool(es, retry, ES_TEST_OUTPUT_INDEX_NAME); + const esTestIndexToolAAD = new ESTestIndexTool( + es, + retry, + `.internal.alerts-default.alerts-default-000001` + ); - describe('alert', async () => { + describe('rule', async () => { const objectRemover = new ObjectRemover(supertest); - let actionId: string; + let connectorId: string; const transformId = 'test_transform_01'; const destinationIndex = generateDestIndex(transformId); @@ -76,10 +92,12 @@ export default function alertTests({ getService }: FtrProviderContext) { await esTestIndexToolOutput.destroy(); await esTestIndexToolOutput.setup(); + await esTestIndexToolAAD.removeAll(); + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); await transform.testResources.setKibanaTimeZoneToUTC(); - actionId = await createAction(); + connectorId = await createConnector(); await transform.api.createIndices(destinationIndex); await createTransform(transformId); @@ -89,18 +107,19 @@ export default function alertTests({ getService }: FtrProviderContext) { await objectRemover.removeAll(); await esTestIndexTool.destroy(); await esTestIndexToolOutput.destroy(); + await esTestIndexToolAAD.removeAll(); await transform.api.cleanTransformIndices(); }); it('runs correctly', async () => { - await createAlert({ + const ruleId = await createRule({ name: 'Test all transforms', includeTransforms: ['*'], }); await stopTransform(transformId); - log.debug('Checking created alert instances...'); + log.debug('Checking created alerts...'); const docs = await waitForDocs(1); for (const doc of docs) { @@ -109,6 +128,18 @@ export default function alertTests({ getService }: FtrProviderContext) { expect(name).to.be('Test all transforms'); expect(message).to.be('Transform test_transform_01 is not started.'); } + + const aadDocs = await getAllAADDocs(1); + const alertDoc = aadDocs.body.hits.hits[0]._source; + expect(alertDoc[ALERT_REASON]).to.be(`Transform test_transform_01 is not started.`); + expect(alertDoc[ALERT_RULE_CATEGORY]).to.be(`Transform health`); + expect(alertDoc[ALERT_RULE_NAME]).to.be(`Test all transforms`); + expect(alertDoc[ALERT_RULE_TYPE_ID]).to.be(`transform_health`); + expect(alertDoc[ALERT_RULE_UUID]).to.be(ruleId); + expect(alertDoc[EVENT_ACTION]).to.be(`open`); + expect(alertDoc[ALERT_ACTION_GROUP]).to.be(`transform_issue`); + expect(alertDoc[ALERT_INSTANCE_ID]).to.be(`Transform is not started`); + expect(alertDoc[ALERT_STATUS]).to.be(`active`); }); async function waitForDocs(count: number): Promise { @@ -119,15 +150,19 @@ export default function alertTests({ getService }: FtrProviderContext) { ); } + async function getAllAADDocs(count: number): Promise { + return await esTestIndexToolAAD.getAll(count); + } + async function createTransform(id: string) { const config = generateTransformConfig(id); await transform.api.createAndRunTransform(id, config); } - async function createAlert(params: CreateAlertParams): Promise { + async function createRule(params: CreateRuleParams): Promise { log.debug(`Creating an alerting rule "${params.name}"...`); const action = { - id: actionId, + id: connectorId, group: 'transform_issue', params: { documents: [ @@ -143,15 +178,15 @@ export default function alertTests({ getService }: FtrProviderContext) { }, }; - const { status, body: createdAlert } = await supertest + const { status, body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send({ name: params.name, consumer: 'alerts', enabled: true, - rule_type_id: ALERT_TYPE_ID, - schedule: { interval: `${ALERT_INTERVAL_SECONDS}s` }, + rule_type_id: RULE_TYPE_ID, + schedule: { interval: `${RULE_INTERVAL_SECONDS}s` }, actions: [action], notify_when: 'onActiveAlert', params: { @@ -160,29 +195,29 @@ export default function alertTests({ getService }: FtrProviderContext) { }); // will print the error body, if an error occurred - // if (statusCode !== 200) console.log(createdAlert); + // if (statusCode !== 200) console.log(createdRule); expect(status).to.be(200); - const alertId = createdAlert.id; - objectRemover.add(Spaces.space1.id, alertId, 'rule', 'alerting'); + const ruleId = createdRule.id; + objectRemover.add(Spaces.space1.id, ruleId, 'rule', 'alerting'); - return alertId; + return ruleId; } async function stopTransform(id: string) { await transform.api.stopTransform(id); } - async function createAction(): Promise { - log.debug('Creating an action...'); + async function createConnector(): Promise { + log.debug('Creating a connector...'); // @ts-ignore - const { statusCode, body: createdAction } = await supertest + const { statusCode, body: createdConnector } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'index action for transform health FT', - connector_type_id: ACTION_TYPE_ID, + connector_type_id: CONNECTOR_TYPE_ID, config: { index: ES_TEST_OUTPUT_INDEX_NAME, }, @@ -191,9 +226,9 @@ export default function alertTests({ getService }: FtrProviderContext) { expect(statusCode).to.be(200); - log.debug(`Action with id "${createdAction.id}" has been created.`); + log.debug(`Connector with id "${createdConnector.id}" has been created.`); - const resultId = createdAction.id; + const resultId = createdConnector.id; objectRemover.add(Spaces.space1.id, resultId, 'connector', 'actions'); return resultId; From e78b8e5579f1b8aa147a74f38503a8d712860c88 Mon Sep 17 00:00:00 2001 From: Ioana Tagirta Date: Wed, 10 Jan 2024 17:17:34 +0100 Subject: [PATCH 20/26] [Search Relevance] Remove E5 Multilingual Callout (#174589) ## Summary Issue: SEARCH-210 Removes the E5 calllout since it's no longer needed. The PR is easier to review by commit, the one change I kept from https://github.com/elastic/kibana/pull/171887 is the ELSER description that we want to keep. --- .../create_e5_multilingual_model_api_logic.ts | 35 -- .../fetch_e5_multilingual_model_api_logic.ts | 37 -- .../start_e5_multilingual_model_api_logic.ts | 35 -- .../deploy_model.test.tsx | 81 --- .../e5_multilingual_callout/deploy_model.tsx | 120 ----- .../e5_multilingual_callout.test.tsx | 77 --- .../e5_multilingual_callout.tsx | 150 ------ .../e5_multilingual_callout_logic.test.ts | 469 ------------------ .../e5_multilingual_callout_logic.ts | 308 ------------ .../e5_multilingual_errors.test.tsx | 34 -- .../e5_multilingual_errors.tsx | 37 -- .../model_deployed.test.tsx | 81 --- .../model_deployed.tsx | 113 ----- .../model_deployment_in_progress.test.tsx | 39 -- .../model_deployment_in_progress.tsx | 58 --- .../model_started.test.tsx | 57 --- .../e5_multilingual_callout/model_started.tsx | 135 ----- .../text_expansion_callout/deploy_model.tsx | 2 +- 18 files changed, 1 insertion(+), 1867 deletions(-) delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/e5_multilingual/create_e5_multilingual_model_api_logic.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/e5_multilingual/fetch_e5_multilingual_model_api_logic.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/e5_multilingual/start_e5_multilingual_model_api_logic.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/deploy_model.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/deploy_model.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_callout.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_callout.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_callout_logic.test.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_callout_logic.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_errors.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_errors.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_deployed.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_deployed.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_deployment_in_progress.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_deployment_in_progress.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_started.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_started.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/e5_multilingual/create_e5_multilingual_model_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/e5_multilingual/create_e5_multilingual_model_api_logic.ts deleted file mode 100644 index e4b50c70bfc84..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/e5_multilingual/create_e5_multilingual_model_api_logic.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { Actions, createApiLogic } from '../../../../shared/api_logic/create_api_logic'; -import { HttpLogic } from '../../../../shared/http'; - -export interface CreateE5MultilingualModelArgs { - modelId: string; -} - -export interface CreateE5MultilingualModelResponse { - deploymentState: string; - modelId: string; -} - -export const createE5MultilingualModel = async ({ - modelId, -}: CreateE5MultilingualModelArgs): Promise => { - const route = `/internal/enterprise_search/ml/models/${modelId}`; - return await HttpLogic.values.http.post(route); -}; - -export const CreateE5MultilingualModelApiLogic = createApiLogic( - ['create_e5_multilingual_model_api_logic'], - createE5MultilingualModel, - { showErrorFlash: false } -); - -export type CreateE5MultilingualModelApiLogicActions = Actions< - CreateE5MultilingualModelArgs, - CreateE5MultilingualModelResponse ->; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/e5_multilingual/fetch_e5_multilingual_model_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/e5_multilingual/fetch_e5_multilingual_model_api_logic.ts deleted file mode 100644 index 88918a56fb331..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/e5_multilingual/fetch_e5_multilingual_model_api_logic.ts +++ /dev/null @@ -1,37 +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 { Actions, createApiLogic } from '../../../../shared/api_logic/create_api_logic'; -import { HttpLogic } from '../../../../shared/http'; - -export interface FetchE5MultilingualModelArgs { - modelId: string; -} - -export interface FetchE5MultilingualModelResponse { - deploymentState: string; - modelId: string; - targetAllocationCount: number; - nodeAllocationCount: number; - threadsPerAllocation: number; -} - -export const fetchE5MultilingualModelStatus = async ({ modelId }: FetchE5MultilingualModelArgs) => { - return await HttpLogic.values.http.get( - `/internal/enterprise_search/ml/models/${modelId}` - ); -}; - -export const FetchE5MultilingualModelApiLogic = createApiLogic( - ['fetch_e5_multilingual_model_api_logic'], - fetchE5MultilingualModelStatus, - { showErrorFlash: false } -); - -export type FetchE5MultilingualModelApiLogicActions = Actions< - FetchE5MultilingualModelArgs, - FetchE5MultilingualModelResponse ->; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/e5_multilingual/start_e5_multilingual_model_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/e5_multilingual/start_e5_multilingual_model_api_logic.ts deleted file mode 100644 index 9b0e1520da710..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/e5_multilingual/start_e5_multilingual_model_api_logic.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { Actions, createApiLogic } from '../../../../shared/api_logic/create_api_logic'; -import { HttpLogic } from '../../../../shared/http'; - -export interface StartE5MultilingualModelArgs { - modelId: string; -} - -export interface StartE5MultilingualModelResponse { - deploymentState: string; - modelId: string; -} - -export const startE5MultilingualModel = async ({ - modelId, -}: StartE5MultilingualModelArgs): Promise => { - const route = `/internal/enterprise_search/ml/models/${modelId}/deploy`; - return await HttpLogic.values.http.post(route); -}; - -export const StartE5MultilingualModelApiLogic = createApiLogic( - ['start_e5_multilingual_model_api_logic'], - startE5MultilingualModel, - { showErrorFlash: false } -); - -export type StartE5MultilingualModelApiLogicActions = Actions< - StartE5MultilingualModelArgs, - StartE5MultilingualModelResponse ->; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/deploy_model.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/deploy_model.test.tsx deleted file mode 100644 index 739e709ea10f0..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/deploy_model.test.tsx +++ /dev/null @@ -1,81 +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 { setMockValues } from '../../../../../../__mocks__/kea_logic'; - -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { EuiButton } from '@elastic/eui'; - -import { DeployModel } from './deploy_model'; -import { E5MultilingualDismissButton } from './e5_multilingual_callout'; - -const DEFAULT_VALUES = { - startE5MultilingualModelError: undefined, - isCreateButtonDisabled: false, - isModelDownloadInProgress: false, - isModelDownloaded: false, - isModelStarted: false, - isStartButtonDisabled: false, -}; - -describe('DeployModel', () => { - beforeEach(() => { - jest.clearAllMocks(); - setMockValues(DEFAULT_VALUES); - }); - it('renders deploy button', () => { - const wrapper = shallow( - {}} - ingestionMethod="crawler" - isCreateButtonDisabled={false} - isDismissable={false} - /> - ); - expect(wrapper.find(EuiButton).length).toBe(1); - const button = wrapper.find(EuiButton); - expect(button.prop('disabled')).toBe(false); - }); - it('renders disabled deploy button if it is set to disabled', () => { - const wrapper = shallow( - {}} - ingestionMethod="crawler" - isCreateButtonDisabled - isDismissable={false} - /> - ); - expect(wrapper.find(EuiButton).length).toBe(1); - const button = wrapper.find(EuiButton); - expect(button.prop('disabled')).toBe(true); - }); - it('renders dismiss button if it is set to dismissable', () => { - const wrapper = shallow( - {}} - ingestionMethod="crawler" - isCreateButtonDisabled={false} - isDismissable - /> - ); - expect(wrapper.find(E5MultilingualDismissButton).length).toBe(1); - }); - it('does not render dismiss button if it is set to non-dismissable', () => { - const wrapper = shallow( - {}} - ingestionMethod="crawler" - isCreateButtonDisabled={false} - isDismissable={false} - /> - ); - expect(wrapper.find(E5MultilingualDismissButton).length).toBe(0); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/deploy_model.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/deploy_model.tsx deleted file mode 100644 index 6d616a453e9f7..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/deploy_model.tsx +++ /dev/null @@ -1,120 +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 React from 'react'; - -import { useActions } from 'kea'; - -import { - EuiBadge, - EuiButton, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiText, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedHTMLMessage, FormattedMessage } from '@kbn/i18n-react'; - -import { E5MultilingualCallOutState, E5MultilingualDismissButton } from './e5_multilingual_callout'; -import { E5MultilingualCalloutLogic } from './e5_multilingual_callout_logic'; - -export const DeployModel = ({ - dismiss, - ingestionMethod, - isCreateButtonDisabled, - isDismissable, -}: Pick< - E5MultilingualCallOutState, - 'dismiss' | 'ingestionMethod' | 'isCreateButtonDisabled' | 'isDismissable' ->) => { - const { createE5MultilingualModel } = useActions(E5MultilingualCalloutLogic); - - return ( - - - - - - - - - - - -

- {i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.e5MultilingualCallOut.title', - { defaultMessage: 'Improve your results with E5' } - )} -

-
-
- {isDismissable && ( - - - - )} -
-
- - - - - - - - - - - createE5MultilingualModel()} - > - {i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.e5MultilingualCallOut.deployButton.label', - { - defaultMessage: 'Deploy', - } - )} - - - - - - - - - - - -
-
- ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_callout.test.tsx deleted file mode 100644 index ad9f99960ad04..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_callout.test.tsx +++ /dev/null @@ -1,77 +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 { setMockValues } from '../../../../../../__mocks__/kea_logic'; - -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { DeployModel } from './deploy_model'; -import { E5MultilingualCallOut } from './e5_multilingual_callout'; -import { E5MultilingualErrors } from './e5_multilingual_errors'; -import { ModelDeployed } from './model_deployed'; -import { ModelDeploymentInProgress } from './model_deployment_in_progress'; -import { ModelStarted } from './model_started'; - -const DEFAULT_VALUES = { - isCreateButtonDisabled: false, - isModelDownloadInProgress: false, - isModelDownloaded: false, - isModelStarted: false, - isStartButtonDisabled: false, -}; - -describe('E5MultilingualCallOut', () => { - beforeEach(() => { - jest.clearAllMocks(); - setMockValues(DEFAULT_VALUES); - }); - it('renders error panel instead of normal panel if there are some errors', () => { - setMockValues({ - ...DEFAULT_VALUES, - e5MultilingualError: { - title: 'Error with E5 Multilingual deployment', - message: 'Mocked error message', - }, - }); - - const wrapper = shallow(); - expect(wrapper.find(E5MultilingualErrors).length).toBe(1); - }); - it('renders panel with deployment instructions if the model is not deployed', () => { - const wrapper = shallow(); - expect(wrapper.find(DeployModel).length).toBe(1); - }); - it('renders panel with deployment in progress status if the model is being deployed', () => { - setMockValues({ - ...DEFAULT_VALUES, - isModelDownloadInProgress: true, - }); - - const wrapper = shallow(); - expect(wrapper.find(ModelDeploymentInProgress).length).toBe(1); - }); - it('renders panel with deployment in progress status if the model has been deployed', () => { - setMockValues({ - ...DEFAULT_VALUES, - isModelDownloaded: true, - }); - - const wrapper = shallow(); - expect(wrapper.find(ModelDeployed).length).toBe(1); - }); - it('renders panel with deployment in progress status if the model has been started', () => { - setMockValues({ - ...DEFAULT_VALUES, - isModelStarted: true, - }); - - const wrapper = shallow(); - expect(wrapper.find(ModelStarted).length).toBe(1); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_callout.tsx deleted file mode 100644 index bfe92e5b7c96e..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_callout.tsx +++ /dev/null @@ -1,150 +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 React from 'react'; - -import { useCallback, useState } from 'react'; - -import { useValues } from 'kea'; - -import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; - -import { KibanaLogic } from '../../../../../../shared/kibana'; - -import { useLocalStorage } from '../../../../../../shared/use_local_storage'; - -import { IndexViewLogic } from '../../../index_view_logic'; - -import { TRAINED_MODELS_PATH } from '../utils'; - -import { DeployModel } from './deploy_model'; -import { E5MultilingualCalloutLogic } from './e5_multilingual_callout_logic'; -import { E5MultilingualErrors } from './e5_multilingual_errors'; -import { ModelDeployed } from './model_deployed'; -import { ModelDeploymentInProgress } from './model_deployment_in_progress'; -import { ModelStarted } from './model_started'; - -export interface E5MultilingualCallOutState { - dismiss: () => void; - ingestionMethod: string; - isCompact: boolean; - isCreateButtonDisabled: boolean; - isDismissable: boolean; - isSingleThreaded: boolean; - isStartButtonDisabled: boolean; - show: boolean; -} - -export interface E5MultilingualCallOutProps { - isCompact?: boolean; - isDismissable?: boolean; -} - -export const E5_MULTILINGUAL_CALL_OUT_DISMISSED_KEY = - 'enterprise-search-e5-multilingual-callout-dismissed'; - -export const E5MultilingualDismissButton = ({ - dismiss, -}: Pick) => { - return ( - - ); -}; - -export const FineTuneModelsButton: React.FC = () => ( - - KibanaLogic.values.navigateToUrl(TRAINED_MODELS_PATH, { - shouldNotCreateHref: true, - }) - } - > - {i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.e5MultilingualCallOut.fineTuneModelButton', - { - defaultMessage: 'Fine-tune performance', - } - )} - -); - -export const E5MultilingualCallOut: React.FC = (props) => { - const isCompact = props.isCompact !== undefined ? props.isCompact : false; - const isDismissable = props.isDismissable !== undefined ? props.isDismissable : false; - - const [calloutDismissed, setCalloutDismissed] = useLocalStorage( - E5_MULTILINGUAL_CALL_OUT_DISMISSED_KEY, - false - ); - - const [show, setShow] = useState(() => { - if (!isDismissable) return true; - return !calloutDismissed; - }); - - const dismiss = useCallback(() => { - setShow(false); - setCalloutDismissed(true); - }, []); - - const { ingestionMethod } = useValues(IndexViewLogic); - const { - isCreateButtonDisabled, - isModelDownloadInProgress, - isModelDownloaded, - isModelRunningSingleThreaded, - isModelStarted, - e5MultilingualError, - isStartButtonDisabled, - } = useValues(E5MultilingualCalloutLogic); - - if (e5MultilingualError) return ; - - if (!show) return null; - - if (isModelDownloadInProgress) { - return ; - } else if (isModelDownloaded) { - return ( - - ); - } else if (isModelStarted) { - return ( - - ); - } - - return ( - - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_callout_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_callout_logic.test.ts deleted file mode 100644 index 0ea1d546d407f..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_callout_logic.test.ts +++ /dev/null @@ -1,469 +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 { LogicMounter } from '../../../../../../__mocks__/kea_logic'; - -import { HttpResponse } from '@kbn/core/public'; - -import { ErrorResponse, HttpError, Status } from '../../../../../../../../common/types/api'; -import { MlModelDeploymentState } from '../../../../../../../../common/types/ml'; -import { CreateE5MultilingualModelApiLogic } from '../../../../../api/ml_models/e5_multilingual/create_e5_multilingual_model_api_logic'; -import { FetchE5MultilingualModelApiLogic } from '../../../../../api/ml_models/e5_multilingual/fetch_e5_multilingual_model_api_logic'; -import { StartE5MultilingualModelApiLogic } from '../../../../../api/ml_models/e5_multilingual/start_e5_multilingual_model_api_logic'; - -import { - E5MultilingualCalloutLogic, - E5MultilingualCalloutValues, - getE5MultilingualError, -} from './e5_multilingual_callout_logic'; - -const DEFAULT_VALUES: E5MultilingualCalloutValues = { - createE5MultilingualModelError: undefined, - createE5MultilingualModelStatus: Status.IDLE, - createdE5MultilingualModel: undefined, - fetchE5MultilingualModelError: undefined, - isCreateButtonDisabled: false, - isModelDownloadInProgress: false, - isModelDownloaded: false, - isModelRunningSingleThreaded: false, - isModelStarted: false, - isPollingE5MultilingualModelActive: false, - isStartButtonDisabled: false, - startE5MultilingualModelError: undefined, - startE5MultilingualModelStatus: Status.IDLE, - e5MultilingualModel: undefined, - e5MultilingualModelPollTimeoutId: null, - e5MultilingualError: null, -}; - -jest.useFakeTimers(); - -describe('E5MultilingualCalloutLogic', () => { - const { mount } = new LogicMounter(E5MultilingualCalloutLogic); - const { mount: mountCreateE5MultilingualModelApiLogic } = new LogicMounter( - CreateE5MultilingualModelApiLogic - ); - const { mount: mountFetchE5MultilingualModelApiLogic } = new LogicMounter( - FetchE5MultilingualModelApiLogic - ); - const { mount: mountStartE5MultilingualModelApiLogic } = new LogicMounter( - StartE5MultilingualModelApiLogic - ); - - beforeEach(() => { - jest.clearAllMocks(); - mountCreateE5MultilingualModelApiLogic(); - mountFetchE5MultilingualModelApiLogic(); - mountStartE5MultilingualModelApiLogic(); - mount(); - }); - - it('has expected default values', () => { - expect(E5MultilingualCalloutLogic.values).toEqual(DEFAULT_VALUES); - }); - - describe('getE5MultilingualError', () => { - const error = { - body: { - error: 'some-error', - message: 'some-error-message', - statusCode: 500, - }, - } as HttpError; - it('returns null if there is no error', () => { - expect(getE5MultilingualError(undefined, undefined, undefined)).toBe(null); - }); - it('uses the correct title and message from a create error', () => { - expect(getE5MultilingualError(error, undefined, undefined)).toEqual({ - title: 'Error with E5 Multilingual deployment', - message: error.body?.message, - }); - }); - it('uses the correct title and message from a fetch error', () => { - expect(getE5MultilingualError(undefined, error, undefined)).toEqual({ - title: 'Error fetching E5 Multilingual model', - message: error.body?.message, - }); - }); - it('uses the correct title and message from a start error', () => { - expect(getE5MultilingualError(undefined, undefined, error)).toEqual({ - title: 'Error starting E5 Multilingual deployment', - message: error.body?.message, - }); - }); - }); - - describe('listeners', () => { - describe('createE5MultilingualModelPollingTimeout', () => { - const duration = 5000; - it('sets polling timeout', () => { - jest.spyOn(global, 'setTimeout'); - jest.spyOn(E5MultilingualCalloutLogic.actions, 'setE5MultilingualModelPollingId'); - - E5MultilingualCalloutLogic.actions.createE5MultilingualModelPollingTimeout(duration); - - expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), duration); - expect( - E5MultilingualCalloutLogic.actions.setE5MultilingualModelPollingId - ).toHaveBeenCalled(); - }); - it('clears polling timeout if it is set', () => { - mount({ - ...DEFAULT_VALUES, - e5MultilingualModelPollTimeoutId: 'timeout-id', - }); - - jest.spyOn(global, 'clearTimeout'); - - E5MultilingualCalloutLogic.actions.createE5MultilingualModelPollingTimeout(duration); - - expect(clearTimeout).toHaveBeenCalledWith('timeout-id'); - }); - }); - - describe('createE5MultilingualModelSuccess', () => { - it('sets createdE5MultilingualModel', () => { - jest.spyOn(E5MultilingualCalloutLogic.actions, 'fetchE5MultilingualModel'); - jest.spyOn(E5MultilingualCalloutLogic.actions, 'startPollingE5MultilingualModel'); - - E5MultilingualCalloutLogic.actions.createE5MultilingualModelSuccess({ - deploymentState: MlModelDeploymentState.Downloading, - modelId: 'mock-model-id', - }); - - expect(E5MultilingualCalloutLogic.actions.fetchE5MultilingualModel).toHaveBeenCalled(); - expect( - E5MultilingualCalloutLogic.actions.startPollingE5MultilingualModel - ).toHaveBeenCalled(); - }); - }); - - describe('fetchE5MultilingualModelSuccess', () => { - const data = { - deploymentState: MlModelDeploymentState.Downloading, - modelId: 'mock-model-id', - targetAllocationCount: 1, - nodeAllocationCount: 1, - threadsPerAllocation: 1, - }; - - it('starts polling when the model is downloading and polling is not active', () => { - mount({ - ...DEFAULT_VALUES, - }); - jest.spyOn(E5MultilingualCalloutLogic.actions, 'startPollingE5MultilingualModel'); - - E5MultilingualCalloutLogic.actions.fetchE5MultilingualModelSuccess(data); - - expect( - E5MultilingualCalloutLogic.actions.startPollingE5MultilingualModel - ).toHaveBeenCalled(); - }); - it('sets polling timeout when the model is downloading and polling is active', () => { - mount({ - ...DEFAULT_VALUES, - e5MultilingualModelPollTimeoutId: 'timeout-id', - }); - jest.spyOn(E5MultilingualCalloutLogic.actions, 'createE5MultilingualModelPollingTimeout'); - - E5MultilingualCalloutLogic.actions.fetchE5MultilingualModelSuccess(data); - - expect( - E5MultilingualCalloutLogic.actions.createE5MultilingualModelPollingTimeout - ).toHaveBeenCalled(); - }); - it('stops polling when the model is downloaded and polling is active', () => { - mount({ - ...DEFAULT_VALUES, - e5MultilingualModelPollTimeoutId: 'timeout-id', - }); - jest.spyOn(E5MultilingualCalloutLogic.actions, 'stopPollingE5MultilingualModel'); - - E5MultilingualCalloutLogic.actions.fetchE5MultilingualModelSuccess({ - deploymentState: MlModelDeploymentState.Downloaded, - modelId: 'mock-model-id', - targetAllocationCount: 1, - nodeAllocationCount: 1, - threadsPerAllocation: 1, - }); - - expect( - E5MultilingualCalloutLogic.actions.stopPollingE5MultilingualModel - ).toHaveBeenCalled(); - }); - }); - - describe('fetchE5MultilingualModelError', () => { - it('stops polling if it is active', () => { - mount({ - ...DEFAULT_VALUES, - e5MultilingualModelPollTimeoutId: 'timeout-id', - }); - jest.spyOn(E5MultilingualCalloutLogic.actions, 'createE5MultilingualModelPollingTimeout'); - - E5MultilingualCalloutLogic.actions.fetchE5MultilingualModelError({ - body: { - error: '', - message: 'some error', - statusCode: 500, - }, - } as HttpResponse); - - expect( - E5MultilingualCalloutLogic.actions.createE5MultilingualModelPollingTimeout - ).toHaveBeenCalled(); - }); - }); - - describe('startPollingE5MultilingualModel', () => { - it('sets polling timeout', () => { - jest.spyOn(E5MultilingualCalloutLogic.actions, 'createE5MultilingualModelPollingTimeout'); - - E5MultilingualCalloutLogic.actions.startPollingE5MultilingualModel(); - - expect( - E5MultilingualCalloutLogic.actions.createE5MultilingualModelPollingTimeout - ).toHaveBeenCalled(); - }); - it('clears polling timeout if it is set', () => { - mount({ - ...DEFAULT_VALUES, - e5MultilingualModelPollTimeoutId: 'timeout-id', - }); - - jest.spyOn(global, 'clearTimeout'); - - E5MultilingualCalloutLogic.actions.startPollingE5MultilingualModel(); - - expect(clearTimeout).toHaveBeenCalledWith('timeout-id'); - }); - }); - - describe('startE5MultilingualModelSuccess', () => { - it('sets startedE5MultilingualModel', () => { - jest.spyOn(E5MultilingualCalloutLogic.actions, 'fetchE5MultilingualModel'); - - E5MultilingualCalloutLogic.actions.startE5MultilingualModelSuccess({ - deploymentState: MlModelDeploymentState.FullyAllocated, - modelId: 'mock-model-id', - }); - - expect(E5MultilingualCalloutLogic.actions.fetchE5MultilingualModel).toHaveBeenCalled(); - }); - }); - - describe('stopPollingE5MultilingualModel', () => { - it('clears polling timeout and poll timeout ID if it is set', () => { - mount({ - ...DEFAULT_VALUES, - e5MultilingualModelPollTimeoutId: 'timeout-id', - }); - - jest.spyOn(global, 'clearTimeout'); - jest.spyOn(E5MultilingualCalloutLogic.actions, 'clearE5MultilingualModelPollingId'); - - E5MultilingualCalloutLogic.actions.stopPollingE5MultilingualModel(); - - expect(clearTimeout).toHaveBeenCalledWith('timeout-id'); - expect( - E5MultilingualCalloutLogic.actions.clearE5MultilingualModelPollingId - ).toHaveBeenCalled(); - }); - }); - }); - - describe('reducers', () => { - describe('e5MultilingualModelPollTimeoutId', () => { - it('gets cleared on clearE5MultilingualModelPollingId', () => { - E5MultilingualCalloutLogic.actions.clearE5MultilingualModelPollingId(); - - expect(E5MultilingualCalloutLogic.values.e5MultilingualModelPollTimeoutId).toBe(null); - }); - it('gets set on setE5MultilingualModelPollingId', () => { - const timeout = setTimeout(() => {}, 500); - E5MultilingualCalloutLogic.actions.setE5MultilingualModelPollingId(timeout); - - expect(E5MultilingualCalloutLogic.values.e5MultilingualModelPollTimeoutId).toEqual(timeout); - }); - }); - }); - - describe('selectors', () => { - describe('isCreateButtonDisabled', () => { - it('is set to false if the fetch model API is idle', () => { - CreateE5MultilingualModelApiLogic.actions.apiReset(); - expect(E5MultilingualCalloutLogic.values.isCreateButtonDisabled).toBe(false); - }); - it('is set to true if the fetch model API is not idle', () => { - CreateE5MultilingualModelApiLogic.actions.apiSuccess({ - deploymentState: MlModelDeploymentState.Downloading, - modelId: 'mock-model-id', - }); - expect(E5MultilingualCalloutLogic.values.isCreateButtonDisabled).toBe(true); - }); - }); - - describe('e5MultilingualError', () => { - const error = { - body: { - error: 'Error with E5 Multilingual deployment', - message: 'Mocked error message', - statusCode: 500, - }, - } as HttpError; - - it('returns null when there are no errors', () => { - CreateE5MultilingualModelApiLogic.actions.apiReset(); - FetchE5MultilingualModelApiLogic.actions.apiReset(); - StartE5MultilingualModelApiLogic.actions.apiReset(); - expect(E5MultilingualCalloutLogic.values.e5MultilingualError).toBe(null); - }); - it('returns extracted error for create', () => { - CreateE5MultilingualModelApiLogic.actions.apiError(error); - expect(E5MultilingualCalloutLogic.values.e5MultilingualError).toStrictEqual({ - title: 'Error with E5 Multilingual deployment', - message: 'Mocked error message', - }); - }); - it('returns extracted error for fetch', () => { - FetchE5MultilingualModelApiLogic.actions.apiError(error); - expect(E5MultilingualCalloutLogic.values.e5MultilingualError).toStrictEqual({ - title: 'Error fetching E5 Multilingual model', - message: 'Mocked error message', - }); - }); - it('returns extracted error for start', () => { - StartE5MultilingualModelApiLogic.actions.apiError(error); - expect(E5MultilingualCalloutLogic.values.e5MultilingualError).toStrictEqual({ - title: 'Error starting E5 Multilingual deployment', - message: 'Mocked error message', - }); - }); - }); - - describe('isModelDownloadInProgress', () => { - it('is set to true if the model is downloading', () => { - FetchE5MultilingualModelApiLogic.actions.apiSuccess({ - deploymentState: MlModelDeploymentState.Downloading, - modelId: 'mock-model-id', - targetAllocationCount: 1, - nodeAllocationCount: 1, - threadsPerAllocation: 1, - }); - expect(E5MultilingualCalloutLogic.values.isModelDownloadInProgress).toBe(true); - }); - it('is set to false if the model is downloading', () => { - FetchE5MultilingualModelApiLogic.actions.apiSuccess({ - deploymentState: MlModelDeploymentState.Started, - modelId: 'mock-model-id', - targetAllocationCount: 1, - nodeAllocationCount: 1, - threadsPerAllocation: 1, - }); - expect(E5MultilingualCalloutLogic.values.isModelDownloadInProgress).toBe(false); - }); - }); - - describe('isModelDownloaded', () => { - it('is set to true if the model is downloaded', () => { - FetchE5MultilingualModelApiLogic.actions.apiSuccess({ - deploymentState: MlModelDeploymentState.Downloaded, - modelId: 'mock-model-id', - targetAllocationCount: 1, - nodeAllocationCount: 1, - threadsPerAllocation: 1, - }); - expect(E5MultilingualCalloutLogic.values.isModelDownloaded).toBe(true); - }); - it('is set to false if the model is not downloaded', () => { - FetchE5MultilingualModelApiLogic.actions.apiSuccess({ - deploymentState: MlModelDeploymentState.NotDeployed, - modelId: 'mock-model-id', - targetAllocationCount: 1, - nodeAllocationCount: 1, - threadsPerAllocation: 1, - }); - expect(E5MultilingualCalloutLogic.values.isModelDownloaded).toBe(false); - }); - }); - - describe('isModelRunningSingleThreaded', () => { - it('is set to true if the model has 1 target allocation and 1 thread', () => { - FetchE5MultilingualModelApiLogic.actions.apiSuccess({ - deploymentState: MlModelDeploymentState.FullyAllocated, - modelId: 'mock-model-id', - targetAllocationCount: 1, - nodeAllocationCount: 1, - threadsPerAllocation: 1, - }); - expect(E5MultilingualCalloutLogic.values.isModelRunningSingleThreaded).toBe(true); - }); - it('is set to false if the model has multiple target allocations', () => { - FetchE5MultilingualModelApiLogic.actions.apiSuccess({ - deploymentState: MlModelDeploymentState.FullyAllocated, - modelId: 'mock-model-id', - targetAllocationCount: 2, - nodeAllocationCount: 2, - threadsPerAllocation: 1, - }); - expect(E5MultilingualCalloutLogic.values.isModelRunningSingleThreaded).toBe(false); - }); - it('is set to false if the model runs on multiple threads', () => { - FetchE5MultilingualModelApiLogic.actions.apiSuccess({ - deploymentState: MlModelDeploymentState.FullyAllocated, - modelId: 'mock-model-id', - targetAllocationCount: 1, - nodeAllocationCount: 1, - threadsPerAllocation: 4, - }); - expect(E5MultilingualCalloutLogic.values.isModelRunningSingleThreaded).toBe(false); - }); - }); - - describe('isModelStarted', () => { - it('is set to true if the model is started', () => { - FetchE5MultilingualModelApiLogic.actions.apiSuccess({ - deploymentState: MlModelDeploymentState.FullyAllocated, - modelId: 'mock-model-id', - targetAllocationCount: 1, - nodeAllocationCount: 1, - threadsPerAllocation: 1, - }); - expect(E5MultilingualCalloutLogic.values.isModelStarted).toBe(true); - }); - it('is set to false if the model is not started', () => { - FetchE5MultilingualModelApiLogic.actions.apiSuccess({ - deploymentState: MlModelDeploymentState.NotDeployed, - modelId: 'mock-model-id', - targetAllocationCount: 1, - nodeAllocationCount: 1, - threadsPerAllocation: 1, - }); - expect(E5MultilingualCalloutLogic.values.isModelStarted).toBe(false); - }); - }); - - describe('isPollingE5MultilingualModelActive', () => { - it('is set to false if polling is not active', () => { - mount({ - ...DEFAULT_VALUES, - e5MultilingualModelPollTimeoutId: null, - }); - - expect(E5MultilingualCalloutLogic.values.isPollingE5MultilingualModelActive).toBe(false); - }); - it('is set to true if polling is active', () => { - mount({ - ...DEFAULT_VALUES, - e5MultilingualModelPollTimeoutId: 'timeout-id', - }); - - expect(E5MultilingualCalloutLogic.values.isPollingE5MultilingualModelActive).toBe(true); - }); - }); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_callout_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_callout_logic.ts deleted file mode 100644 index 4c2a9acb453bf..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_callout_logic.ts +++ /dev/null @@ -1,308 +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 { kea, MakeLogicType } from 'kea'; - -import { i18n } from '@kbn/i18n'; - -import { HttpError, Status } from '../../../../../../../../common/types/api'; -import { MlModelDeploymentState } from '../../../../../../../../common/types/ml'; -import { getErrorsFromHttpResponse } from '../../../../../../shared/flash_messages/handle_api_errors'; - -import { - CreateE5MultilingualModelApiLogic, - CreateE5MultilingualModelApiLogicActions, - CreateE5MultilingualModelResponse, -} from '../../../../../api/ml_models/e5_multilingual/create_e5_multilingual_model_api_logic'; -import { - FetchE5MultilingualModelApiLogic, - FetchE5MultilingualModelApiLogicActions, - FetchE5MultilingualModelResponse, -} from '../../../../../api/ml_models/e5_multilingual/fetch_e5_multilingual_model_api_logic'; -import { - StartE5MultilingualModelApiLogic, - StartE5MultilingualModelApiLogicActions, -} from '../../../../../api/ml_models/e5_multilingual/start_e5_multilingual_model_api_logic'; - -const FETCH_E5_MULTILINGUAL_MODEL_POLLING_DURATION = 5000; // 5 seconds -const FETCH_E5_MULTILINGUAL_MODEL_POLLING_DURATION_ON_FAILURE = 30000; // 30 seconds -const E5_MULTILINGUAL_MODEL_ID = '.multilingual-e5-small'; - -interface E5MultilingualCalloutActions { - clearE5MultilingualModelPollingId: () => void; - createE5MultilingualModel: () => void; - createE5MultilingualModelMakeRequest: CreateE5MultilingualModelApiLogicActions['makeRequest']; - createE5MultilingualModelPollingTimeout: (duration: number) => { duration: number }; - createE5MultilingualModelSuccess: CreateE5MultilingualModelApiLogicActions['apiSuccess']; - fetchE5MultilingualModel: () => void; - fetchE5MultilingualModelMakeRequest: FetchE5MultilingualModelApiLogicActions['makeRequest']; - fetchE5MultilingualModelError: FetchE5MultilingualModelApiLogicActions['apiError']; - fetchE5MultilingualModelSuccess: FetchE5MultilingualModelApiLogicActions['apiSuccess']; - setE5MultilingualModelPollingId: (pollTimeoutId: ReturnType) => { - pollTimeoutId: ReturnType; - }; - startPollingE5MultilingualModel: () => void; - startE5MultilingualModel: () => void; - startE5MultilingualModelMakeRequest: StartE5MultilingualModelApiLogicActions['makeRequest']; - startE5MultilingualModelSuccess: StartE5MultilingualModelApiLogicActions['apiSuccess']; - stopPollingE5MultilingualModel: () => void; - e5MultilingualModel: FetchE5MultilingualModelApiLogicActions['apiSuccess']; -} - -export interface E5MultilingualCalloutError { - title: string; - message: string; -} - -export interface E5MultilingualCalloutValues { - createE5MultilingualModelError: HttpError | undefined; - createE5MultilingualModelStatus: Status; - createdE5MultilingualModel: CreateE5MultilingualModelResponse | undefined; - fetchE5MultilingualModelError: HttpError | undefined; - isCreateButtonDisabled: boolean; - isModelDownloadInProgress: boolean; - isModelDownloaded: boolean; - isModelRunningSingleThreaded: boolean; - isModelStarted: boolean; - isPollingE5MultilingualModelActive: boolean; - isStartButtonDisabled: boolean; - startE5MultilingualModelError: HttpError | undefined; - startE5MultilingualModelStatus: Status; - e5MultilingualModel: FetchE5MultilingualModelResponse | undefined; - e5MultilingualModelPollTimeoutId: null | ReturnType; - e5MultilingualError: E5MultilingualCalloutError | null; -} - -/** - * Extracts the topmost error in precedence order (create > start > fetch). - * @param createError - * @param fetchError - * @param startError - * @returns the extracted error or null if there is no error - */ -export const getE5MultilingualError = ( - createError: HttpError | undefined, - fetchError: HttpError | undefined, - startError: HttpError | undefined -) => { - return createError !== undefined - ? { - title: i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.e5MultilingualCreateError.title', - { - defaultMessage: 'Error with E5 Multilingual deployment', - } - ), - message: getErrorsFromHttpResponse(createError)[0], - } - : startError !== undefined - ? { - title: i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.e5MultilingualStartError.title', - { - defaultMessage: 'Error starting E5 Multilingual deployment', - } - ), - message: getErrorsFromHttpResponse(startError)[0], - } - : fetchError !== undefined - ? { - title: i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.e5MultilingualFetchError.title', - { - defaultMessage: 'Error fetching E5 Multilingual model', - } - ), - message: getErrorsFromHttpResponse(fetchError)[0], - } - : null; -}; - -export const E5MultilingualCalloutLogic = kea< - MakeLogicType ->({ - actions: { - clearE5MultilingualModelPollingId: true, - createE5MultilingualModelPollingTimeout: (duration) => ({ duration }), - setE5MultilingualModelPollingId: (pollTimeoutId: ReturnType) => ({ - pollTimeoutId, - }), - startPollingE5MultilingualModel: true, - stopPollingE5MultilingualModel: true, - createE5MultilingualModel: true, - fetchE5MultilingualModel: true, - startE5MultilingualModel: true, - }, - connect: { - actions: [ - CreateE5MultilingualModelApiLogic, - [ - 'makeRequest as createE5MultilingualModelMakeRequest', - 'apiSuccess as createE5MultilingualModelSuccess', - 'apiError as createE5MultilingualModelError', - ], - FetchE5MultilingualModelApiLogic, - [ - 'makeRequest as fetchE5MultilingualModelMakeRequest', - 'apiSuccess as fetchE5MultilingualModelSuccess', - 'apiError as fetchE5MultilingualModelError', - ], - StartE5MultilingualModelApiLogic, - [ - 'makeRequest as startE5MultilingualModelMakeRequest', - 'apiSuccess as startE5MultilingualModelSuccess', - 'apiError as startE5MultilingualModelError', - ], - ], - values: [ - CreateE5MultilingualModelApiLogic, - [ - 'data as createdE5MultilingualModel', - 'status as createE5MultilingualModelStatus', - 'error as createE5MultilingualModelError', - ], - FetchE5MultilingualModelApiLogic, - ['data as e5MultilingualModel', 'error as fetchE5MultilingualModelError'], - StartE5MultilingualModelApiLogic, - ['status as startE5MultilingualModelStatus', 'error as startE5MultilingualModelError'], - ], - }, - events: ({ actions, values }) => ({ - afterMount: () => { - actions.fetchE5MultilingualModel(); - }, - beforeUnmount: () => { - if (values.e5MultilingualModelPollTimeoutId !== null) { - actions.stopPollingE5MultilingualModel(); - } - }, - }), - listeners: ({ actions, values }) => ({ - createE5MultilingualModel: () => - actions.createE5MultilingualModelMakeRequest({ modelId: E5_MULTILINGUAL_MODEL_ID }), - fetchE5MultilingualModel: () => - actions.fetchE5MultilingualModelMakeRequest({ modelId: E5_MULTILINGUAL_MODEL_ID }), - startE5MultilingualModel: () => - actions.startE5MultilingualModelMakeRequest({ modelId: E5_MULTILINGUAL_MODEL_ID }), - createE5MultilingualModelPollingTimeout: ({ duration }) => { - if (values.e5MultilingualModelPollTimeoutId !== null) { - clearTimeout(values.e5MultilingualModelPollTimeoutId); - } - const timeoutId = setTimeout(() => { - actions.fetchE5MultilingualModel(); - }, duration); - actions.setE5MultilingualModelPollingId(timeoutId); - }, - createE5MultilingualModelSuccess: () => { - actions.fetchE5MultilingualModel(); - actions.startPollingE5MultilingualModel(); - }, - fetchE5MultilingualModelError: () => { - if (values.isPollingE5MultilingualModelActive) { - actions.createE5MultilingualModelPollingTimeout( - FETCH_E5_MULTILINGUAL_MODEL_POLLING_DURATION_ON_FAILURE - ); - } - }, - fetchE5MultilingualModelSuccess: (data) => { - if (data?.deploymentState === MlModelDeploymentState.Downloading) { - if (!values.isPollingE5MultilingualModelActive) { - actions.startPollingE5MultilingualModel(); - } else { - actions.createE5MultilingualModelPollingTimeout( - FETCH_E5_MULTILINGUAL_MODEL_POLLING_DURATION - ); - } - } else if ( - data?.deploymentState === MlModelDeploymentState.Downloaded && - values.isPollingE5MultilingualModelActive - ) { - actions.stopPollingE5MultilingualModel(); - } - }, - startPollingE5MultilingualModel: () => { - if (values.e5MultilingualModelPollTimeoutId !== null) { - clearTimeout(values.e5MultilingualModelPollTimeoutId); - } - actions.createE5MultilingualModelPollingTimeout(FETCH_E5_MULTILINGUAL_MODEL_POLLING_DURATION); - }, - startE5MultilingualModelSuccess: () => { - actions.fetchE5MultilingualModel(); - }, - stopPollingE5MultilingualModel: () => { - if (values.e5MultilingualModelPollTimeoutId !== null) { - clearTimeout(values.e5MultilingualModelPollTimeoutId); - actions.clearE5MultilingualModelPollingId(); - } - }, - }), - path: ['enterprise_search', 'content', 'e5_multilingual_callout_logic'], - reducers: { - e5MultilingualModelPollTimeoutId: [ - null, - { - clearE5MultilingualModelPollingId: () => null, - setE5MultilingualModelPollingId: (_, { pollTimeoutId }) => pollTimeoutId, - }, - ], - }, - selectors: ({ selectors }) => ({ - isCreateButtonDisabled: [ - () => [selectors.createE5MultilingualModelStatus], - (status: Status) => status !== Status.IDLE && status !== Status.ERROR, - ], - isModelDownloadInProgress: [ - () => [selectors.e5MultilingualModel], - (data: FetchE5MultilingualModelResponse) => - data?.deploymentState === MlModelDeploymentState.Downloading, - ], - isModelDownloaded: [ - () => [selectors.e5MultilingualModel], - (data: FetchE5MultilingualModelResponse) => - data?.deploymentState === MlModelDeploymentState.Downloaded, - ], - isModelStarted: [ - () => [selectors.e5MultilingualModel], - (data: FetchE5MultilingualModelResponse) => - data?.deploymentState === MlModelDeploymentState.Starting || - data?.deploymentState === MlModelDeploymentState.Started || - data?.deploymentState === MlModelDeploymentState.FullyAllocated, - ], - isPollingE5MultilingualModelActive: [ - () => [selectors.e5MultilingualModelPollTimeoutId], - (pollingTimeoutId: E5MultilingualCalloutValues['e5MultilingualModelPollTimeoutId']) => - pollingTimeoutId !== null, - ], - e5MultilingualError: [ - () => [ - selectors.createE5MultilingualModelError, - selectors.fetchE5MultilingualModelError, - selectors.startE5MultilingualModelError, - ], - ( - createE5MultilingualError: E5MultilingualCalloutValues['createE5MultilingualModelError'], - fetchE5MultilingualError: E5MultilingualCalloutValues['fetchE5MultilingualModelError'], - startE5MultilingualError: E5MultilingualCalloutValues['startE5MultilingualModelError'] - ) => - getE5MultilingualError( - createE5MultilingualError, - fetchE5MultilingualError, - startE5MultilingualError - ), - ], - isStartButtonDisabled: [ - () => [selectors.startE5MultilingualModelStatus], - (status: Status) => status !== Status.IDLE && status !== Status.ERROR, - ], - isModelRunningSingleThreaded: [ - () => [selectors.e5MultilingualModel], - (data: FetchE5MultilingualModelResponse) => - // Running single threaded if model has max 1 deployment on 1 node with 1 thread - data?.targetAllocationCount * data?.threadsPerAllocation <= 1, - ], - }), -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_errors.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_errors.test.tsx deleted file mode 100644 index d218c8d111a2b..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_errors.test.tsx +++ /dev/null @@ -1,34 +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 { setMockValues } from '../../../../../../__mocks__/kea_logic'; - -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { EuiCallOut } from '@elastic/eui'; - -import { E5MultilingualErrors } from './e5_multilingual_errors'; - -describe('E5MultilingualErrors', () => { - beforeEach(() => { - jest.clearAllMocks(); - setMockValues({}); - }); - const error = { - title: 'some-error-title', - message: 'some-error-message', - }; - it('extracts error panel with the given title and message', () => { - const wrapper = shallow(); - expect(wrapper.find(EuiCallOut).length).toBe(1); - expect(wrapper.find(EuiCallOut).prop('title')).toEqual(error.title); - expect(wrapper.find(EuiCallOut).find('p').length).toBe(1); - expect(wrapper.find(EuiCallOut).find('p').text()).toEqual(error.message); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_errors.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_errors.tsx deleted file mode 100644 index 78038fee5d122..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/e5_multilingual_errors.tsx +++ /dev/null @@ -1,37 +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 React from 'react'; - -import { EuiCallOut } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; - -import { EuiLinkTo } from '../../../../../../shared/react_router_helpers'; - -import { SendEnterpriseSearchTelemetry } from '../../../../../../shared/telemetry'; - -import { ML_NOTIFICATIONS_PATH } from '../../../../../routes'; - -export function E5MultilingualErrors({ error }: { error: { title: string; message: string } }) { - return ( - <> - - -

{error.message}

- - {i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.e5MultilingualCreateError.mlNotificationsLink', - { - defaultMessage: 'Machine Learning notifications', - } - )} - -
- - ); -} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_deployed.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_deployed.test.tsx deleted file mode 100644 index 58df830f43e2d..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_deployed.test.tsx +++ /dev/null @@ -1,81 +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 { setMockValues } from '../../../../../../__mocks__/kea_logic'; - -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { EuiButton } from '@elastic/eui'; - -import { E5MultilingualDismissButton } from './e5_multilingual_callout'; -import { ModelDeployed } from './model_deployed'; - -const DEFAULT_VALUES = { - startE5MultilingualModelError: undefined, - isCreateButtonDisabled: false, - isModelDownloadInProgress: false, - isModelDownloaded: false, - isModelStarted: false, - isStartButtonDisabled: false, -}; - -describe('ModelDeployed', () => { - beforeEach(() => { - jest.clearAllMocks(); - setMockValues(DEFAULT_VALUES); - }); - it('renders start button', () => { - const wrapper = shallow( - {}} - ingestionMethod="crawler" - isDismissable={false} - isStartButtonDisabled={false} - /> - ); - expect(wrapper.find(EuiButton).length).toBe(1); - const button = wrapper.find(EuiButton); - expect(button.prop('disabled')).toBe(false); - }); - it('renders disabled start button if it is set to disabled', () => { - const wrapper = shallow( - {}} - ingestionMethod="crawler" - isDismissable={false} - isStartButtonDisabled - /> - ); - expect(wrapper.find(EuiButton).length).toBe(1); - const button = wrapper.find(EuiButton); - expect(button.prop('disabled')).toBe(true); - }); - it('renders dismiss button if it is set to dismissable', () => { - const wrapper = shallow( - {}} - ingestionMethod="crawler" - isDismissable - isStartButtonDisabled={false} - /> - ); - expect(wrapper.find(E5MultilingualDismissButton).length).toBe(1); - }); - it('does not render dismiss button if it is set to non-dismissable', () => { - const wrapper = shallow( - {}} - ingestionMethod="crawler" - isDismissable={false} - isStartButtonDisabled={false} - /> - ); - expect(wrapper.find(E5MultilingualDismissButton).length).toBe(0); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_deployed.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_deployed.tsx deleted file mode 100644 index 20444dd5f7054..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_deployed.tsx +++ /dev/null @@ -1,113 +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 React from 'react'; - -import { useActions } from 'kea'; - -import { - EuiButton, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiSpacer, - EuiText, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { - E5MultilingualCallOutState, - E5MultilingualDismissButton, - FineTuneModelsButton, -} from './e5_multilingual_callout'; -import { E5MultilingualCalloutLogic } from './e5_multilingual_callout_logic'; - -export const ModelDeployed = ({ - dismiss, - ingestionMethod, - isDismissable, - isStartButtonDisabled, -}: Pick< - E5MultilingualCallOutState, - 'dismiss' | 'ingestionMethod' | 'isDismissable' | 'isStartButtonDisabled' ->) => { - const { startE5MultilingualModel } = useActions(E5MultilingualCalloutLogic); - - return ( - - - - - - - - - -

- {i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.e5MultilingualCallOut.deployedTitle', - { defaultMessage: 'Your E5 model has deployed but not started.' } - )} -

-
-
- {isDismissable && ( - - - - )} -
-
- - -

- {i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.e5MultilingualCallOut.deployedBody', - { - defaultMessage: - 'You may start the model in a single-threaded configuration for testing, or tune the performance for a production environment.', - } - )} -

-
-
- - - - - - - startE5MultilingualModel()} - > - {i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.e5MultilingualCallOut.startModelButton.label', - { - defaultMessage: 'Start single-threaded', - } - )} - - - - - - - -
-
- ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_deployment_in_progress.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_deployment_in_progress.test.tsx deleted file mode 100644 index 1594824aa1a85..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_deployment_in_progress.test.tsx +++ /dev/null @@ -1,39 +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 { setMockValues } from '../../../../../../__mocks__/kea_logic'; - -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { E5MultilingualDismissButton } from './e5_multilingual_callout'; -import { ModelDeploymentInProgress } from './model_deployment_in_progress'; - -const DEFAULT_VALUES = { - startE5MultilingualModelError: undefined, - isCreateButtonDisabled: false, - isModelDownloadInProgress: false, - isModelDownloaded: false, - isModelStarted: false, - isStartButtonDisabled: false, -}; - -describe('ModelDeploymentInProgress', () => { - beforeEach(() => { - jest.clearAllMocks(); - setMockValues(DEFAULT_VALUES); - }); - it('renders dismiss button if it is set to dismissable', () => { - const wrapper = shallow( {}} isDismissable />); - expect(wrapper.find(E5MultilingualDismissButton).length).toBe(1); - }); - it('does not render dismiss button if it is set to non-dismissable', () => { - const wrapper = shallow( {}} isDismissable={false} />); - expect(wrapper.find(E5MultilingualDismissButton).length).toBe(0); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_deployment_in_progress.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_deployment_in_progress.tsx deleted file mode 100644 index 7f7627a6f03fc..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_deployment_in_progress.tsx +++ /dev/null @@ -1,58 +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 React from 'react'; - -import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { E5MultilingualCallOutState, E5MultilingualDismissButton } from './e5_multilingual_callout'; - -export const ModelDeploymentInProgress = ({ - dismiss, - isDismissable, -}: Pick) => ( - - - - - - - - - -

- {i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.e5MultilingualCallOut.deployingTitle', - { defaultMessage: 'Your E5 model is deploying.' } - )} -

-
-
- {isDismissable && ( - - - - )} -
-
- - -

- {i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.e5MultilingualCallOut.deployingBody', - { - defaultMessage: - 'You can continue creating your pipeline with other uploaded models in the meantime.', - } - )} -

-
-
-
-
-); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_started.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_started.test.tsx deleted file mode 100644 index 80563cef58cc4..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_started.test.tsx +++ /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 { setMockValues } from '../../../../../../__mocks__/kea_logic'; - -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { EuiText } from '@elastic/eui'; - -import { E5MultilingualDismissButton, FineTuneModelsButton } from './e5_multilingual_callout'; -import { ModelStarted } from './model_started'; - -const DEFAULT_VALUES = { - startE5MultilingualModelError: undefined, - isCreateButtonDisabled: false, - isModelDownloadInProgress: false, - isModelDownloaded: false, - isModelStarted: false, - isStartButtonDisabled: false, -}; - -describe('ModelStarted', () => { - beforeEach(() => { - jest.clearAllMocks(); - setMockValues(DEFAULT_VALUES); - }); - it('renders dismiss button if it is set to dismissable', () => { - const wrapper = shallow( - {}} isCompact={false} isDismissable isSingleThreaded /> - ); - expect(wrapper.find(E5MultilingualDismissButton).length).toBe(1); - }); - it('does not render dismiss button if it is set to non-dismissable', () => { - const wrapper = shallow( - {}} isCompact={false} isDismissable={false} isSingleThreaded /> - ); - expect(wrapper.find(E5MultilingualDismissButton).length).toBe(0); - }); - it('renders fine-tune button if the model is running single-threaded', () => { - const wrapper = shallow( - {}} isCompact={false} isDismissable isSingleThreaded /> - ); - expect(wrapper.find(FineTuneModelsButton).length).toBe(1); - }); - it('does not render description if it is set to compact', () => { - const wrapper = shallow( - {}} isCompact isDismissable isSingleThreaded /> - ); - expect(wrapper.find(EuiText).length).toBe(1); // Title only - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_started.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_started.tsx deleted file mode 100644 index c6a444373886c..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/e5_multilingual_callout/model_started.tsx +++ /dev/null @@ -1,135 +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 React from 'react'; - -import { - EuiButtonEmpty, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiText, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { KibanaLogic } from '../../../../../../shared/kibana'; - -import { TRAINED_MODELS_PATH } from '../utils'; - -import { - E5MultilingualCallOutState, - E5MultilingualDismissButton, - FineTuneModelsButton, -} from './e5_multilingual_callout'; - -export const ModelStarted = ({ - dismiss, - isCompact, - isDismissable, - isSingleThreaded, -}: Pick< - E5MultilingualCallOutState, - 'dismiss' | 'isCompact' | 'isDismissable' | 'isSingleThreaded' ->) => ( - - - - - - - - - -

- {isSingleThreaded - ? isCompact - ? i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.e5MultilingualCallOut.startedSingleThreadedTitleCompact', - { defaultMessage: 'Your E5 model is running single-threaded.' } - ) - : i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.e5MultilingualCallOut.startedSingleThreadedTitle', - { defaultMessage: 'Your E5 model has started single-threaded.' } - ) - : isCompact - ? i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.e5MultilingualCallOut.startedTitleCompact', - { defaultMessage: 'Your E5 model is running.' } - ) - : i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.e5MultilingualCallOut.startedTitle', - { defaultMessage: 'Your E5 model has started.' } - )} -

-
-
- {isDismissable && ( - - - - )} -
-
- {!isCompact && ( - <> - - -

- {isSingleThreaded - ? i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.e5MultilingualCallOut.startedSingleThreadedBody', - { - defaultMessage: - 'This single-threaded configuration is great for testing your custom inference pipelines, however performance should be fine-tuned for production.', - } - ) - : i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.e5MultilingualCallOut.startedBody', - { - defaultMessage: 'Enjoy the power of E5 in your custom Inference pipeline.', - } - )} -

-
-
- - - - {isSingleThreaded ? ( - - ) : ( - - KibanaLogic.values.navigateToUrl(TRAINED_MODELS_PATH, { - shouldNotCreateHref: true, - }) - } - > - {i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.e5MultilingualCallOut.viewModelsButton', - { - defaultMessage: 'View details', - } - )} - - )} - - - - - )} -
-
-); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout/deploy_model.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout/deploy_model.tsx index 74ce840252a8b..4320fff515b4e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout/deploy_model.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout/deploy_model.tsx @@ -19,7 +19,7 @@ import { EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedHTMLMessage, FormattedMessage } from '@kbn/i18n-react'; +import { FormattedMessage, FormattedHTMLMessage } from '@kbn/i18n-react'; import { docLinks } from '../../../../../../shared/doc_links'; From 7c0f5ef8cfc73204de85832f107d67b3d803c628 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 10 Jan 2024 16:18:38 +0000 Subject: [PATCH 21/26] skip flaky suite (#174384) --- .../registered_attachments_property_actions.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/public/components/user_actions/property_actions/registered_attachments_property_actions.test.tsx b/x-pack/plugins/cases/public/components/user_actions/property_actions/registered_attachments_property_actions.test.tsx index 273ac1db45eaf..8cbc69e30039f 100644 --- a/x-pack/plugins/cases/public/components/user_actions/property_actions/registered_attachments_property_actions.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/property_actions/registered_attachments_property_actions.test.tsx @@ -18,7 +18,8 @@ import { import { RegisteredAttachmentsPropertyActions } from './registered_attachments_property_actions'; import { AttachmentActionType } from '../../../client/attachment_framework/types'; -describe('RegisteredAttachmentsPropertyActions', () => { +// FLAKY: https://github.com/elastic/kibana/issues/174384 +describe.skip('RegisteredAttachmentsPropertyActions', () => { let appMock: AppMockRenderer; const props = { From 88b74ba39f1c2cac267b7cd2d4ff4951e81f22f3 Mon Sep 17 00:00:00 2001 From: Kyle Pollich Date: Wed, 10 Jan 2024 11:22:50 -0500 Subject: [PATCH 22/26] [Fleet] Don't throw concurrent installation error when encountering conflicts (#173190) ## Summary Closes https://github.com/elastic/kibana/issues/171986 ## How to test You'll need to get into a state where a tag with a conflicting ID exists in a second Kibana space outside of the default space. It seems like the tags API doesn't support providing an ID at least from the REST API, so that makes creating duplicate tags in other spaces a bit challenging. e.g. ``` POST kbn:api/saved_objects_tagging/tags/create { "id": "my-tag", "name": "My tag", "description": "", "color": "#FFFFFF" } ``` results in ``` { "statusCode": 400, "error": "Bad Request", "message": "[request body.id]: definition for this key is missing" } ``` So, in order to enable creating tags with ID's easily, I made some quick code changes to the tags service ```diff diff --git a/x-pack/plugins/saved_objects_tagging/server/routes/tags/create_tag.ts b/x-pack/plugins/saved_objects_tagging/server/routes/tags/create_tag.ts index 0c48168eed2..fa0e7fbe7b9 100644 --- a/x-pack/plugins/saved_objects_tagging/server/routes/tags/create_tag.ts +++ b/x-pack/plugins/saved_objects_tagging/server/routes/tags/create_tag.ts @@ -6,6 +6,7 @@ */ import { schema } from '@kbn/config-schema'; +import { omit } from 'lodash'; import type { TagsPluginRouter } from '../../types'; import { TagValidationError } from '../../services/tags'; @@ -15,6 +16,7 @@ export const registerCreateTagRoute = (router: TagsPluginRouter) => { path: '/api/saved_objects_tagging/tags/create', validate: { body: schema.object({ + id: schema.maybe(schema.string()), name: schema.string(), description: schema.string(), color: schema.string(), @@ -32,7 +34,7 @@ export const registerCreateTagRoute = (router: TagsPluginRouter) => { }); } - const tag = await tagsClient.create(req.body); + const tag = await tagsClient.create(omit(req.body, 'id'), { id: req.body.id }); return res.ok({ body: { tag, ``` With those changes in place (I don't think committing them is necessary), I was able to create a tag in my second space e.g. ``` POST kbn:api/saved_objects_tagging/tags/create { "id": "fleet-pkg-nginx-default", "name": "Nginx duplicate tag", "description": "", "color": "#DADADA" } ``` Then, from my default space I installed the nginx package ``` POST kbn:/api/fleet/epm/packages/nginx ``` This throws the concurrent install error as expected on `main` ``` { "statusCode": 409, "error": "Conflict", "message": "Concurrent installation or upgrade of nginx-1.17.0 detected, aborting. Original error: Saved object [tag/fleet-pkg-nginx-default] conflict" } ``` With this PR, we get this error instead ``` { "statusCode": 400, "error": "Bad Request", "message": "Saved Object conflict encountered while installing nginx-1.17.0. There may be a conflicting Saved Object saved to another Space. Original error: Saved object [tag/fleet-pkg-nginx-default] conflict" } ``` --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/fleet/server/errors/handlers.ts | 4 ++ x-pack/plugins/fleet/server/errors/index.ts | 1 + .../epm/packages/_install_package.test.ts | 55 ++++++++++++++++++- .../services/epm/packages/_install_package.ts | 10 ++-- 4 files changed, 64 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/fleet/server/errors/handlers.ts b/x-pack/plugins/fleet/server/errors/handlers.ts index c85bfeced9db1..fef8cd1872413 100644 --- a/x-pack/plugins/fleet/server/errors/handlers.ts +++ b/x-pack/plugins/fleet/server/errors/handlers.ts @@ -42,6 +42,7 @@ import { AgentRequestInvalidError, PackagePolicyRequestError, FleetNotFoundError, + PackageSavedObjectConflictError, } from '.'; type IngestErrorHandler = ( @@ -100,6 +101,9 @@ const getHTTPResponseCode = (error: FleetError): number => { if (error instanceof ConcurrentInstallOperationError) { return 409; } + if (error instanceof PackageSavedObjectConflictError) { + return 409; + } if (error instanceof PackagePolicyNameExistsError) { return 409; } diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts index ce7245672e623..6a69581c11965 100644 --- a/x-pack/plugins/fleet/server/errors/index.ts +++ b/x-pack/plugins/fleet/server/errors/index.ts @@ -43,6 +43,7 @@ export class PackageInvalidArchiveError extends FleetError {} export class PackageRemovalError extends FleetError {} export class PackageESError extends FleetError {} export class ConcurrentInstallOperationError extends FleetError {} +export class PackageSavedObjectConflictError extends FleetError {} export class KibanaSOReferenceError extends FleetError {} export class PackageAlreadyInstalledError extends FleetError {} diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts index d6d653fd98c4e..ce416a6277313 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts @@ -14,7 +14,7 @@ import { savedObjectsClientMock, elasticsearchServiceMock } from '@kbn/core/serv import { loggerMock } from '@kbn/logging-mocks'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; -import { ConcurrentInstallOperationError } from '../../../errors'; +import { ConcurrentInstallOperationError, PackageSavedObjectConflictError } from '../../../errors'; import type { Installation } from '../../../../common'; @@ -254,7 +254,6 @@ describe('_installPackage', () => { }); describe('when package is stuck in `installing`', () => { - afterEach(() => {}); const mockInstalledPackageSo: SavedObject = { id: 'mocked-package', attributes: { @@ -387,4 +386,56 @@ describe('_installPackage', () => { }); }); }); + + it('surfaces saved object conflicts error', () => { + appContextService.start( + createAppContextStartContractMock({ + internal: { + disableILMPolicies: false, + disableProxies: false, + fleetServerStandalone: false, + onlyAllowAgentUpgradeToKnownVersions: false, + retrySetupOnBoot: false, + registry: { + kibanaVersionCheckEnabled: true, + capabilities: [], + excludePackages: [], + }, + }, + }) + ); + + mockedInstallKibanaAssetsAndReferences.mockRejectedValueOnce( + new PackageSavedObjectConflictError('test') + ); + + expect( + _installPackage({ + savedObjectsClient: soClient, + // @ts-ignore + savedObjectsImporter: jest.fn(), + esClient, + logger: loggerMock.create(), + packageInstallContext: { + packageInfo: { + title: 'title', + name: 'xyz', + version: '4.5.6', + description: 'test', + type: 'integration', + categories: ['cloud', 'custom'], + format_version: 'string', + release: 'experimental', + conditions: { kibana: { version: 'x.y.z' } }, + owner: { github: 'elastic/fleet' }, + } as any, + assetsMap: new Map(), + paths: [], + }, + installType: 'install', + installSource: 'registry', + spaceId: DEFAULT_SPACE_ID, + }) + ).rejects.toThrowError(PackageSavedObjectConflictError); + }); }); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index 6c30d3a8d332d..e182fd8721075 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -45,7 +45,7 @@ import { installTransforms } from '../elasticsearch/transform/install'; import { installMlModel } from '../elasticsearch/ml_model'; import { installIlmForDataStream } from '../elasticsearch/datastream_ilm/install'; import { saveArchiveEntriesFromAssetsMap } from '../archive/storage'; -import { ConcurrentInstallOperationError } from '../../../errors'; +import { ConcurrentInstallOperationError, PackageSavedObjectConflictError } from '../../../errors'; import { appContextService, packagePolicyService } from '../..'; import { auditLoggingService } from '../../audit_logging'; @@ -387,10 +387,12 @@ export async function _installPackage({ return [...installedKibanaAssetsRefs, ...esReferences]; } catch (err) { if (SavedObjectsErrorHelpers.isConflictError(err)) { - throw new ConcurrentInstallOperationError( - `Concurrent installation or upgrade of ${pkgName || 'unknown'}-${ + throw new PackageSavedObjectConflictError( + `Saved Object conflict encountered while installing ${pkgName || 'unknown'}-${ pkgVersion || 'unknown' - } detected, aborting. Original error: ${err.message}` + }. There may be a conflicting Saved Object saved to another Space. Original error: ${ + err.message + }` ); } else { throw err; From 5a8b7afe0bf89c094707fabdc5fd26457346632a Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Wed, 10 Jan 2024 09:58:34 -0700 Subject: [PATCH 23/26] [Reporting ] run task function simplifications of image export types (#174410) ## Summary This PR cleans up extra layers of abstraction in image export types that could complicate progress of the proposal of "auto" timeouts. See https://github.com/elastic/kibana/issues/131852 ## Changes Minor code cleanup of the "runTask" functions of export types that create screenshots, by removing the `generatePdf*` / `generatePng` helper callback functions and inlining the work those modules were doing. The helper modules were an integral part of the screenshotting pipelines, but in the unit tests they were completely mocked. Now that we have a proper mock utility of the `screenshotting` plugin start contract, we no longer need mockable code in a separate layer of the pipelines. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../export_types/pdf/generate_pdf.ts | 58 ---------- .../export_types/pdf/generate_pdf_v2.ts | 75 ------------- .../export_types/pdf/printable_pdf.test.ts | 42 ++++---- .../export_types/pdf/printable_pdf.ts | 60 +++++++---- .../export_types/pdf/printable_pdf_v2.test.ts | 77 +++++++------ .../export_types/pdf/printable_pdf_v2.ts | 101 ++++++++++-------- .../export_types/png/generate_png.ts | 73 ------------- .../export_types/png/png_v2.test.ts | 69 ++++++------ .../kbn-reporting/export_types/png/png_v2.ts | 71 ++++++++---- .../export_types/png/tsconfig.json | 1 - 10 files changed, 251 insertions(+), 376 deletions(-) delete mode 100644 packages/kbn-reporting/export_types/pdf/generate_pdf.ts delete mode 100644 packages/kbn-reporting/export_types/pdf/generate_pdf_v2.ts delete mode 100644 packages/kbn-reporting/export_types/png/generate_png.ts diff --git a/packages/kbn-reporting/export_types/pdf/generate_pdf.ts b/packages/kbn-reporting/export_types/pdf/generate_pdf.ts deleted file mode 100644 index 5703e16e48abc..0000000000000 --- a/packages/kbn-reporting/export_types/pdf/generate_pdf.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Observable } from 'rxjs'; -import { mergeMap, tap } from 'rxjs/operators'; - -import type { PdfScreenshotOptions, PdfScreenshotResult } from '@kbn/screenshotting-plugin/server'; -import { getTracker } from './pdf_tracker'; - -interface PdfResult { - buffer: Uint8Array | null; - metrics?: PdfScreenshotResult['metrics']; - warnings: string[]; -} - -type GetScreenshotsFn = (options: PdfScreenshotOptions) => Observable; - -export function generatePdfObservable( - getScreenshots: GetScreenshotsFn, - options: PdfScreenshotOptions -): Observable { - const tracker = getTracker(); - tracker.startScreenshots(); - - return getScreenshots(options).pipe( - tap(({ metrics }) => { - if (metrics.cpu) { - tracker.setCpuUsage(metrics.cpu); - } - if (metrics.memory) { - tracker.setMemoryUsage(metrics.memory); - } - }), - mergeMap(async ({ data: buffer, errors, metrics, renderErrors }) => { - tracker.endScreenshots(); - const warnings: string[] = []; - if (errors) { - warnings.push(...errors.map((error) => error.message)); - } - if (renderErrors) { - warnings.push(...renderErrors); - } - - tracker.end(); - - return { - buffer, - metrics, - warnings, - }; - }) - ); -} diff --git a/packages/kbn-reporting/export_types/pdf/generate_pdf_v2.ts b/packages/kbn-reporting/export_types/pdf/generate_pdf_v2.ts deleted file mode 100644 index 54489c6258bd8..0000000000000 --- a/packages/kbn-reporting/export_types/pdf/generate_pdf_v2.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Observable } from 'rxjs'; -import { mergeMap, tap } from 'rxjs/operators'; - -import type { LocatorParams, ReportingServerInfo } from '@kbn/reporting-common/types'; -import type { TaskPayloadPDFV2 } from '@kbn/reporting-export-types-pdf-common'; -import type { PdfScreenshotOptions, PdfScreenshotResult } from '@kbn/screenshotting-plugin/server'; -import type { UrlOrUrlWithContext } from '@kbn/screenshotting-plugin/server/screenshots'; -import { type ReportingConfigType, getFullRedirectAppUrl } from '@kbn/reporting-server'; - -import { getTracker } from './pdf_tracker'; - -interface PdfResult { - buffer: Uint8Array | null; - metrics?: PdfScreenshotResult['metrics']; - warnings: string[]; -} - -type GetScreenshotsFn = (options: PdfScreenshotOptions) => Observable; - -export function generatePdfObservableV2( - config: ReportingConfigType, - serverInfo: ReportingServerInfo, - getScreenshots: GetScreenshotsFn, - job: TaskPayloadPDFV2, - locatorParams: LocatorParams[], - options: Omit -): Observable { - const tracker = getTracker(); - tracker.startScreenshots(); - - /** - * For each locator we get the relative URL to the redirect app - */ - const urls = locatorParams.map((locator) => [ - getFullRedirectAppUrl(config, serverInfo, job.spaceId, job.forceNow), - locator, - ]) as unknown as UrlOrUrlWithContext[]; - - const screenshots$ = getScreenshots({ ...options, urls }).pipe( - tap(({ metrics }) => { - if (metrics.cpu) { - tracker.setCpuUsage(metrics.cpu); - } - if (metrics.memory) { - tracker.setMemoryUsage(metrics.memory); - } - }), - mergeMap(async ({ data: buffer, errors, metrics, renderErrors }) => { - tracker.endScreenshots(); - const warnings: string[] = []; - if (errors) { - warnings.push(...errors.map((error) => error.message)); - } - if (renderErrors) { - warnings.push(...renderErrors); - } - - return { - buffer, - metrics, - warnings, - }; - }) - ); - - return screenshots$; -} diff --git a/packages/kbn-reporting/export_types/pdf/printable_pdf.test.ts b/packages/kbn-reporting/export_types/pdf/printable_pdf.test.ts index 9ca0fa34effcd..10c21ede18227 100644 --- a/packages/kbn-reporting/export_types/pdf/printable_pdf.test.ts +++ b/packages/kbn-reporting/export_types/pdf/printable_pdf.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { of } from 'rxjs'; +import * as Rx from 'rxjs'; import { Writable } from 'stream'; import { coreMock, elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; @@ -14,12 +14,8 @@ import { CancellationToken } from '@kbn/reporting-common'; import { TaskPayloadPDF } from '@kbn/reporting-export-types-pdf-common'; import { createMockConfigSchema } from '@kbn/reporting-mocks-server'; import { cryptoFactory } from '@kbn/reporting-server'; -import { ScreenshottingStart } from '@kbn/screenshotting-plugin/server'; - +import { createMockScreenshottingStart } from '@kbn/screenshotting-plugin/server/mock'; import { PdfV1ExportType } from '.'; -import { generatePdfObservable } from './generate_pdf'; - -jest.mock('./generate_pdf'); let content: string; let mockPdfExportType: PdfV1ExportType; @@ -34,6 +30,9 @@ const encryptHeaders = async (headers: Record) => { return await crypto.encrypt(headers); }; +const screenshottingMock = createMockScreenshottingStart(); +const getScreenshotsSpy = jest.spyOn(screenshottingMock, 'getScreenshots'); +const testContent = 'raw string from get_screenhots'; const getBasePayload = (baseObj: any) => baseObj as TaskPayloadPDF; beforeEach(async () => { @@ -54,15 +53,20 @@ beforeEach(async () => { esClient: elasticsearchServiceMock.createClusterClient(), savedObjects: mockCoreStart.savedObjects, uiSettings: mockCoreStart.uiSettings, - screenshotting: {} as unknown as ScreenshottingStart, + screenshotting: screenshottingMock, + }); + getScreenshotsSpy.mockImplementation(() => { + return Rx.of({ + metrics: { cpu: 0, pages: 1 }, + data: Buffer.from(testContent), + errors: [], + renderErrors: [], + }); }); }); -afterEach(() => (generatePdfObservable as jest.Mock).mockReset()); - test(`passes browserTimezone to generatePdf`, async () => { const encryptedHeaders = await encryptHeaders({}); - (generatePdfObservable as jest.Mock).mockReturnValue(of({ buffer: Buffer.from('') })); const browserTimezone = 'UTC'; await mockPdfExportType.runTask( @@ -76,17 +80,20 @@ test(`passes browserTimezone to generatePdf`, async () => { stream ); - expect(generatePdfObservable).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ browserTimezone: 'UTC' }) - ); + expect(getScreenshotsSpy).toHaveBeenCalledWith({ + browserTimezone: 'UTC', + format: 'pdf', + headers: {}, + layout: undefined, + logo: false, + title: undefined, + urls: ['http://localhost:80/mock-server-basepath/app/kibana#/something'], + }); }); test(`returns content_type of application/pdf`, async () => { const encryptedHeaders = await encryptHeaders({}); - (generatePdfObservable as jest.Mock).mockReturnValue(of({ buffer: Buffer.from('') })); - const { content_type: contentType } = await mockPdfExportType.runTask( 'pdfJobId', getBasePayload({ objects: [], headers: encryptedHeaders }), @@ -97,9 +104,6 @@ test(`returns content_type of application/pdf`, async () => { }); test(`returns content of generatePdf getBuffer base64 encoded`, async () => { - const testContent = 'test content'; - (generatePdfObservable as jest.Mock).mockReturnValue(of({ buffer: Buffer.from(testContent) })); - const encryptedHeaders = await encryptHeaders({}); await mockPdfExportType.runTask( 'pdfJobId', diff --git a/packages/kbn-reporting/export_types/pdf/printable_pdf.ts b/packages/kbn-reporting/export_types/pdf/printable_pdf.ts index 6c8ce2a5b1d0d..6aaf0e5c491e2 100644 --- a/packages/kbn-reporting/export_types/pdf/printable_pdf.ts +++ b/packages/kbn-reporting/export_types/pdf/printable_pdf.ts @@ -29,10 +29,10 @@ import { } from '@kbn/reporting-export-types-pdf-common'; import { ExportType, decryptJobHeaders } from '@kbn/reporting-server'; -import { generatePdfObservable } from './generate_pdf'; -import { validateUrls } from './validate_urls'; import { getCustomLogo } from './get_custom_logo'; import { getFullUrls } from './get_full_urls'; +import { getTracker } from './pdf_tracker'; +import { validateUrls } from './validate_urls'; /** * @deprecated @@ -76,15 +76,15 @@ export class PdfV1ExportType extends ExportType { - const jobLogger = this.logger.get(`execute-job:${jobId}`); + const logger = this.logger.get(`execute-job:${jobId}`); const apmTrans = apm.startTransaction('execute-job-pdf', REPORTING_TRANSACTION_TYPE); const apmGetAssets = apmTrans.startSpan('get-assets', 'setup'); let apmGeneratePdf: { end: () => void } | null | undefined; const process$: Observable = of(1).pipe( - mergeMap(() => decryptJobHeaders(this.config.encryptionKey, job.headers, jobLogger)), + mergeMap(() => decryptJobHeaders(this.config.encryptionKey, job.headers, logger)), mergeMap(async (headers) => { - const fakeRequest = this.getFakeRequest(headers, job.spaceId, jobLogger); + const fakeRequest = this.getFakeRequest(headers, job.spaceId, logger); const uiSettingsClient = await this.getUiSettingsClient(fakeRequest); return getCustomLogo(uiSettingsClient, headers); }), @@ -96,18 +96,11 @@ export class PdfV1ExportType extends ExportType - this.startDeps.screenshotting!.getScreenshots({ - format: 'pdf', - title, - logo, - urls, - browserTimezone, - headers, - layout, - }), - { + const tracker = getTracker(); + tracker.startScreenshots(); + + return this.startDeps + .screenshotting!.getScreenshots({ format: 'pdf', title, logo, @@ -115,8 +108,35 @@ export class PdfV1ExportType extends ExportType { + if (metrics.cpu) { + tracker.setCpuUsage(metrics.cpu); + } + if (metrics.memory) { + tracker.setMemoryUsage(metrics.memory); + } + }), + mergeMap(async ({ data: buffer, errors, metrics, renderErrors }) => { + tracker.endScreenshots(); + const warnings: string[] = []; + if (errors) { + warnings.push(...errors.map((error) => error.message)); + } + if (renderErrors) { + warnings.push(...renderErrors); + } + + tracker.end(); + + return { + buffer, + metrics, + warnings, + }; + }) + ); }), tap(({ buffer }) => { apmGeneratePdf?.end(); @@ -130,7 +150,7 @@ export class PdfV1ExportType extends ExportType { - jobLogger.error(err); + logger.error(err); return throwError(err); }) ); diff --git a/packages/kbn-reporting/export_types/pdf/printable_pdf_v2.test.ts b/packages/kbn-reporting/export_types/pdf/printable_pdf_v2.test.ts index 4e4f0c1491130..28d28ade31e97 100644 --- a/packages/kbn-reporting/export_types/pdf/printable_pdf_v2.test.ts +++ b/packages/kbn-reporting/export_types/pdf/printable_pdf_v2.test.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { of } from 'rxjs'; -import type { Writable } from 'stream'; +import * as Rx from 'rxjs'; +import { Writable } from 'stream'; import { coreMock, elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { CancellationToken } from '@kbn/reporting-common'; @@ -15,12 +15,8 @@ import type { LocatorParams } from '@kbn/reporting-common/types'; import type { TaskPayloadPDFV2 } from '@kbn/reporting-export-types-pdf-common'; import { createMockConfigSchema } from '@kbn/reporting-mocks-server'; import { cryptoFactory } from '@kbn/reporting-server'; -import type { ScreenshottingStart } from '@kbn/screenshotting-plugin/server'; - +import { createMockScreenshottingStart } from '@kbn/screenshotting-plugin/server/mock'; import { PdfExportType } from '.'; -import { generatePdfObservableV2 } from './generate_pdf_v2'; - -jest.mock('./generate_pdf_v2'); let content: string; let mockPdfExportType: PdfExportType; @@ -34,7 +30,11 @@ const encryptHeaders = async (headers: Record) => { const crypto = cryptoFactory(mockEncryptionKey); return await crypto.encrypt(headers); }; +let encryptedHeaders: string; +const screenshottingMock = createMockScreenshottingStart(); +const getScreenshotsSpy = jest.spyOn(screenshottingMock, 'getScreenshots'); +const testContent = 'raw string from get_screenhots'; const getBasePayload = (baseObj: any) => ({ params: { forceNow: 'test' }, @@ -50,8 +50,10 @@ beforeEach(async () => { const mockCoreSetup = coreMock.createSetup(); const mockCoreStart = coreMock.createStart(); - mockPdfExportType = new PdfExportType(mockCoreSetup, configType, mockLogger, context); + encryptedHeaders = await encryptHeaders({}); + + mockPdfExportType = new PdfExportType(mockCoreSetup, configType, mockLogger, context); mockPdfExportType.setup({ basePath: { set: jest.fn() }, }); @@ -59,21 +61,26 @@ beforeEach(async () => { esClient: elasticsearchServiceMock.createClusterClient(), savedObjects: mockCoreStart.savedObjects, uiSettings: mockCoreStart.uiSettings, - screenshotting: {} as unknown as ScreenshottingStart, + screenshotting: screenshottingMock, }); -}); -afterEach(() => (generatePdfObservableV2 as jest.Mock).mockReset()); + getScreenshotsSpy.mockImplementation(() => { + return Rx.of({ + metrics: { cpu: 0, pages: 1 }, + data: Buffer.from(testContent), + errors: [], + renderErrors: [], + }); + }); +}); test(`passes browserTimezone to generatePdf`, async () => { - const encryptedHeaders = await encryptHeaders({}); - (generatePdfObservableV2 as jest.Mock).mockReturnValue(of(Buffer.from(''))); - const browserTimezone = 'UTC'; await mockPdfExportType.runTask( 'pdfJobId', getBasePayload({ forceNow: 'test', + layout: { dimensions: {} }, title: 'PDF Params Timezone Test', locatorParams: [{ version: 'test', id: 'test' }] as LocatorParams[], browserTimezone, @@ -83,24 +90,30 @@ test(`passes browserTimezone to generatePdf`, async () => { stream ); - expect(generatePdfObservableV2).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.objectContaining({ browserTimezone: 'UTC' }) - ); + expect(getScreenshotsSpy).toHaveBeenCalledWith({ + browserTimezone: 'UTC', + format: 'pdf', + headers: {}, + layout: { dimensions: {} }, + logo: false, + title: 'PDF Params Timezone Test', + urls: [ + [ + 'http://localhost:80/mock-server-basepath/app/reportingRedirect?forceNow=test', + { __REPORTING_REDIRECT_LOCATOR_STORE_KEY__: { id: 'test', version: 'test' } }, + ], + ], + }); }); test(`returns content_type of application/pdf`, async () => { - const encryptedHeaders = await encryptHeaders({}); - - (generatePdfObservableV2 as jest.Mock).mockReturnValue(of({ buffer: Buffer.from('') })); - const { content_type: contentType } = await mockPdfExportType.runTask( 'pdfJobId', - getBasePayload({ locatorParams: [], headers: encryptedHeaders }), + getBasePayload({ + layout: { dimensions: {} }, + locatorParams: [{ version: 'test', id: 'test' }] as LocatorParams[], + headers: encryptedHeaders, + }), cancellationToken, stream ); @@ -108,13 +121,13 @@ test(`returns content_type of application/pdf`, async () => { }); test(`returns content of generatePdf getBuffer base64 encoded`, async () => { - const testContent = 'test content'; - (generatePdfObservableV2 as jest.Mock).mockReturnValue(of({ buffer: Buffer.from(testContent) })); - - const encryptedHeaders = await encryptHeaders({}); await mockPdfExportType.runTask( 'pdfJobId', - getBasePayload({ locatorParams: [], headers: encryptedHeaders }), + getBasePayload({ + layout: { dimensions: {} }, + locatorParams: [{ version: 'test', id: 'test' }] as LocatorParams[], + headers: encryptedHeaders, + }), cancellationToken, stream ); diff --git a/packages/kbn-reporting/export_types/pdf/printable_pdf_v2.ts b/packages/kbn-reporting/export_types/pdf/printable_pdf_v2.ts index cc975c536803d..24e445e503d51 100644 --- a/packages/kbn-reporting/export_types/pdf/printable_pdf_v2.ts +++ b/packages/kbn-reporting/export_types/pdf/printable_pdf_v2.ts @@ -22,17 +22,18 @@ import { REPORTING_REDIRECT_LOCATOR_STORE_KEY, REPORTING_TRANSACTION_TYPE, } from '@kbn/reporting-common'; -import type { TaskRunResult, UrlOrUrlLocatorTuple } from '@kbn/reporting-common/types'; +import type { TaskRunResult } from '@kbn/reporting-common/types'; +import type { TaskPayloadPDFV2 } from '@kbn/reporting-export-types-pdf-common'; import { JobParamsPDFV2, PDF_JOB_TYPE_V2, PDF_REPORT_TYPE_V2, - TaskPayloadPDFV2, } from '@kbn/reporting-export-types-pdf-common'; -import { decryptJobHeaders, getFullRedirectAppUrl, ExportType } from '@kbn/reporting-server'; +import { ExportType, decryptJobHeaders, getFullRedirectAppUrl } from '@kbn/reporting-server'; +import type { UrlOrUrlWithContext } from '@kbn/screenshotting-plugin/server/screenshots'; -import { generatePdfObservableV2 } from './generate_pdf_v2'; import { getCustomLogo } from './get_custom_logo'; +import { getTracker } from './pdf_tracker'; export class PdfExportType extends ExportType { id = PDF_REPORT_TYPE_V2; @@ -80,66 +81,82 @@ export class PdfExportType extends ExportType cancellationToken: CancellationToken, stream: Writable ) => { - const jobLogger = this.logger.get(`execute-job:${jobId}`); + const logger = this.logger.get(`execute-job:${jobId}`); const apmTrans = apm.startTransaction('execute-job-pdf-v2', REPORTING_TRANSACTION_TYPE); const apmGetAssets = apmTrans.startSpan('get-assets', 'setup'); let apmGeneratePdf: { end: () => void } | null | undefined; const { encryptionKey } = this.config; const process$: Rx.Observable = of(1).pipe( - mergeMap(() => decryptJobHeaders(encryptionKey, payload.headers, jobLogger)), + mergeMap(() => decryptJobHeaders(encryptionKey, payload.headers, logger)), mergeMap(async (headers: Headers) => { - const fakeRequest = this.getFakeRequest(headers, payload.spaceId, jobLogger); + const fakeRequest = this.getFakeRequest(headers, payload.spaceId, logger); const uiSettingsClient = await this.getUiSettingsClient(fakeRequest); return await getCustomLogo(uiSettingsClient, headers); }), mergeMap(({ logo, headers }) => { const { browserTimezone, layout, title, locatorParams } = payload; - let urls: UrlOrUrlLocatorTuple[]; - if (locatorParams) { - urls = locatorParams.map((locator) => [ - getFullRedirectAppUrl( - this.config, - this.getServerInfo(), - payload.spaceId, - payload.forceNow - ), - locator, - ]); - } apmGetAssets?.end(); apmGeneratePdf = apmTrans.startSpan('generate-pdf-pipeline', 'execute'); - return generatePdfObservableV2( - this.config, - this.getServerInfo(), - () => - this.startDeps.screenshotting!.getScreenshots({ - format: 'pdf', - title, - logo, - browserTimezone, - headers, - layout, - urls: urls.map((url) => - typeof url === 'string' - ? url - : [url[0], { [REPORTING_REDIRECT_LOCATOR_STORE_KEY]: url[1] }] - ), - }), - payload, - locatorParams, - { + const tracker = getTracker(); + tracker.startScreenshots(); + + /** + * For each locator we get the relative URL to the redirect app + */ + const urls = locatorParams.map((locator) => [ + getFullRedirectAppUrl( + this.config, + this.getServerInfo(), + payload.spaceId, + payload.forceNow + ), + locator, + ]) as unknown as UrlOrUrlWithContext[]; + + return this.startDeps + .screenshotting!.getScreenshots({ format: 'pdf', title, logo, browserTimezone, headers, layout, - } - ); + urls: urls.map((url) => + typeof url === 'string' + ? url + : [url[0], { [REPORTING_REDIRECT_LOCATOR_STORE_KEY]: url[1] }] + ), + }) + .pipe( + tap(({ metrics }) => { + if (metrics.cpu) { + tracker.setCpuUsage(metrics.cpu); + } + if (metrics.memory) { + tracker.setMemoryUsage(metrics.memory); + } + }), + mergeMap(async ({ data: buffer, errors, metrics, renderErrors }) => { + tracker.endScreenshots(); + const warnings: string[] = []; + if (errors) { + warnings.push(...errors.map((error) => error.message)); + } + if (renderErrors) { + warnings.push(...renderErrors); + } + + return { + buffer, + metrics, + warnings, + }; + }) + ); }), tap(({ buffer }) => { apmGeneratePdf?.end(); @@ -154,7 +171,7 @@ export class PdfExportType extends ExportType warnings, })), catchError((err) => { - jobLogger.error(err); + logger.error(err); return Rx.throwError(() => err); }) ); diff --git a/packages/kbn-reporting/export_types/png/generate_png.ts b/packages/kbn-reporting/export_types/png/generate_png.ts deleted file mode 100644 index d0ce9b9791690..0000000000000 --- a/packages/kbn-reporting/export_types/png/generate_png.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import apm from 'elastic-apm-node'; -import { Observable } from 'rxjs'; -import { finalize, map, tap } from 'rxjs/operators'; - -import type { Logger } from '@kbn/logging'; -import { REPORTING_TRANSACTION_TYPE } from '@kbn/reporting-common/constants'; -import type { PngScreenshotOptions, PngScreenshotResult } from '@kbn/screenshotting-plugin/server'; - -interface PngResult { - buffer: Buffer; - metrics?: PngScreenshotResult['metrics']; - warnings: string[]; -} - -type GetScreenshotsFn = (options: PngScreenshotOptions) => Observable; - -export function generatePngObservable( - getScreenshots: GetScreenshotsFn, - logger: Logger, - options: Omit -): Observable { - const apmTrans = apm.startTransaction('generate-png', REPORTING_TRANSACTION_TYPE); - if (!options.layout?.dimensions) { - throw new Error(`LayoutParams.Dimensions is undefined.`); - } - - const apmScreenshots = apmTrans?.startSpan('screenshots-pipeline', 'setup'); - let apmBuffer: typeof apm.currentSpan; - - return getScreenshots({ - ...options, - format: 'png', - layout: { id: 'preserve_layout', ...options.layout }, - }).pipe( - tap(({ metrics }) => { - if (metrics) { - apmTrans.setLabel('cpu', metrics.cpu, false); - apmTrans.setLabel('memory', metrics.memory, false); - } - apmScreenshots?.end(); - apmBuffer = apmTrans.startSpan('get-buffer', 'output') ?? null; - }), - map(({ metrics, results }) => ({ - metrics, - buffer: results[0].screenshots[0].data, - warnings: results.reduce((found, current) => { - if (current.error) { - found.push(current.error.message); - } - if (current.renderErrors) { - found.push(...current.renderErrors); - } - return found; - }, [] as string[]), - })), - tap(({ buffer }) => { - logger.debug(`PNG buffer byte length: ${buffer.byteLength}`); - apmTrans.setLabel('byte-length', buffer.byteLength, false); - }), - finalize(() => { - apmBuffer?.end(); - apmTrans.end(); - }) - ); -} diff --git a/packages/kbn-reporting/export_types/png/png_v2.test.ts b/packages/kbn-reporting/export_types/png/png_v2.test.ts index a6cc8b1891eef..bd59306af8168 100644 --- a/packages/kbn-reporting/export_types/png/png_v2.test.ts +++ b/packages/kbn-reporting/export_types/png/png_v2.test.ts @@ -15,12 +15,9 @@ import type { LocatorParams } from '@kbn/reporting-common/types'; import type { TaskPayloadPNGV2 } from '@kbn/reporting-export-types-png-common'; import { createMockConfigSchema } from '@kbn/reporting-mocks-server'; import { cryptoFactory } from '@kbn/reporting-server'; -import type { ScreenshottingStart } from '@kbn/screenshotting-plugin/server'; - +import { createMockScreenshottingStart } from '@kbn/screenshotting-plugin/server/mock'; +import type { CaptureResult } from '@kbn/screenshotting-plugin/server/screenshots'; import { PngExportType } from '.'; -import { generatePngObservable } from './generate_png'; - -jest.mock('./generate_png'); let content: string; let mockPngExportType: PngExportType; @@ -34,49 +31,51 @@ const encryptHeaders = async (headers: Record) => { const crypto = cryptoFactory(mockEncryptionKey); return await crypto.encrypt(headers); }; +let encryptedHeaders: string; +const screenshottingMock = createMockScreenshottingStart(); +const getScreenshotsSpy = jest.spyOn(screenshottingMock, 'getScreenshots'); +const testContent = 'raw string from get_screenhots'; const getBasePayload = (baseObj: unknown) => baseObj as TaskPayloadPNGV2; beforeEach(async () => { content = ''; stream = { write: jest.fn((chunk) => (content += chunk)) } as unknown as typeof stream; - const configType = createMockConfigSchema({ - encryptionKey: mockEncryptionKey, - queue: { - indexInterval: 'daily', - timeout: Infinity, - }, - }); - + const configType = createMockConfigSchema({ encryptionKey: mockEncryptionKey }); const context = coreMock.createPluginInitializerContext(configType); const mockCoreSetup = coreMock.createSetup(); const mockCoreStart = coreMock.createStart(); + encryptedHeaders = await encryptHeaders({}); + mockPngExportType = new PngExportType(mockCoreSetup, configType, mockLogger, context); mockPngExportType.setup({ basePath: { set: jest.fn() }, }); mockPngExportType.start({ + esClient: elasticsearchServiceMock.createClusterClient(), savedObjects: mockCoreStart.savedObjects, uiSettings: mockCoreStart.uiSettings, - screenshotting: {} as unknown as ScreenshottingStart, - esClient: elasticsearchServiceMock.createClusterClient(), + screenshotting: screenshottingMock, }); -}); -afterEach(() => (generatePngObservable as jest.Mock).mockReset()); + getScreenshotsSpy.mockImplementation(() => { + return Rx.of({ + metrics: { cpu: 0 }, + results: [{ screenshots: [{ data: Buffer.from(testContent) }] }] as CaptureResult['results'], + }); + }); +}); test(`passes browserTimezone to generatePng`, async () => { - const encryptedHeaders = await encryptHeaders({}); - (generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from('') })); - const browserTimezone = 'UTC'; await mockPngExportType.runTask( 'pngJobId', getBasePayload({ forceNow: 'test', + layout: { dimensions: {} }, locatorParams: [], browserTimezone, headers: encryptedHeaders, @@ -85,25 +84,24 @@ test(`passes browserTimezone to generatePng`, async () => { stream ); - expect(generatePngObservable).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.objectContaining({ - browserTimezone: 'UTC', - headers: {}, - layout: { id: 'preserve_layout' }, - }) - ); + expect(getScreenshotsSpy).toHaveBeenCalledWith({ + format: 'png', + headers: {}, + layout: { dimensions: {}, id: 'preserve_layout' }, + urls: [ + [ + 'http://localhost:80/mock-server-basepath/app/reportingRedirect?forceNow=test', + { __REPORTING_REDIRECT_LOCATOR_STORE_KEY__: undefined }, + ], + ], + }); }); test(`returns content_type of application/png`, async () => { - const encryptedHeaders = await encryptHeaders({}); - - (generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from('foo') })); - const { content_type: contentType } = await mockPngExportType.runTask( 'pngJobId', getBasePayload({ + layout: { dimensions: {} }, locatorParams: [{ version: 'test', id: 'test' }] as LocatorParams[], headers: encryptedHeaders, }), @@ -114,13 +112,10 @@ test(`returns content_type of application/png`, async () => { }); test(`returns content of generatePng getBuffer base64 encoded`, async () => { - const testContent = 'raw string from get_screenhots'; - (generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); - - const encryptedHeaders = await encryptHeaders({}); await mockPngExportType.runTask( 'pngJobId', getBasePayload({ + layout: { dimensions: {} }, locatorParams: [{ version: 'test', id: 'test' }] as LocatorParams[], headers: encryptedHeaders, }), diff --git a/packages/kbn-reporting/export_types/png/png_v2.ts b/packages/kbn-reporting/export_types/png/png_v2.ts index 15f0c26d45932..2b824bde18a0b 100644 --- a/packages/kbn-reporting/export_types/png/png_v2.ts +++ b/packages/kbn-reporting/export_types/png/png_v2.ts @@ -38,9 +38,7 @@ import { PNG_REPORT_TYPE_V2, TaskPayloadPNGV2, } from '@kbn/reporting-export-types-png-common'; -import { decryptJobHeaders, getFullRedirectAppUrl, ExportType } from '@kbn/reporting-server'; - -import { generatePngObservable } from './generate_png'; +import { decryptJobHeaders, ExportType, getFullRedirectAppUrl } from '@kbn/reporting-server'; export class PngExportType extends ExportType { id = PNG_REPORT_TYPE_V2; @@ -88,14 +86,14 @@ export class PngExportType extends ExportType cancellationToken: CancellationToken, stream: Writable ) => { - const jobLogger = this.logger.get(`execute-job:${jobId}`); + const logger = this.logger.get(`execute-job:${jobId}`); const apmTrans = apm.startTransaction('execute-job-pdf-v2', REPORTING_TRANSACTION_TYPE); const apmGetAssets = apmTrans.startSpan('get-assets', 'setup'); let apmGeneratePng: { end: () => void } | null | undefined; const { encryptionKey } = this.config; const process$: Observable = of(1).pipe( - mergeMap(() => decryptJobHeaders(encryptionKey, payload.headers, jobLogger)), + mergeMap(() => decryptJobHeaders(encryptionKey, payload.headers, logger)), mergeMap((headers) => { const url = getFullRedirectAppUrl( this.config, @@ -108,22 +106,57 @@ export class PngExportType extends ExportType apmGetAssets?.end(); apmGeneratePng = apmTrans.startSpan('generate-png-pipeline', 'execute'); + const options = { + headers, + browserTimezone: payload.browserTimezone, + layout: { ...payload.layout, id: 'preserve_layout' as const }, + }; - return generatePngObservable( - () => - this.startDeps.screenshotting!.getScreenshots({ - format: 'png', - headers, - layout: { ...payload.layout, id: 'preserve_layout' }, - urls: [[url, { [REPORTING_REDIRECT_LOCATOR_STORE_KEY]: locatorParams }]], - }), - jobLogger, - { + if (!options.layout?.dimensions) { + throw new Error(`LayoutParams.Dimensions is undefined.`); + } + + const apmScreenshots = apmTrans?.startSpan('screenshots-pipeline', 'setup'); + let apmBuffer: typeof apm.currentSpan; + + return this.startDeps + .screenshotting!.getScreenshots({ + format: 'png', headers, - browserTimezone: payload.browserTimezone, layout: { ...payload.layout, id: 'preserve_layout' }, - } - ); + urls: [[url, { [REPORTING_REDIRECT_LOCATOR_STORE_KEY]: locatorParams }]], + }) + .pipe( + tap(({ metrics }) => { + if (metrics) { + apmTrans.setLabel('cpu', metrics.cpu, false); + apmTrans.setLabel('memory', metrics.memory, false); + } + apmScreenshots?.end(); + apmBuffer = apmTrans.startSpan('get-buffer', 'output') ?? null; + }), + map(({ metrics, results }) => ({ + metrics, + buffer: results[0].screenshots[0].data, + warnings: results.reduce((found, current) => { + if (current.error) { + found.push(current.error.message); + } + if (current.renderErrors) { + found.push(...current.renderErrors); + } + return found; + }, [] as string[]), + })), + tap(({ buffer }) => { + logger.debug(`PNG buffer byte length: ${buffer.byteLength}`); + apmTrans.setLabel('byte-length', buffer.byteLength, false); + }), + finalize(() => { + apmBuffer?.end(); + apmTrans.end(); + }) + ); }), tap(({ buffer }) => stream.write(buffer)), map(({ metrics, warnings }) => ({ @@ -131,7 +164,7 @@ export class PngExportType extends ExportType metrics: { png: metrics }, warnings, })), - tap({ error: (error) => jobLogger.error(error) }), + tap({ error: (error) => logger.error(error) }), finalize(() => apmGeneratePng?.end()) ); diff --git a/packages/kbn-reporting/export_types/png/tsconfig.json b/packages/kbn-reporting/export_types/png/tsconfig.json index bac09188079b5..b8a141a320dd8 100644 --- a/packages/kbn-reporting/export_types/png/tsconfig.json +++ b/packages/kbn-reporting/export_types/png/tsconfig.json @@ -23,6 +23,5 @@ "@kbn/reporting-export-types-png-common", "@kbn/core", "@kbn/reporting-mocks-server", - "@kbn/logging", ] } From 153dcf4bc8cdd154c6d2a6df0a1e5323637c9481 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Wed, 10 Jan 2024 18:01:57 +0100 Subject: [PATCH 24/26] Stabilize index templates and cases navigation tests for MKI (#174501) ## Summary This PR stabilizes the index templates and cases navigation FTR test suites for MKI runs on the security project. ### Details - The index templates tests was failing on a retried step, because the cleanup of `TEST_TEMPLATE_NAME` didn't work as intended. Changing the name to include the date as "random" part instead of a random floating point number fixes the cleanup. - The navigation to the cases app in a security project sometimes failed with error that look like incomplete loading of assets. This PR wraps this navigation step in a retry and adds some waits for global loading between navigation and assertion. As part of that, the security `navigateToLandingPage` has been adjusted to check for an actual object on the landing page. --- .../functional/services/svl_sec_navigation.ts | 5 +---- .../index_management/index_templates.ts | 2 +- .../test_suites/security/ftr/navigation.ts | 18 +++++++++++++----- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/x-pack/test_serverless/functional/services/svl_sec_navigation.ts b/x-pack/test_serverless/functional/services/svl_sec_navigation.ts index 85f21940d5ab1..c990258cd8060 100644 --- a/x-pack/test_serverless/functional/services/svl_sec_navigation.ts +++ b/x-pack/test_serverless/functional/services/svl_sec_navigation.ts @@ -19,10 +19,7 @@ export function SvlSecNavigationServiceProvider({ async navigateToLandingPage() { await retry.tryForTime(60 * 1000, async () => { await PageObjects.common.navigateToApp('landingPage'); - // Currently, the security landing page app is not loading correctly. - // Replace '~kbnAppWrapper' with a proper test subject of the landing - // page once it loads successfully. - await testSubjects.existOrFail('~kbnAppWrapper', { timeout: 2000 }); + await testSubjects.existOrFail('welcome-header', { timeout: 2000 }); }); }, }; diff --git a/x-pack/test_serverless/functional/test_suites/common/management/index_management/index_templates.ts b/x-pack/test_serverless/functional/test_suites/common/management/index_management/index_templates.ts index 371ee4debe98f..6092473ad27bc 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/index_management/index_templates.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/index_management/index_templates.ts @@ -75,7 +75,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); describe('Create index template', () => { - const TEST_TEMPLATE_NAME = `test_template_${Math.random()}`; + const TEST_TEMPLATE_NAME = `test_template_${Date.now()}`; after(async () => { await es.indices.deleteIndexTemplate({ name: TEST_TEMPLATE_NAME }, { ignore: [404] }); diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/navigation.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/navigation.ts index e3c8de72570b5..69bc9d517e7e8 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/navigation.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/navigation.ts @@ -16,6 +16,8 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { const svlCommonNavigation = getPageObject('svlCommonNavigation'); const testSubjects = getService('testSubjects'); const browser = getService('browser'); + const headerPage = getPageObject('header'); + const retry = getService('retry'); describe('navigation', function () { before(async () => { @@ -49,6 +51,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await svlCommonNavigation.search.searchFor('security dashboards'); await svlCommonNavigation.search.clickOnOption(0); await svlCommonNavigation.search.hideSearch(); + await headerPage.waitUntilLoadingHasFinished(); await expect(await browser.getCurrentUrl()).contain('app/security/dashboards'); }); @@ -63,12 +66,17 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { }); it('navigates to cases app', async () => { - await svlCommonNavigation.sidenav.clickLink({ - deepLinkId: 'securitySolutionUI:cases' as AppDeepLinkId, - }); + await retry.tryForTime(30 * 1000, async () => { + // start navigation to the cases app from the landing page + await svlSecNavigation.navigateToLandingPage(); + await svlCommonNavigation.sidenav.clickLink({ + deepLinkId: 'securitySolutionUI:cases' as AppDeepLinkId, + }); + await headerPage.waitUntilLoadingHasFinished(); - expect(await browser.getCurrentUrl()).contain('/app/security/cases'); - await testSubjects.existOrFail('cases-all-title'); + expect(await browser.getCurrentUrl()).contain('/app/security/cases'); + await testSubjects.existOrFail('cases-all-title'); + }); }); }); } From f1415345bac26e594ab76fa223f0b439e1920be8 Mon Sep 17 00:00:00 2001 From: "Joey F. Poon" Date: Wed, 10 Jan 2024 09:21:10 -0800 Subject: [PATCH 25/26] [Security Solution] unskip endpoint alerts cypress tests (#174493) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Unskip endpoint alerts cypress test. Various enhancements to our tests were previously made, this is just retroactively unskipping after verifying with flaky test runner. Flaky test runs: - https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/4798 (50x ✅ ) ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../public/management/cypress/e2e/endpoint_alerts.cy.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_alerts.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_alerts.cy.ts index e4f913d851735..06b33141bad1b 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_alerts.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_alerts.cy.ts @@ -19,8 +19,7 @@ import { login, ROLE } from '../tasks/login'; import { EXECUTE_ROUTE } from '../../../../common/endpoint/constants'; import { waitForActionToComplete } from '../tasks/response_actions'; -// FLAKY: https://github.com/elastic/kibana/issues/169958 -describe.skip('Endpoint generated alerts', { tags: ['@ess', '@serverless'] }, () => { +describe('Endpoint generated alerts', { tags: ['@ess', '@serverless'] }, () => { let indexedPolicy: IndexedFleetEndpointPolicyResponse; let policy: PolicyData; let createdHost: CreateAndEnrollEndpointHostResponse; From ed7c6477ced43d639eecb339ee4f5bf74026d6de Mon Sep 17 00:00:00 2001 From: amyjtechwriter <61687663+amyjtechwriter@users.noreply.github.com> Date: Wed, 10 Jan 2024 17:23:55 +0000 Subject: [PATCH 26/26] [DOCS] Adds the 8.11.4 release notes (#174636) Adds the 8.11.4 release notes (there aren't any for Kibana in this release). --- docs/CHANGELOG.asciidoc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index 63b13250a1591..f7fc138caf4eb 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -10,6 +10,7 @@ Review important information about the {kib} 8.x releases. +* <> * <> * <> * <> @@ -56,6 +57,15 @@ Review important information about the {kib} 8.x releases. * <> -- + +[[release-notes-8.11.4]] +== {kib} 8.11.4 + +[float] +[[fixes-v8.11.4]] +=== Bug fixes and enhancements +There are no user-facing changes in the 8.11.4 release. + [[release-notes-8.11.3]] == {kib} 8.11.3