diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9adb05cb22ecc..457458ec5b1c6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -461,7 +461,7 @@ src/plugins/field_formats @elastic/kibana-data-discovery packages/kbn-field-types @elastic/kibana-data-discovery packages/kbn-field-utils @elastic/kibana-data-discovery x-pack/plugins/fields_metadata @elastic/obs-ux-logs-team -x-pack/plugins/file_upload @elastic/kibana-gis @elastic/ml-ui +x-pack/plugins/file_upload @elastic/kibana-presentation @elastic/ml-ui examples/files_example @elastic/appex-sharedux src/plugins/files_management @elastic/appex-sharedux src/plugins/files @elastic/appex-sharedux @@ -583,11 +583,11 @@ packages/kbn-management/settings/types @elastic/kibana-management packages/kbn-management/settings/utilities @elastic/kibana-management packages/kbn-management/storybook/config @elastic/kibana-management test/plugin_functional/plugins/management_test_plugin @elastic/kibana-management -packages/kbn-mapbox-gl @elastic/kibana-gis -x-pack/examples/third_party_maps_source_example @elastic/kibana-gis -src/plugins/maps_ems @elastic/kibana-gis -x-pack/plugins/maps @elastic/kibana-gis -x-pack/packages/maps/vector_tile_utils @elastic/kibana-gis +packages/kbn-mapbox-gl @elastic/kibana-presentation +x-pack/examples/third_party_maps_source_example @elastic/kibana-presentation +src/plugins/maps_ems @elastic/kibana-presentation +x-pack/plugins/maps @elastic/kibana-presentation +x-pack/packages/maps/vector_tile_utils @elastic/kibana-presentation x-pack/plugins/observability_solution/metrics_data_access @elastic/obs-knowledge-team @elastic/obs-ux-infra_services-team x-pack/packages/ml/agg_utils @elastic/ml-ui x-pack/packages/ml/anomaly_utils @elastic/ml-ui diff --git a/docs/setup/access.asciidoc b/docs/setup/access.asciidoc index a0bd1207a6a35..3b6457b42f04d 100644 --- a/docs/setup/access.asciidoc +++ b/docs/setup/access.asciidoc @@ -65,4 +65,4 @@ For example: * When {kib} is unable to connect to a healthy {es} cluster, errors like `master_not_discovered_exception` or `unable to revive connection` or `license is not available` errors appear. * When one or more {kib}-backing indices are unhealthy, the `index_not_green_timeout` error appears. -For more information, refer to our https://www.elastic.co/blog/troubleshooting-kibana-health[walkthrough on troubleshooting Kibana Health]. +You can find a Kibana health troubleshooting walkthrough in https://www.elastic.co/blog/troubleshooting-kibana-health[this blog] or in link:https://www.youtube.com/watch?v=AlgGYcpGvOA[this video]. diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json index 0447ba6a226dd..ec14f4519d344 100644 --- a/packages/kbn-check-mappings-update-cli/current_fields.json +++ b/packages/kbn-check-mappings-update-cli/current_fields.json @@ -312,6 +312,12 @@ "entity-discovery-api-key": [ "apiKey" ], + "entity-engine-status": [ + "filter", + "indexPattern", + "status", + "type" + ], "epm-packages": [ "additional_spaces_installed_kibana", "es_index_patterns", diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index bda2270001bd9..2bdf6e75ad1cb 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -1057,6 +1057,23 @@ } } }, + "entity-engine-status": { + "dynamic": false, + "properties": { + "filter": { + "type": "keyword" + }, + "indexPattern": { + "type": "keyword" + }, + "status": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, "epm-packages": { "properties": { "additional_spaces_installed_kibana": { diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index d2ac9c6678797..3ad5d271bde47 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -219,6 +219,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D searchApplicationsSearch: `${ELASTICSEARCH_DOCS}search-application-client.html`, searchLabs: `${SEARCH_LABS_URL}`, searchLabsRepo: `${SEARCH_LABS_REPO}`, + semanticSearch: `${ELASTICSEARCH_DOCS}semantic-search.html`, searchTemplates: `${ELASTICSEARCH_DOCS}search-template.html`, semanticTextField: `${ELASTICSEARCH_DOCS}semantic-text.html`, start: `${ENTERPRISE_SEARCH_DOCS}start.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index ae6e56a9ac385..cbf085623c3a6 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -183,6 +183,7 @@ export interface DocLinks { readonly searchApplicationsSearch: string; readonly searchLabs: string; readonly searchLabsRepo: string; + readonly semanticSearch: string; readonly searchTemplates: string; readonly semanticTextField: string; readonly start: string; diff --git a/packages/kbn-expandable-flyout/index.ts b/packages/kbn-expandable-flyout/index.ts index d2d304bd6aa2c..f06be45a68914 100644 --- a/packages/kbn-expandable-flyout/index.ts +++ b/packages/kbn-expandable-flyout/index.ts @@ -12,7 +12,7 @@ export { ExpandableFlyout } from './src'; export { useExpandableFlyoutApi } from './src/hooks/use_expandable_flyout_api'; export { useExpandableFlyoutState } from './src/hooks/use_expandable_flyout_state'; -export { type FlyoutState as ExpandableFlyoutState } from './src/state'; +export { type FlyoutPanels as ExpandableFlyoutState } from './src/store/state'; export { ExpandableFlyoutProvider } from './src/provider'; export { withExpandableFlyoutProvider } from './src/with_provider'; diff --git a/packages/kbn-expandable-flyout/src/components/preview_section.test.tsx b/packages/kbn-expandable-flyout/src/components/preview_section.test.tsx index 831e916f84f05..ba2b8987cc0a8 100644 --- a/packages/kbn-expandable-flyout/src/components/preview_section.test.tsx +++ b/packages/kbn-expandable-flyout/src/components/preview_section.test.tsx @@ -16,18 +16,24 @@ import { PREVIEW_SECTION_TEST_ID, } from './test_ids'; import { TestProvider } from '../test/provider'; -import { State } from '../state'; +import { State } from '../store/state'; describe('PreviewSection', () => { - const context = { - right: {}, - left: {}, - preview: [ - { - id: 'key', + const context: State = { + panels: { + byId: { + flyout: { + right: undefined, + left: undefined, + preview: [ + { + id: 'key', + }, + ], + }, }, - ], - } as unknown as State; + }, + }; const component =
{'component'}
; const left = 500; diff --git a/packages/kbn-expandable-flyout/src/hooks/use_expandable_flyout_api.ts b/packages/kbn-expandable-flyout/src/hooks/use_expandable_flyout_api.ts index 5ae8a4e474887..e1fe482f448f9 100644 --- a/packages/kbn-expandable-flyout/src/hooks/use_expandable_flyout_api.ts +++ b/packages/kbn-expandable-flyout/src/hooks/use_expandable_flyout_api.ts @@ -21,8 +21,8 @@ import { openPreviewPanelAction, openRightPanelAction, previousPreviewPanelAction, -} from '../actions'; -import { useDispatch } from '../redux'; +} from '../store/actions'; +import { useDispatch } from '../store/redux'; import { FlyoutPanelProps, type ExpandableFlyoutApi } from '../types'; export type { ExpandableFlyoutApi }; diff --git a/packages/kbn-expandable-flyout/src/hooks/use_expandable_flyout_state.ts b/packages/kbn-expandable-flyout/src/hooks/use_expandable_flyout_state.ts index f4fbb0f1f2a3f..49cac7d97a895 100644 --- a/packages/kbn-expandable-flyout/src/hooks/use_expandable_flyout_state.ts +++ b/packages/kbn-expandable-flyout/src/hooks/use_expandable_flyout_state.ts @@ -9,7 +9,7 @@ import { REDUX_ID_FOR_MEMORY_STORAGE } from '../constants'; import { useExpandableFlyoutContext } from '../context'; -import { selectPanelsById, useSelector } from '../redux'; +import { selectPanelsById, useSelector } from '../store/redux'; /** * This hook allows you to access the flyout state, read open right, left and preview panels. diff --git a/packages/kbn-expandable-flyout/src/index.stories.tsx b/packages/kbn-expandable-flyout/src/index.stories.tsx index dab81e62f9a0e..a7b1e95e43805 100644 --- a/packages/kbn-expandable-flyout/src/index.stories.tsx +++ b/packages/kbn-expandable-flyout/src/index.stories.tsx @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import { ExpandableFlyout } from '.'; import { TestProvider } from './test/provider'; -import { State } from './state'; +import { State } from './store/state'; export default { component: ExpandableFlyout, @@ -103,13 +103,15 @@ const registeredPanels = [ export const Right: Story = () => { const state: State = { - byId: { - memory: { - right: { - id: 'right', + panels: { + byId: { + memory: { + right: { + id: 'right', + }, + left: undefined, + preview: undefined, }, - left: undefined, - preview: undefined, }, }, }; @@ -126,15 +128,17 @@ export const Right: Story = () => { export const Left: Story = () => { const state: State = { - byId: { - memory: { - right: { - id: 'right', - }, - left: { - id: 'left', + panels: { + byId: { + memory: { + right: { + id: 'right', + }, + left: { + id: 'left', + }, + preview: undefined, }, - preview: undefined, }, }, }; @@ -151,19 +155,21 @@ export const Left: Story = () => { export const Preview: Story = () => { const state: State = { - byId: { - memory: { - right: { - id: 'right', - }, - left: { - id: 'left', - }, - preview: [ - { - id: 'preview1', + panels: { + byId: { + memory: { + right: { + id: 'right', + }, + left: { + id: 'left', }, - ], + preview: [ + { + id: 'preview1', + }, + ], + }, }, }, }; @@ -180,22 +186,24 @@ export const Preview: Story = () => { export const MultiplePreviews: Story = () => { const state: State = { - byId: { - memory: { - right: { - id: 'right', - }, - left: { - id: 'left', - }, - preview: [ - { - id: 'preview1', + panels: { + byId: { + memory: { + right: { + id: 'right', }, - { - id: 'preview2', + left: { + id: 'left', }, - ], + preview: [ + { + id: 'preview1', + }, + { + id: 'preview2', + }, + ], + }, }, }, }; @@ -212,13 +220,15 @@ export const MultiplePreviews: Story = () => { export const CollapsedPushVsOverlay: Story = () => { const state: State = { - byId: { - memory: { - right: { - id: 'right', + panels: { + byId: { + memory: { + right: { + id: 'right', + }, + left: undefined, + preview: undefined, }, - left: undefined, - preview: undefined, }, }, }; @@ -232,15 +242,17 @@ export const CollapsedPushVsOverlay: Story = () => { export const ExpandedPushVsOverlay: Story = () => { const state: State = { - byId: { - memory: { - right: { - id: 'right', - }, - left: { - id: 'left', + panels: { + byId: { + memory: { + right: { + id: 'right', + }, + left: { + id: 'left', + }, + preview: undefined, }, - preview: undefined, }, }, }; @@ -254,15 +266,17 @@ export const ExpandedPushVsOverlay: Story = () => { export const DisableTypeSelection: Story = () => { const state: State = { - byId: { - memory: { - right: { - id: 'right', - }, - left: { - id: 'left', + panels: { + byId: { + memory: { + right: { + id: 'right', + }, + left: { + id: 'left', + }, + preview: undefined, }, - preview: undefined, }, }, }; diff --git a/packages/kbn-expandable-flyout/src/index.test.tsx b/packages/kbn-expandable-flyout/src/index.test.tsx index 1ec37bcd547c0..14146e2da4541 100644 --- a/packages/kbn-expandable-flyout/src/index.test.tsx +++ b/packages/kbn-expandable-flyout/src/index.test.tsx @@ -18,7 +18,7 @@ import { SETTINGS_MENU_BUTTON_TEST_ID, RIGHT_SECTION_TEST_ID, } from './components/test_ids'; -import { type State } from './state'; +import { type State } from './store/state'; import { TestProvider } from './test/provider'; import { REDUX_ID_FOR_MEMORY_STORAGE } from './constants'; @@ -33,7 +33,9 @@ const registeredPanels: Panel[] = [ describe('ExpandableFlyout', () => { it(`shouldn't render flyout if no panels`, () => { const state: State = { - byId: {}, + panels: { + byId: {}, + }, }; const result = render( @@ -47,13 +49,15 @@ describe('ExpandableFlyout', () => { it('should render right section', () => { const state = { - byId: { - [id]: { - right: { - id: 'key', + panels: { + byId: { + [id]: { + right: { + id: 'key', + }, + left: undefined, + preview: undefined, }, - left: undefined, - preview: undefined, }, }, }; @@ -69,13 +73,15 @@ describe('ExpandableFlyout', () => { it('should render left section', () => { const state = { - byId: { - [id]: { - right: undefined, - left: { - id: 'key', + panels: { + byId: { + [id]: { + right: undefined, + left: { + id: 'key', + }, + preview: undefined, }, - preview: undefined, }, }, }; @@ -91,15 +97,17 @@ describe('ExpandableFlyout', () => { it('should render preview section', () => { const state = { - byId: { - [id]: { - right: undefined, - left: undefined, - preview: [ - { - id: 'key', - }, - ], + panels: { + byId: { + [id]: { + right: undefined, + left: undefined, + preview: [ + { + id: 'key', + }, + ], + }, }, }, }; @@ -115,13 +123,15 @@ describe('ExpandableFlyout', () => { it('should not render flyout when right has value but does not matches registered panels', () => { const state = { - byId: { - [id]: { - right: { - id: 'key1', + panels: { + byId: { + [id]: { + right: { + id: 'key1', + }, + left: undefined, + preview: undefined, }, - left: undefined, - preview: undefined, }, }, }; @@ -138,13 +148,15 @@ describe('ExpandableFlyout', () => { it('should render the menu to change display options', () => { const state = { - byId: { - [id]: { - right: { - id: 'key', + panels: { + byId: { + [id]: { + right: { + id: 'key', + }, + left: undefined, + preview: undefined, }, - left: undefined, - preview: undefined, }, }, }; diff --git a/packages/kbn-expandable-flyout/src/provider.test.tsx b/packages/kbn-expandable-flyout/src/provider.test.tsx index 5bf71f31653e9..5aa386090aa30 100644 --- a/packages/kbn-expandable-flyout/src/provider.test.tsx +++ b/packages/kbn-expandable-flyout/src/provider.test.tsx @@ -11,8 +11,8 @@ import React from 'react'; import { render } from '@testing-library/react'; import { TestProvider } from './test/provider'; import { UrlSynchronizer } from './provider'; -import * as actions from './actions'; -import { State } from './state'; +import * as actions from './store/actions'; +import { State } from './store/state'; import { of } from 'rxjs'; const mockGet = jest.fn(); @@ -28,14 +28,16 @@ describe('UrlSynchronizer', () => { const urlChangedAction = jest.spyOn(actions, 'urlChangedAction'); const initialState: State = { - byId: { - [urlKey]: { - right: { id: 'key1' }, - left: { id: 'key11' }, - preview: undefined, + panels: { + byId: { + [urlKey]: { + right: { id: 'key1' }, + left: { id: 'key11' }, + preview: undefined, + }, }, + needsSync: true, }, - needsSync: true, }; render( @@ -55,8 +57,10 @@ describe('UrlSynchronizer', () => { change$: mockChange$, }); const initialState: State = { - byId: {}, - needsSync: true, + panels: { + byId: {}, + needsSync: true, + }, }; render( @@ -81,14 +85,16 @@ describe('UrlSynchronizer', () => { change$: mockChange$, }); const initialState: State = { - byId: { - [urlKey]: { - right: { id: 'key1' }, - left: { id: 'key2' }, - preview: undefined, + panels: { + byId: { + [urlKey]: { + right: { id: 'key1' }, + left: { id: 'key2' }, + preview: undefined, + }, }, + needsSync: true, }, - needsSync: true, }; render( diff --git a/packages/kbn-expandable-flyout/src/provider.tsx b/packages/kbn-expandable-flyout/src/provider.tsx index 15bcabc11fc10..cad83bb0ee808 100644 --- a/packages/kbn-expandable-flyout/src/provider.tsx +++ b/packages/kbn-expandable-flyout/src/provider.tsx @@ -12,10 +12,10 @@ import React, { FC, PropsWithChildren, useEffect, useMemo } from 'react'; import { Provider as ReduxProvider } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { ExpandableFlyoutContextProvider, useExpandableFlyoutContext } from './context'; -import { FlyoutState } from './state'; +import { FlyoutPanels } from './store/state'; import { useExpandableFlyoutState } from './hooks/use_expandable_flyout_state'; -import { Context, selectNeedsSync, store, useDispatch, useSelector } from './redux'; -import { urlChangedAction } from './actions'; +import { Context, selectNeedsSync, store, useDispatch, useSelector } from './store/redux'; +import { urlChangedAction } from './store/actions'; /** * Dispatches actions when url state changes and initializes the state when the app is loaded with flyout url parameters @@ -43,7 +43,7 @@ export const UrlSynchronizer = () => { return; } - const currentValue = urlStorage.get(urlKey); + const currentValue = urlStorage.get(urlKey); // Dispatch current value to redux store as it does not happen automatically if (currentValue) { @@ -56,7 +56,7 @@ export const UrlSynchronizer = () => { ); } - const subscription = urlStorage.change$(urlKey).subscribe((value) => { + const subscription = urlStorage.change$(urlKey).subscribe((value) => { dispatch(urlChangedAction({ ...value, preview: value?.preview?.at(-1), id: urlKey })); }); diff --git a/packages/kbn-expandable-flyout/src/actions.ts b/packages/kbn-expandable-flyout/src/store/actions.ts similarity index 98% rename from packages/kbn-expandable-flyout/src/actions.ts rename to packages/kbn-expandable-flyout/src/store/actions.ts index 6b127da797271..237a3d0226b05 100644 --- a/packages/kbn-expandable-flyout/src/actions.ts +++ b/packages/kbn-expandable-flyout/src/store/actions.ts @@ -8,7 +8,7 @@ */ import { createAction } from '@reduxjs/toolkit'; -import { FlyoutPanelProps } from './types'; +import { FlyoutPanelProps } from '../types'; export enum ActionType { openFlyout = 'open_flyout', diff --git a/packages/kbn-expandable-flyout/src/reducer.test.ts b/packages/kbn-expandable-flyout/src/store/reducers.test.ts similarity index 78% rename from packages/kbn-expandable-flyout/src/reducer.test.ts rename to packages/kbn-expandable-flyout/src/store/reducers.test.ts index 6cb56f86c6794..aafd72196d0f5 100644 --- a/packages/kbn-expandable-flyout/src/reducer.test.ts +++ b/packages/kbn-expandable-flyout/src/store/reducers.test.ts @@ -7,9 +7,9 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { FlyoutPanelProps } from './types'; -import { reducer } from './reducer'; -import { initialState, State } from './state'; +import { FlyoutPanelProps } from '../types'; +import { panelsReducer } from './reducers'; +import { initialPanelsState, PanelsState } from './state'; import { closeLeftPanelAction, closePanelsAction, @@ -49,17 +49,18 @@ const previewPanel2: FlyoutPanelProps = { id: 'preview2', state: { id: 'state' }, }; -describe('reducer', () => { + +describe('panelsReducer', () => { describe('should handle openFlyout action', () => { it('should add panels to empty state', () => { - const state: State = initialState; + const state: PanelsState = initialPanelsState; const action = openPanelsAction({ right: rightPanel1, left: leftPanel1, preview: previewPanel1, id: id1, }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { @@ -74,7 +75,7 @@ describe('reducer', () => { }); it('should override all panels in the state', () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: leftPanel1, @@ -89,7 +90,7 @@ describe('reducer', () => { preview: previewPanel2, id: id1, }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { @@ -104,7 +105,7 @@ describe('reducer', () => { }); it('should remove all panels despite only passing a single section ', () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: leftPanel1, @@ -117,7 +118,7 @@ describe('reducer', () => { right: rightPanel2, id: id1, }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { @@ -132,7 +133,7 @@ describe('reducer', () => { }); it('should add panels to a new key', () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: leftPanel1, @@ -145,7 +146,7 @@ describe('reducer', () => { right: rightPanel2, id: id2, }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { @@ -167,9 +168,9 @@ describe('reducer', () => { describe('should handle openRightPanel action', () => { it('should add right panel to empty state', () => { - const state: State = initialState; + const state: PanelsState = initialPanelsState; const action = openRightPanelAction({ right: rightPanel1, id: id1 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { @@ -184,7 +185,7 @@ describe('reducer', () => { }); it('should replace right panel', () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: leftPanel1, @@ -194,7 +195,7 @@ describe('reducer', () => { }, }; const action = openRightPanelAction({ right: rightPanel2, id: id1 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { @@ -209,7 +210,7 @@ describe('reducer', () => { }); it('should add right panel to a different key', () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: leftPanel1, @@ -219,7 +220,7 @@ describe('reducer', () => { }, }; const action = openRightPanelAction({ right: rightPanel2, id: id2 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { @@ -241,9 +242,9 @@ describe('reducer', () => { describe('should handle openLeftPanel action', () => { it('should add left panel to empty state', () => { - const state: State = initialState; + const state: PanelsState = initialPanelsState; const action = openLeftPanelAction({ left: leftPanel1, id: id1 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { @@ -258,7 +259,7 @@ describe('reducer', () => { }); it('should replace only left panel', () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: leftPanel1, @@ -268,7 +269,7 @@ describe('reducer', () => { }, }; const action = openLeftPanelAction({ left: leftPanel2, id: id1 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { @@ -283,7 +284,7 @@ describe('reducer', () => { }); it('should add left panel to a different key', () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: leftPanel1, @@ -293,7 +294,7 @@ describe('reducer', () => { }, }; const action = openLeftPanelAction({ left: leftPanel2, id: id2 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { @@ -315,9 +316,9 @@ describe('reducer', () => { describe('should handle openPreviewPanel action', () => { it('should add preview panel to empty state', () => { - const state: State = initialState; + const state: PanelsState = initialPanelsState; const action = openPreviewPanelAction({ preview: previewPanel1, id: id1 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { @@ -332,7 +333,7 @@ describe('reducer', () => { }); it('should add preview panel to the list of preview panels', () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: leftPanel1, @@ -342,7 +343,7 @@ describe('reducer', () => { }, }; const action = openPreviewPanelAction({ preview: previewPanel2, id: id1 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { @@ -357,7 +358,7 @@ describe('reducer', () => { }); it('should add preview panel to a different key', () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: leftPanel1, @@ -367,7 +368,7 @@ describe('reducer', () => { }, }; const action = openPreviewPanelAction({ preview: previewPanel2, id: id2 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { @@ -389,15 +390,18 @@ describe('reducer', () => { describe('should handle closeRightPanel action', () => { it('should return empty state when removing right panel from empty state', () => { - const state: State = initialState; + const state: PanelsState = initialPanelsState; const action = closeRightPanelAction({ id: id1 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); - expect(newState).toEqual({ ...state, needsSync: true }); + expect(newState).toEqual({ + ...state, + needsSync: true, + }); }); it(`should return unmodified state when removing right panel when no right panel exist`, () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: leftPanel1, @@ -407,13 +411,16 @@ describe('reducer', () => { }, }; const action = closeRightPanelAction({ id: id1 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); - expect(newState).toEqual({ ...state, needsSync: true }); + expect(newState).toEqual({ + ...state, + needsSync: true, + }); }); it('should remove right panel', () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: leftPanel1, @@ -424,7 +431,7 @@ describe('reducer', () => { }; const action = closeRightPanelAction({ id: id1 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { @@ -439,7 +446,7 @@ describe('reducer', () => { }); it('should not remove right panel for a different key', () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: leftPanel1, @@ -450,7 +457,7 @@ describe('reducer', () => { }; const action = closeRightPanelAction({ id: id2 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { @@ -467,15 +474,18 @@ describe('reducer', () => { describe('should handle closeLeftPanel action', () => { it('should return empty state when removing left panel on empty state', () => { - const state: State = initialState; + const state: PanelsState = initialPanelsState; const action = closeLeftPanelAction({ id: id1 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); - expect(newState).toEqual({ ...state, needsSync: true }); + expect(newState).toEqual({ + ...state, + needsSync: true, + }); }); it(`should return unmodified state when removing left panel when no left panel exist`, () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: undefined, @@ -485,13 +495,16 @@ describe('reducer', () => { }, }; const action = closeLeftPanelAction({ id: id1 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); - expect(newState).toEqual({ ...state, needsSync: true }); + expect(newState).toEqual({ + ...state, + needsSync: true, + }); }); it('should remove left panel', () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: leftPanel1, @@ -501,7 +514,7 @@ describe('reducer', () => { }, }; const action = closeLeftPanelAction({ id: id1 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { @@ -516,7 +529,7 @@ describe('reducer', () => { }); it('should not remove left panel for a different key', () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: leftPanel1, @@ -526,7 +539,7 @@ describe('reducer', () => { }, }; const action = closeLeftPanelAction({ id: id2 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { @@ -543,15 +556,18 @@ describe('reducer', () => { describe('should handle closePreviewPanel action', () => { it('should return empty state when removing preview panel on empty state', () => { - const state: State = initialState; + const state: PanelsState = initialPanelsState; const action = closePreviewPanelAction({ id: id1 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); - expect(newState).toEqual({ ...state, needsSync: true }); + expect(newState).toEqual({ + ...state, + needsSync: true, + }); }); it(`should return unmodified state when removing preview panel when no preview panel exist`, () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: leftPanel1, @@ -561,13 +577,16 @@ describe('reducer', () => { }, }; const action = closePreviewPanelAction({ id: id1 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); - expect(newState).toEqual({ ...state, needsSync: true }); + expect(newState).toEqual({ + ...state, + needsSync: true, + }); }); it('should remove all preview panels', () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: rightPanel1, @@ -577,7 +596,7 @@ describe('reducer', () => { }, }; const action = closePreviewPanelAction({ id: id1 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { @@ -592,7 +611,7 @@ describe('reducer', () => { }); it('should not remove preview panels for a different key', () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: leftPanel1, @@ -602,7 +621,7 @@ describe('reducer', () => { }, }; const action = closePreviewPanelAction({ id: id2 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { @@ -619,15 +638,18 @@ describe('reducer', () => { describe('should handle previousPreviewPanel action', () => { it('should return empty state when previous preview panel on an empty state', () => { - const state: State = initialState; + const state: PanelsState = initialPanelsState; const action = previousPreviewPanelAction({ id: id1 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); - expect(newState).toEqual({ ...initialState, needsSync: false }); + expect(newState).toEqual({ + ...initialPanelsState, + needsSync: false, + }); }); it(`should return unmodified state when previous preview panel when no preview panel exist`, () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: leftPanel1, @@ -637,13 +659,16 @@ describe('reducer', () => { }, }; const action = previousPreviewPanelAction({ id: id1 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); - expect(newState).toEqual({ ...state, needsSync: false }); + expect(newState).toEqual({ + ...state, + needsSync: false, + }); }); it('should remove only last preview panel', () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: rightPanel1, @@ -653,7 +678,7 @@ describe('reducer', () => { }, }; const action = previousPreviewPanelAction({ id: id1 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { @@ -668,7 +693,7 @@ describe('reducer', () => { }); it('should not remove the last preview panel for a different key', () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: leftPanel1, @@ -678,7 +703,7 @@ describe('reducer', () => { }, }; const action = previousPreviewPanelAction({ id: id2 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { @@ -695,15 +720,18 @@ describe('reducer', () => { describe('should handle closeFlyout action', () => { it('should return empty state when closing flyout on an empty state', () => { - const state: State = initialState; + const state: PanelsState = initialPanelsState; const action = closePanelsAction({ id: id1 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); - expect(newState).toEqual({ ...initialState, needsSync: true }); + expect(newState).toEqual({ + ...initialPanelsState, + needsSync: true, + }); }); it('should remove all panels', () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: leftPanel1, @@ -713,7 +741,7 @@ describe('reducer', () => { }, }; const action = closePanelsAction({ id: id1 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { @@ -728,7 +756,7 @@ describe('reducer', () => { }); it('should not remove panels for a different key', () => { - const state: State = { + const state: PanelsState = { byId: { [id1]: { left: leftPanel1, @@ -738,7 +766,7 @@ describe('reducer', () => { }, }; const action = closePanelsAction({ id: id2 }); - const newState: State = reducer(state, action); + const newState: PanelsState = panelsReducer(state, action); expect(newState).toEqual({ byId: { diff --git a/packages/kbn-expandable-flyout/src/reducer.ts b/packages/kbn-expandable-flyout/src/store/reducers.ts similarity index 96% rename from packages/kbn-expandable-flyout/src/reducer.ts rename to packages/kbn-expandable-flyout/src/store/reducers.ts index 49c4c4b9774b1..8971fd55f7571 100644 --- a/packages/kbn-expandable-flyout/src/reducer.ts +++ b/packages/kbn-expandable-flyout/src/store/reducers.ts @@ -21,9 +21,9 @@ import { openPreviewPanelAction, urlChangedAction, } from './actions'; -import { initialState } from './state'; +import { initialPanelsState } from './state'; -export const reducer = createReducer(initialState, (builder) => { +export const panelsReducer = createReducer(initialPanelsState, (builder) => { builder.addCase(openPanelsAction, (state, { payload: { preview, left, right, id } }) => { if (id in state.byId) { state.byId[id].right = right; @@ -72,7 +72,7 @@ export const reducer = createReducer(initialState, (builder) => { if (id in state.byId) { if (state.byId[id].preview) { const previewIdenticalToLastOne = deepEqual(preview, state.byId[id].preview?.at(-1)); - // Only append preview when it does not match the last item in state.byId[id].preview + // Only append preview when it does not match the last item in state.data.byId[id].preview if (!previewIdenticalToLastOne) { state.byId[id].preview?.push(preview); } diff --git a/packages/kbn-expandable-flyout/src/redux.ts b/packages/kbn-expandable-flyout/src/store/redux.ts similarity index 77% rename from packages/kbn-expandable-flyout/src/redux.ts rename to packages/kbn-expandable-flyout/src/store/redux.ts index 5cc80517c5c9f..0e81ba74de2de 100644 --- a/packages/kbn-expandable-flyout/src/redux.ts +++ b/packages/kbn-expandable-flyout/src/store/redux.ts @@ -11,13 +11,14 @@ import { createContext } from 'react'; import { createDispatchHook, createSelectorHook, ReactReduxContextValue } from 'react-redux'; import { configureStore } from '@reduxjs/toolkit'; import { createSelector } from 'reselect'; -import { reducer } from './reducer'; +import { panelsReducer } from './reducers'; import { initialState, State } from './state'; export const store = configureStore({ - reducer, + reducer: { + panels: panelsReducer, + }, devTools: process.env.NODE_ENV !== 'production', - enhancers: [], }); export const Context = createContext>({ @@ -30,7 +31,7 @@ export const useSelector = createSelectorHook(Context); const stateSelector = (state: State) => state; +const panelsSelector = createSelector(stateSelector, (state) => state.panels); export const selectPanelsById = (id: string) => - createSelector(stateSelector, (state) => state.byId[id] || {}); - -export const selectNeedsSync = () => createSelector(stateSelector, (state) => state.needsSync); + createSelector(panelsSelector, (state) => state.byId[id] || {}); +export const selectNeedsSync = () => createSelector(panelsSelector, (state) => state.needsSync); diff --git a/packages/kbn-expandable-flyout/src/state.ts b/packages/kbn-expandable-flyout/src/store/state.ts similarity index 79% rename from packages/kbn-expandable-flyout/src/state.ts rename to packages/kbn-expandable-flyout/src/store/state.ts index 40cf03f43d868..12f1b0135460b 100644 --- a/packages/kbn-expandable-flyout/src/state.ts +++ b/packages/kbn-expandable-flyout/src/store/state.ts @@ -7,9 +7,9 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { FlyoutPanelProps } from './types'; +import { FlyoutPanelProps } from '../..'; -export interface FlyoutState { +export interface FlyoutPanels { /** * Panel to render in the left section */ @@ -24,12 +24,12 @@ export interface FlyoutState { preview: FlyoutPanelProps[] | undefined; } -export interface State { +export interface PanelsState { /** * Store the panels for multiple flyouts */ byId: { - [id: string]: FlyoutState; + [id: string]: FlyoutPanels; }; /** * Is the flyout in sync with external storage (eg. url)? @@ -39,7 +39,18 @@ export interface State { needsSync?: boolean; } -export const initialState: State = { +export const initialPanelsState: PanelsState = { byId: {}, needsSync: false, }; + +export interface State { + /** + * All panels related information + */ + panels: PanelsState; +} + +export const initialState: State = { + panels: initialPanelsState, +}; diff --git a/packages/kbn-expandable-flyout/src/test/provider.tsx b/packages/kbn-expandable-flyout/src/test/provider.tsx index bf0ca914927b2..b6914099e2e42 100644 --- a/packages/kbn-expandable-flyout/src/test/provider.tsx +++ b/packages/kbn-expandable-flyout/src/test/provider.tsx @@ -12,9 +12,9 @@ import { configureStore } from '@reduxjs/toolkit'; import React, { FC, PropsWithChildren } from 'react'; import { I18nProvider } from '@kbn/i18n-react'; import { ExpandableFlyoutContextProvider } from '../context'; -import { reducer } from '../reducer'; -import { Context } from '../redux'; -import { initialState, State } from '../state'; +import { panelsReducer } from '../store/reducers'; +import { Context } from '../store/redux'; +import { initialState, State } from '../store/state'; interface TestProviderProps { state?: State; @@ -27,7 +27,9 @@ export const TestProvider: FC> = ({ urlKey, }) => { const store = configureStore({ - reducer, + reducer: { + panels: panelsReducer, + }, devTools: false, preloadedState: state, enhancers: [], diff --git a/packages/kbn-ftr-common-functional-services/index.ts b/packages/kbn-ftr-common-functional-services/index.ts index e156949f0daf9..506566216c721 100644 --- a/packages/kbn-ftr-common-functional-services/index.ts +++ b/packages/kbn-ftr-common-functional-services/index.ts @@ -14,7 +14,7 @@ import { KibanaServerProvider } from './services/kibana_server'; export { KibanaServerProvider } from './services/kibana_server'; export type KibanaServer = ProvidedType; -export { RetryService } from './services/retry'; +export { RetryService, type TryWithRetriesOptions } from './services/retry'; import { EsArchiverProvider } from './services/es_archiver'; export type EsArchiver = ProvidedType; diff --git a/packages/kbn-ftr-common-functional-services/services/retry/index.ts b/packages/kbn-ftr-common-functional-services/services/retry/index.ts index 6f42e0368364d..f96e413da2680 100644 --- a/packages/kbn-ftr-common-functional-services/services/retry/index.ts +++ b/packages/kbn-ftr-common-functional-services/services/retry/index.ts @@ -7,4 +7,4 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { RetryService } from './retry'; +export { RetryService, type TryWithRetriesOptions } from './retry'; diff --git a/packages/kbn-ftr-common-functional-services/services/retry/retry.ts b/packages/kbn-ftr-common-functional-services/services/retry/retry.ts index 9ddd13ea583a7..614f57064512c 100644 --- a/packages/kbn-ftr-common-functional-services/services/retry/retry.ts +++ b/packages/kbn-ftr-common-functional-services/services/retry/retry.ts @@ -11,7 +11,7 @@ import { FtrService } from '../ftr_provider_context'; import { retryForSuccess } from './retry_for_success'; import { retryForTruthy } from './retry_for_truthy'; -interface TryWithRetriesOptions { +export interface TryWithRetriesOptions { retryCount: number; retryDelay?: number; timeout?: number; diff --git a/packages/kbn-ftr-common-functional-services/services/retry/retry_for_success.ts b/packages/kbn-ftr-common-functional-services/services/retry/retry_for_success.ts index 5401eb21286d1..921efacd88fcc 100644 --- a/packages/kbn-ftr-common-functional-services/services/retry/retry_for_success.ts +++ b/packages/kbn-ftr-common-functional-services/services/retry/retry_for_success.ts @@ -92,7 +92,7 @@ export async function retryForSuccess(log: ToolingLog, options: Options) { if (lastError && onFailureBlock) { const before = await runAttempt(onFailureBlock); if ('error' in before) { - log.debug(`--- onRetryBlock error: ${before.error.message}`); + log.debug(`--- onRetryBlock error: ${before.error.message} - Attempt #: ${attemptCounter}`); } } @@ -104,9 +104,13 @@ export async function retryForSuccess(log: ToolingLog, options: Options) { if ('error' in attempt) { if (lastError && lastError.message === attempt.error.message) { - log.debug(`--- ${methodName} failed again with the same message...`); + log.debug( + `--- ${methodName} failed again with the same message... - Attempt #: ${attemptCounter}` + ); } else { - log.debug(`--- ${methodName} error: ${attempt.error.message}`); + log.debug( + `--- ${methodName} error: ${attempt.error.message} - Attempt #: ${attemptCounter}` + ); } lastError = attempt.error; diff --git a/packages/kbn-investigation-shared/src/rest_specs/find.ts b/packages/kbn-investigation-shared/src/rest_specs/find.ts index 2a3eab76fbb54..7a938212cfba4 100644 --- a/packages/kbn-investigation-shared/src/rest_specs/find.ts +++ b/packages/kbn-investigation-shared/src/rest_specs/find.ts @@ -15,8 +15,10 @@ const findInvestigationsParamsSchema = z query: z .object({ alertId: z.string(), - page: z.string(), - perPage: z.string(), + search: z.string(), + filter: z.string(), + page: z.coerce.number(), + perPage: z.coerce.number(), }) .partial(), }) diff --git a/packages/kbn-investigation-shared/src/rest_specs/get_all_investigation_stats.ts b/packages/kbn-investigation-shared/src/rest_specs/get_all_investigation_stats.ts new file mode 100644 index 0000000000000..bee9f15db587d --- /dev/null +++ b/packages/kbn-investigation-shared/src/rest_specs/get_all_investigation_stats.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { z } from '@kbn/zod'; +import { statusSchema } from '../schema'; + +const getAllInvestigationStatsParamsSchema = z.object({ + query: z.object({}), +}); + +const getAllInvestigationStatsResponseSchema = z.object({ + count: z.record(statusSchema, z.number()), + total: z.number(), +}); + +type GetAllInvestigationStatsResponse = z.output; + +export { getAllInvestigationStatsParamsSchema, getAllInvestigationStatsResponseSchema }; +export type { GetAllInvestigationStatsResponse }; diff --git a/packages/kbn-investigation-shared/src/rest_specs/get_all_investigation_tags.ts b/packages/kbn-investigation-shared/src/rest_specs/get_all_investigation_tags.ts new file mode 100644 index 0000000000000..35665b1b3c695 --- /dev/null +++ b/packages/kbn-investigation-shared/src/rest_specs/get_all_investigation_tags.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { z } from '@kbn/zod'; + +const getAllInvestigationTagsParamsSchema = z.object({ + query: z.object({}), +}); + +const getAllInvestigationTagsResponseSchema = z.string().array(); + +type GetAllInvestigationTagsResponse = z.output; + +export { getAllInvestigationTagsParamsSchema, getAllInvestigationTagsResponseSchema }; +export type { GetAllInvestigationTagsResponse }; diff --git a/packages/kbn-investigation-shared/src/rest_specs/index.ts b/packages/kbn-investigation-shared/src/rest_specs/index.ts index eb30920430673..c00ec5035765e 100644 --- a/packages/kbn-investigation-shared/src/rest_specs/index.ts +++ b/packages/kbn-investigation-shared/src/rest_specs/index.ts @@ -17,6 +17,8 @@ export type * from './find'; export type * from './get'; export type * from './get_items'; export type * from './get_notes'; +export type * from './get_all_investigation_stats'; +export type * from './get_all_investigation_tags'; export type * from './investigation'; export type * from './investigation_item'; export type * from './investigation_note'; @@ -34,6 +36,8 @@ export * from './find'; export * from './get'; export * from './get_items'; export * from './get_notes'; +export * from './get_all_investigation_stats'; +export * from './get_all_investigation_tags'; export * from './investigation'; export * from './investigation_item'; export * from './investigation_note'; diff --git a/packages/kbn-investigation-shared/src/schema/index.ts b/packages/kbn-investigation-shared/src/schema/index.ts index f48b6a40416d0..7491ecce76cc2 100644 --- a/packages/kbn-investigation-shared/src/schema/index.ts +++ b/packages/kbn-investigation-shared/src/schema/index.ts @@ -11,3 +11,5 @@ export * from './investigation'; export * from './investigation_item'; export * from './investigation_note'; export * from './origin'; + +export type * from './investigation'; diff --git a/packages/kbn-investigation-shared/src/schema/investigation.ts b/packages/kbn-investigation-shared/src/schema/investigation.ts index cd99de702c9e5..9be39b5b2a7b3 100644 --- a/packages/kbn-investigation-shared/src/schema/investigation.ts +++ b/packages/kbn-investigation-shared/src/schema/investigation.ts @@ -25,6 +25,7 @@ const investigationSchema = z.object({ title: z.string(), createdAt: z.number(), createdBy: z.string(), + updatedAt: z.number(), params: z.object({ timeRange: z.object({ from: z.number(), to: z.number() }), }), @@ -35,4 +36,7 @@ const investigationSchema = z.object({ items: z.array(investigationItemSchema), }); +type Status = z.infer; + +export type { Status }; export { investigationSchema, statusSchema }; diff --git a/packages/kbn-investigation-shared/src/schema/investigation_item.ts b/packages/kbn-investigation-shared/src/schema/investigation_item.ts index e7578977bd254..820db8500e5dc 100644 --- a/packages/kbn-investigation-shared/src/schema/investigation_item.ts +++ b/packages/kbn-investigation-shared/src/schema/investigation_item.ts @@ -20,6 +20,7 @@ const investigationItemSchema = z.intersection( id: z.string(), createdAt: z.number(), createdBy: z.string(), + updatedAt: z.number(), }), itemSchema ); diff --git a/packages/kbn-investigation-shared/src/schema/investigation_note.ts b/packages/kbn-investigation-shared/src/schema/investigation_note.ts index a4ca46158e1bb..ff877ab884127 100644 --- a/packages/kbn-investigation-shared/src/schema/investigation_note.ts +++ b/packages/kbn-investigation-shared/src/schema/investigation_note.ts @@ -13,6 +13,7 @@ const investigationNoteSchema = z.object({ id: z.string(), content: z.string(), createdAt: z.number(), + updatedAt: z.number(), createdBy: z.string(), }); diff --git a/packages/kbn-management/settings/setting_ids/index.ts b/packages/kbn-management/settings/setting_ids/index.ts index 08ce7f3579229..0f79a5fff0506 100644 --- a/packages/kbn-management/settings/setting_ids/index.ts +++ b/packages/kbn-management/settings/setting_ids/index.ts @@ -150,6 +150,7 @@ export const OBSERVABILITY_AI_ASSISTANT_SIMULATED_FUNCTION_CALLING = 'observability:aiAssistantSimulatedFunctionCalling'; export const OBSERVABILITY_AI_ASSISTANT_SEARCH_CONNECTOR_INDEX_PATTERN = 'observability:aiAssistantSearchConnectorIndexPattern'; +export const OBSERVABILITY_SEARCH_EXCLUDED_DATA_TIERS = 'observability:searchExcludedDataTiers'; // Reporting settings export const XPACK_REPORTING_CUSTOM_PDF_LOGO_ID = 'xpackReporting:customPdfLogo'; diff --git a/packages/kbn-mapbox-gl/kibana.jsonc b/packages/kbn-mapbox-gl/kibana.jsonc index 4238b33f6aefd..6cc7e1f7b2b30 100644 --- a/packages/kbn-mapbox-gl/kibana.jsonc +++ b/packages/kbn-mapbox-gl/kibana.jsonc @@ -1,5 +1,5 @@ { "type": "shared-common", "id": "@kbn/mapbox-gl", - "owner": "@elastic/kibana-gis" + "owner": "@elastic/kibana-presentation" } diff --git a/packages/serverless/settings/observability_project/index.ts b/packages/serverless/settings/observability_project/index.ts index 470964954d166..85f6327bf0a07 100644 --- a/packages/serverless/settings/observability_project/index.ts +++ b/packages/serverless/settings/observability_project/index.ts @@ -37,4 +37,5 @@ export const OBSERVABILITY_PROJECT_SETTINGS = [ settings.OBSERVABILITY_AI_ASSISTANT_LOGS_INDEX_PATTERN_ID, settings.OBSERVABILITY_AI_ASSISTANT_SIMULATED_FUNCTION_CALLING, settings.OBSERVABILITY_AI_ASSISTANT_SEARCH_CONNECTOR_INDEX_PATTERN, + settings.OBSERVABILITY_SEARCH_EXCLUDED_DATA_TIERS, ]; diff --git a/renovate.json b/renovate.json index 02ec0d0c127a4..6f3b61c6e1b12 100644 --- a/renovate.json +++ b/renovate.json @@ -371,7 +371,6 @@ "team:kibana-presentation", "team:kibana-data-discovery", "team:kibana-management", - "team:kibana-gis", "team:security-solution" ], "matchBaseBranches": ["main"], diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index 92de0c925951b..170cfa5958782 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -93,6 +93,7 @@ describe('checking migration metadata changes on all registered SO types', () => "enterprise_search_telemetry": "9ac912e1417fc8681e0cd383775382117c9e3d3d", "entity-definition": "61be3e95966045122b55e181bb39658b1dc9bbe9", "entity-discovery-api-key": "c267a65c69171d1804362155c1378365f5acef88", + "entity-engine-status": "0738aa1a06d3361911740f8f166071ea43a00927", "epm-packages": "8042d4a1522f6c4e6f5486e791b3ffe3a22f88fd", "epm-packages-assets": "7a3e58efd9a14191d0d1a00b8aaed30a145fd0b1", "event-annotation-group": "715ba867d8c68f3c9438052210ea1c30a9362582", diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts index 4320b0eb689d9..e95a82e63d0ff 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts @@ -124,6 +124,7 @@ const previouslyRegisteredTypes = [ 'security-rule', 'security-solution-signals-migration', 'risk-engine-configuration', + 'entity-engine-status', 'server', 'siem-detection-engine-rule-actions', 'siem-detection-engine-rule-execution-info', diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index d1ab81f3e60a7..52c0df738246a 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -694,4 +694,11 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'observability:searchExcludedDataTiers': { + type: 'array', + items: { + type: 'keyword', + _meta: { description: 'Non-default value of setting.' }, + }, + }, }; diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index c66f4f07a296e..0a0ebe8ebbac6 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -181,4 +181,5 @@ export interface UsageStats { 'aiAssistant:preferredAIAssistantType': string; 'observability:profilingFetchTopNFunctionsFromStacktraces': boolean; 'securitySolution:excludedDataTiersForRuleExecution': string[]; + 'observability:searchExcludedDataTiers': string[]; } diff --git a/src/plugins/maps_ems/kibana.jsonc b/src/plugins/maps_ems/kibana.jsonc index f71542e94ae71..a341ad05f4e4b 100644 --- a/src/plugins/maps_ems/kibana.jsonc +++ b/src/plugins/maps_ems/kibana.jsonc @@ -1,7 +1,7 @@ { "type": "plugin", "id": "@kbn/maps-ems-plugin", - "owner": "@elastic/kibana-gis", + "owner": "@elastic/kibana-presentation", "plugin": { "id": "mapsEms", "server": true, diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 70fbeec73bc5d..77e050334803b 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -10355,6 +10355,15 @@ } } }, + "observability:searchExcludedDataTiers": { + "type": "array", + "items": { + "type": "keyword", + "_meta": { + "description": "Non-default value of setting." + } + } + }, "banners:placement": { "type": "keyword", "_meta": { diff --git a/test/functional/apps/console/monaco/_autocomplete.ts b/test/functional/apps/console/monaco/_autocomplete.ts index 36d96443a5a69..6e0d83ffcd56e 100644 --- a/test/functional/apps/console/monaco/_autocomplete.ts +++ b/test/functional/apps/console/monaco/_autocomplete.ts @@ -34,7 +34,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); } - describe('console autocomplete feature', function describeIndexTests() { + // Failing: See https://github.com/elastic/kibana/issues/191808 + describe.skip('console autocomplete feature', function describeIndexTests() { this.tags('includeFirefox'); before(async () => { log.debug('navigateTo console'); diff --git a/x-pack/examples/third_party_maps_source_example/kibana.jsonc b/x-pack/examples/third_party_maps_source_example/kibana.jsonc index 6b1317437401d..5b987dcd966ab 100644 --- a/x-pack/examples/third_party_maps_source_example/kibana.jsonc +++ b/x-pack/examples/third_party_maps_source_example/kibana.jsonc @@ -1,7 +1,7 @@ { "type": "plugin", "id": "@kbn/maps-custom-raster-source-plugin", - "owner": "@elastic/kibana-gis", + "owner": "@elastic/kibana-presentation", "description": "An example plugin for creating a custom raster source for Elastic Maps", "plugin": { "id": "mapsCustomRasterSource", diff --git a/x-pack/packages/maps/vector_tile_utils/kibana.jsonc b/x-pack/packages/maps/vector_tile_utils/kibana.jsonc index 7fa54b903a4a5..5e1e9957ecdf3 100644 --- a/x-pack/packages/maps/vector_tile_utils/kibana.jsonc +++ b/x-pack/packages/maps/vector_tile_utils/kibana.jsonc @@ -1,5 +1,5 @@ { "type": "shared-common", "id": "@kbn/maps-vector-tile-utils", - "owner": "@elastic/kibana-gis" + "owner": "@elastic/kibana-presentation" } diff --git a/x-pack/plugins/cases/public/components/case_form_fields/assignees.test.tsx b/x-pack/plugins/cases/public/components/case_form_fields/assignees.test.tsx index 77a781535eb91..fd0a75f6d7a28 100644 --- a/x-pack/plugins/cases/public/components/case_form_fields/assignees.test.tsx +++ b/x-pack/plugins/cases/public/components/case_form_fields/assignees.test.tsx @@ -23,7 +23,8 @@ jest.mock('../../containers/user_profiles/api'); const currentUserProfile = userProfiles[0]; -describe('Assignees', () => { +// Failing: See https://github.com/elastic/kibana/issues/189719 +describe.skip('Assignees', () => { let globalForm: FormHook; let appMockRender: AppMockRenderer; diff --git a/x-pack/plugins/cases/public/components/files/file_name_link.test.tsx b/x-pack/plugins/cases/public/components/files/file_name_link.test.tsx index 0fa3324d295f3..efededf3fba89 100644 --- a/x-pack/plugins/cases/public/components/files/file_name_link.test.tsx +++ b/x-pack/plugins/cases/public/components/files/file_name_link.test.tsx @@ -15,7 +15,8 @@ import { createAppMockRenderer } from '../../common/mock'; import { basicFileMock } from '../../containers/mock'; import { FileNameLink } from './file_name_link'; -describe('FileNameLink', () => { +// Failing: See https://github.com/elastic/kibana/issues/192944 +describe.skip('FileNameLink', () => { let appMockRender: AppMockRenderer; const defaultProps = { diff --git a/x-pack/plugins/file_upload/kibana.jsonc b/x-pack/plugins/file_upload/kibana.jsonc index 6c6e3fddd0e7c..9d8143dafcb46 100644 --- a/x-pack/plugins/file_upload/kibana.jsonc +++ b/x-pack/plugins/file_upload/kibana.jsonc @@ -1,7 +1,7 @@ { "type": "plugin", "id": "@kbn/file-upload-plugin", - "owner": ["@elastic/kibana-gis", "@elastic/ml-ui"], + "owner": ["@elastic/kibana-presentation", "@elastic/ml-ui"], "description": "The file upload plugin contains components and services for uploading a file, analyzing its data, and then importing the data into an Elasticsearch index. Supported file types include CSV, TSV, newline-delimited JSON and GeoJSON.", "plugin": { "id": "fileUpload", diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx index 4617cc9bd8b1f..58c512a26c294 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx @@ -39,7 +39,8 @@ import { Detail } from '.'; // @ts-ignore this saves us having to define all experimental features ExperimentalFeaturesService.init({}); -describe('when on integration detail', () => { +// Failing: See https://github.com/elastic/kibana/issues/192999 +describe.skip('when on integration detail', () => { const pkgkey = 'nginx-0.3.7'; const detailPageUrlPath = pagePathGetters.integration_details_overview({ pkgkey })[1]; let testRenderer: TestRenderer; diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 3967c3b6abc7c..21c3f1bf97f12 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -66,7 +66,12 @@ import { } from '../common'; import type { ExperimentalFeatures } from '../common/experimental_features'; import { parseExperimentalConfigValue } from '../common/experimental_features'; - +import { + LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, + PACKAGE_POLICY_SAVED_OBJECT_TYPE, + AGENT_POLICY_SAVED_OBJECT_TYPE, + LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, +} from '../common/constants'; import { getFilesClientFactory } from './services/files/get_files_client_factory'; import type { MessageSigningServiceInterface } from './services/security'; @@ -79,12 +84,10 @@ import { } from './services/security'; import { - LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, ASSETS_SAVED_OBJECT_TYPE, DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE, FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, OUTPUT_SAVED_OBJECT_TYPE, - PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGES_SAVED_OBJECT_TYPE, PLUGIN_ID, PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, @@ -191,6 +194,8 @@ export type FleetSetupContract = void; const allSavedObjectTypes = [ OUTPUT_SAVED_OBJECT_TYPE, LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, + AGENT_POLICY_SAVED_OBJECT_TYPE, + LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGES_SAVED_OBJECT_TYPE, ASSETS_SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts index c24b36c382dc0..a86c627688207 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts @@ -191,6 +191,11 @@ export const bulkGetAgentPoliciesHandler: FleetRequestHandler< const fleetContext = await context.fleet; const soClient = fleetContext.internalSoClient; const { full: withPackagePolicies = false, ignoreMissing = false, ids } = request.body; + if (!fleetContext.authz.fleet.readAgentPolicies && withPackagePolicies) { + throw new FleetUnauthorizedError( + 'full query parameter require agent policies read permissions' + ); + } let items = await agentPolicyService.getByIDs(soClient, ids, { withPackagePolicies, ignoreMissing, diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/index.ts b/x-pack/plugins/fleet/server/routes/agent_policy/index.ts index 66e84cf4a76fe..0ff76addd1b16 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/index.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/index.ts @@ -61,8 +61,9 @@ export const registerRoutes = (router: FleetAuthzRouter) => { router.versioned .post({ path: AGENT_POLICY_API_ROUTES.BULK_GET_PATTERN, - fleetAuthz: { - fleet: { readAgentPolicies: true }, + fleetAuthz: (authz) => { + // Allow to retrieve agent policies metadata (no full) for user with only read agents permissions + return authz.fleet.readAgentPolicies || authz.fleet.readAgents; }, }) .addVersion( diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts index 8f4397883eb05..276478099daf8 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts @@ -146,35 +146,12 @@ export async function installKibanaAssets(options: { await makeManagedIndexPatternsGlobal(savedObjectsClient); - let installedAssets: SavedObjectsImportSuccess[] = []; - - if ( - assetsToInstall.length > MAX_ASSETS_TO_INSTALL_IN_PARALLEL && - !hasReferences(assetsToInstall) - ) { - // If the package size is too large, we need to install in chunks to avoid - // memory issues as the SO import creates a lot of objects in memory - - // NOTE: if there are references, we can't chunk the install because - // referenced objects might end up in different chunks leading to import - // errors. - for (const assetChunk of chunk(assetsToInstall, MAX_ASSETS_TO_INSTALL_IN_PARALLEL)) { - const result = await installKibanaSavedObjects({ - logger, - savedObjectsImporter, - kibanaAssets: assetChunk, - }); - installedAssets = installedAssets.concat(result); - } - } else { - installedAssets = await installKibanaSavedObjects({ - logger, - savedObjectsImporter, - kibanaAssets: assetsToInstall, - }); - } - - return installedAssets; + return await installKibanaSavedObjects({ + logger, + savedObjectsImporter, + kibanaAssets: assetsToInstall, + assetsChunkSize: MAX_ASSETS_TO_INSTALL_IN_PARALLEL, + }); } export async function installKibanaAssetsAndReferencesMultispace({ @@ -411,13 +388,71 @@ async function retryImportOnConflictError( // only exported for testing export async function installKibanaSavedObjects({ + savedObjectsImporter, + kibanaAssets, + assetsChunkSize, + logger, +}: { + kibanaAssets: ArchiveAsset[]; + savedObjectsImporter: SavedObjectsImporterContract; + logger: Logger; + assetsChunkSize?: number; +}): Promise { + if (!assetsChunkSize || kibanaAssets.length <= assetsChunkSize || hasReferences(kibanaAssets)) { + return await installKibanaSavedObjectsChunk({ + logger, + savedObjectsImporter, + kibanaAssets, + refresh: 'wait_for', + }); + } + + const installedAssets: SavedObjectsImportSuccess[] = []; + + // If the package size is too large, we need to install in chunks to avoid + // memory issues as the SO import creates a lot of objects in memory + + // NOTE: if there are references, we can't chunk the install because + // referenced objects might end up in different chunks leading to import + // errors. + const assetChunks = chunk(kibanaAssets, assetsChunkSize); + const allAssetChunksButLast = assetChunks.slice(0, -1); + const lastAssetChunk = assetChunks.slice(-1)[0]; + + for (const assetChunk of allAssetChunksButLast) { + const result = await installKibanaSavedObjectsChunk({ + logger, + savedObjectsImporter, + kibanaAssets: assetChunk, + refresh: false, + }); + + installedAssets.push(...result); + } + + const result = await installKibanaSavedObjectsChunk({ + logger, + savedObjectsImporter, + kibanaAssets: lastAssetChunk, + refresh: 'wait_for', + }); + + installedAssets.push(...result); + + return installedAssets; +} + +// only exported for testing +async function installKibanaSavedObjectsChunk({ savedObjectsImporter, kibanaAssets, logger, + refresh, }: { kibanaAssets: ArchiveAsset[]; savedObjectsImporter: SavedObjectsImporterContract; logger: Logger; + refresh?: boolean | 'wait_for'; }) { if (!kibanaAssets.length) { return []; @@ -437,8 +472,8 @@ export async function installKibanaSavedObjects({ overwrite: true, readStream, createNewCopies: false, - refresh: false, managed: true, + refresh, }); }); diff --git a/x-pack/plugins/integration_assistant/server/integration_builder/build_integration.ts b/x-pack/plugins/integration_assistant/server/integration_builder/build_integration.ts index 73113f6bf7b04..0598ee3ba2cca 100644 --- a/x-pack/plugins/integration_assistant/server/integration_builder/build_integration.ts +++ b/x-pack/plugins/integration_assistant/server/integration_builder/build_integration.ts @@ -174,7 +174,7 @@ function createPackageManifestDict( ], owner: { github: package_owner, - type: 'elastic', + type: 'community', }, }; diff --git a/x-pack/plugins/maps/kibana.jsonc b/x-pack/plugins/maps/kibana.jsonc index b042d0250b0c2..421817e87344f 100644 --- a/x-pack/plugins/maps/kibana.jsonc +++ b/x-pack/plugins/maps/kibana.jsonc @@ -1,7 +1,7 @@ { "type": "plugin", "id": "@kbn/maps-plugin", - "owner": "@elastic/kibana-gis", + "owner": "@elastic/kibana-presentation", "plugin": { "id": "maps", "server": true, diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_edit_form/fields/tags_field.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_edit_form/fields/tags_field.tsx index fb6555de53f34..a912a6d61eb7b 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_edit_form/fields/tags_field.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_edit_form/fields/tags_field.tsx @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import { InvestigationForm } from '../investigation_edit_form'; +import { useFetchAllInvestigationTags } from '../../../hooks/use_fetch_all_investigation_tags'; const I18N_TAGS_LABEL = i18n.translate( 'xpack.investigateApp.investigationEditForm.span.tagsLabel', @@ -18,6 +19,7 @@ const I18N_TAGS_LABEL = i18n.translate( export function TagsField() { const { control, getFieldState } = useFormContext(); + const { isLoading, data: tags } = useFetchAllInvestigationTags(); return ( @@ -32,10 +34,10 @@ export function TagsField() { aria-label={I18N_TAGS_LABEL} placeholder={I18N_TAGS_LABEL} fullWidth - noSuggestions isInvalid={fieldState.invalid} isClearable - options={[]} + isLoading={isLoading} + options={tags?.map((tag) => ({ label: tag, value: tag })) ?? []} selectedOptions={generateTagOptions(field.value)} onChange={(selected) => { if (selected.length) { diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/query_key_factory.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/query_key_factory.ts index 253c38a972fbc..85a8c35b63a5e 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/hooks/query_key_factory.ts +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/query_key_factory.ts @@ -9,8 +9,10 @@ export const investigationKeys = { all: ['investigations'] as const, + tags: () => [...investigationKeys.all, 'tags'] as const, + stats: () => [...investigationKeys.all, 'stats'] as const, lists: () => [...investigationKeys.all, 'list'] as const, - list: (params: { page: number; perPage: number }) => + list: (params: { page: number; perPage: number; search?: string; filter?: string }) => [...investigationKeys.lists(), params] as const, details: () => [...investigationKeys.all, 'detail'] as const, detail: (investigationId: string) => [...investigationKeys.details(), investigationId] as const, diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_all_investigation_stats.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_all_investigation_stats.ts new file mode 100644 index 0000000000000..2b2c8b92b0d4f --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_all_investigation_stats.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import type { GetAllInvestigationStatsResponse, Status } from '@kbn/investigation-shared'; +import { useQuery } from '@tanstack/react-query'; +import { investigationKeys } from './query_key_factory'; +import { useKibana } from './use_kibana'; + +export interface Response { + isInitialLoading: boolean; + isLoading: boolean; + isRefetching: boolean; + isSuccess: boolean; + isError: boolean; + data: { count: Record; total: number } | undefined; +} + +export function useFetchAllInvestigationStats(): Response { + const { + core: { + http, + notifications: { toasts }, + }, + } = useKibana(); + + const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({ + queryKey: investigationKeys.stats(), + queryFn: async ({ signal }) => { + const response = await http.get( + `/api/observability/investigations/_stats`, + { + version: '2023-10-31', + signal, + } + ); + + return { + count: { + triage: response.count.triage ?? 0, + active: response.count.active ?? 0, + mitigated: response.count.mitigated ?? 0, + resolved: response.count.resolved ?? 0, + cancelled: response.count.cancelled ?? 0, + }, + total: response.total ?? 0, + }; + }, + retry: false, + cacheTime: 600 * 1000, // 10 minutes + staleTime: 0, + onError: (error: Error) => { + toasts.addError(error, { + title: i18n.translate('xpack.investigateApp.useFetchAllInvestigationStats.errorTitle', { + defaultMessage: 'Something went wrong while fetching the investigation stats', + }), + }); + }, + }); + + return { + data, + isInitialLoading, + isLoading, + isRefetching, + isSuccess, + isError, + }; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_all_investigation_tags.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_all_investigation_tags.ts new file mode 100644 index 0000000000000..083742f09b685 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_all_investigation_tags.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { useQuery } from '@tanstack/react-query'; +import { investigationKeys } from './query_key_factory'; +import { useKibana } from './use_kibana'; + +export interface Response { + isInitialLoading: boolean; + isLoading: boolean; + isRefetching: boolean; + isSuccess: boolean; + isError: boolean; + data: string[] | undefined; +} + +export function useFetchAllInvestigationTags(): Response { + const { + core: { + http, + notifications: { toasts }, + }, + } = useKibana(); + + const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({ + queryKey: investigationKeys.tags(), + queryFn: async ({ signal }) => { + return await http.get(`/api/observability/investigations/_tags`, { + version: '2023-10-31', + signal, + }); + }, + cacheTime: 600 * 1000, // 10_minutes + staleTime: 0, + refetchOnWindowFocus: false, + retry: false, + onError: (error: Error) => { + toasts.addError(error, { + title: i18n.translate('xpack.investigateApp.useFetchAllInvestigationTags.errorTitle', { + defaultMessage: 'Something went wrong while fetching the investigation tags', + }), + }); + }, + }); + + return { + data, + isInitialLoading, + isLoading, + isRefetching, + isSuccess, + isError, + }; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_investigation_list.ts b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_investigation_list.ts index 2423a76e06464..cadd0de89a8e3 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_investigation_list.ts +++ b/x-pack/plugins/observability_solution/investigate_app/public/hooks/use_fetch_investigation_list.ts @@ -16,6 +16,8 @@ const DEFAULT_PAGE_SIZE = 25; export interface InvestigationListParams { page?: number; perPage?: number; + search?: string; + filter?: string; } export interface UseFetchInvestigationListResponse { @@ -30,6 +32,8 @@ export interface UseFetchInvestigationListResponse { export function useFetchInvestigationList({ page = 1, perPage = DEFAULT_PAGE_SIZE, + search, + filter, }: InvestigationListParams = {}): UseFetchInvestigationListResponse { const { core: { @@ -42,6 +46,8 @@ export function useFetchInvestigationList({ queryKey: investigationKeys.list({ page, perPage, + search, + filter, }), queryFn: async ({ signal }) => { return await http.get(`/api/observability/investigations`, { @@ -49,12 +55,17 @@ export function useFetchInvestigationList({ query: { ...(page !== undefined && { page }), ...(perPage !== undefined && { perPage }), + ...(!!search && { search }), + ...(!!filter && { filter }), }, signal, }); }, + retry: false, refetchInterval: 60 * 1000, refetchOnWindowFocus: false, + cacheTime: 600 * 1000, // 10 minutes + staleTime: 0, onError: (error: Error) => { toasts.addError(error, { title: i18n.translate('xpack.investigateApp.useFetchInvestigationList.errorTitle', { diff --git a/x-pack/plugins/observability_solution/investigate_app/public/items/embeddable_item/register_embeddable_item.tsx b/x-pack/plugins/observability_solution/investigate_app/public/items/embeddable_item/register_embeddable_item.tsx index 3be16c83c8018..8ebf3829b073d 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/items/embeddable_item/register_embeddable_item.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/items/embeddable_item/register_embeddable_item.tsx @@ -89,6 +89,11 @@ function LegacyEmbeddable({ type, config, timeRange: { from, to }, savedObjectId from, to, }, + overrides: { + axisX: { hide: true }, + axisLeft: { style: { axisTitle: { visible: false } } }, + settings: { showLegend: false }, + }, }; if (savedObjectId) { @@ -188,7 +193,7 @@ export function registerEmbeddableItem({ grow={true} className={css` > div { - height: 196px; + height: 128px; } `} > diff --git a/x-pack/plugins/observability_solution/investigate_app/public/items/esql_item/register_esql_item.tsx b/x-pack/plugins/observability_solution/investigate_app/public/items/esql_item/register_esql_item.tsx index 5f2f95807b4e0..54d3698a5148b 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/items/esql_item/register_esql_item.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/items/esql_item/register_esql_item.tsx @@ -4,8 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; -import { css } from '@emotion/css'; +import { EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import type { DataView } from '@kbn/data-views-plugin/common'; import type { ESQLSearchResponse } from '@kbn/es-types'; import { i18n } from '@kbn/i18n'; @@ -123,29 +122,28 @@ export function EsqlWidget({ suggestion, dataView, esqlQuery, dateHistogramResul [dataView, lens, dateHistogramResults] ); + // in the case of a lnsDatatable, we want to render the preview of the histogram and not the datable (input) itself if (input.attributes.visualizationType === 'lnsDatatable') { let innerElement: React.ReactElement; if (previewInput.error) { innerElement = ; } else if (previewInput.value) { - innerElement = ; + innerElement = ( + + ); } else { innerElement = ; } - return ( - - div { - height: 128px; - } - `} - > - {innerElement} - - - ); + + return {innerElement}; } return ( @@ -153,7 +151,11 @@ export function EsqlWidget({ suggestion, dataView, esqlQuery, dateHistogramResul ); diff --git a/x-pack/plugins/observability_solution/investigate_app/public/items/lens_item/register_lens_item.tsx b/x-pack/plugins/observability_solution/investigate_app/public/items/lens_item/register_lens_item.tsx index 3f2b1d9f9c1bf..2896719a49e20 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/items/lens_item/register_lens_item.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/items/lens_item/register_lens_item.tsx @@ -178,17 +178,6 @@ export function LensWidget({ const attributesLens = new LensAttributesBuilder({ visualization: new XYChart({ - visualOptions: { - axisTitlesVisibilitySettings: { - x: false, - yLeft: false, - yRight: false, - }, - legend: { - isVisible: false, - position: 'right', - }, - }, layers, formulaAPI: formulaAsync.value.formula, dataView, @@ -227,7 +216,11 @@ export function LensWidget({ query={(searchConfiguration?.query as Query) || defaultQuery} disableTriggers={true} filters={filters} - overrides={{ axisX: { hide: true } }} + overrides={{ + axisX: { hide: true }, + axisLeft: { style: { axisTitle: { visible: false } } }, + settings: { showLegend: false }, + }} /> ); diff --git a/x-pack/plugins/observability_solution/investigate_app/public/pages/list/components/investigation_list.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/list/components/investigation_list.tsx index ec16e4244d6d1..d75710f817703 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/pages/list/components/investigation_list.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/list/components/investigation_list.tsx @@ -9,43 +9,47 @@ import { EuiBadge, EuiBasicTable, EuiBasicTableColumn, + EuiFlexGroup, + EuiFlexItem, EuiLink, EuiLoadingSpinner, + EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { InvestigationResponse } from '@kbn/investigation-shared/src/rest_specs/investigation'; import moment from 'moment'; import React, { useState } from 'react'; import { paths } from '../../../../common/paths'; -import { InvestigationNotFound } from '../../../components/investigation_not_found/investigation_not_found'; import { InvestigationStatusBadge } from '../../../components/investigation_status_badge/investigation_status_badge'; import { useFetchInvestigationList } from '../../../hooks/use_fetch_investigation_list'; import { useKibana } from '../../../hooks/use_kibana'; import { InvestigationListActions } from './investigation_list_actions'; +import { InvestigationStats } from './investigation_stats'; +import { InvestigationsError } from './investigations_error'; +import { SearchBar } from './search_bar/search_bar'; export function InvestigationList() { - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(10); const { core: { http: { basePath }, uiSettings, }, } = useKibana(); - const { data, isLoading, isError } = useFetchInvestigationList({ - page: pageIndex + 1, - perPage: pageSize, - }); const dateFormat = uiSettings.get('dateFormat'); const tz = uiSettings.get('dateFormat:tz'); - if (isLoading) { - return ; - } + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(10); + const [search, setSearch] = useState(undefined); + const [status, setStatus] = useState([]); + const [tags, setTags] = useState([]); - if (isError) { - return ; - } + const { data, isLoading, isError } = useFetchInvestigationList({ + page: pageIndex + 1, + perPage: pageSize, + search, + filter: toFilter(status, tags), + }); const investigations = data?.results ?? []; const totalItemCount = data?.total ?? 0; @@ -74,6 +78,23 @@ export function InvestigationList() { }), truncateText: true, }, + { + field: 'tags', + name: i18n.translate('xpack.investigateApp.investigationList.tagsLabel', { + defaultMessage: 'Tags', + }), + render: (value: InvestigationResponse['tags']) => { + return ( + + {value.map((tag) => ( + + {tag} + + ))} + + ); + }, + }, { field: 'notes', name: i18n.translate('xpack.investigateApp.investigationList.notesLabel', { @@ -82,32 +103,22 @@ export function InvestigationList() { render: (notes: InvestigationResponse['notes']) => {notes?.length || 0}, }, { - field: 'createdAt', - name: i18n.translate('xpack.investigateApp.investigationList.createdAtLabel', { - defaultMessage: 'Created at', + field: 'updatedAt', + name: i18n.translate('xpack.investigateApp.investigationList.updatedAtLabel', { + defaultMessage: 'Updated at', }), - render: (createdAt: InvestigationResponse['createdAt']) => ( - {moment(createdAt).tz(tz).format(dateFormat)} + render: (updatedAt: InvestigationResponse['updatedAt']) => ( + {moment(updatedAt).tz(tz).format(dateFormat)} ), }, { field: 'status', name: 'Status', - render: (status: InvestigationResponse['status']) => { - return ; - }, - }, - { - field: 'tags', - name: 'Tags', - render: (tags: InvestigationResponse['tags']) => { - return tags.map((tag) => ( - - {tag} - - )); + render: (s: InvestigationResponse['status']) => { + return ; }, }, + { name: 'Actions', render: (investigation: InvestigationResponse) => ( @@ -120,10 +131,24 @@ export function InvestigationList() { pageIndex, pageSize, totalItemCount, - pageSizeOptions: [10, 50], + pageSizeOptions: [10, 25, 50, 100], showPerPageOptions: true, }; + const resultsCount = + pageSize === 0 + ? i18n.translate('xpack.investigateApp.investigationList.allLabel', { + defaultMessage: 'Showing All', + }) + : i18n.translate('xpack.investigateApp.investigationList.showingLabel', { + defaultMessage: 'Showing {startItem}-{endItem} of {totalItemCount}', + values: { + startItem: pageSize * pageIndex + 1, + endItem: pageSize * pageIndex + pageSize, + totalItemCount, + }, + }); + const onTableChange = ({ page }: Criteria) => { if (page) { const { index, size } = page; @@ -133,15 +158,49 @@ export function InvestigationList() { }; return ( - + + + setSearch(value)} + onStatusFilterChange={(selected) => setStatus(selected)} + onTagsFilterChange={(selected) => setTags(selected)} + /> + + {isLoading && } + {isError && } + {!isLoading && !isError && ( + <> + {resultsCount} + + + )} + + ); } + +function toFilter(status: string[], tags: string[]) { + const statusFitler = status.map((s) => `investigation.attributes.status:${s}`).join(' OR '); + const tagsFilter = tags.map((tag) => `investigation.attributes.tags:${tag}`).join(' OR '); + + if (statusFitler && tagsFilter) { + return `(${statusFitler}) AND (${tagsFilter})`; + } + if (statusFitler) { + return statusFitler; + } + + if (tagsFilter) { + return tagsFilter; + } +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/pages/list/components/investigation_stats.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/list/components/investigation_stats.tsx new file mode 100644 index 0000000000000..7f654dce415c9 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/list/components/investigation_stats.tsx @@ -0,0 +1,83 @@ +/* + * 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 numeral from '@elastic/numeral'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStat } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useFetchAllInvestigationStats } from '../../../hooks/use_fetch_all_investigation_stats'; +import { useKibana } from '../../../hooks/use_kibana'; + +export function InvestigationStats() { + const { + core: { uiSettings }, + } = useKibana(); + const { data, isLoading: isStatsLoading } = useFetchAllInvestigationStats(); + const numberFormat = uiSettings.get('format:number:defaultPattern'); + + return ( + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/pages/list/components/investigations_error.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/list/components/investigations_error.tsx new file mode 100644 index 0000000000000..232dc7a417e93 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/list/components/investigations_error.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiEmptyPrompt } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +export function InvestigationsError() { + return ( + + {i18n.translate('xpack.investigateApp.InvestigationsNotFound.title', { + defaultMessage: 'Unable to load investigations', + })} + + } + body={ +

+ {i18n.translate('xpack.investigateApp.InvestigationsNotFound.body', { + defaultMessage: + 'There was an error loading the investigations. Contact your administrator for help.', + })} +

+ } + /> + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/pages/list/components/search_bar/search_bar.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/list/components/search_bar/search_bar.tsx new file mode 100644 index 0000000000000..6c89df8532b71 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/list/components/search_bar/search_bar.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { StatusFilter } from './status_filter'; +import { TagsFilter } from './tags_filter'; + +interface Props { + isLoading: boolean; + onSearch: (value: string) => void; + onStatusFilterChange: (status: string[]) => void; + onTagsFilterChange: (tags: string[]) => void; +} + +const SEARCH_LABEL = i18n.translate('xpack.investigateApp.investigationList.searchField.label', { + defaultMessage: 'Search...', +}); + +export function SearchBar({ + onSearch, + onStatusFilterChange, + onTagsFilterChange, + isLoading, +}: Props) { + return ( + + + onSearch(value)} + isLoading={isLoading} + /> + + + + + + + + + ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/pages/list/components/search_bar/status_filter.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/list/components/search_bar/status_filter.tsx new file mode 100644 index 0000000000000..df65845595905 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/list/components/search_bar/status_filter.tsx @@ -0,0 +1,85 @@ +/* + * 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 { + useGeneratedHtmlId, + EuiFilterButton, + EuiFilterGroup, + EuiPopover, + EuiSelectable, + EuiPopoverTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useState } from 'react'; + +const STATUS_LABEL = i18n.translate('xpack.investigateApp.searchBar.statusFilterButtonLabel', { + defaultMessage: 'Status', +}); + +interface Props { + isLoading: boolean; + onChange: (status: string[]) => void; +} + +export function StatusFilter({ isLoading, onChange }: Props) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const filterStatusPopoverId = useGeneratedHtmlId({ + prefix: 'filterStatusPopover', + }); + + const [items, setItems] = useState>([ + { label: 'triage' }, + { label: 'active' }, + { label: 'mitigated' }, + { label: 'resolved' }, + { label: 'cancelled' }, + ]); + + const button = ( + setIsPopoverOpen(!isPopoverOpen)} + isSelected={isPopoverOpen} + numFilters={items.length} + hasActiveFilters={!!items.find((item) => item.checked === 'on')} + numActiveFilters={items.filter((item) => item.checked === 'on').length} + > + {STATUS_LABEL} + + ); + return ( + + setIsPopoverOpen(false)} + panelPaddingSize="none" + > + { + setItems(newOptions); + onChange(newOptions.filter((item) => item.checked === 'on').map((item) => item.label)); + }} + isLoading={isLoading} + > + {(list, search) => ( +
+ {search} + {list} +
+ )} +
+
+
+ ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/pages/list/components/search_bar/tags_filter.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/list/components/search_bar/tags_filter.tsx new file mode 100644 index 0000000000000..5a82f84a47fe1 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/list/components/search_bar/tags_filter.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + useGeneratedHtmlId, + EuiFilterButton, + EuiFilterGroup, + EuiPopover, + EuiSelectable, + EuiPopoverTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useEffect, useState } from 'react'; +import { useFetchAllInvestigationTags } from '../../../../hooks/use_fetch_all_investigation_tags'; + +const TAGS_LABEL = i18n.translate('xpack.investigateApp.searchBar.tagsFilterButtonLabel', { + defaultMessage: 'Tags', +}); + +interface Props { + isLoading: boolean; + onChange: (tags: string[]) => void; +} + +export function TagsFilter({ isLoading, onChange }: Props) { + const { isLoading: isTagsLoading, data: tags } = useFetchAllInvestigationTags(); + const [items, setItems] = useState>([]); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const filterTagsPopoverId = useGeneratedHtmlId({ + prefix: 'filterTagsPopover', + }); + + useEffect(() => { + if (tags) { + setItems(tags.map((tag) => ({ label: tag }))); + } + }, [tags]); + + const button = ( + setIsPopoverOpen(!isPopoverOpen)} + isSelected={isPopoverOpen} + numFilters={items.length} + hasActiveFilters={!!items.find((item) => item.checked === 'on')} + numActiveFilters={items.filter((item) => item.checked === 'on').length} + > + {TAGS_LABEL} + + ); + return ( + + setIsPopoverOpen(false)} + panelPaddingSize="none" + > + { + setItems(newOptions); + onChange(newOptions.filter((item) => item.checked === 'on').map((item) => item.label)); + }} + isLoading={isLoading || isTagsLoading} + > + {(list, search) => ( +
+ {search} + {list} +
+ )} +
+
+
+ ); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/utils/get_lens_attrs_for_suggestion.ts b/x-pack/plugins/observability_solution/investigate_app/public/utils/get_lens_attrs_for_suggestion.ts index 79693ff2941aa..0483d771954c0 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/utils/get_lens_attrs_for_suggestion.ts +++ b/x-pack/plugins/observability_solution/investigate_app/public/utils/get_lens_attrs_for_suggestion.ts @@ -32,19 +32,6 @@ export function getLensAttrsForSuggestion({ dataView, }) as TypedLensByValueInput['attributes']; - attrs.state.visualization = { - ...(attrs.state.visualization as any), - axisTitlesVisibilitySettings: { - x: false, - yLeft: false, - yRight: false, - }, - legend: { - isVisible: false, - position: 'right', - }, - }; - const lensEmbeddableInput: TypedLensByValueInput = { attributes: attrs, id: v4(), diff --git a/x-pack/plugins/observability_solution/investigate_app/server/routes/get_global_investigate_app_server_route_repository.ts b/x-pack/plugins/observability_solution/investigate_app/server/routes/get_global_investigate_app_server_route_repository.ts index 1755d283b3763..b0ecd89275914 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/routes/get_global_investigate_app_server_route_repository.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/routes/get_global_investigate_app_server_route_repository.ts @@ -13,6 +13,8 @@ import { deleteInvestigationNoteParamsSchema, deleteInvestigationParamsSchema, findInvestigationsParamsSchema, + getAllInvestigationStatsParamsSchema, + getAllInvestigationTagsParamsSchema, getInvestigationItemsParamsSchema, getInvestigationNotesParamsSchema, getInvestigationParamsSchema, @@ -27,14 +29,16 @@ import { deleteInvestigation } from '../services/delete_investigation'; import { deleteInvestigationItem } from '../services/delete_investigation_item'; import { deleteInvestigationNote } from '../services/delete_investigation_note'; import { findInvestigations } from '../services/find_investigations'; +import { getAllInvestigationTags } from '../services/get_all_investigation_tags'; import { getInvestigation } from '../services/get_investigation'; +import { getInvestigationItems } from '../services/get_investigation_items'; import { getInvestigationNotes } from '../services/get_investigation_notes'; import { investigationRepositoryFactory } from '../services/investigation_repository'; -import { createInvestigateAppServerRoute } from './create_investigate_app_server_route'; -import { getInvestigationItems } from '../services/get_investigation_items'; -import { updateInvestigationNote } from '../services/update_investigation_note'; -import { updateInvestigationItem } from '../services/update_investigation_item'; import { updateInvestigation } from '../services/update_investigation'; +import { updateInvestigationItem } from '../services/update_investigation_item'; +import { updateInvestigationNote } from '../services/update_investigation_note'; +import { createInvestigateAppServerRoute } from './create_investigate_app_server_route'; +import { getAllInvestigationStats } from '../services/get_all_investigation_stats'; const createInvestigationRoute = createInvestigateAppServerRoute({ endpoint: 'POST /api/observability/investigations 2023-10-31', @@ -138,6 +142,34 @@ const createInvestigationNoteRoute = createInvestigateAppServerRoute({ }, }); +const getAllInvestigationTagsRoute = createInvestigateAppServerRoute({ + endpoint: 'GET /api/observability/investigations/_tags 2023-10-31', + options: { + tags: [], + }, + params: getAllInvestigationTagsParamsSchema, + handler: async ({ params, context, request, logger }) => { + const soClient = (await context.core).savedObjects.client; + const repository = investigationRepositoryFactory({ soClient, logger }); + + return await getAllInvestigationTags(repository); + }, +}); + +const getAllInvestigationStatsRoute = createInvestigateAppServerRoute({ + endpoint: 'GET /api/observability/investigations/_stats 2023-10-31', + options: { + tags: [], + }, + params: getAllInvestigationStatsParamsSchema, + handler: async ({ params, context, request, logger }) => { + const soClient = (await context.core).savedObjects.client; + const repository = investigationRepositoryFactory({ soClient, logger }); + + return await getAllInvestigationStats(repository); + }, +}); + const getInvestigationNotesRoute = createInvestigateAppServerRoute({ endpoint: 'GET /api/observability/investigations/{investigationId}/notes 2023-10-31', options: { @@ -296,6 +328,8 @@ export function getGlobalInvestigateAppServerRouteRepository() { ...deleteInvestigationItemRoute, ...updateInvestigationItemRoute, ...getInvestigationItemsRoute, + ...getAllInvestigationStatsRoute, + ...getAllInvestigationTagsRoute, }; } diff --git a/x-pack/plugins/observability_solution/investigate_app/server/saved_objects/investigation.ts b/x-pack/plugins/observability_solution/investigate_app/server/saved_objects/investigation.ts index eeb937fb16cfa..20ed328689050 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/saved_objects/investigation.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/saved_objects/investigation.ts @@ -27,6 +27,7 @@ export const investigation: SavedObjectsType = { }, }, status: { type: 'keyword' }, + tags: { type: 'keyword' }, }, }, management: { diff --git a/x-pack/plugins/observability_solution/investigate_app/server/services/create_investigation.ts b/x-pack/plugins/observability_solution/investigate_app/server/services/create_investigation.ts index 2aed0baed8923..eb8277d7d6f83 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/services/create_investigation.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/services/create_investigation.ts @@ -18,9 +18,11 @@ export async function createInvestigation( throw new Error(`Investigation [id=${params.id}] already exists`); } + const now = Date.now(); const investigation: Investigation = { ...params, - createdAt: Date.now(), + updatedAt: now, + createdAt: now, createdBy: user.username, status: 'triage', notes: [], diff --git a/x-pack/plugins/observability_solution/investigate_app/server/services/create_investigation_item.ts b/x-pack/plugins/observability_solution/investigate_app/server/services/create_investigation_item.ts index 1ed6f1289280b..cf77887aab0a3 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/services/create_investigation_item.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/services/create_investigation_item.ts @@ -20,10 +20,12 @@ export async function createInvestigationItem( ): Promise { const investigation = await repository.findById(investigationId); + const now = Date.now(); const investigationItem = { id: v4(), createdBy: user.username, - createdAt: Date.now(), + createdAt: now, + updatedAt: now, ...params, }; investigation.items.push(investigationItem); diff --git a/x-pack/plugins/observability_solution/investigate_app/server/services/create_investigation_note.ts b/x-pack/plugins/observability_solution/investigate_app/server/services/create_investigation_note.ts index 9ce727c0f2e08..2f74123b6f269 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/services/create_investigation_note.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/services/create_investigation_note.ts @@ -20,11 +20,13 @@ export async function createInvestigationNote( ): Promise { const investigation = await repository.findById(investigationId); + const now = Date.now(); const investigationNote = { id: v4(), content: params.content, createdBy: user.username, - createdAt: Date.now(), + updatedAt: now, + createdAt: now, }; investigation.notes.push(investigationNote); diff --git a/x-pack/plugins/observability_solution/investigate_app/server/services/find_investigations.ts b/x-pack/plugins/observability_solution/investigate_app/server/services/find_investigations.ts index 7530b3c768610..c3d4606645764 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/services/find_investigations.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/services/find_investigations.ts @@ -10,14 +10,18 @@ import { FindInvestigationsResponse, findInvestigationsResponseSchema, } from '@kbn/investigation-shared'; -import { InvestigationRepository } from './investigation_repository'; +import { InvestigationRepository, Search } from './investigation_repository'; import { InvestigationStatus } from '../models/investigation'; export async function findInvestigations( params: FindInvestigationsParams, repository: InvestigationRepository ): Promise { - const investigations = await repository.search(toFilter(params), toPagination(params)); + const investigations = await repository.search({ + search: toSearch(params), + filter: toFilter(params), + pagination: toPagination(params), + }); return findInvestigationsResponseSchema.parse(investigations); } @@ -26,16 +30,28 @@ function toPagination(params: FindInvestigationsParams) { const DEFAULT_PER_PAGE = 10; const DEFAULT_PAGE = 1; return { - page: params?.page ? parseInt(params.page, 10) : DEFAULT_PAGE, - perPage: params?.perPage ? parseInt(params.perPage, 10) : DEFAULT_PER_PAGE, + page: params?.page && params.page >= 1 ? params.page : DEFAULT_PAGE, + perPage: + params?.perPage && params.perPage > 0 && params.perPage <= 100 + ? params.perPage + : DEFAULT_PER_PAGE, }; } -function toFilter(params: FindInvestigationsParams) { +function toSearch(params: FindInvestigationsParams): Search | undefined { + if (params?.search) { + return { search: params.search }; + } +} + +function toFilter(params: FindInvestigationsParams): string | undefined { if (params?.alertId) { const activeStatus: InvestigationStatus = 'active'; const triageStatus: InvestigationStatus = 'triage'; return `investigation.attributes.origin.id:(${params.alertId}) AND (investigation.attributes.status: ${activeStatus} OR investigation.attributes.status: ${triageStatus})`; } - return ''; + + if (params?.filter) { + return params.filter; + } } diff --git a/x-pack/plugins/observability_solution/investigate_app/server/services/get_all_investigation_stats.ts b/x-pack/plugins/observability_solution/investigate_app/server/services/get_all_investigation_stats.ts new file mode 100644 index 0000000000000..eb2304b4950c5 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/server/services/get_all_investigation_stats.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 { + GetAllInvestigationStatsResponse, + getAllInvestigationStatsResponseSchema, +} from '@kbn/investigation-shared'; +import { InvestigationRepository } from './investigation_repository'; + +export async function getAllInvestigationStats( + repository: InvestigationRepository +): Promise { + const stats = await repository.getStats(); + return getAllInvestigationStatsResponseSchema.parse(stats); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/server/services/get_all_investigation_tags.ts b/x-pack/plugins/observability_solution/investigate_app/server/services/get_all_investigation_tags.ts new file mode 100644 index 0000000000000..48b1624a434d7 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/server/services/get_all_investigation_tags.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 { + GetAllInvestigationTagsResponse, + getAllInvestigationTagsResponseSchema, +} from '@kbn/investigation-shared'; +import { InvestigationRepository } from './investigation_repository'; + +export async function getAllInvestigationTags( + repository: InvestigationRepository +): Promise { + const tags = await repository.findAllTags(); + return getAllInvestigationTagsResponseSchema.parse(tags); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/server/services/investigation_repository.ts b/x-pack/plugins/observability_solution/investigate_app/server/services/investigation_repository.ts index 73c9136cb3673..ffefe757c7c72 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/services/investigation_repository.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/services/investigation_repository.ts @@ -6,16 +6,35 @@ */ import { Logger, SavedObjectsClientContract } from '@kbn/core/server'; +import type { Status } from '@kbn/investigation-shared'; import { investigationSchema } from '@kbn/investigation-shared'; import { Investigation, StoredInvestigation } from '../models/investigation'; import { Paginated, Pagination } from '../models/pagination'; import { SO_INVESTIGATION_TYPE } from '../saved_objects/investigation'; +export interface Search { + search: string; +} +interface Stats { + count: Record; + total: number; +} + export interface InvestigationRepository { save(investigation: Investigation): Promise; findById(id: string): Promise; deleteById(id: string): Promise; - search(filter: string, pagination: Pagination): Promise>; + search({ + search, + filter, + pagination, + }: { + search?: Search; + filter?: string; + pagination: Pagination; + }): Promise>; + findAllTags(): Promise; + getStats(): Promise; } export function investigationRepositoryFactory({ @@ -89,12 +108,15 @@ export function investigationRepositoryFactory({ await soClient.delete(SO_INVESTIGATION_TYPE, response.saved_objects[0].id); }, - async search(filter: string, pagination: Pagination): Promise> { + async search({ search, filter, pagination }): Promise> { const response = await soClient.find({ type: SO_INVESTIGATION_TYPE, page: pagination.page, perPage: pagination.perPage, - filter, + sortField: 'updated_at', + sortOrder: 'desc', + ...(filter && { filter }), + ...(search && { search: search.search }), }); return { @@ -106,5 +128,60 @@ export function investigationRepositoryFactory({ .filter((investigation) => investigation !== undefined) as Investigation[], }; }, + + async findAllTags(): Promise { + interface AggsTagsTerms { + tags: { buckets: [{ key: string }] }; + } + + const response = await soClient.find({ + type: SO_INVESTIGATION_TYPE, + aggs: { + tags: { + terms: { + field: 'investigation.attributes.tags', + size: 10000, + }, + }, + }, + }); + + return response.aggregations?.tags?.buckets.map((bucket) => bucket.key) ?? []; + }, + + async getStats(): Promise<{ count: Record; total: number }> { + interface AggsStatusTerms { + status: { buckets: [{ key: string; doc_count: number }] }; + } + + const response = await soClient.find({ + type: SO_INVESTIGATION_TYPE, + aggs: { + status: { + terms: { + field: 'investigation.attributes.status', + size: 10, + }, + }, + }, + }); + + const countByStatus: Record = { + active: 0, + triage: 0, + mitigated: 0, + resolved: 0, + cancelled: 0, + }; + + return { + count: + response.aggregations?.status?.buckets.reduce( + (acc, bucket) => ({ ...acc, [bucket.key]: bucket.doc_count }), + countByStatus + ) ?? countByStatus, + total: response.total, + }; + }, }; } diff --git a/x-pack/plugins/observability_solution/investigate_app/server/services/update_investigation.ts b/x-pack/plugins/observability_solution/investigate_app/server/services/update_investigation.ts index b4e33c4a5f673..ee1289ec4b9fa 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/services/update_investigation.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/services/update_investigation.ts @@ -7,7 +7,7 @@ import type { AuthenticatedUser } from '@kbn/core-security-common'; import { UpdateInvestigationParams, UpdateInvestigationResponse } from '@kbn/investigation-shared'; -import { isEqual } from 'lodash'; +import { isEqual, omit } from 'lodash'; import { InvestigationRepository } from './investigation_repository'; import { Investigation } from '../models/investigation'; @@ -18,9 +18,13 @@ export async function updateInvestigation( ): Promise { const originalInvestigation = await repository.findById(investigationId); - const updatedInvestigation: Investigation = Object.assign({}, originalInvestigation, params); + const updatedInvestigation: Investigation = Object.assign({}, originalInvestigation, params, { + updatedAt: Date.now(), + }); - if (isEqual(originalInvestigation, updatedInvestigation)) { + if ( + isEqual(omit(originalInvestigation, ['updatedAt']), omit(updatedInvestigation, ['updatedAt'])) + ) { return originalInvestigation; } diff --git a/x-pack/plugins/observability_solution/investigate_app/server/services/update_investigation_note.ts b/x-pack/plugins/observability_solution/investigate_app/server/services/update_investigation_note.ts index dda5ae34f2a71..fc4c5a2c0b1fc 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/services/update_investigation_note.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/services/update_investigation_note.ts @@ -27,7 +27,7 @@ export async function updateInvestigationNote( investigation.notes = investigation.notes.filter((currNote) => { if (currNote.id === noteId) { - currNote.content = params.content; + currNote = Object.assign(currNote, { content: params.content, updatedAt: Date.now() }); } return currNote; diff --git a/x-pack/plugins/observability_solution/observability/common/ui_settings_keys.ts b/x-pack/plugins/observability_solution/observability/common/ui_settings_keys.ts index fe43cd30705db..efceaca9a0427 100644 --- a/x-pack/plugins/observability_solution/observability/common/ui_settings_keys.ts +++ b/x-pack/plugins/observability_solution/observability/common/ui_settings_keys.ts @@ -48,3 +48,4 @@ export const profilingAzureCostDiscountRate = 'observability:profilingAzureCostD export const apmEnableTransactionProfiling = 'observability:apmEnableTransactionProfiling'; export const profilingFetchTopNFunctionsFromStacktraces = 'observability:profilingFetchTopNFunctionsFromStacktraces'; +export const searchExcludedDataTiers = 'observability:searchExcludedDataTiers'; diff --git a/x-pack/plugins/observability_solution/observability/server/ui_settings.ts b/x-pack/plugins/observability_solution/observability/server/ui_settings.ts index d404606b4ce79..81c0596722106 100644 --- a/x-pack/plugins/observability_solution/observability/server/ui_settings.ts +++ b/x-pack/plugins/observability_solution/observability/server/ui_settings.ts @@ -46,6 +46,7 @@ import { apmEnableServiceInventoryTableSearchBar, profilingFetchTopNFunctionsFromStacktraces, enableInfrastructureContainerAssetView, + searchExcludedDataTiers, } from '../common/ui_settings_keys'; const betaLabel = i18n.translate('xpack.observability.uiSettings.betaLabel', { @@ -640,6 +641,24 @@ export const uiSettings: Record = { schema: schema.boolean(), requiresPageReload: false, }, + [searchExcludedDataTiers]: { + category: [observabilityFeatureId], + name: i18n.translate('xpack.observability.searchExcludedDataTiers', { + defaultMessage: 'Excluded data tiers from search', + }), + description: i18n.translate( + 'xpack.observability.advancedSettings.searchExcludedDataTiersDesc', + { + defaultMessage: `Specify the data tiers to exclude from search, such as data_cold and/or data_frozen. + When configured, indices allocated in the selected tiers will be ignored from search requests. Affected apps: APM`, + } + ), + value: [], + schema: schema.arrayOf( + schema.oneOf([schema.literal('data_cold'), schema.literal('data_frozen')]) + ), + requiresPageReload: false, + }, }; function throttlingDocsLink({ href }: { href: string }) { diff --git a/x-pack/plugins/observability_solution/synthetics/common/constants/synthetics/client_defaults.ts b/x-pack/plugins/observability_solution/synthetics/common/constants/synthetics/client_defaults.ts index 6ae9dbfef955f..3e5722ce59f10 100644 --- a/x-pack/plugins/observability_solution/synthetics/common/constants/synthetics/client_defaults.ts +++ b/x-pack/plugins/observability_solution/synthetics/common/constants/synthetics/client_defaults.ts @@ -16,11 +16,11 @@ export const CLIENT_DEFAULTS_SYNTHETICS = { DATE_RANGE_END: 'now', /** - * The application auto refreshes every 30s by default. + * The application auto refreshes every 60s by default. */ AUTOREFRESH_INTERVAL_SECONDS: 60, /** - * The application's autorefresh feature is enabled. + * The application's autorefresh feature is disabled by default. */ - AUTOREFRESH_IS_PAUSED: false, + AUTOREFRESH_IS_PAUSED: true, }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/auto_refresh_button.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/auto_refresh_button.tsx index 6f40b000a6873..cea6a7d726926 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/auto_refresh_button.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/auto_refresh_button.tsx @@ -5,69 +5,17 @@ * 2.0. */ -import React, { useEffect, useRef } from 'react'; +import React from 'react'; import { EuiAutoRefreshButton, OnRefreshChangeProps } from '@elastic/eui'; -import { useDispatch, useSelector } from 'react-redux'; -import { CLIENT_DEFAULTS_SYNTHETICS } from '../../../../../../common/constants/synthetics/client_defaults'; -import { SyntheticsUrlParams } from '../../../utils/url_params'; -import { useUrlParams } from '../../../hooks'; -import { - selectRefreshInterval, - selectRefreshPaused, - setRefreshIntervalAction, - setRefreshPausedAction, -} from '../../../state'; -const { AUTOREFRESH_INTERVAL_SECONDS, AUTOREFRESH_IS_PAUSED } = CLIENT_DEFAULTS_SYNTHETICS; +import { useSyntheticsRefreshContext } from '../../../contexts/synthetics_refresh_context'; -const replaceDefaults = ({ refreshPaused, refreshInterval }: Partial) => { - return { - refreshInterval: refreshInterval === AUTOREFRESH_INTERVAL_SECONDS ? undefined : refreshInterval, - refreshPaused: refreshPaused === AUTOREFRESH_IS_PAUSED ? undefined : refreshPaused, - }; -}; export const AutoRefreshButton = () => { - const dispatch = useDispatch(); - - const refreshPaused = useSelector(selectRefreshPaused); - const refreshInterval = useSelector(selectRefreshInterval); - - const [getUrlsParams, updateUrlParams] = useUrlParams(); - - const { refreshInterval: urlRefreshInterval, refreshPaused: urlIsPaused } = getUrlsParams(); - - const isFirstRender = useRef(true); - - useEffect(() => { - if (isFirstRender.current) { - // sync url state with redux state on first render - dispatch(setRefreshIntervalAction(urlRefreshInterval)); - dispatch(setRefreshPausedAction(urlIsPaused)); - isFirstRender.current = false; - } else { - // sync redux state with url state on subsequent renders - if (urlRefreshInterval !== refreshInterval || urlIsPaused !== refreshPaused) { - updateUrlParams( - replaceDefaults({ - refreshInterval, - refreshPaused, - }), - true - ); - } - } - }, [updateUrlParams, refreshInterval, refreshPaused, urlRefreshInterval, urlIsPaused, dispatch]); + const { refreshInterval, setRefreshInterval, refreshPaused, setRefreshPaused } = + useSyntheticsRefreshContext(); const onRefreshChange = (newProps: OnRefreshChangeProps) => { - dispatch(setRefreshIntervalAction(newProps.refreshInterval / 1000)); - dispatch(setRefreshPausedAction(newProps.isPaused)); - - updateUrlParams( - replaceDefaults({ - refreshInterval: newProps.refreshInterval / 1000, - refreshPaused: newProps.isPaused, - }), - true - ); + setRefreshPaused(newProps.isPaused); + setRefreshInterval(newProps.refreshInterval / 1000); }; return ( diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/last_refreshed.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/last_refreshed.tsx index bc086f67c822b..210170b7e3b8f 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/last_refreshed.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/last_refreshed.tsx @@ -9,16 +9,12 @@ import React, { useEffect, useState } from 'react'; import moment from 'moment'; import { EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useSelector } from 'react-redux'; import { useSyntheticsRefreshContext } from '../../../contexts'; -import { selectRefreshPaused } from '../../../state'; export function LastRefreshed() { - const { lastRefresh: lastRefreshed } = useSyntheticsRefreshContext(); + const { lastRefresh: lastRefreshed, refreshPaused } = useSyntheticsRefreshContext(); const [refresh, setRefresh] = useState(() => Date.now()); - const refreshPaused = useSelector(selectRefreshPaused); - useEffect(() => { const interVal = setInterval(() => { setRefresh(Date.now()); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_selected_monitor.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_selected_monitor.tsx index 622dcff46e902..1f2eadb7c09fc 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_selected_monitor.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_selected_monitor.tsx @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; @@ -16,7 +15,6 @@ import { selectMonitorListState, selectorMonitorDetailsState, selectorError, - selectRefreshInterval, } from '../../../state'; export const useSelectedMonitor = (monId?: string) => { @@ -27,14 +25,13 @@ export const useSelectedMonitor = (monId?: string) => { } const monitorsList = useSelector(selectEncryptedSyntheticsSavedMonitors); const { loading: monitorListLoading } = useSelector(selectMonitorListState); - const refreshInterval = useSelector(selectRefreshInterval); const monitorFromList = useMemo( () => monitorsList.find((monitor) => monitor[ConfigKey.CONFIG_ID] === monitorId) ?? null, [monitorId, monitorsList] ); const error = useSelector(selectorError); - const { lastRefresh } = useSyntheticsRefreshContext(); + const { lastRefresh, refreshInterval } = useSyntheticsRefreshContext(); const { syntheticsMonitor, syntheticsMonitorLoading, syntheticsMonitorDispatchedAt } = useSelector(selectorMonitorDetailsState); const dispatch = useDispatch(); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/contexts/synthetics_refresh_context.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/contexts/synthetics_refresh_context.tsx index b53620921fdd1..9f3902b8ccaf2 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/contexts/synthetics_refresh_context.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/contexts/synthetics_refresh_context.tsx @@ -14,15 +14,21 @@ import React, { useState, FC, } from 'react'; -import { useSelector } from 'react-redux'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; import { useEvent } from 'react-use'; import moment from 'moment'; import { Subject } from 'rxjs'; -import { selectRefreshInterval, selectRefreshPaused } from '../state'; +import { i18n } from '@kbn/i18n'; +import { CLIENT_DEFAULTS_SYNTHETICS } from '../../../../common/constants/synthetics/client_defaults'; +const { AUTOREFRESH_INTERVAL_SECONDS, AUTOREFRESH_IS_PAUSED } = CLIENT_DEFAULTS_SYNTHETICS; interface SyntheticsRefreshContext { lastRefresh: number; refreshApp: () => void; + refreshInterval: number; + refreshPaused: boolean; + setRefreshInterval: (interval: number) => void; + setRefreshPaused: (paused: boolean) => void; } const defaultContext: SyntheticsRefreshContext = { @@ -30,6 +36,22 @@ const defaultContext: SyntheticsRefreshContext = { refreshApp: () => { throw new Error('App refresh was not initialized, set it when you invoke the context'); }, + refreshInterval: AUTOREFRESH_INTERVAL_SECONDS, + refreshPaused: AUTOREFRESH_IS_PAUSED, + setRefreshInterval: () => { + throw new Error( + i18n.translate('xpack.synthetics.refreshContext.intervalNotInitialized', { + defaultMessage: 'Refresh interval was not initialized, set it when you invoke the context', + }) + ); + }, + setRefreshPaused: () => { + throw new Error( + i18n.translate('xpack.synthetics.refreshContext.pausedNotInitialized', { + defaultMessage: 'Refresh paused was not initialized, set it when you invoke the context', + }) + ); + }, }; export const SyntheticsRefreshContext = createContext(defaultContext); @@ -41,8 +63,14 @@ export const SyntheticsRefreshContextProvider: FC< > = ({ children, reload$ }) => { const [lastRefresh, setLastRefresh] = useState(Date.now()); - const refreshPaused = useSelector(selectRefreshPaused); - const refreshInterval = useSelector(selectRefreshInterval); + const [refreshInterval, setRefreshInterval] = useLocalStorage( + 'xpack.synthetics.refreshInterval', + AUTOREFRESH_INTERVAL_SECONDS + ); + const [refreshPaused, setRefreshPaused] = useLocalStorage( + 'xpack.synthetics.refreshPaused', + AUTOREFRESH_IS_PAUSED + ); const refreshApp = useCallback(() => { const refreshTime = Date.now(); @@ -66,13 +94,26 @@ export const SyntheticsRefreshContextProvider: FC< return { lastRefresh, refreshApp, + refreshInterval: refreshInterval ?? AUTOREFRESH_INTERVAL_SECONDS, + refreshPaused: refreshPaused ?? AUTOREFRESH_IS_PAUSED, + setRefreshInterval, + setRefreshPaused, }; - }, [lastRefresh, refreshApp]); + }, [ + lastRefresh, + refreshApp, + refreshInterval, + refreshPaused, + setRefreshInterval, + setRefreshPaused, + ]); useEvent( 'visibilitychange', () => { - const isOutdated = moment().diff(new Date(lastRefresh), 'seconds') > refreshInterval; + const isOutdated = + moment().diff(new Date(lastRefresh), 'seconds') > + (refreshInterval || AUTOREFRESH_INTERVAL_SECONDS); if (document.visibilityState !== 'hidden' && !refreshPaused && isOutdated) { refreshApp(); } @@ -88,7 +129,7 @@ export const SyntheticsRefreshContextProvider: FC< if (document.visibilityState !== 'hidden') { refreshApp(); } - }, refreshInterval * 1000); + }, (refreshInterval || AUTOREFRESH_INTERVAL_SECONDS) * 1000); return () => clearInterval(interval); }, [refreshPaused, refreshApp, refreshInterval]); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/actions.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/actions.ts index e3738f3737cf0..06b9506ead191 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/actions.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/actions.ts @@ -31,5 +31,3 @@ export const toggleIntegrationsPopover = createAction( ); export const setSelectedMonitorId = createAction('[UI] SET MONITOR ID'); -export const setRefreshPausedAction = createAction('[UI] SET REFRESH PAUSED'); -export const setRefreshIntervalAction = createAction('[UI] SET REFRESH INTERVAL'); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/index.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/index.ts index 6c6ef93bbf3a7..2c7d5e5ce3d4c 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/index.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/index.ts @@ -11,7 +11,6 @@ import { SYNTHETICS_STATUS_RULE, SYNTHETICS_TLS_RULE, } from '../../../../../common/constants/synthetics_alerts'; -import { CLIENT_DEFAULTS_SYNTHETICS } from '../../../../../common/constants/synthetics/client_defaults'; import { PopoverState, toggleIntegrationsPopover, @@ -20,10 +19,7 @@ import { setAlertFlyoutVisible, setSearchTextAction, setSelectedMonitorId, - setRefreshPausedAction, - setRefreshIntervalAction, } from './actions'; -const { AUTOREFRESH_INTERVAL_SECONDS, AUTOREFRESH_IS_PAUSED } = CLIENT_DEFAULTS_SYNTHETICS; export interface UiState { alertFlyoutVisible: typeof SYNTHETICS_TLS_RULE | typeof SYNTHETICS_STATUS_RULE | null; @@ -32,8 +28,6 @@ export interface UiState { searchText: string; integrationsPopoverOpen: PopoverState | null; monitorId: string; - refreshInterval: number; - refreshPaused: boolean; } const initialState: UiState = { @@ -43,8 +37,6 @@ const initialState: UiState = { searchText: '', integrationsPopoverOpen: null, monitorId: '', - refreshInterval: AUTOREFRESH_INTERVAL_SECONDS, - refreshPaused: AUTOREFRESH_IS_PAUSED, }; export const uiReducer = createReducer(initialState, (builder) => { @@ -66,12 +58,6 @@ export const uiReducer = createReducer(initialState, (builder) => { }) .addCase(setSelectedMonitorId, (state, action) => { state.monitorId = action.payload; - }) - .addCase(setRefreshPausedAction, (state, action) => { - state.refreshPaused = action.payload; - }) - .addCase(setRefreshIntervalAction, (state, action) => { - state.refreshInterval = action.payload; }); }); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/selectors.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/selectors.ts index 4e365d8343555..f02b1fb564c37 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/selectors.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ui/selectors.ts @@ -14,12 +14,3 @@ export const selectAlertFlyoutVisibility = createSelector( uiStateSelector, ({ alertFlyoutVisible }) => alertFlyoutVisible ); - -export const selectRefreshPaused = createSelector( - uiStateSelector, - ({ refreshPaused }) => refreshPaused -); -export const selectRefreshInterval = createSelector( - uiStateSelector, - ({ refreshInterval }) => refreshInterval -); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts index b861fe36b9b96..aa52c54c21b78 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts @@ -30,8 +30,6 @@ export const mockState: SyntheticsAppState = { integrationsPopoverOpen: null, searchText: '', monitorId: '', - refreshInterval: 60, - refreshPaused: true, }, serviceLocations: { throttling: DEFAULT_THROTTLING, diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.test.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.test.ts index efabb2034e434..2a01b9d7aeefb 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.test.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.test.ts @@ -51,12 +51,7 @@ describe('getSupportedUrlParams', () => { it('returns default values', () => { const { FILTERS, SEARCH, STATUS_FILTER } = CLIENT_DEFAULTS; - const { - DATE_RANGE_START, - DATE_RANGE_END, - AUTOREFRESH_INTERVAL_SECONDS, - AUTOREFRESH_IS_PAUSED, - } = CLIENT_DEFAULTS_SYNTHETICS; + const { DATE_RANGE_START, DATE_RANGE_END } = CLIENT_DEFAULTS_SYNTHETICS; const result = getSupportedUrlParams({}); expect(result).toEqual({ absoluteDateRangeStart: MOCK_DATE_VALUE, @@ -75,8 +70,6 @@ describe('getSupportedUrlParams', () => { projects: [], schedules: [], tags: [], - refreshInterval: AUTOREFRESH_INTERVAL_SECONDS, - refreshPaused: AUTOREFRESH_IS_PAUSED, }); }); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.ts index ce2eb6f30829f..8b4612b1e0f39 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.ts @@ -7,8 +7,6 @@ import { MonitorOverviewState } from '../../state'; import { CLIENT_DEFAULTS_SYNTHETICS } from '../../../../../common/constants/synthetics/client_defaults'; -import { parseIsPaused } from './parse_is_paused'; -import { parseUrlInt } from './parse_url_int'; import { CLIENT_DEFAULTS } from '../../../../../common/constants'; import { parseAbsoluteDate } from './parse_absolute_date'; @@ -16,8 +14,6 @@ import { parseAbsoluteDate } from './parse_absolute_date'; export interface SyntheticsUrlParams { absoluteDateRangeStart: number; absoluteDateRangeEnd: number; - refreshInterval: number; - refreshPaused: boolean; dateRangeStart: string; dateRangeEnd: string; pagination?: string; @@ -43,8 +39,7 @@ export interface SyntheticsUrlParams { const { ABSOLUTE_DATE_RANGE_START, ABSOLUTE_DATE_RANGE_END, SEARCH, FILTERS, STATUS_FILTER } = CLIENT_DEFAULTS; -const { DATE_RANGE_START, DATE_RANGE_END, AUTOREFRESH_INTERVAL_SECONDS, AUTOREFRESH_IS_PAUSED } = - CLIENT_DEFAULTS_SYNTHETICS; +const { DATE_RANGE_START, DATE_RANGE_END } = CLIENT_DEFAULTS_SYNTHETICS; /** * Gets the current URL values for the application. If no item is present @@ -76,8 +71,6 @@ export const getSupportedUrlParams = (params: { }); const { - refreshInterval, - refreshPaused, dateRangeStart, dateRangeEnd, filters, @@ -112,8 +105,6 @@ export const getSupportedUrlParams = (params: { ABSOLUTE_DATE_RANGE_END, { roundUp: true } ), - refreshInterval: parseUrlInt(refreshInterval, AUTOREFRESH_INTERVAL_SECONDS), - refreshPaused: parseIsPaused(refreshPaused, AUTOREFRESH_IS_PAUSED), dateRangeStart: dateRangeStart || DATE_RANGE_START, dateRangeEnd: dateRangeEnd || DATE_RANGE_END, filters: filters || FILTERS, diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/url_params/stringify_url_params.test.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/url_params/stringify_url_params.test.ts index 6f9ace8634d64..c8f8649fd56db 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/url_params/stringify_url_params.test.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/url_params/stringify_url_params.test.ts @@ -12,8 +12,6 @@ describe('stringifyUrlParams', () => { const result = stringifyUrlParams({ absoluteDateRangeStart: 1000, absoluteDateRangeEnd: 2000, - refreshInterval: 50000, - refreshPaused: false, dateRangeStart: 'now-15m', dateRangeEnd: 'now', filters: 'monitor.id: bar', @@ -22,7 +20,7 @@ describe('stringifyUrlParams', () => { statusFilter: 'up', }); expect(result).toMatchInlineSnapshot( - `"?absoluteDateRangeStart=1000&absoluteDateRangeEnd=2000&refreshInterval=50000&refreshPaused=false&dateRangeStart=now-15m&dateRangeEnd=now&filters=monitor.id%3A%20bar&focusConnectorField=true&search=monitor.id%3A%20foo&statusFilter=up"` + `"?absoluteDateRangeStart=1000&absoluteDateRangeEnd=2000&dateRangeStart=now-15m&dateRangeEnd=now&filters=monitor.id%3A%20bar&focusConnectorField=true&search=monitor.id%3A%20foo&statusFilter=up"` ); }); @@ -31,8 +29,6 @@ describe('stringifyUrlParams', () => { { absoluteDateRangeStart: 1000, absoluteDateRangeEnd: 2000, - refreshInterval: 50000, - refreshPaused: false, dateRangeStart: 'now-15m', dateRangeEnd: 'now', filters: 'monitor.id: bar', @@ -43,9 +39,7 @@ describe('stringifyUrlParams', () => { }, true ); - expect(result).toMatchInlineSnapshot( - `"?refreshInterval=50000&dateRangeStart=now-15m&filters=monitor.id%3A%20bar"` - ); + expect(result).toMatchInlineSnapshot(`"?dateRangeStart=now-15m&filters=monitor.id%3A%20bar"`); expect(result.includes('pagination')).toBeFalsy(); expect(result.includes('search')).toBeFalsy(); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/url_params/stringify_url_params.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/url_params/stringify_url_params.ts index 7f0dd94237593..7f465e7272dc6 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/url_params/stringify_url_params.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/url_params/stringify_url_params.ts @@ -12,8 +12,7 @@ import { CLIENT_DEFAULTS } from '../../../../../common/constants'; const { FOCUS_CONNECTOR_FIELD } = CLIENT_DEFAULTS; -const { DATE_RANGE_START, DATE_RANGE_END, AUTOREFRESH_INTERVAL_SECONDS, AUTOREFRESH_IS_PAUSED } = - CLIENT_DEFAULTS_SYNTHETICS; +const { DATE_RANGE_START, DATE_RANGE_END } = CLIENT_DEFAULTS_SYNTHETICS; export const stringifyUrlParams = (params: Partial, ignoreEmpty = false) => { if (ignoreEmpty) { @@ -41,12 +40,6 @@ const replaceDefaults = (params: Partial) => { if (key === 'dateRangeEnd' && val === DATE_RANGE_END) { delete params[key]; } - if (key === 'refreshPaused' && val === AUTOREFRESH_IS_PAUSED) { - delete params[key]; - } - if (key === 'refreshInterval' && val === AUTOREFRESH_INTERVAL_SECONDS) { - delete params[key]; - } if (key === 'focusConnectorField' && val === FOCUS_CONNECTOR_FIELD) { delete params[key]; } diff --git a/x-pack/plugins/search_indices/common/doc_links.ts b/x-pack/plugins/search_indices/common/doc_links.ts index dbffa8f9f0f33..8cceb45041ab9 100644 --- a/x-pack/plugins/search_indices/common/doc_links.ts +++ b/x-pack/plugins/search_indices/common/doc_links.ts @@ -9,11 +9,13 @@ import { DocLinks } from '@kbn/doc-links'; class SearchIndicesDocLinks { public apiReference: string = ''; + public setupSemanticSearch: string = ''; constructor() {} setDocLinks(newDocLinks: DocLinks) { this.apiReference = newDocLinks.apiReference; + this.setupSemanticSearch = newDocLinks.enterpriseSearch.semanticSearch; } } export const docLinks = new SearchIndicesDocLinks(); diff --git a/x-pack/plugins/search_indices/public/components/connection_details/connection_details.tsx b/x-pack/plugins/search_indices/public/components/connection_details/connection_details.tsx new file mode 100644 index 0000000000000..d7ce8f308b683 --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/connection_details/connection_details.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { + EuiButtonIcon, + EuiCopy, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + useEuiTheme, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { FormattedMessage } from '@kbn/i18n-react'; +import { useElasticsearchUrl } from '../../hooks/use_elasticsearch_url'; + +export const ConnectionDetails: React.FC = () => { + const { euiTheme } = useEuiTheme(); + const elasticsearchUrl = useElasticsearchUrl(); + + return ( + + + +

+ +

+
+
+ +

+ {elasticsearchUrl} +

+
+ + + {(copy) => ( + + )} + + +
+ ); +}; diff --git a/x-pack/plugins/search_indices/public/components/indices/details_page.tsx b/x-pack/plugins/search_indices/public/components/indices/details_page.tsx index afa798814d864..85021e79edbf2 100644 --- a/x-pack/plugins/search_indices/public/components/indices/details_page.tsx +++ b/x-pack/plugins/search_indices/public/components/indices/details_page.tsx @@ -27,13 +27,22 @@ import { i18n } from '@kbn/i18n'; import { SectionLoading } from '@kbn/es-ui-shared-plugin/public'; import { useIndex } from '../../hooks/api/use_index'; import { useKibana } from '../../hooks/use_kibana'; +import { ConnectionDetails } from '../connection_details/connection_details'; +import { QuickStats } from '../quick_stats/quick_stats'; +import { useIndexMapping } from '../../hooks/api/use_index_mappings'; import { DeleteIndexModal } from './delete_index_modal'; import { IndexloadingError } from './details_page_loading_error'; export const SearchIndexDetailsPage = () => { const indexName = decodeURIComponent(useParams<{ indexName: string }>().indexName); const { console: consolePlugin, docLinks, application } = useKibana().services; - const { data: index, refetch, isSuccess, isInitialLoading } = useIndex(indexName); + + const { data: index, refetch, isError: isIndexError, isInitialLoading } = useIndex(indexName); + const { + data: mappings, + isError: isMappingsError, + isInitialLoading: isMappingsInitialLoading, + } = useIndexMapping(indexName); const embeddableConsole = useMemo( () => (consolePlugin?.EmbeddableConsole ? : null), @@ -87,7 +96,7 @@ export const SearchIndexDetailsPage = () => { /> ); - if (isInitialLoading) { + if (isInitialLoading || isMappingsInitialLoading) { return ( {i18n.translate('xpack.searchIndices.loadingDescription', { @@ -103,9 +112,10 @@ export const SearchIndexDetailsPage = () => { restrictWidth={false} data-test-subj="searchIndicesDetailsPage" grow={false} - bottomBorder={false} + panelled + bottomBorder > - {!isSuccess || !index ? ( + {isIndexError || isMappingsError || !index || !mappings ? ( { navigateToIndexListPage={navigateToIndexListPage} /> )} + + + + + + {/* TODO: API KEY */} + + + -
+ + + + )} {embeddableConsole} diff --git a/x-pack/plugins/search_indices/public/components/quick_stats/mappings_convertor.test.ts b/x-pack/plugins/search_indices/public/components/quick_stats/mappings_convertor.test.ts new file mode 100644 index 0000000000000..da182123ab4c1 --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/quick_stats/mappings_convertor.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Mappings } from '../../types'; +import { countVectorBasedTypesFromMappings } from './mappings_convertor'; + +describe('mappings convertor', () => { + it('should count vector based types from mappings', () => { + const mappings = { + mappings: { + properties: { + field1: { + type: 'dense_vector', + }, + field2: { + type: 'dense_vector', + }, + field3: { + type: 'sparse_vector', + }, + field4: { + type: 'dense_vector', + }, + field5: { + type: 'semantic_text', + }, + }, + }, + }; + const result = countVectorBasedTypesFromMappings(mappings as unknown as Mappings); + expect(result).toEqual({ + dense_vector: 3, + sparse_vector: 1, + semantic_text: 1, + }); + }); +}); diff --git a/x-pack/plugins/search_indices/public/components/quick_stats/mappings_convertor.ts b/x-pack/plugins/search_indices/public/components/quick_stats/mappings_convertor.ts new file mode 100644 index 0000000000000..749fe05de1f54 --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/quick_stats/mappings_convertor.ts @@ -0,0 +1,62 @@ +/* + * 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 { + MappingProperty, + MappingPropertyBase, +} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { Mappings } from '../../types'; + +interface VectorFieldTypes { + semantic_text: number; + dense_vector: number; + sparse_vector: number; +} + +export function countVectorBasedTypesFromMappings(mappings: Mappings): VectorFieldTypes { + const typeCounts: VectorFieldTypes = { + semantic_text: 0, + dense_vector: 0, + sparse_vector: 0, + }; + + const typeCountKeys = Object.keys(typeCounts); + + function recursiveCount(fields: MappingProperty | Mappings | MappingPropertyBase['fields']) { + if (!fields) { + return; + } + if ('mappings' in fields) { + recursiveCount(fields.mappings); + } + if ('properties' in fields && fields.properties) { + Object.keys(fields.properties).forEach((key) => { + const value = (fields.properties as Record)?.[key]; + + if (value && value.type) { + if (typeCountKeys.includes(value.type)) { + const type = value.type as keyof VectorFieldTypes; + typeCounts[type] = typeCounts[type] + 1; + } + + if ('fields' in value) { + recursiveCount(value.fields); + } + + if ('properties' in value) { + recursiveCount(value.properties); + } + } else if (value.properties || value.fields) { + recursiveCount(value); + } + }); + } + } + + recursiveCount(mappings); + return typeCounts; +} diff --git a/x-pack/plugins/search_indices/public/components/quick_stats/quick_stat.tsx b/x-pack/plugins/search_indices/public/components/quick_stats/quick_stat.tsx new file mode 100644 index 0000000000000..0d72835ad5779 --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/quick_stats/quick_stat.tsx @@ -0,0 +1,116 @@ +/* + * 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 { + EuiAccordion, + EuiDescriptionList, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPanel, + EuiText, + useEuiTheme, + useGeneratedHtmlId, +} from '@elastic/eui'; + +interface BaseQuickStatProps { + icon: string; + iconColor: string; + title: string; + secondaryTitle: React.ReactNode; + open: boolean; + content?: React.ReactNode; + stats: Array<{ + title: string; + description: NonNullable; + }>; + setOpen: (open: boolean) => void; + first?: boolean; +} + +export const QuickStat: React.FC = ({ + icon, + title, + stats, + open, + setOpen, + first, + secondaryTitle, + iconColor, + content, + ...rest +}) => { + const { euiTheme } = useEuiTheme(); + + const id = useGeneratedHtmlId({ + prefix: 'formAccordion', + suffix: title, + }); + + return ( + setOpen(!open)} + paddingSize="none" + id={id} + buttonElement="div" + arrowDisplay="right" + {...rest} + css={{ + borderLeft: euiTheme.border.thin, + ...(first ? { borderLeftWidth: 0 } : {}), + '.euiAccordion__arrow': { + marginRight: euiTheme.size.s, + }, + '.euiAccordion__triggerWrapper': { + background: euiTheme.colors.ghost, + }, + '.euiAccordion__children': { + borderTop: euiTheme.border.thin, + padding: euiTheme.size.m, + }, + }} + buttonContent={ + + + + + + + +

{title}

+
+
+ + {secondaryTitle} + +
+
+ } + > + {content ? ( + content + ) : ( + + + + + + )} +
+ ); +}; diff --git a/x-pack/plugins/search_indices/public/components/quick_stats/quick_stats.tsx b/x-pack/plugins/search_indices/public/components/quick_stats/quick_stats.tsx new file mode 100644 index 0000000000000..cece2b1d39910 --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/quick_stats/quick_stats.tsx @@ -0,0 +1,167 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useState } from 'react'; +import type { Index } from '@kbn/index-management-shared-types'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiI18nNumber, + EuiPanel, + EuiText, + useEuiTheme, + EuiButton, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Mappings } from '../../types'; +import { countVectorBasedTypesFromMappings } from './mappings_convertor'; +import { QuickStat } from './quick_stat'; +import { useKibana } from '../../hooks/use_kibana'; + +export interface QuickStatsProps { + index: Index; + mappings: Mappings; +} + +export const SetupAISearchButton: React.FC = () => { + const { + services: { docLinks }, + } = useKibana(); + return ( + + + + +
+ {i18n.translate('xpack.searchIndices.quickStats.setup_ai_search_description', { + defaultMessage: 'Build AI-powered search experiences with Elastic', + })} +
+
+
+ + + {i18n.translate('xpack.searchIndices.quickStats.setup_ai_search_button', { + defaultMessage: 'Set up now', + })} + + +
+
+ ); +}; + +export const QuickStats: React.FC = ({ index, mappings }) => { + const [open, setOpen] = useState(false); + const { euiTheme } = useEuiTheme(); + const mappingStats = useMemo(() => countVectorBasedTypesFromMappings(mappings), [mappings]); + const vectorFieldCount = + mappingStats.sparse_vector + mappingStats.dense_vector + mappingStats.semantic_text; + + return ( + ({ + border: euiTheme.border.thin, + background: euiTheme.colors.lightestShade, + overflow: 'hidden', + })} + > + + + } + stats={[ + { + title: i18n.translate('xpack.searchIndices.quickStats.documents.totalTitle', { + defaultMessage: 'Total', + }), + description: , + }, + { + title: i18n.translate('xpack.searchIndices.quickStats.documents.indexSize', { + defaultMessage: 'Index Size', + }), + description: index.size ?? '0b', + }, + ]} + first + /> + + + 0 + ? i18n.translate('xpack.searchIndices.quickStats.total_count', { + defaultMessage: '{value, plural, one {# Field} other {# Fields}}', + values: { + value: vectorFieldCount, + }, + }) + : i18n.translate('xpack.searchIndices.quickStats.no_vector_fields', { + defaultMessage: 'Not configured', + }) + } + content={vectorFieldCount === 0 && } + stats={[ + { + title: i18n.translate('xpack.searchIndices.quickStats.sparse_vector', { + defaultMessage: 'Sparse Vector', + }), + description: i18n.translate('xpack.searchIndices.quickStats.sparse_vector_count', { + defaultMessage: '{value, plural, one {# Field} other {# Fields}}', + values: { value: mappingStats.sparse_vector }, + }), + }, + { + title: i18n.translate('xpack.searchIndices.quickStats.dense_vector', { + defaultMessage: 'Dense Vector', + }), + description: i18n.translate('xpack.searchIndices.quickStats.dense_vector_count', { + defaultMessage: '{value, plural, one {# Field} other {# Fields}}', + values: { value: mappingStats.dense_vector }, + }), + }, + { + title: i18n.translate('xpack.searchIndices.quickStats.semantic_text', { + defaultMessage: 'Semantic Text', + }), + description: i18n.translate('xpack.searchIndices.quickStats.semantic_text_count', { + defaultMessage: '{value, plural, one {# Field} other {# Fields}}', + values: { value: mappingStats.semantic_text }, + }), + }, + ]} + /> + + + + ); +}; diff --git a/x-pack/plugins/search_indices/public/components/start/create_index_code.tsx b/x-pack/plugins/search_indices/public/components/start/create_index_code.tsx index b58bf6c0926f1..b78137e7a3fdd 100644 --- a/x-pack/plugins/search_indices/public/components/start/create_index_code.tsx +++ b/x-pack/plugins/search_indices/public/components/start/create_index_code.tsx @@ -12,12 +12,12 @@ import { TryInConsoleButton } from '@kbn/try-in-console'; import { useKibana } from '../../hooks/use_kibana'; import { CodeSample } from './code_sample'; import { CreateIndexFormState } from './types'; -import { ELASTICSEARCH_URL_PLACEHOLDER } from '../../constants'; import { Languages, AvailableLanguages, LanguageOptions } from '../../code_examples'; import { DenseVectorSeverlessCodeExamples } from '../../code_examples/create_index'; import { LanguageSelector } from '../shared/language_selector'; +import { useElasticsearchUrl } from '../../hooks/use_elasticsearch_url'; export interface CreateIndexCodeViewProps { createIndexForm: CreateIndexFormState; @@ -27,15 +27,17 @@ export interface CreateIndexCodeViewProps { const SelectedCodeExamples = DenseVectorSeverlessCodeExamples; export const CreateIndexCodeView = ({ createIndexForm }: CreateIndexCodeViewProps) => { - const { application, cloud, share, console: consolePlugin } = useKibana().services; + const { application, share, console: consolePlugin } = useKibana().services; // TODO: initing this should be dynamic and possibly saved in the form state const [selectedLanguage, setSelectedLanguage] = useState('python'); + const elasticsearchUrl = useElasticsearchUrl(); + const codeParams = useMemo(() => { return { indexName: createIndexForm.indexName || undefined, - elasticsearchURL: cloud?.elasticsearchUrl ?? ELASTICSEARCH_URL_PLACEHOLDER, + elasticsearchURL: elasticsearchUrl, }; - }, [createIndexForm.indexName, cloud]); + }, [createIndexForm.indexName, elasticsearchUrl]); const selectedCodeExample = useMemo(() => { return SelectedCodeExamples[selectedLanguage]; }, [selectedLanguage]); @@ -43,21 +45,23 @@ export const CreateIndexCodeView = ({ createIndexForm }: CreateIndexCodeViewProp return ( - + setSelectedLanguage(value)} /> - - - + {selectedLanguage === 'curl' && ( + + + + )} {selectedCodeExample.installCommand && ( { + const { http } = useKibana().services; + const queryKey = ['fetchMapping', indexName]; + const result = useQuery({ + queryKey, + refetchOnWindowFocus: 'always', + queryFn: () => + http.fetch(`/api/index_management/mapping/${encodeURIComponent(indexName)}`), + }); + return { queryKey, ...result }; +}; diff --git a/x-pack/plugins/search_indices/public/hooks/use_elasticsearch_url.ts b/x-pack/plugins/search_indices/public/hooks/use_elasticsearch_url.ts new file mode 100644 index 0000000000000..d07cc62b210de --- /dev/null +++ b/x-pack/plugins/search_indices/public/hooks/use_elasticsearch_url.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useKibana } from './use_kibana'; + +import { ELASTICSEARCH_URL_PLACEHOLDER } from '../constants'; + +export const useElasticsearchUrl = (): string => { + const { + services: { cloud }, + } = useKibana(); + + return cloud?.elasticsearchUrl ?? ELASTICSEARCH_URL_PLACEHOLDER; +}; diff --git a/x-pack/plugins/search_indices/public/types.ts b/x-pack/plugins/search_indices/public/types.ts index 8e7853543f76f..6e0192e34f87c 100644 --- a/x-pack/plugins/search_indices/public/types.ts +++ b/x-pack/plugins/search_indices/public/types.ts @@ -11,6 +11,7 @@ import type { AppMountParameters, CoreStart } from '@kbn/core/public'; import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; +import type { MappingPropertyBase } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; export interface SearchIndicesPluginSetup { enabled: boolean; @@ -44,11 +45,18 @@ export interface AppUsageTracker { load: (eventName: string | string[]) => void; } +export interface Mappings { + mappings: { + properties: MappingPropertyBase['properties']; + }; +} + export interface CodeSnippetParameters { indexName?: string; apiKey?: string; elasticsearchURL: string; } + export type CodeSnippetFunction = (params: CodeSnippetParameters) => string; export interface CodeLanguage { diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/common.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/common.gen.ts new file mode 100644 index 0000000000000..e5f8c631fcbae --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/common.gen.ts @@ -0,0 +1,38 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Entity Store Common Schema + * version: 1 + */ + +import { z } from '@kbn/zod'; + +export type EntityType = z.infer; +export const EntityType = z.enum(['user', 'host']); +export type EntityTypeEnum = typeof EntityType.enum; +export const EntityTypeEnum = EntityType.enum; + +export type IndexPattern = z.infer; +export const IndexPattern = z.string(); + +export type EngineStatus = z.infer; +export const EngineStatus = z.enum(['installing', 'started', 'stopped']); +export type EngineStatusEnum = typeof EngineStatus.enum; +export const EngineStatusEnum = EngineStatus.enum; + +export type EngineDescriptor = z.infer; +export const EngineDescriptor = z.object({ + type: EntityType.optional(), + indexPattern: IndexPattern.optional(), + status: EngineStatus.optional(), + filter: z.string().optional(), +}); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/common.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/common.schema.yaml new file mode 100644 index 0000000000000..dc17ad6193ee5 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/common.schema.yaml @@ -0,0 +1,37 @@ +openapi: 3.0.0 +info: + title: Entity Store Common Schema + description: Common schema for Entity Store + version: '1' +paths: {} +components: + schemas: + + EntityType: + type: string + enum: + - user + - host + + EngineDescriptor: + type: object + properties: + type: + $ref: '#/components/schemas/EntityType' + indexPattern: + $ref: '#/components/schemas/IndexPattern' + status: + $ref: '#/components/schemas/EngineStatus' + filter: + type: string + + EngineStatus: + type: string + enum: + - installing + - started + - stopped + + IndexPattern: + type: string + \ No newline at end of file diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/delete.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/delete.gen.ts new file mode 100644 index 0000000000000..34acf2a802076 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/delete.gen.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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Delete the entity store engine + * version: 2023-10-31 + */ + +import { z } from '@kbn/zod'; +import { BooleanFromString } from '@kbn/zod-helpers'; + +import { EntityType } from '../common.gen'; + +export type DeleteEntityStoreRequestQuery = z.infer; +export const DeleteEntityStoreRequestQuery = z.object({ + /** + * Control flag to also delete the entity data. + */ + data: BooleanFromString.optional(), +}); +export type DeleteEntityStoreRequestQueryInput = z.input; + +export type DeleteEntityStoreRequestParams = z.infer; +export const DeleteEntityStoreRequestParams = z.object({ + /** + * The entity type of the store (either 'user' or 'host'). + */ + entityType: EntityType, +}); +export type DeleteEntityStoreRequestParamsInput = z.input; + +export type DeleteEntityStoreResponse = z.infer; +export const DeleteEntityStoreResponse = z.object({ + deleted: z.boolean().optional(), +}); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/delete.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/delete.schema.yaml new file mode 100644 index 0000000000000..c766d9895c5fa --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/delete.schema.yaml @@ -0,0 +1,37 @@ +openapi: 3.0.0 + +info: + title: Delete the entity store engine + version: '2023-10-31' +paths: + /api/entity_store/engines/{entityType}: + delete: + x-labels: [ess, serverless] + x-codegen-enabled: true + operationId: DeleteEntityStore + summary: Delete the Entity Store engine + parameters: + - name: entityType + in: path + required: true + schema: + $ref: '../common.schema.yaml#/components/schemas/EntityType' + description: The entity type of the store (either 'user' or 'host'). + + - name: data + in: query + required: false + schema: + type: boolean + description: Control flag to also delete the entity data. + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + deleted: + type: boolean + \ No newline at end of file diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/get.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/get.gen.ts new file mode 100644 index 0000000000000..44f6f45844fc1 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/get.gen.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Get Entity Store engine + * version: 2023-10-31 + */ + +import { z } from '@kbn/zod'; + +import { EntityType, EngineDescriptor } from '../common.gen'; + +export type GetEntityStoreEngineRequestParams = z.infer; +export const GetEntityStoreEngineRequestParams = z.object({ + /** + * The entity type of the store (either 'user' or 'host'). + */ + entityType: EntityType, +}); +export type GetEntityStoreEngineRequestParamsInput = z.input< + typeof GetEntityStoreEngineRequestParams +>; + +export type GetEntityStoreEngineResponse = z.infer; +export const GetEntityStoreEngineResponse = EngineDescriptor; diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/get.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/get.schema.yaml new file mode 100644 index 0000000000000..d65a5906e54d9 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/get.schema.yaml @@ -0,0 +1,25 @@ +openapi: 3.0.0 +info: + title: Get Entity Store engine + version: '2023-10-31' +paths: + /api/entity_store/engines/{entityType}: + get: + x-labels: [ess, serverless] + x-codegen-enabled: true + operationId: GetEntityStoreEngine + summary: Get the Entity Store engine + parameters: + - name: entityType + in: path + required: true + schema: + $ref: '../common.schema.yaml#/components/schemas/EntityType' + description: The entity type of the store (either 'user' or 'host'). + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '../common.schema.yaml#/components/schemas/EngineDescriptor' diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/init.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/init.gen.ts new file mode 100644 index 0000000000000..07f32f4cb7144 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/init.gen.ts @@ -0,0 +1,38 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Init Entity Store types + * version: 2023-10-31 + */ + +import { z } from '@kbn/zod'; + +import { EntityType, IndexPattern, EngineDescriptor } from '../common.gen'; + +export type InitEntityStoreRequestParams = z.infer; +export const InitEntityStoreRequestParams = z.object({ + /** + * The entity type of the store (either 'user' or 'host'). + */ + entityType: EntityType, +}); +export type InitEntityStoreRequestParamsInput = z.input; + +export type InitEntityStoreRequestBody = z.infer; +export const InitEntityStoreRequestBody = z.object({ + indexPattern: IndexPattern.optional(), + filter: z.string().optional(), +}); +export type InitEntityStoreRequestBodyInput = z.input; + +export type InitEntityStoreResponse = z.infer; +export const InitEntityStoreResponse = EngineDescriptor; diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/init.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/init.schema.yaml new file mode 100644 index 0000000000000..8e826d57ce40a --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/init.schema.yaml @@ -0,0 +1,39 @@ +openapi: 3.0.0 + +info: + title: Init Entity Store types + version: '2023-10-31' +paths: + /api/entity_store/engines/{entityType}/init: + post: + x-labels: [ess, serverless] + x-codegen-enabled: true + operationId: InitEntityStore + summary: Initialize the Entity Store + parameters: + - name: entityType + in: path + required: true + schema: + $ref: '../common.schema.yaml#/components/schemas/EntityType' + description: The entity type of the store (either 'user' or 'host'). + requestBody: + description: Schema for the engine initialization + required: true + content: + application/json: + schema: + type: object + properties: + indexPattern: + $ref: '../common.schema.yaml#/components/schemas/IndexPattern' + filter: + type: string + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '../common.schema.yaml#/components/schemas/EngineDescriptor' + diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/list.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/list.gen.ts new file mode 100644 index 0000000000000..926549a329a4b --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/list.gen.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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: List Entity Store engines + * version: 2023-10-31 + */ + +import { z } from '@kbn/zod'; + +import { EngineDescriptor } from '../common.gen'; + +export type ListEntityStoreEnginesResponse = z.infer; +export const ListEntityStoreEnginesResponse = z.object({ + count: z.number().int().optional(), + engines: z.array(EngineDescriptor).optional(), +}); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/list.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/list.schema.yaml new file mode 100644 index 0000000000000..efad1a4380352 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/list.schema.yaml @@ -0,0 +1,25 @@ +openapi: 3.0.0 +info: + title: List Entity Store engines + version: '2023-10-31' +paths: + /api/entity_store/engines: + get: + x-labels: [ess, serverless] + x-codegen-enabled: true + operationId: ListEntityStoreEngines + summary: List the Entity Store engines + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + count: + type: integer + engines: + type: array + items: + $ref: '../common.schema.yaml#/components/schemas/EngineDescriptor' \ No newline at end of file diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/start.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/start.gen.ts new file mode 100644 index 0000000000000..b8e94d00551c0 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/start.gen.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Start the entity store engine + * version: 2023-10-31 + */ + +import { z } from '@kbn/zod'; + +import { EntityType } from '../common.gen'; + +export type StartEntityStoreRequestParams = z.infer; +export const StartEntityStoreRequestParams = z.object({ + /** + * The entity type of the store (either 'user' or 'host'). + */ + entityType: EntityType, +}); +export type StartEntityStoreRequestParamsInput = z.input; + +export type StartEntityStoreResponse = z.infer; +export const StartEntityStoreResponse = z.object({ + started: z.boolean().optional(), +}); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/start.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/start.schema.yaml new file mode 100644 index 0000000000000..5c048bf3d973c --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/start.schema.yaml @@ -0,0 +1,31 @@ +openapi: 3.0.0 + +info: + title: Start the entity store engine + version: '2023-10-31' +paths: + /api/entity_store/engines/{entityType}/start: + post: + x-labels: [ess, serverless] + x-codegen-enabled: true + operationId: StartEntityStore + summary: Start the Entity Store engine + parameters: + - name: entityType + in: path + required: true + schema: + $ref: '../common.schema.yaml#/components/schemas/EntityType' + description: The entity type of the store (either 'user' or 'host'). + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + started: + type: boolean + + \ No newline at end of file diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stats.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stats.gen.ts new file mode 100644 index 0000000000000..631399faebc96 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stats.gen.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Get the entity store engine stats + * version: 2023-10-31 + */ + +import { z } from '@kbn/zod'; + +import { EntityType, IndexPattern, EngineStatus } from '../common.gen'; + +export type GetEntityStoreStatsRequestParams = z.infer; +export const GetEntityStoreStatsRequestParams = z.object({ + /** + * The entity type of the store (either 'user' or 'host'). + */ + entityType: EntityType, +}); +export type GetEntityStoreStatsRequestParamsInput = z.input< + typeof GetEntityStoreStatsRequestParams +>; + +export type GetEntityStoreStatsResponse = z.infer; +export const GetEntityStoreStatsResponse = z.object({ + type: EntityType.optional(), + indexPattern: IndexPattern.optional(), + status: EngineStatus.optional(), + transforms: z.array(z.object({})).optional(), + indices: z.array(z.object({})).optional(), +}); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stats.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stats.schema.yaml new file mode 100644 index 0000000000000..8d8327d5e6468 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stats.schema.yaml @@ -0,0 +1,41 @@ +openapi: 3.0.0 + +info: + title: Get the entity store engine stats + version: '2023-10-31' +paths: + /api/entity_store/engines/{entityType}/stats: + post: + x-labels: [ess, serverless] + x-codegen-enabled: true + operationId: GetEntityStoreStats + summary: Get the Entity Store engine stats + parameters: + - name: entityType + in: path + required: true + schema: + $ref: '../common.schema.yaml#/components/schemas/EntityType' + description: The entity type of the store (either 'user' or 'host'). + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + type: + $ref : '../common.schema.yaml#/components/schemas/EntityType' + indexPattern: + $ref : '../common.schema.yaml#/components/schemas/IndexPattern' + status: + $ref : '../common.schema.yaml#/components/schemas/EngineStatus' + transforms: + type: array + items: + type: object + indices: + type: array + items: + type: object diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stop.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stop.gen.ts new file mode 100644 index 0000000000000..ff3ef7a2f3eac --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stop.gen.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Stop the entity store engine + * version: 2023-10-31 + */ + +import { z } from '@kbn/zod'; + +import { EntityType } from '../common.gen'; + +export type StopEntityStoreRequestParams = z.infer; +export const StopEntityStoreRequestParams = z.object({ + /** + * The entity type of the store (either 'user' or 'host'). + */ + entityType: EntityType, +}); +export type StopEntityStoreRequestParamsInput = z.input; + +export type StopEntityStoreResponse = z.infer; +export const StopEntityStoreResponse = z.object({ + stopped: z.boolean().optional(), +}); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stop.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stop.schema.yaml new file mode 100644 index 0000000000000..214f803a76e34 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stop.schema.yaml @@ -0,0 +1,30 @@ +openapi: 3.0.0 + +info: + title: Stop the entity store engine + version: '2023-10-31' +paths: + /api/entity_store/engines/{entityType}/stop: + post: + x-labels: [ess, serverless] + x-codegen-enabled: true + operationId: StopEntityStore + summary: Stop the Entity Store engine + parameters: + - name: entityType + in: path + required: true + schema: + $ref: '../common.schema.yaml#/components/schemas/EntityType' + description: The entity type of the store (either 'user' or 'host'). + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + stopped: + type: boolean + diff --git a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts index edd0bfe89fc8c..c08f807d4926b 100644 --- a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -242,6 +242,33 @@ import type { InternalUploadAssetCriticalityRecordsResponse, UploadAssetCriticalityRecordsResponse, } from './entity_analytics/asset_criticality/upload_asset_criticality_csv.gen'; +import type { + DeleteEntityStoreRequestQueryInput, + DeleteEntityStoreRequestParamsInput, + DeleteEntityStoreResponse, +} from './entity_analytics/entity_store/engine/delete.gen'; +import type { + GetEntityStoreEngineRequestParamsInput, + GetEntityStoreEngineResponse, +} from './entity_analytics/entity_store/engine/get.gen'; +import type { + InitEntityStoreRequestParamsInput, + InitEntityStoreRequestBodyInput, + InitEntityStoreResponse, +} from './entity_analytics/entity_store/engine/init.gen'; +import type { ListEntityStoreEnginesResponse } from './entity_analytics/entity_store/engine/list.gen'; +import type { + StartEntityStoreRequestParamsInput, + StartEntityStoreResponse, +} from './entity_analytics/entity_store/engine/start.gen'; +import type { + GetEntityStoreStatsRequestParamsInput, + GetEntityStoreStatsResponse, +} from './entity_analytics/entity_store/engine/stats.gen'; +import type { + StopEntityStoreRequestParamsInput, + StopEntityStoreResponse, +} from './entity_analytics/entity_store/engine/stop.gen'; import type { DisableRiskEngineResponse } from './entity_analytics/risk_engine/engine_disable_route.gen'; import type { EnableRiskEngineResponse } from './entity_analytics/risk_engine/engine_enable_route.gen'; import type { InitRiskEngineResponse } from './entity_analytics/risk_engine/engine_init_route.gen'; @@ -620,6 +647,20 @@ Migrations are initiated per index. While the process is neither destructive nor }) .catch(catchAxiosErrorFormatAndThrow); } + async deleteEntityStore(props: DeleteEntityStoreProps) { + this.log.info(`${new Date().toISOString()} Calling API DeleteEntityStore`); + return this.kbnClient + .request({ + path: replaceParams('/api/entity_store/engines/{entityType}', props.params), + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', + }, + method: 'DELETE', + + query: props.query, + }) + .catch(catchAxiosErrorFormatAndThrow); + } async deleteNote(props: DeleteNoteProps) { this.log.info(`${new Date().toISOString()} Calling API DeleteNote`); return this.kbnClient @@ -1155,6 +1196,30 @@ finalize it. }) .catch(catchAxiosErrorFormatAndThrow); } + async getEntityStoreEngine(props: GetEntityStoreEngineProps) { + this.log.info(`${new Date().toISOString()} Calling API GetEntityStoreEngine`); + return this.kbnClient + .request({ + path: replaceParams('/api/entity_store/engines/{entityType}', props.params), + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', + }, + method: 'GET', + }) + .catch(catchAxiosErrorFormatAndThrow); + } + async getEntityStoreStats(props: GetEntityStoreStatsProps) { + this.log.info(`${new Date().toISOString()} Calling API GetEntityStoreStats`); + return this.kbnClient + .request({ + path: replaceParams('/api/entity_store/engines/{entityType}/stats', props.params), + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', + }, + method: 'POST', + }) + .catch(catchAxiosErrorFormatAndThrow); + } /** * Gets notes */ @@ -1311,6 +1376,19 @@ finalize it. }) .catch(catchAxiosErrorFormatAndThrow); } + async initEntityStore(props: InitEntityStoreProps) { + this.log.info(`${new Date().toISOString()} Calling API InitEntityStore`); + return this.kbnClient + .request({ + path: replaceParams('/api/entity_store/engines/{entityType}/init', props.params), + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', + }, + method: 'POST', + body: props.body, + }) + .catch(catchAxiosErrorFormatAndThrow); + } /** * Initializes the Risk Engine by creating the necessary indices and mappings, removing old transforms, and starting the new risk engine */ @@ -1367,6 +1445,18 @@ finalize it. }) .catch(catchAxiosErrorFormatAndThrow); } + async listEntityStoreEngines() { + this.log.info(`${new Date().toISOString()} Calling API ListEntityStoreEngines`); + return this.kbnClient + .request({ + path: '/api/entity_store/engines', + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', + }, + method: 'GET', + }) + .catch(catchAxiosErrorFormatAndThrow); + } /** * Update specific fields of an existing detection rule using the `rule_id` or `id` field. */ @@ -1699,6 +1789,30 @@ detection engine rules. }) .catch(catchAxiosErrorFormatAndThrow); } + async startEntityStore(props: StartEntityStoreProps) { + this.log.info(`${new Date().toISOString()} Calling API StartEntityStore`); + return this.kbnClient + .request({ + path: replaceParams('/api/entity_store/engines/{entityType}/start', props.params), + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', + }, + method: 'POST', + }) + .catch(catchAxiosErrorFormatAndThrow); + } + async stopEntityStore(props: StopEntityStoreProps) { + this.log.info(`${new Date().toISOString()} Calling API StopEntityStore`); + return this.kbnClient + .request({ + path: replaceParams('/api/entity_store/engines/{entityType}/stop', props.params), + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', + }, + method: 'POST', + }) + .catch(catchAxiosErrorFormatAndThrow); + } /** * Suggests user profiles. */ @@ -1809,6 +1923,10 @@ export interface CreateUpdateProtectionUpdatesNoteProps { export interface DeleteAssetCriticalityRecordProps { query: DeleteAssetCriticalityRecordRequestQueryInput; } +export interface DeleteEntityStoreProps { + query: DeleteEntityStoreRequestQueryInput; + params: DeleteEntityStoreRequestParamsInput; +} export interface DeleteNoteProps { body: DeleteNoteRequestBodyInput; } @@ -1902,6 +2020,12 @@ export interface GetEndpointSuggestionsProps { params: GetEndpointSuggestionsRequestParamsInput; body: GetEndpointSuggestionsRequestBodyInput; } +export interface GetEntityStoreEngineProps { + params: GetEntityStoreEngineRequestParamsInput; +} +export interface GetEntityStoreStatsProps { + params: GetEntityStoreStatsRequestParamsInput; +} export interface GetNotesProps { query: GetNotesRequestQueryInput; } @@ -1932,6 +2056,10 @@ export interface ImportRulesProps { export interface ImportTimelinesProps { body: ImportTimelinesRequestBodyInput; } +export interface InitEntityStoreProps { + params: InitEntityStoreRequestParamsInput; + body: InitEntityStoreRequestBodyInput; +} export interface InstallPrepackedTimelinesProps { body: InstallPrepackedTimelinesRequestBodyInput; } @@ -1984,6 +2112,12 @@ export interface SetAlertsStatusProps { export interface SetAlertTagsProps { body: SetAlertTagsRequestBodyInput; } +export interface StartEntityStoreProps { + params: StartEntityStoreRequestParamsInput; +} +export interface StopEntityStoreProps { + params: StopEntityStoreRequestParamsInput; +} export interface SuggestUserProfilesProps { query: SuggestUserProfilesRequestQueryInput; } diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 7d3edafedd1a9..e11965653526f 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -223,6 +223,11 @@ export const allowedExperimentalValues = Object.freeze({ * Enables the new data ingestion hub */ dataIngestionHubEnabled: false, + + /** + * Enables the new Entity Store engine routes + */ + entityStoreEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml index 7ee7a3748df4b..9e56395f2af75 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml @@ -256,6 +256,187 @@ paths: summary: List Asset Criticality Records tags: - Security Solution Entity Analytics API + /api/entity_store/engines: + get: + operationId: ListEntityStoreEngines + responses: + '200': + content: + application/json: + schema: + type: object + properties: + count: + type: integer + engines: + items: + $ref: '#/components/schemas/EngineDescriptor' + type: array + description: Successful response + summary: List the Entity Store engines + tags: + - Security Solution Entity Analytics API + '/api/entity_store/engines/{entityType}': + delete: + operationId: DeleteEntityStore + parameters: + - description: The entity type of the store (either 'user' or 'host'). + in: path + name: entityType + required: true + schema: + $ref: '#/components/schemas/EntityType' + - description: Control flag to also delete the entity data. + in: query + name: data + required: false + schema: + type: boolean + responses: + '200': + content: + application/json: + schema: + type: object + properties: + deleted: + type: boolean + description: Successful response + summary: Delete the Entity Store engine + tags: + - Security Solution Entity Analytics API + get: + operationId: GetEntityStoreEngine + parameters: + - description: The entity type of the store (either 'user' or 'host'). + in: path + name: entityType + required: true + schema: + $ref: '#/components/schemas/EntityType' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/EngineDescriptor' + description: Successful response + summary: Get the Entity Store engine + tags: + - Security Solution Entity Analytics API + '/api/entity_store/engines/{entityType}/init': + post: + operationId: InitEntityStore + parameters: + - description: The entity type of the store (either 'user' or 'host'). + in: path + name: entityType + required: true + schema: + $ref: '#/components/schemas/EntityType' + requestBody: + content: + application/json: + schema: + type: object + properties: + filter: + type: string + indexPattern: + $ref: '#/components/schemas/IndexPattern' + description: Schema for the engine initialization + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/EngineDescriptor' + description: Successful response + summary: Initialize the Entity Store + tags: + - Security Solution Entity Analytics API + '/api/entity_store/engines/{entityType}/start': + post: + operationId: StartEntityStore + parameters: + - description: The entity type of the store (either 'user' or 'host'). + in: path + name: entityType + required: true + schema: + $ref: '#/components/schemas/EntityType' + responses: + '200': + content: + application/json: + schema: + type: object + properties: + started: + type: boolean + description: Successful response + summary: Start the Entity Store engine + tags: + - Security Solution Entity Analytics API + '/api/entity_store/engines/{entityType}/stats': + post: + operationId: GetEntityStoreStats + parameters: + - description: The entity type of the store (either 'user' or 'host'). + in: path + name: entityType + required: true + schema: + $ref: '#/components/schemas/EntityType' + responses: + '200': + content: + application/json: + schema: + type: object + properties: + indexPattern: + $ref: '#/components/schemas/IndexPattern' + indices: + items: + type: object + type: array + status: + $ref: '#/components/schemas/EngineStatus' + transforms: + items: + type: object + type: array + type: + $ref: '#/components/schemas/EntityType' + description: Successful response + summary: Get the Entity Store engine stats + tags: + - Security Solution Entity Analytics API + '/api/entity_store/engines/{entityType}/stop': + post: + operationId: StopEntityStore + parameters: + - description: The entity type of the store (either 'user' or 'host'). + in: path + name: entityType + required: true + schema: + $ref: '#/components/schemas/EntityType' + responses: + '200': + content: + application/json: + schema: + type: object + properties: + stopped: + type: boolean + description: Successful response + summary: Stop the Entity Store engine + tags: + - Security Solution Entity Analytics API /api/risk_score/engine/schedule_now: post: operationId: ScheduleRiskEngineNow @@ -351,11 +532,35 @@ components: $ref: '#/components/schemas/AssetCriticalityLevel' required: - criticality_level + EngineDescriptor: + type: object + properties: + filter: + type: string + indexPattern: + $ref: '#/components/schemas/IndexPattern' + status: + $ref: '#/components/schemas/EngineStatus' + type: + $ref: '#/components/schemas/EntityType' + EngineStatus: + enum: + - installing + - started + - stopped + type: string + EntityType: + enum: + - user + - host + type: string IdField: enum: - host.name - user.name type: string + IndexPattern: + type: string RiskEngineScheduleNowErrorResponse: type: object properties: diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml index 845b4ced91545..754c8f94d1c63 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml @@ -256,6 +256,187 @@ paths: summary: List Asset Criticality Records tags: - Security Solution Entity Analytics API + /api/entity_store/engines: + get: + operationId: ListEntityStoreEngines + responses: + '200': + content: + application/json: + schema: + type: object + properties: + count: + type: integer + engines: + items: + $ref: '#/components/schemas/EngineDescriptor' + type: array + description: Successful response + summary: List the Entity Store engines + tags: + - Security Solution Entity Analytics API + '/api/entity_store/engines/{entityType}': + delete: + operationId: DeleteEntityStore + parameters: + - description: The entity type of the store (either 'user' or 'host'). + in: path + name: entityType + required: true + schema: + $ref: '#/components/schemas/EntityType' + - description: Control flag to also delete the entity data. + in: query + name: data + required: false + schema: + type: boolean + responses: + '200': + content: + application/json: + schema: + type: object + properties: + deleted: + type: boolean + description: Successful response + summary: Delete the Entity Store engine + tags: + - Security Solution Entity Analytics API + get: + operationId: GetEntityStoreEngine + parameters: + - description: The entity type of the store (either 'user' or 'host'). + in: path + name: entityType + required: true + schema: + $ref: '#/components/schemas/EntityType' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/EngineDescriptor' + description: Successful response + summary: Get the Entity Store engine + tags: + - Security Solution Entity Analytics API + '/api/entity_store/engines/{entityType}/init': + post: + operationId: InitEntityStore + parameters: + - description: The entity type of the store (either 'user' or 'host'). + in: path + name: entityType + required: true + schema: + $ref: '#/components/schemas/EntityType' + requestBody: + content: + application/json: + schema: + type: object + properties: + filter: + type: string + indexPattern: + $ref: '#/components/schemas/IndexPattern' + description: Schema for the engine initialization + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/EngineDescriptor' + description: Successful response + summary: Initialize the Entity Store + tags: + - Security Solution Entity Analytics API + '/api/entity_store/engines/{entityType}/start': + post: + operationId: StartEntityStore + parameters: + - description: The entity type of the store (either 'user' or 'host'). + in: path + name: entityType + required: true + schema: + $ref: '#/components/schemas/EntityType' + responses: + '200': + content: + application/json: + schema: + type: object + properties: + started: + type: boolean + description: Successful response + summary: Start the Entity Store engine + tags: + - Security Solution Entity Analytics API + '/api/entity_store/engines/{entityType}/stats': + post: + operationId: GetEntityStoreStats + parameters: + - description: The entity type of the store (either 'user' or 'host'). + in: path + name: entityType + required: true + schema: + $ref: '#/components/schemas/EntityType' + responses: + '200': + content: + application/json: + schema: + type: object + properties: + indexPattern: + $ref: '#/components/schemas/IndexPattern' + indices: + items: + type: object + type: array + status: + $ref: '#/components/schemas/EngineStatus' + transforms: + items: + type: object + type: array + type: + $ref: '#/components/schemas/EntityType' + description: Successful response + summary: Get the Entity Store engine stats + tags: + - Security Solution Entity Analytics API + '/api/entity_store/engines/{entityType}/stop': + post: + operationId: StopEntityStore + parameters: + - description: The entity type of the store (either 'user' or 'host'). + in: path + name: entityType + required: true + schema: + $ref: '#/components/schemas/EntityType' + responses: + '200': + content: + application/json: + schema: + type: object + properties: + stopped: + type: boolean + description: Successful response + summary: Stop the Entity Store engine + tags: + - Security Solution Entity Analytics API /api/risk_score/engine/schedule_now: post: operationId: ScheduleRiskEngineNow @@ -351,11 +532,35 @@ components: $ref: '#/components/schemas/AssetCriticalityLevel' required: - criticality_level + EngineDescriptor: + type: object + properties: + filter: + type: string + indexPattern: + $ref: '#/components/schemas/IndexPattern' + status: + $ref: '#/components/schemas/EngineStatus' + type: + $ref: '#/components/schemas/EntityType' + EngineStatus: + enum: + - installing + - started + - stopped + type: string + EntityType: + enum: + - user + - host + type: string IdField: enum: - host.name - user.name type: string + IndexPattern: + type: string RiskEngineScheduleNowErrorResponse: type: object properties: diff --git a/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/upgrade_review_algorithms.md b/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/upgrade_review_algorithms.md index 26b01da200903..e65d366e0f44c 100644 --- a/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/upgrade_review_algorithms.md +++ b/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/upgrade_review_algorithms.md @@ -29,10 +29,11 @@ Status: `in progress`. - [**Scenario: `ABC` - Rule field is an array of scalar values**](#scenario-abc---rule-field-is-an-array-of-scalar-values) - [**Scenario: `ABC` - Rule field is a solvable `data_source` object**](#scenario-abc---rule-field-is-a-solvable-data_source-object) - [**Scenario: `ABC` - Rule field is a non-solvable `data_source` object**](#scenario-abc---rule-field-is-a-non-solvable-data_source-object) + - [**Scenario: `ABC` - Rule field is a `kql_query`, `eql_query`, or `esql_query` object**](#scenario-abc---rule-field-is-a-kql_query-eql_query-or-esql_query-object) - [Rule field has an update and a custom value that are the same and the rule base version doesn't exist - `-AA`](#rule-field-has-an-update-and-a-custom-value-that-are-the-same-and-the-rule-base-version-doesnt-exist----aa) - [**Scenario: `-AA` - Rule field is any type**](#scenario--aa---rule-field-is-any-type) - [Rule field has an update and a custom value that are NOT the same and the rule base version doesn't exist - `-AB`](#rule-field-has-an-update-and-a-custom-value-that-are-not-the-same-and-the-rule-base-version-doesnt-exist----ab) - - [**Scenario: `-AB` - Rule field is a number or single line string**](#scenario--ab---rule-field-is-a-number-or-single-line-string) + - [**Scenario: `-AB` - Rule field is a number, single line string, multi line string, `data_source` object, `kql_query` object, `eql_query` object, or `esql_query` object**](#scenario--ab---rule-field-is-a-number-single-line-string-multi-line-string-data_source-object-kql_query-object-eql_query-object-or-esql_query-object) - [**Scenario: `-AB` - Rule field is an array of scalar values**](#scenario--ab---rule-field-is-an-array-of-scalar-values) - [**Scenario: `-AB` - Rule field is a solvable `data_source` object**](#scenario--ab---rule-field-is-a-solvable-data_source-object) - [**Scenario: `-AB` - Rule field is a non-solvable `data_source` object**](#scenario--ab---rule-field-is-a-non-solvable-data_source-object) @@ -58,6 +59,9 @@ Status: `in progress`. - **Grouped fields** - `data_source`: an object that contains a `type` field with a value of `data_view_id` or `index_patterns` and another field that's either `data_view_id` of type string OR `index_patterns` of type string array + - `kql_query`: an object that contains a `type` field with a value of `inline_query` or `saved_query` and other fields based on whichever type is defined. If it's `inline_query`, the object contains a `query` string field, a `language` field that's either `kuery` or `lucene`, and a `filters` field which is an array of kibana filters. If the type field is `saved_query`, the object only contains a `saved_query_id` string field. + - `eql_query`: an object that contains a `query` string field, a `language` field that always has the value: `eql`, and a `filters` field that contains an array of kibana filters. + - `esql_query`: an object that contains a `query` string field and a `language` field that always has the value: `esql`. ### Assumptions @@ -70,7 +74,7 @@ Status: `in progress`. #### **Scenario: `AAA` - Rule field is any type** -**Automation**: 6 integration tests with mock rules + a set of unit tests for each algorithm +**Automation**: 10 integration tests with mock rules + a set of unit tests for each algorithm ```Gherkin Given field is not customized by the user (current version == base version) @@ -80,20 +84,24 @@ And field should not be returned from the `upgrade/_review` API end And field should not be shown in the upgrade preview UI Examples: -| algorithm | field_name | base_version | current_version | target_version | merged_version | -| single line string | name | "A" | "A" | "A" | "A" | -| multi line string | description | "My description.\nThis is a second line." | "My description.\nThis is a second line." | "My description.\nThis is a second line." | "My description.\nThis is a second line." | -| number | risk_score | 1 | 1 | 1 | 1 | -| array of scalars | tags | ["one", "two", "three"] | ["one", "three", "two"] | ["three", "one", "two"] | ["one", "three", "two"] | -| data_source | data_source | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | -| data_source | data_source | {type: "data_view", "data_view_id": "A"} | {type: "data_view", "data_view_id": "A"} | {type: "data_view", "data_view_id": "A"} | {type: "data_view", "data_view_id": "A"} | +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| single line string | name | "A" | "A" | "A" | "A" | +| multi line string | description | "My description.\nThis is a second line." | "My description.\nThis is a second line." | "My description.\nThis is a second line." | "My description.\nThis is a second line." | +| number | risk_score | 1 | 1 | 1 | 1 | +| array of scalars | tags | ["one", "two", "three"] | ["one", "three", "two"] | ["three", "one", "two"] | ["one", "three", "two"] | +| data_source | data_source | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | +| data_source | data_source | {type: "data_view", "data_view_id": "A"} | {type: "data_view", "data_view_id": "A"} | {type: "data_view", "data_view_id": "A"} | {type: "data_view", "data_view_id": "A"} | +| kql_query | kql_query | {type: "inline_query", query: "query string = true", language: "kuery", filters: []} | {type: "inline_query", query: "query string = true", language: "kuery", filters: []} | {type: "inline_query", query: "query string = true", language: "kuery", filters: []} | {type: "inline_query", query: "query string = true", language: "kuery", filters: []} | +| kql_query | kql_query | {type: "saved_query", saved_query_id: 'saved-query-id'} | {type: "saved_query", saved_query_id: 'saved-query-id'} | {type: "saved_query", saved_query_id: 'saved-query-id'} | {type: "saved_query", saved_query_id: 'saved-query-id'} | +| eql_query | eql_query | {query: "query where true", language: "eql", filters: []} | {query: "query where true", language: "eql", filters: []} | {query: "query where true", language: "eql", filters: []} | {query: "query where true", language: "eql", filters: []} | +| esql_query | esql_query | {query: "FROM query WHERE true", language: "esql"} | {query: "FROM query WHERE true", language: "esql"} | {query: "FROM query WHERE true", language: "esql"} | {query: "FROM query WHERE true", language: "esql"} | ``` ### Rule field doesn't have an update but has a custom value - `ABA` #### **Scenario: `ABA` - Rule field is any type** -**Automation**: 6 integration tests with mock rules + a set of unit tests for each algorithm +**Automation**: 10 integration tests with mock rules + a set of unit tests for each algorithm ```Gherkin Given field is customized by the user (current version != base version) @@ -103,20 +111,24 @@ And field should be returned from the `upgrade/_review` API endpoin And field should be shown in the upgrade preview UI Examples: -| algorithm | field_name | base_version | current_version | target_version | merged_version | -| single line string | name | "A" | "B" | "A" | "B" | -| multi line string | description | "My description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | "My description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | -| number | risk_score | 1 | 2 | 1 | 2 | -| array of scalars | tags | ["one", "two", "three"] | ["one", "two", "four"] | ["one", "two", "three"] | ["one", "two", "four"] | -| data_source | data_source | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "data_view", "data_view_id": "A"} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "data_view", "data_view_id": "A"} | -| data_source | data_source | {type: "data_view", "data_view_id": "A"} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "data_view", "data_view_id": "A"} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| single line string | name | "A" | "B" | "A" | "B" | +| multi line string | description | "My description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | "My description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | +| number | risk_score | 1 | 2 | 1 | 2 | +| array of scalars | tags | ["one", "two", "three"] | ["one", "two", "four"] | ["one", "two", "three"] | ["one", "two", "four"] | +| data_source | data_source | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "data_view", "data_view_id": "A"} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "data_view", "data_view_id": "A"} | +| data_source | data_source | {type: "data_view", "data_view_id": "A"} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "data_view", "data_view_id": "A"} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | +| kql_query | kql_query | {type: "inline_query", query: "query string = true", language: "kuery", filters: []} | {type: "inline_query", query: "query string = false", language: "kuery", filters: []} | {type: "inline_query", query: "query string = true", language: "kuery", filters: []} | {type: "inline_query", query: "query string = false", language: "kuery", filters: []} | +| kql_query | kql_query | {type: "saved_query", saved_query_id: 'saved-query-id'} | {type: "saved_query", saved_query_id: 'new-saved-query-id'} | {type: "saved_query", saved_query_id: 'saved-query-id'} | {type: "saved_query", saved_query_id: 'new-saved-query-id'} | +| eql_query | eql_query | {query: "query where true", language: "eql", filters: []} | {query: "query where false", language: "eql", filters: []} | {query: "query where true", language: "eql", filters: []} | {query: "query where false", language: "eql", filters: []} | +| esql_query | esql_query | {query: "FROM query WHERE true", language: "esql"} | {query: "FROM query WHERE false", language: "esql"} | {query: "FROM query WHERE true", language: "esql"} | {query: "FROM query WHERE false", language: "esql"} | ``` ### Rule field has an update and doesn't have a custom value - `AAB` #### **Scenario: `AAB` - Rule field is any type** -**Automation**: 6 integration tests with mock rules + a set of unit tests for each algorithm +**Automation**: 10 integration tests with mock rules + a set of unit tests for each algorithm ```Gherkin Given field is not customized by the user (current version == base version) @@ -126,20 +138,24 @@ And field should be returned from the `upgrade/_review` API endpoin And field should be shown in the upgrade preview UI Examples: -| algorithm | field_name | base_version | current_version | target_version | merged_version | -| single line string | name | "A" | "A" | "B" | "B" | -| multi line string | description | "My description.\nThis is a second line." | "My description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | -| number | risk_score | 1 | 1 | 2 | 2 | -| array of scalars | tags | ["one", "two", "three"] | ["one", "two", "three"] | ["one", "two", "four"] | ["one", "two", "four"] | -| data_source | data_source | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "data_view", "data_view_id": "A"} | {type: "data_view", "data_view_id": "A"} | -| data_source | data_source | {type: "data_view", "data_view_id": "A"} | {type: "data_view", "data_view_id": "A"} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| single line string | name | "A" | "A" | "B" | "B" | +| multi line string | description | "My description.\nThis is a second line." | "My description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | +| number | risk_score | 1 | 1 | 2 | 2 | +| array of scalars | tags | ["one", "two", "three"] | ["one", "two", "three"] | ["one", "two", "four"] | ["one", "two", "four"] | +| data_source | data_source | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "data_view", "data_view_id": "A"} | {type: "data_view", "data_view_id": "A"} | +| data_source | data_source | {type: "data_view", "data_view_id": "A"} | {type: "data_view", "data_view_id": "A"} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | +| kql_query | kql_query | {type: "inline_query", query: "query string = true", language: "kuery", filters: []} | {type: "inline_query", query: "query string = true", language: "kuery", filters: []} | {type: "saved_query", saved_query_id: 'saved-query-id'} | {type: "saved_query", saved_query_id: 'saved-query-id'} | +| kql_query | kql_query | {type: "saved_query", saved_query_id: 'saved-query-id'} | {type: "saved_query", saved_query_id: 'saved-query-id'} | {type: "inline_query", query: "query string = true", language: "kuery", filters: []} | {type: "inline_query", query: "query string = true", language: "kuery", filters: []} | +| eql_query | eql_query | {query: "query where true", language: "eql", filters: []} | {query: "query where true", language: "eql", filters: []} | {query: "query where true", language: "eql", filters: [{ field: 'some query' }]} | {query: "query where true", language: "eql", filters: [{ field: 'some query' }]} | +| esql_query | esql_query | {query: "FROM query WHERE true", language: "esql"} | {query: "FROM query WHERE true", language: "esql"} | {query: "FROM query WHERE false", language: "esql"} | {query: "FROM query WHERE false", language: "esql"} | ``` ### Rule field has an update and a custom value that are the same - `ABB` #### **Scenario: `ABB` - Rule field is any type** -**Automation**: 6 integration tests with mock rules + a set of unit tests for each algorithm +**Automation**: 10 integration tests with mock rules + a set of unit tests for each algorithm ```Gherkin Given field is customized by the user (current version != base version) @@ -150,13 +166,17 @@ And field should be returned from the `upgrade/_review` API endpoin And field should be shown in the upgrade preview UI Examples: -| algorithm | field_name | base_version | current_version | target_version | merged_version | -| single line string | name | "A" | "B" | "B" | "B" | -| multi line string | description | "My description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | -| number | risk_score | 1 | 2 | 2 | 2 | -| array of scalars | tags | ["one", "two", "three"] | ["one", "two", "four"] | ["one", "two", "four"] | ["one", "two", "four"] | -| data_source | data_source | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "data_view", "data_view_id": "A"} | {type: "data_view", "data_view_id": "A"} | {type: "data_view", "data_view_id": "A"} | -| data_source | data_source | {type: "data_view", "data_view_id": "A"} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| single line string | name | "A" | "B" | "B" | "B" | +| multi line string | description | "My description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | +| number | risk_score | 1 | 2 | 2 | 2 | +| array of scalars | tags | ["one", "two", "three"] | ["one", "two", "four"] | ["one", "two", "four"] | ["one", "two", "four"] | +| data_source | data_source | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "data_view", "data_view_id": "A"} | {type: "data_view", "data_view_id": "A"} | {type: "data_view", "data_view_id": "A"} | +| data_source | data_source | {type: "data_view", "data_view_id": "A"} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | +| kql_query | kql_query | {type: "inline_query", query: "query string = true", language: "kuery", filters: []} | {type: "inline_query", query: "query string = true", language: "lucene", filters: []} | {type: "inline_query", query: "query string = true", language: "lucene", filters: []} | {type: "inline_query", query: "query string = true", language: "lucene", filters: []} | +| kql_query | kql_query | {type: "saved_query", saved_query_id: 'saved-query-id'} | {type: "saved_query", saved_query_id: 'new-saved-query-id'} | {type: "saved_query", saved_query_id: 'new-saved-query-id'} | {type: "saved_query", saved_query_id: 'new-saved-query-id'} | +| eql_query | eql_query | {query: "query where true", language: "eql", filters: []} | {query: "query where false", language: "eql", filters: []} | {query: "query where false", language: "eql", filters: []} | {query: "query where false", language: "eql", filters: []} | +| esql_query | esql_query | {query: "FROM query WHERE true", language: "esql"} | {query: "FROM query WHERE false", language: "esql"} | {query: "FROM query WHERE false", language: "esql"} | {query: "FROM query WHERE false", language: "esql"} | ``` ### Rule field has an update and a custom value that are NOT the same - `ABC` @@ -284,11 +304,31 @@ Examples: | data_source | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "data_view", "data_view_id": "A"} | {type: "index_patterns", "index_patterns": ["one", "two", "five"]} | {type: "data_view", "data_view_id": "A"} | ``` +#### **Scenario: `ABC` - Rule field is a `kql_query`, `eql_query`, or `esql_query` object** + +**Automation**: 4 integration tests with mock rules + a set of unit tests for the algorithms + +```Gherkin +Given field is customized by the user (current version != base version) +And field is updated by Elastic in this upgrade (target version != base version) +And customized field is different than the Elastic update in this upgrade (current version != target version) +Then for field the diff algorithm should output the current version as the merged one with a non-solvable conflict +And field should be returned from the `upgrade/_review` API endpoint +And field should be shown in the upgrade preview UI + +Examples: +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| kql_query | kql_query | {type: "inline_query", query: "query string = true", language: "kuery", filters: []} | {type: "saved_query", saved_query_id: 'saved-query-id'} | {type: "inline_query", query: "query string = false", language: "kuery", filters: []} | {type: "saved_query", saved_query_id: 'saved-query-id'} | +| kql_query | kql_query | {type: "saved_query", saved_query_id: 'saved-query-id'} | {type: "inline_query", query: "query string = true", language: "kuery", filters: []} | {type: "saved_query", saved_query_id: 'new-saved-query-id'} | {type: "inline_query", query: "query string = true", language: "kuery", filters: []} | +| eql_query | eql_query | {query: "query where true", language: "eql", filters: []} | {query: "query where true", language: "eql", filters: [{ field: 'some query' }]} | {query: "query where false", language: "eql", filters: []} | {query: "query where true", language: "eql", filters: [{ field: 'some query' }]} | +| esql_query | esql_query | {query: "FROM query WHERE true", language: "esql"} | {query: "FROM query WHERE false", language: "esql"} | {query: "FROM different query WHERE true", language: "esql"} | {query: "FROM query WHERE false", language: "esql"} | +``` + ### Rule field has an update and a custom value that are the same and the rule base version doesn't exist - `-AA` #### **Scenario: `-AA` - Rule field is any type** -**Automation**: 5 integration tests with mock rules + a set of unit tests for each algorithm +**Automation**: 9 integration tests with mock rules + a set of unit tests for each algorithm ```Gherkin Given at least 1 installed prebuilt rule has a new version available @@ -299,20 +339,24 @@ And field should not be returned from the `upgrade/_review` API end And field should not be shown in the upgrade preview UI Examples: -| algorithm | field_name | base_version | current_version | target_version | merged_version | -| single line string | name | N/A | "A" | "A" | "A" | -| multi line string | description | N/A | "My description.\nThis is a second line." | "My description.\nThis is a second line." | "My description.\nThis is a second line." | -| number | risk_score | N/A | 1 | 1 | 1 | -| array of scalars | tags | N/A | ["one", "three", "two"] | ["three", "one", "two"] | ["one", "three", "two"] | -| data_source | data_source | N/A | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | -| data_source | data_source | N/A | {type: "data_view", "data_view_id": "A"} | {type: "data_view", "data_view_id": "A"} | {type: "data_view", "data_view_id": "A"} | +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| single line string | name | N/A | "A" | "A" | "A" | +| multi line string | description | N/A | "My description.\nThis is a second line." | "My description.\nThis is a second line." | "My description.\nThis is a second line." | +| number | risk_score | N/A | 1 | 1 | 1 | +| array of scalars | tags | N/A | ["one", "three", "two"] | ["three", "one", "two"] | ["one", "three", "two"] | +| data_source | data_source | N/A | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | {type: "index_patterns", "index_patterns": ["one", "two", "three"]} | +| data_source | data_source | N/A | {type: "data_view", "data_view_id": "A"} | {type: "data_view", "data_view_id": "A"} | {type: "data_view", "data_view_id": "A"} | +| kql_query | kql_query | N/A | {type: "inline_query", query: "query string = true", language: "kuery", filters: []} | {type: "inline_query", query: "query string = true", language: "kuery", filters: []} | {type: "inline_query", query: "query string = true", language: "kuery", filters: []} | +| kql_query | kql_query | N/A | {type: "saved_query", saved_query_id: 'saved-query-id'} | {type: "saved_query", saved_query_id: 'saved-query-id'} | {type: "saved_query", saved_query_id: 'saved-query-id'} | +| eql_query | eql_query | N/A | {query: "query where true", language: "eql", filters: []} | {query: "query where true", language: "eql", filters: []} | {query: "query where true", language: "eql", filters: []} | +| esql_query | esql_query | N/A | {query: "FROM query WHERE true", language: "esql"} | {query: "FROM query WHERE true", language: "esql"} | {query: "FROM query WHERE true", language: "esql"} | ``` ### Rule field has an update and a custom value that are NOT the same and the rule base version doesn't exist - `-AB` -#### **Scenario: `-AB` - Rule field is a number or single line string** +#### **Scenario: `-AB` - Rule field is a number, single line string, multi line string, `data_source` object, `kql_query` object, `eql_query` object, or `esql_query` object** -**Automation**: 4 integration tests with mock rules + a set of unit tests for the algorithms +**Automation**: 8 integration tests with mock rules + a set of unit tests for the algorithms ```Gherkin Given at least 1 installed prebuilt rule has a new version available @@ -323,11 +367,15 @@ And field should be returned from the `upgrade/_review` API endpoin And field should be shown in the upgrade preview UI Examples: -| algorithm | field_name | base_version | current_version | target_version | merged_version | -| single line string | name | N/A | "B" | "C" | "C" | -| multi line string | description | N/A | "My description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | -| number | risk_score | N/A | 2 | 3 | 3 | -| data_source | data_source | N/A | {type: "data_view", "data_view_id": "A"} | {type: "data_view", "data_view_id": "B"} | {type: "data_view", "data_view_id": "B"} | +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| single line string | name | N/A | "B" | "C" | "C" | +| multi line string | description | N/A | "My description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | +| number | risk_score | N/A | 2 | 3 | 3 | +| data_source | data_source | N/A | {type: "data_view", "data_view_id": "A"} | {type: "data_view", "data_view_id": "B"} | {type: "data_view", "data_view_id": "B"} | +| kql_query | kql_query | N/A | {type: "inline_query", query: "query string = true", language: "kuery", filters: []} | {type: "inline_query", query: "query string = false", language: "kuery", filters: []} | {type: "inline_query", query: "query string = false", language: "kuery", filters: []} | +| kql_query | kql_query | N/A | {type: "saved_query", saved_query_id: 'saved-query-id'} | {type: "saved_query", saved_query_id: 'new-saved-query-id'} | {type: "saved_query", saved_query_id: 'new-saved-query-id'} | +| eql_query | eql_query | N/A | {query: "query where true", language: "eql", filters: []} | {query: "query where false", language: "eql", filters: []} | {query: "query where false", language: "eql", filters: []} | +| esql_query | esql_query | N/A | {query: "FROM query WHERE true", language: "esql"} | {query: "FROM query WHERE false", language: "esql"} | {query: "FROM query WHERE false", language: "esql"} | ``` #### **Scenario: `-AB` - Rule field is an array of scalar values** diff --git a/x-pack/plugins/security_solution/kibana.jsonc b/x-pack/plugins/security_solution/kibana.jsonc index f682ca478a17f..e5840a6662e79 100644 --- a/x-pack/plugins/security_solution/kibana.jsonc +++ b/x-pack/plugins/security_solution/kibana.jsonc @@ -53,7 +53,8 @@ "notifications", "savedSearch", "unifiedDocViewer", - "charts" + "charts", + "entityManager" ], "optionalPlugins": [ "cloudExperiments", @@ -87,4 +88,4 @@ "common" ] } -} +} \ No newline at end of file diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts index b39cba0cf4952..a5e0c8c60b1fc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -36,6 +36,7 @@ import { getEndpointAuthzInitialStateMock } from '../../../../../common/endpoint import type { EndpointAuthz } from '../../../../../common/endpoint/types/authz'; import { riskEngineDataClientMock } from '../../../entity_analytics/risk_engine/risk_engine_data_client.mock'; import { riskScoreDataClientMock } from '../../../entity_analytics/risk_score/risk_score_data_client.mock'; +import { entityStoreDataClientMock } from '../../../entity_analytics/entity_store/entity_store_data_client.mock'; import { assetCriticalityDataClientMock } from '../../../entity_analytics/asset_criticality/asset_criticality_data_client.mock'; import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks'; import { detectionRulesClientMock } from '../../rule_management/logic/detection_rules_client/__mocks__/detection_rules_client'; @@ -72,6 +73,7 @@ export const createMockClients = () => { riskEngineDataClient: riskEngineDataClientMock.create(), riskScoreDataClient: riskScoreDataClientMock.create(), assetCriticalityDataClient: assetCriticalityDataClientMock.create(), + entityStoreDataClient: entityStoreDataClientMock.create(), internalFleetServices: { packages: packageServiceMock.createClient(), @@ -159,6 +161,7 @@ const createSecuritySolutionRequestContextMock = ( getRiskScoreDataClient: jest.fn(() => clients.riskScoreDataClient), getAssetCriticalityDataClient: jest.fn(() => clients.assetCriticalityDataClient), getAuditLogger: jest.fn(() => mockAuditLogger), + getEntityStoreDataClient: jest.fn(() => clients.entityStoreDataClient), }; }; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/constants.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/constants.ts new file mode 100644 index 0000000000000..ce5a61fa7e6c9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/constants.ts @@ -0,0 +1,21 @@ +/* + * 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 { EngineStatus } from '../../../../common/api/entity_analytics/entity_store/common.gen'; +import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; + +/** + * Default index pattern for entity store + * This is the same as the default index pattern for the SIEM app but might diverge in the future + */ +export const ENTITY_STORE_DEFAULT_SOURCE_INDICES = DEFAULT_INDEX_PATTERN; + +export const ENGINE_STATUS: Record, EngineStatus> = { + INSTALLING: 'installing', + STARTED: 'started', + STOPPED: 'stopped', +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/definition.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/definition.ts new file mode 100644 index 0000000000000..32859b9841e7f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/definition.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { entityDefinitionSchema, type EntityDefinition } from '@kbn/entities-schema'; +import { ENTITY_STORE_DEFAULT_SOURCE_INDICES } from './constants'; + +export const HOST_ENTITY_DEFINITION: EntityDefinition = entityDefinitionSchema.parse({ + id: 'ea_host_entity_store', + name: 'EA Host Store', + type: 'host', + indexPatterns: ENTITY_STORE_DEFAULT_SOURCE_INDICES, + identityFields: ['host.name'], + displayNameTemplate: '{{host.name}}', + metadata: [ + 'host.domain', + 'host.hostname', + 'host.id', + 'host.ip', + 'host.mac', + 'host.name', + 'host.type', + 'host.architecture', + ], + history: { + timestampField: '@timestamp', + interval: '1m', + }, + version: '1.0.0', +}); + +export const USER_ENTITY_DEFINITION: EntityDefinition = entityDefinitionSchema.parse({ + id: 'ea_user_entity_store', + name: 'EA User Store', + type: 'user', + indexPatterns: ENTITY_STORE_DEFAULT_SOURCE_INDICES, + identityFields: ['user.name'], + displayNameTemplate: '{{user.name}}', + metadata: [ + 'user.domain', + 'user.email', + 'user.full_name', + 'user.hash', + 'user.id', + 'user.name', + 'user.roles', + ], + history: { + timestampField: '@timestamp', + interval: '1m', + }, + version: '1.0.0', +}); diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.mock.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.mock.ts new file mode 100644 index 0000000000000..095565343e130 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.mock.ts @@ -0,0 +1,20 @@ +/* + * 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 { EntityStoreDataClient } from './entity_store_data_client'; + +const createEntityStoreDataClientMock = () => + ({ + init: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + get: jest.fn(), + list: jest.fn(), + delete: jest.fn(), + } as unknown as jest.Mocked); + +export const entityStoreDataClientMock = { create: createEntityStoreDataClientMock }; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts new file mode 100644 index 0000000000000..cb4d59139a25f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger, ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; +import type { EntityClient } from '@kbn/entityManager-plugin/server/lib/entity_client'; + +import type { + InitEntityStoreRequestBody, + InitEntityStoreResponse, +} from '../../../../common/api/entity_analytics/entity_store/engine/init.gen'; +import type { + EngineDescriptor, + EntityType, +} from '../../../../common/api/entity_analytics/entity_store/common.gen'; +import { entityEngineDescriptorTypeName } from './saved_object'; +import { EngineDescriptorClient } from './saved_object/engine_descriptor'; +import { getEntityDefinition } from './utils/utils'; +import { ENGINE_STATUS } from './constants'; + +interface EntityStoreClientOpts { + logger: Logger; + esClient: ElasticsearchClient; + entityClient: EntityClient; + namespace: string; + soClient: SavedObjectsClientContract; +} + +export class EntityStoreDataClient { + private engineClient: EngineDescriptorClient; + constructor(private readonly options: EntityStoreClientOpts) { + this.engineClient = new EngineDescriptorClient(options.soClient); + } + + public async init( + entityType: EntityType, + { indexPattern = '', filter = '' }: InitEntityStoreRequestBody + ): Promise { + const definition = getEntityDefinition(entityType); + + this.options.logger.info(`Initializing entity store for ${entityType}`); + + const descriptor = await this.engineClient.init(entityType, definition, filter); + await this.options.entityClient.createEntityDefinition({ + definition: { + ...definition, + filter, + indexPatterns: indexPattern + ? [...definition.indexPatterns, ...indexPattern.split(',')] + : definition.indexPatterns, + }, + }); + const updated = await this.engineClient.update(definition.id, ENGINE_STATUS.STARTED); + + return { ...descriptor, ...updated }; + } + + public async start(entityType: EntityType) { + const definition = getEntityDefinition(entityType); + + const descriptor = await this.engineClient.get(entityType); + + if (descriptor.status !== ENGINE_STATUS.STOPPED) { + throw new Error( + `Cannot start Entity engine for ${entityType} when current status is: ${descriptor.status}` + ); + } + + this.options.logger.info(`Starting entity store for ${entityType}`); + await this.options.entityClient.startEntityDefinition(definition); + + return this.engineClient.update(definition.id, ENGINE_STATUS.STARTED); + } + + public async stop(entityType: EntityType) { + const definition = getEntityDefinition(entityType); + + const descriptor = await this.engineClient.get(entityType); + + if (descriptor.status !== ENGINE_STATUS.STARTED) { + throw new Error( + `Cannot stop Entity engine for ${entityType} when current status is: ${descriptor.status}` + ); + } + + this.options.logger.info(`Stopping entity store for ${entityType}`); + await this.options.entityClient.stopEntityDefinition(definition); + + return this.engineClient.update(definition.id, ENGINE_STATUS.STOPPED); + } + + public async get(entityType: EntityType) { + return this.engineClient.get(entityType); + } + + public async list() { + return this.options.soClient + .find({ + type: entityEngineDescriptorTypeName, + }) + .then(({ saved_objects: engines }) => ({ + engines: engines.map((engine) => engine.attributes), + count: engines.length, + })); + } + + public async delete(entityType: EntityType, deleteData: boolean) { + const { id } = getEntityDefinition(entityType); + + this.options.logger.info(`Deleting entity store for ${entityType}`); + + await this.options.entityClient.deleteEntityDefinition({ id, deleteData }); + await this.engineClient.delete(id); + + return { deleted: true }; + } +} diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/delete.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/delete.ts new file mode 100644 index 0000000000000..44352cfa47c57 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/delete.ts @@ -0,0 +1,64 @@ +/* + * 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 { IKibanaResponse, Logger } from '@kbn/core/server'; +import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; + +import type { DeleteEntityStoreResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/delete.gen'; +import { + DeleteEntityStoreRequestQuery, + DeleteEntityStoreRequestParams, +} from '../../../../../common/api/entity_analytics/entity_store/engine/delete.gen'; +import { API_VERSIONS, APP_ID } from '../../../../../common/constants'; +import type { EntityAnalyticsRoutesDeps } from '../../types'; + +export const deleteEntityEngineRoute = ( + router: EntityAnalyticsRoutesDeps['router'], + logger: Logger +) => { + router.versioned + .delete({ + access: 'public', + path: '/api/entity_store/engines/{entityType}', + options: { + tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`], + }, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: { + request: { + query: buildRouteValidationWithZod(DeleteEntityStoreRequestQuery), + params: buildRouteValidationWithZod(DeleteEntityStoreRequestParams), + }, + }, + }, + + async (context, request, response): Promise> => { + const siemResponse = buildSiemResponse(response); + + try { + const secSol = await context.securitySolution; + const body = await secSol + .getEntityStoreDataClient() + .delete(request.params.entityType, !!request.query.data); + + return response.ok({ body }); + } catch (e) { + logger.error('Error in DeleteEntityStore:', e); + const error = transformError(e); + return siemResponse.error({ + statusCode: error.statusCode, + body: error.message, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/get.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/get.ts new file mode 100644 index 0000000000000..79a74303c49c2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/get.ts @@ -0,0 +1,62 @@ +/* + * 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 { IKibanaResponse, Logger } from '@kbn/core/server'; +import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; + +import type { GetEntityStoreEngineResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/get.gen'; +import { GetEntityStoreEngineRequestParams } from '../../../../../common/api/entity_analytics/entity_store/engine/get.gen'; +import { API_VERSIONS, APP_ID } from '../../../../../common/constants'; +import type { EntityAnalyticsRoutesDeps } from '../../types'; + +export const getEntityEngineRoute = ( + router: EntityAnalyticsRoutesDeps['router'], + logger: Logger +) => { + router.versioned + .get({ + access: 'public', + path: '/api/entity_store/engines/{entityType}', + options: { + tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`], + }, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: { + request: { + params: buildRouteValidationWithZod(GetEntityStoreEngineRequestParams), + }, + }, + }, + + async ( + context, + request, + response + ): Promise> => { + const siemResponse = buildSiemResponse(response); + + try { + const secSol = await context.securitySolution; + const body = await secSol.getEntityStoreDataClient().get(request.params.entityType); + + return response.ok({ body }); + } catch (e) { + logger.error('Error in GetEntityStoreEngine:', e); + const error = transformError(e); + return siemResponse.error({ + statusCode: error.statusCode, + body: error.message, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/index.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/index.ts new file mode 100644 index 0000000000000..52aa6b22c2df8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { registerEntityStoreRoutes } from './register_entity_store_routes'; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/init.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/init.ts new file mode 100644 index 0000000000000..6159cd584b06d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/init.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IKibanaResponse, Logger } from '@kbn/core/server'; +import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; + +import type { InitEntityStoreResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/init.gen'; +import { + InitEntityStoreRequestBody, + InitEntityStoreRequestParams, +} from '../../../../../common/api/entity_analytics/entity_store/engine/init.gen'; +import { API_VERSIONS, APP_ID } from '../../../../../common/constants'; +import type { EntityAnalyticsRoutesDeps } from '../../types'; + +export const initEntityEngineRoute = ( + router: EntityAnalyticsRoutesDeps['router'], + logger: Logger +) => { + router.versioned + .post({ + access: 'public', + path: '/api/entity_store/engines/{entityType}/init', + options: { + tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`], + }, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: { + request: { + params: buildRouteValidationWithZod(InitEntityStoreRequestParams), + body: buildRouteValidationWithZod(InitEntityStoreRequestBody), + }, + }, + }, + + async (context, request, response): Promise> => { + const siemResponse = buildSiemResponse(response); + + try { + const secSol = await context.securitySolution; + + const body: InitEntityStoreResponse = await secSol + .getEntityStoreDataClient() + .init(request.params.entityType, request.body); + + return response.ok({ body }); + } catch (e) { + logger.error('Error in InitEntityStore:', e); + const error = transformError(e); + return siemResponse.error({ + statusCode: error.statusCode, + body: error.message, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/list.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/list.ts new file mode 100644 index 0000000000000..53d9a8521ce00 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/list.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 type { IKibanaResponse, Logger } from '@kbn/core/server'; +import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; + +import type { ListEntityStoreEnginesResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/list.gen'; +import { API_VERSIONS, APP_ID } from '../../../../../common/constants'; + +import type { EntityAnalyticsRoutesDeps } from '../../types'; + +export const listEntityEnginesRoute = ( + router: EntityAnalyticsRoutesDeps['router'], + logger: Logger +) => { + router.versioned + .get({ + access: 'public', + path: '/api/entity_store/engines', + options: { + tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`], + }, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: {}, + }, + + async ( + context, + request, + response + ): Promise> => { + const siemResponse = buildSiemResponse(response); + + try { + const secSol = await context.securitySolution; + const body = await secSol.getEntityStoreDataClient().list(); + + return response.ok({ body }); + } catch (e) { + logger.error('Error in ListEntityStoreEngines:', e); + const error = transformError(e); + return siemResponse.error({ + statusCode: error.statusCode, + body: error.message, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/register_entity_store_routes.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/register_entity_store_routes.ts new file mode 100644 index 0000000000000..b78316b02c91e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/register_entity_store_routes.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { EntityAnalyticsRoutesDeps } from '../../types'; +import { deleteEntityEngineRoute } from './delete'; +import { getEntityEngineRoute } from './get'; +import { initEntityEngineRoute } from './init'; +import { listEntityEnginesRoute } from './list'; +import { startEntityEngineRoute } from './start'; +import { stopEntityEngineRoute } from './stop'; + +export const registerEntityStoreRoutes = ({ router, logger }: EntityAnalyticsRoutesDeps) => { + initEntityEngineRoute(router, logger); + startEntityEngineRoute(router, logger); + stopEntityEngineRoute(router, logger); + deleteEntityEngineRoute(router, logger); + getEntityEngineRoute(router, logger); + listEntityEnginesRoute(router, logger); +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/start.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/start.ts new file mode 100644 index 0000000000000..6ec6674a5473d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/start.ts @@ -0,0 +1,59 @@ +/* + * 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 { IKibanaResponse, Logger } from '@kbn/core/server'; +import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; + +import type { StartEntityStoreResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/start.gen'; +import { StartEntityStoreRequestParams } from '../../../../../common/api/entity_analytics/entity_store/engine/start.gen'; +import { API_VERSIONS, APP_ID } from '../../../../../common/constants'; +import type { EntityAnalyticsRoutesDeps } from '../../types'; +import { ENGINE_STATUS } from '../constants'; + +export const startEntityEngineRoute = ( + router: EntityAnalyticsRoutesDeps['router'], + logger: Logger +) => { + router.versioned + .post({ + access: 'public', + path: '/api/entity_store/engines/{entityType}/start', + options: { + tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`], + }, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: { + request: { + params: buildRouteValidationWithZod(StartEntityStoreRequestParams), + }, + }, + }, + + async (context, request, response): Promise> => { + const siemResponse = buildSiemResponse(response); + + try { + const secSol = await context.securitySolution; + const engine = await secSol.getEntityStoreDataClient().start(request.params.entityType); + + return response.ok({ body: { started: engine.status === ENGINE_STATUS.STARTED } }); + } catch (e) { + logger.error('Error in StartEntityStore:', e); + const error = transformError(e); + return siemResponse.error({ + statusCode: error.statusCode, + body: error.message, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/stats.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/stats.ts new file mode 100644 index 0000000000000..1d7534c17f747 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/stats.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IKibanaResponse, Logger } from '@kbn/core/server'; +import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; + +import type { GetEntityStoreStatsResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/stats.gen'; +import { GetEntityStoreStatsRequestParams } from '../../../../../common/api/entity_analytics/entity_store/engine/stats.gen'; +import { API_VERSIONS, APP_ID } from '../../../../../common/constants'; +import type { EntityAnalyticsRoutesDeps } from '../../types'; + +export const getEntityEngineStatsRoute = ( + router: EntityAnalyticsRoutesDeps['router'], + logger: Logger +) => { + router.versioned + .post({ + access: 'public', + path: '/api/entity_store/engines/{entityType}/stats', + options: { + tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`], + }, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: { + request: { + params: buildRouteValidationWithZod(GetEntityStoreStatsRequestParams), + }, + }, + }, + + async (context, request, response): Promise> => { + const siemResponse = buildSiemResponse(response); + + try { + // TODO + throw new Error('Not implemented'); + + // return response.ok({ body }); + } catch (e) { + logger.error('Error in GetEntityStoreStats:', e); + const error = transformError(e); + return siemResponse.error({ + statusCode: error.statusCode, + body: error.message, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/stop.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/stop.ts new file mode 100644 index 0000000000000..e1ddb464d1204 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/stop.ts @@ -0,0 +1,59 @@ +/* + * 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 { IKibanaResponse, Logger } from '@kbn/core/server'; +import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; + +import type { StopEntityStoreResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/stop.gen'; +import { StopEntityStoreRequestParams } from '../../../../../common/api/entity_analytics/entity_store/engine/stop.gen'; +import { API_VERSIONS, APP_ID } from '../../../../../common/constants'; +import type { EntityAnalyticsRoutesDeps } from '../../types'; +import { ENGINE_STATUS } from '../constants'; + +export const stopEntityEngineRoute = ( + router: EntityAnalyticsRoutesDeps['router'], + logger: Logger +) => { + router.versioned + .post({ + access: 'public', + path: '/api/entity_store/engines/{entityType}/stop', + options: { + tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`], + }, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: { + request: { + params: buildRouteValidationWithZod(StopEntityStoreRequestParams), + }, + }, + }, + + async (context, request, response): Promise> => { + const siemResponse = buildSiemResponse(response); + + try { + const secSol = await context.securitySolution; + const engine = await secSol.getEntityStoreDataClient().stop(request.params.entityType); + + return response.ok({ body: { stopped: engine.status === ENGINE_STATUS.STOPPED } }); + } catch (e) { + logger.error('Error in StopEntityStore:', e); + const error = transformError(e); + return siemResponse.error({ + statusCode: error.statusCode, + body: error.message, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/saved_object/engine_descriptor.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/saved_object/engine_descriptor.ts new file mode 100644 index 0000000000000..9d6a7821a2a9b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/saved_object/engine_descriptor.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + SavedObjectsClientContract, + SavedObjectsFindResponse, +} from '@kbn/core-saved-objects-api-server'; +import type { EntityDefinition } from '@kbn/entities-schema'; +import type { + EngineDescriptor, + EngineStatus, + EntityType, +} from '../../../../../common/api/entity_analytics/entity_store/common.gen'; + +import { entityEngineDescriptorTypeName } from './engine_descriptor_type'; +import { getByEntityTypeQuery, getEntityDefinition } from '../utils/utils'; +import { ENGINE_STATUS } from '../constants'; + +export class EngineDescriptorClient { + constructor(private readonly soClient: SavedObjectsClientContract) {} + + async init(entityType: EntityType, definition: EntityDefinition, filter: string) { + const engineDescriptor = await this.find(entityType); + + if (engineDescriptor.total > 0) + throw new Error(`Entity engine for ${entityType} already exists`); + + const { attributes } = await this.soClient.create( + entityEngineDescriptorTypeName, + { + status: ENGINE_STATUS.INSTALLING, + type: entityType, + indexPattern: definition.indexPatterns.join(','), + filter, + }, + { id: definition.id } + ); + return attributes; + } + + async update(id: string, status: EngineStatus) { + const { attributes } = await this.soClient.update( + entityEngineDescriptorTypeName, + id, + { status }, + { refresh: 'wait_for' } + ); + return attributes; + } + + async find(entityType: EntityType): Promise> { + return this.soClient.find({ + type: entityEngineDescriptorTypeName, + filter: getByEntityTypeQuery(entityType), + }); + } + + async get(entityType: EntityType): Promise { + const { id } = getEntityDefinition(entityType); + + const { attributes } = await this.soClient.get( + entityEngineDescriptorTypeName, + id + ); + + return attributes; + } + + async delete(id: string) { + return this.soClient.delete(entityEngineDescriptorTypeName, id); + } +} diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/saved_object/engine_descriptor_type.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/saved_object/engine_descriptor_type.ts new file mode 100644 index 0000000000000..8513dfc018623 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/saved_object/engine_descriptor_type.ts @@ -0,0 +1,36 @@ +/* + * 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 { SECURITY_SOLUTION_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; +import type { SavedObjectsType } from '@kbn/core/server'; + +export const entityEngineDescriptorTypeName = 'entity-engine-status'; + +export const entityEngineDescriptorTypeMappings: SavedObjectsType['mappings'] = { + dynamic: false, + properties: { + indexPattern: { + type: 'keyword', + }, + filter: { + type: 'keyword', + }, + type: { + type: 'keyword', // EntityType: user | host + }, + status: { + type: 'keyword', // EngineStatus: installing | started | stopped + }, + }, +}; +export const entityEngineDescriptorType: SavedObjectsType = { + name: entityEngineDescriptorTypeName, + indexPattern: SECURITY_SOLUTION_SAVED_OBJECT_INDEX, + hidden: false, + namespaceType: 'multiple-isolated', + mappings: entityEngineDescriptorTypeMappings, +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/saved_object/index.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/saved_object/index.ts new file mode 100644 index 0000000000000..d86800da1b5be --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/saved_object/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './engine_descriptor_type'; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/utils/utils.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/utils/utils.ts new file mode 100644 index 0000000000000..864fdb2367eb5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/utils/utils.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsFindResponse } from '@kbn/core-saved-objects-api-server'; +import type { + EngineDescriptor, + EntityType, +} from '../../../../../common/api/entity_analytics/entity_store/common.gen'; +import { HOST_ENTITY_DEFINITION, USER_ENTITY_DEFINITION } from '../definition'; +import { entityEngineDescriptorTypeName } from '../saved_object'; + +export const getEntityDefinition = (entityType: EntityType) => { + if (entityType === 'host') return HOST_ENTITY_DEFINITION; + if (entityType === 'user') return USER_ENTITY_DEFINITION; + + throw new Error(`Unsupported entity type: ${entityType}`); +}; + +export const ensureEngineExists = + (entityType: EntityType) => (results: SavedObjectsFindResponse) => { + if (results.total === 0) { + throw new Error(`Entity engine for ${entityType} does not exist`); + } + return results.saved_objects[0].attributes; + }; + +export const getByEntityTypeQuery = (entityType: EntityType) => { + return `${entityEngineDescriptorTypeName}.attributes.type: ${entityType}`; +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/register_entity_analytics_routes.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/register_entity_analytics_routes.ts index 31a7ccbb6f30c..b4eb0d36e21fb 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/register_entity_analytics_routes.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/register_entity_analytics_routes.ts @@ -9,9 +9,13 @@ import { registerAssetCriticalityRoutes } from './asset_criticality/routes'; import { registerRiskScoreRoutes } from './risk_score/routes'; import { registerRiskEngineRoutes } from './risk_engine/routes'; import type { EntityAnalyticsRoutesDeps } from './types'; +import { registerEntityStoreRoutes } from './entity_store/routes'; export const registerEntityAnalyticsRoutes = (routeDeps: EntityAnalyticsRoutesDeps) => { registerAssetCriticalityRoutes(routeDeps); registerRiskScoreRoutes(routeDeps); registerRiskEngineRoutes(routeDeps); + if (routeDeps.config.experimentalFeatures.entityStoreEnabled) { + registerEntityStoreRoutes(routeDeps); + } }; diff --git a/x-pack/plugins/security_solution/server/request_context_factory.ts b/x-pack/plugins/security_solution/server/request_context_factory.ts index 4bda7e0338aa8..6316ed3622841 100644 --- a/x-pack/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/plugins/security_solution/server/request_context_factory.ts @@ -10,6 +10,7 @@ import { memoize } from 'lodash'; import type { Logger, KibanaRequest, RequestHandlerContext } from '@kbn/core/server'; import type { BuildFlavor } from '@kbn/config'; +import { EntityClient } from '@kbn/entityManager-plugin/server/lib/entity_client'; import { DEFAULT_SPACE_ID } from '../common/constants'; import { AppClientFactory } from './client'; import type { ConfigType } from './config'; @@ -31,6 +32,7 @@ import { RiskScoreDataClient } from './lib/entity_analytics/risk_score/risk_scor import { AssetCriticalityDataClient } from './lib/entity_analytics/asset_criticality'; import { createDetectionRulesClient } from './lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client'; import { buildMlAuthz } from './lib/machine_learning/authz'; +import { EntityStoreDataClient } from './lib/entity_analytics/entity_store/entity_store_data_client'; export interface IRequestContextFactory { create( @@ -190,6 +192,22 @@ export class RequestContextFactory implements IRequestContextFactory { auditLogger: getAuditLogger(), }) ), + getEntityStoreDataClient: memoize(() => { + const esClient = coreContext.elasticsearch.client.asCurrentUser; + const logger = options.logger; + const soClient = coreContext.savedObjects.client; + return new EntityStoreDataClient({ + namespace: getSpaceId(), + esClient, + logger, + soClient, + entityClient: new EntityClient({ + esClient, + soClient, + logger, + }), + }); + }), }; } } diff --git a/x-pack/plugins/security_solution/server/saved_objects.ts b/x-pack/plugins/security_solution/server/saved_objects.ts index 3659b15a04714..9412e62e6315c 100644 --- a/x-pack/plugins/security_solution/server/saved_objects.ts +++ b/x-pack/plugins/security_solution/server/saved_objects.ts @@ -15,6 +15,7 @@ import { prebuiltRuleAssetType } from './lib/detection_engine/prebuilt_rules'; import { type as signalsMigrationType } from './lib/detection_engine/migrations/saved_objects'; import { manifestType, unifiedManifestType } from './endpoint/lib/artifacts/saved_object_mappings'; import { riskEngineConfigurationType } from './lib/entity_analytics/risk_engine/saved_object'; +import { entityEngineDescriptorType } from './lib/entity_analytics/entity_store/saved_object'; const types = [ noteType, @@ -26,6 +27,7 @@ const types = [ unifiedManifestType, signalsMigrationType, riskEngineConfigurationType, + entityEngineDescriptorType, protectionUpdatesNoteType, ]; diff --git a/x-pack/plugins/security_solution/server/types.ts b/x-pack/plugins/security_solution/server/types.ts index 121eb7b1758f4..31e10b70adbcf 100644 --- a/x-pack/plugins/security_solution/server/types.ts +++ b/x-pack/plugins/security_solution/server/types.ts @@ -34,6 +34,7 @@ import type { RiskEngineDataClient } from './lib/entity_analytics/risk_engine/ri import type { RiskScoreDataClient } from './lib/entity_analytics/risk_score/risk_score_data_client'; import type { AssetCriticalityDataClient } from './lib/entity_analytics/asset_criticality'; import type { IDetectionRulesClient } from './lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client_interface'; +import type { EntityStoreDataClient } from './lib/entity_analytics/entity_store/entity_store_data_client'; export { AppClient }; export interface SecuritySolutionApiRequestHandlerContext { @@ -55,6 +56,7 @@ export interface SecuritySolutionApiRequestHandlerContext { getRiskEngineDataClient: () => RiskEngineDataClient; getRiskScoreDataClient: () => RiskScoreDataClient; getAssetCriticalityDataClient: () => AssetCriticalityDataClient; + getEntityStoreDataClient: () => EntityStoreDataClient; } export type SecuritySolutionRequestHandlerContext = CustomRequestHandlerContext<{ diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index 6ccd61fd34394..8264a50988956 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -223,5 +223,7 @@ "@kbn/cloud-security-posture", "@kbn/security-solution-distribution-bar", "@kbn/cloud-security-posture-common", + "@kbn/entityManager-plugin", + "@kbn/entities-schema", ] } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/cancellable/rule.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/cancellable/rule.ts index 41a12004e256a..8a254447b4a4e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/cancellable/rule.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/cancellable/rule.ts @@ -127,9 +127,12 @@ export default function ruleTests({ getService }: FtrProviderContext) { events.filter((event) => event?.event?.action === 'execute'); expect(events[0]?.event?.outcome).to.eql('failure'); expect(events[0]?.kibana?.alerting?.status).to.eql('error'); - expect(events[0]?.error?.message).to.eql( - 'Search has been aborted due to cancelled execution' - ); + // Timeouts will encounter one of the following two messages + const expectedMessages = [ + 'Request timed out', + 'Search has been aborted due to cancelled execution', + ]; + expect(expectedMessages.includes(events[0]?.error?.message || '')).to.be(true); // rule execution status should be in error with reason timeout const { status, body: rule } = await supertest.get( @@ -137,9 +140,12 @@ export default function ruleTests({ getService }: FtrProviderContext) { ); expect(status).to.eql(200); expect(rule.execution_status.status).to.eql('error'); - expect(rule.execution_status.error.message).to.eql( - `test.cancellableRule:${ruleId}: execution cancelled due to timeout - exceeded rule type timeout of 3s` - ); + expect( + [ + 'Request timed out', + `test.cancellableRule:${ruleId}: execution cancelled due to timeout - exceeded rule type timeout of 3s`, + ].includes(rule.execution_status.error.message) + ).to.eql(true); expect(rule.execution_status.error.reason).to.eql('timeout'); }); @@ -183,9 +189,12 @@ export default function ruleTests({ getService }: FtrProviderContext) { ); expect(status).to.eql(200); expect(rule.execution_status.status).to.eql('error'); - expect(rule.execution_status.error.message).to.eql( - `test.cancellableRule:${ruleId}: execution cancelled due to timeout - exceeded rule type timeout of 3s` - ); + expect( + [ + 'Request timed out', + `test.cancellableRule:${ruleId}: execution cancelled due to timeout - exceeded rule type timeout of 3s`, + ].includes(rule.execution_status.error.message) + ).to.eql(true); expect(rule.execution_status.error.reason).to.eql('timeout'); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/long_running/rule.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/long_running/rule.ts index 43524a57cb225..effd35d392a3f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/long_running/rule.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/builtin_alert_types/long_running/rule.ts @@ -74,9 +74,12 @@ export default function ruleTests({ getService }: FtrProviderContext) { expect(errorStatuses.length).to.be.greaterThan(0); const lastErrorStatus = errorStatuses.pop(); expect(lastErrorStatus?.status).to.eql('error'); - expect(lastErrorStatus?.error.message).to.eql( - `test.patternLongRunning.cancelAlertsOnRuleTimeout:${ruleId}: execution cancelled due to timeout - exceeded rule type timeout of 3s` - ); + expect( + [ + 'Request timed out', + `test.patternLongRunning.cancelAlertsOnRuleTimeout:${ruleId}: execution cancelled due to timeout - exceeded rule type timeout of 3s`, + ].includes(lastErrorStatus?.error.message || '') + ).to.eql(true); expect(lastErrorStatus?.error.reason).to.eql('timeout'); }); diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index cf9722e89b408..6a3d0cf8f3dce 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -37,6 +37,10 @@ import { CreateUpdateProtectionUpdatesNoteRequestBodyInput, } from '@kbn/security-solution-plugin/common/api/endpoint/protection_updates_note/protection_updates_note.gen'; import { DeleteAssetCriticalityRecordRequestQueryInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/asset_criticality/delete_asset_criticality.gen'; +import { + DeleteEntityStoreRequestQueryInput, + DeleteEntityStoreRequestParamsInput, +} from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/delete.gen'; import { DeleteNoteRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/delete_note/delete_note_route.gen'; import { DeleteRuleRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/delete_rule/delete_rule_route.gen'; import { DeleteTimelinesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/delete_timelines/delete_timelines_route.gen'; @@ -76,6 +80,8 @@ import { GetEndpointSuggestionsRequestParamsInput, GetEndpointSuggestionsRequestBodyInput, } from '@kbn/security-solution-plugin/common/api/endpoint/suggestions/get_suggestions.gen'; +import { GetEntityStoreEngineRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/get.gen'; +import { GetEntityStoreStatsRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/stats.gen'; import { GetNotesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/timeline/get_notes/get_notes_route.gen'; import { GetPolicyResponseRequestQueryInput } from '@kbn/security-solution-plugin/common/api/endpoint/policy/policy_response.gen'; import { GetProtectionUpdatesNoteRequestParamsInput } from '@kbn/security-solution-plugin/common/api/endpoint/protection_updates_note/protection_updates_note.gen'; @@ -91,6 +97,10 @@ import { GetTimelineRequestQueryInput } from '@kbn/security-solution-plugin/comm import { GetTimelinesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/timeline/get_timelines/get_timelines_route.gen'; import { ImportRulesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/import_rules/import_rules_route.gen'; import { ImportTimelinesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/import_timelines/import_timelines_route.gen'; +import { + InitEntityStoreRequestParamsInput, + InitEntityStoreRequestBodyInput, +} from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/init.gen'; import { InstallPrepackedTimelinesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/install_prepackaged_timelines/install_prepackaged_timelines_route.gen'; import { PatchRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/patch_rule/patch_rule_route.gen'; import { PatchTimelineRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/patch_timelines/patch_timeline_route.gen'; @@ -110,6 +120,8 @@ import { SearchAlertsRequestBodyInput } from '@kbn/security-solution-plugin/comm import { SetAlertAssigneesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/alert_assignees/set_alert_assignees_route.gen'; import { SetAlertsStatusRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals/set_signal_status/set_signals_status_route.gen'; import { SetAlertTagsRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/alert_tags/set_alert_tags/set_alert_tags.gen'; +import { StartEntityStoreRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/start.gen'; +import { StopEntityStoreRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/stop.gen'; import { SuggestUserProfilesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/users/suggest_user_profiles_route.gen'; import { TriggerRiskScoreCalculationRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/risk_engine/entity_calculation_route.gen'; import { UpdateRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/update_rule/update_rule_route.gen'; @@ -313,6 +325,14 @@ Migrations are initiated per index. While the process is neither destructive nor .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .query(props.query); }, + deleteEntityStore(props: DeleteEntityStoreProps) { + return supertest + .delete(replaceParams('/api/entity_store/engines/{entityType}', props.params)) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .query(props.query); + }, deleteNote(props: DeleteNoteProps) { return supertest .delete('/api/note') @@ -668,6 +688,20 @@ finalize it. .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send(props.body as object); }, + getEntityStoreEngine(props: GetEntityStoreEngineProps) { + return supertest + .get(replaceParams('/api/entity_store/engines/{entityType}', props.params)) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, + getEntityStoreStats(props: GetEntityStoreStatsProps) { + return supertest + .post(replaceParams('/api/entity_store/engines/{entityType}/stats', props.params)) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, /** * Gets notes */ @@ -764,6 +798,14 @@ finalize it. .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send(props.body as object); }, + initEntityStore(props: InitEntityStoreProps) { + return supertest + .post(replaceParams('/api/entity_store/engines/{entityType}/init', props.params)) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(props.body as object); + }, /** * Initializes the Risk Engine by creating the necessary indices and mappings, removing old transforms, and starting the new risk engine */ @@ -799,6 +841,13 @@ finalize it. .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); }, + listEntityStoreEngines() { + return supertest + .get('/api/entity_store/engines') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, /** * Update specific fields of an existing detection rule using the `rule_id` or `id` field. */ @@ -1018,6 +1067,20 @@ detection engine rules. .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send(props.body as object); }, + startEntityStore(props: StartEntityStoreProps) { + return supertest + .post(replaceParams('/api/entity_store/engines/{entityType}/start', props.params)) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, + stopEntityStore(props: StopEntityStoreProps) { + return supertest + .post(replaceParams('/api/entity_store/engines/{entityType}/stop', props.params)) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, /** * Suggests user profiles. */ @@ -1107,6 +1170,10 @@ export interface CreateUpdateProtectionUpdatesNoteProps { export interface DeleteAssetCriticalityRecordProps { query: DeleteAssetCriticalityRecordRequestQueryInput; } +export interface DeleteEntityStoreProps { + query: DeleteEntityStoreRequestQueryInput; + params: DeleteEntityStoreRequestParamsInput; +} export interface DeleteNoteProps { body: DeleteNoteRequestBodyInput; } @@ -1200,6 +1267,12 @@ export interface GetEndpointSuggestionsProps { params: GetEndpointSuggestionsRequestParamsInput; body: GetEndpointSuggestionsRequestBodyInput; } +export interface GetEntityStoreEngineProps { + params: GetEntityStoreEngineRequestParamsInput; +} +export interface GetEntityStoreStatsProps { + params: GetEntityStoreStatsRequestParamsInput; +} export interface GetNotesProps { query: GetNotesRequestQueryInput; } @@ -1229,6 +1302,10 @@ export interface ImportRulesProps { export interface ImportTimelinesProps { body: ImportTimelinesRequestBodyInput; } +export interface InitEntityStoreProps { + params: InitEntityStoreRequestParamsInput; + body: InitEntityStoreRequestBodyInput; +} export interface InstallPrepackedTimelinesProps { body: InstallPrepackedTimelinesRequestBodyInput; } @@ -1278,6 +1355,12 @@ export interface SetAlertsStatusProps { export interface SetAlertTagsProps { body: SetAlertTagsRequestBodyInput; } +export interface StartEntityStoreProps { + params: StartEntityStoreRequestParamsInput; +} +export interface StopEntityStoreProps { + params: StopEntityStoreRequestParamsInput; +} export interface SuggestUserProfilesProps { query: SuggestUserProfilesRequestQueryInput; } diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/privileges.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/privileges.ts index d4e9aab9cdbd2..d22fa9380769d 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/privileges.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/privileges.ts @@ -49,6 +49,46 @@ const READ_SCENARIOS = [ }, ]; +const READ_SCENARIOS_FULL_POLICIES = [ + { + user: testUsers.fleet_all_only, + statusCode: 200, + }, + { + user: testUsers.fleet_read_only, + statusCode: 200, + }, + { + user: testUsers.fleet_agent_policies_read_only, + statusCode: 200, + }, + { + user: testUsers.fleet_agent_policies_all_only, + statusCode: 200, + }, + { + // Expect minimal access + user: testUsers.fleet_agents_read_only, + statusCode: 403, + }, + { + user: testUsers.fleet_no_access, + statusCode: 403, + }, + { + user: testUsers.fleet_minimal_all_only, + statusCode: 403, + }, + { + user: testUsers.fleet_minimal_read_only, + statusCode: 403, + }, + { + user: testUsers.fleet_settings_read_only, + statusCode: 403, + }, +]; + const ALL_SCENARIOS = [ { user: testUsers.fleet_all_only, @@ -101,11 +141,33 @@ export default function (providerContext: FtrProviderContext) { path: '/api/fleet/agent_policies', scenarios: READ_SCENARIOS, }, + { + method: 'GET', + path: '/api/fleet/agent_policies?full=true', + scenarios: READ_SCENARIOS_FULL_POLICIES, + }, { method: 'GET', path: '/api/fleet/agent_policies/policy-test-privileges-1', scenarios: READ_SCENARIOS, }, + { + method: 'POST', + path: '/api/fleet/agent_policies/_bulk_get', + scenarios: READ_SCENARIOS, + send: { + ids: ['policy-test-privileges-1'], + }, + }, + { + method: 'POST', + path: '/api/fleet/agent_policies/_bulk_get', + scenarios: READ_SCENARIOS_FULL_POLICIES, + send: { + ids: ['policy-test-privileges-1'], + full: true, + }, + }, { method: 'POST', path: '/api/fleet/agent_policies', diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/agent_policies.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/agent_policies.ts index 40e163dfabf47..037ba332cfefb 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/agent_policies.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/agent_policies.ts @@ -11,10 +11,11 @@ import { FtrProviderContext } from '../../../api_integration/ftr_provider_contex import { skipIfNoDockerRegistry } from '../../helpers'; import { SpaceTestApiClient } from './api_helper'; import { cleanFleetIndices, expectToRejectWithNotFound } from './helpers'; +import { setupTestUsers, testUsers } from '../test_users'; export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const esClient = getService('es'); const kibanaServer = getService('kibanaServer'); const spaces = getService('spaces'); @@ -22,13 +23,17 @@ export default function (providerContext: FtrProviderContext) { describe('agent policies', function () { skipIfNoDockerRegistry(providerContext); - const apiClient = new SpaceTestApiClient(supertest); + const apiClient = new SpaceTestApiClient(supertestWithoutAuth, { + username: testUsers.fleet_all_int_all.username, + password: testUsers.fleet_all_int_all.password, + }); let defaultSpacePolicy1: CreateAgentPolicyResponse; let spaceTest1Policy1: CreateAgentPolicyResponse; let spaceTest1Policy2: CreateAgentPolicyResponse; before(async () => { + await setupTestUsers(getService('security')); TEST_SPACE_1 = spaces.getDefaultTestSpace(); await kibanaServer.savedObjects.cleanStandardList(); await kibanaServer.savedObjects.cleanStandardList({ diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts index 9009e2b81a73b..1de90ae3dcfaa 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts @@ -133,6 +133,7 @@ export class SpaceTestApiClient { async deleteAgentPolicy(agentPolicyId: string, spaceId?: string) { await this.supertest .post(`${this.getBaseUrl(spaceId)}/api/fleet/agent_policies/delete`) + .auth(this.auth.username, this.auth.password) .send({ agentPolicyId, }) @@ -142,6 +143,7 @@ export class SpaceTestApiClient { async getAgentPolicy(policyId: string, spaceId?: string): Promise { const { body: res } = await this.supertest .get(`${this.getBaseUrl(spaceId)}/api/fleet/agent_policies/${policyId}`) + .auth(this.auth.username, this.auth.password) .expect(200); return res; @@ -172,6 +174,7 @@ export class SpaceTestApiClient { async getAgentPolicies(spaceId?: string): Promise { const { body: res } = await this.supertest .get(`${this.getBaseUrl(spaceId)}/api/fleet/agent_policies`) + .auth(this.auth.username, this.auth.password) .expect(200); return res; @@ -482,6 +485,7 @@ export class SpaceTestApiClient { async postEnableSpaceAwareness(spaceId?: string): Promise { const { body: res } = await this.supertest .post(`${this.getBaseUrl(spaceId)}/internal/fleet/enable_space_awareness`) + .auth(this.auth.username, this.auth.password) .set('kbn-xsrf', 'xxxx') .set('elastic-api-version', '1'); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection_jobs/advanced_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/advanced_job.ts index ea5d70fcbe069..370580fe604dc 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection_jobs/advanced_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/advanced_job.ts @@ -423,7 +423,7 @@ export default function ({ getService }: FtrProviderContext) { ...testData.expected.row, }); - await ml.jobTable.assertJobRowDetailsCounts( + await ml.jobExpandedDetails.assertJobRowDetailsCounts( testData.jobId, { job_id: testData.jobId, @@ -638,7 +638,7 @@ export default function ({ getService }: FtrProviderContext) { ...testData.expected.row, }); - await ml.jobTable.assertJobRowDetailsCounts( + await ml.jobExpandedDetails.assertJobRowDetailsCounts( testData.jobIdClone, { job_id: testData.jobIdClone, diff --git a/x-pack/test/functional/apps/ml/anomaly_detection_jobs/categorization_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/categorization_job.ts index 07929a8f9b6f9..eb3708c129205 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection_jobs/categorization_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/categorization_job.ts @@ -228,7 +228,7 @@ export default function ({ getService }: FtrProviderContext) { ); await ml.jobTable.assertJobRowFields(jobId, getExpectedRow(jobId, jobGroups)); - await ml.jobTable.assertJobRowDetailsCounts( + await ml.jobExpandedDetails.assertJobRowDetailsCounts( jobId, getExpectedCounts(jobId), getExpectedModelSizeStats(jobId) @@ -343,7 +343,7 @@ export default function ({ getService }: FtrProviderContext) { ); await ml.jobTable.assertJobRowFields(jobIdClone, getExpectedRow(jobIdClone, jobGroupsClone)); - await ml.jobTable.assertJobRowDetailsCounts( + await ml.jobExpandedDetails.assertJobRowDetailsCounts( jobIdClone, getExpectedCounts(jobIdClone), getExpectedModelSizeStats(jobIdClone) diff --git a/x-pack/test/functional/apps/ml/anomaly_detection_jobs/date_nanos_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/date_nanos_job.ts index e5ffd4c193949..c513e1ee10bdb 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection_jobs/date_nanos_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/date_nanos_job.ts @@ -298,7 +298,7 @@ export default function ({ getService }: FtrProviderContext) { ...testData.expected.row, }); - await ml.jobTable.assertJobRowDetailsCounts( + await ml.jobExpandedDetails.assertJobRowDetailsCounts( testData.jobId, { job_id: testData.jobId, diff --git a/x-pack/test/functional/apps/ml/anomaly_detection_jobs/geo_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/geo_job.ts index a95ba4782c413..f5ed246f939d2 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection_jobs/geo_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/geo_job.ts @@ -219,7 +219,7 @@ export default function ({ getService }: FtrProviderContext) { ); await ml.jobTable.assertJobRowFields(jobId, getExpectedRow(jobId, jobGroups)); - await ml.jobTable.assertJobRowDetailsCounts( + await ml.jobExpandedDetails.assertJobRowDetailsCounts( jobId, getExpectedCounts(jobId), getExpectedModelSizeStats(jobId) @@ -339,7 +339,7 @@ export default function ({ getService }: FtrProviderContext) { ); await ml.jobTable.assertJobRowFields(jobIdClone, getExpectedRow(jobIdClone, jobGroupsClone)); - await ml.jobTable.assertJobRowDetailsCounts( + await ml.jobExpandedDetails.assertJobRowDetailsCounts( jobIdClone, getExpectedCounts(jobIdClone), getExpectedModelSizeStats(jobIdClone) diff --git a/x-pack/test/functional/apps/ml/anomaly_detection_jobs/job_expanded_details.ts b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/job_expanded_details.ts index e48ca875bb1f2..bddcd564bdd18 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection_jobs/job_expanded_details.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/job_expanded_details.ts @@ -113,10 +113,10 @@ export default function ({ getService }: FtrProviderContext) { it('multi-selection with one opened job should only present the opened job when job list is filtered by the Opened button', async () => { await ml.jobTable.selectAllJobs(); - await ml.jobExpandedDetails.assertJobListMultiSelectionText('2 jobs selected'); + await ml.jobTable.assertJobListMultiSelectionText('2 jobs selected'); await ml.jobTable.filterByState(QuickFilterButtonTypes.Opened); await ml.jobTable.assertJobsInTable([jobId]); - await ml.jobExpandedDetails.assertJobListMultiSelectionText('1 job selected'); + await ml.jobTable.assertJobListMultiSelectionText('1 job selected'); }); it('multi-selection with one closed job should only present the closed job when job list is filtered by the Closed button', async () => { diff --git a/x-pack/test/functional/apps/ml/anomaly_detection_jobs/multi_metric_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/multi_metric_job.ts index 24f385704bd71..c60c4d21bc92b 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection_jobs/multi_metric_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/multi_metric_job.ts @@ -244,7 +244,7 @@ export default function ({ getService }: FtrProviderContext) { ); await ml.jobTable.assertJobRowFields(jobId, getExpectedRow(jobId, jobGroups)); - await ml.jobTable.assertJobRowDetailsCounts( + await ml.jobExpandedDetails.assertJobRowDetailsCounts( jobId, getExpectedCounts(jobId), getExpectedModelSizeStats(jobId) @@ -376,7 +376,7 @@ export default function ({ getService }: FtrProviderContext) { ); await ml.jobTable.assertJobRowFields(jobIdClone, getExpectedRow(jobIdClone, jobGroupsClone)); - await ml.jobTable.assertJobRowDetailsCounts( + await ml.jobExpandedDetails.assertJobRowDetailsCounts( jobIdClone, getExpectedCounts(jobIdClone), getExpectedModelSizeStats(jobIdClone) diff --git a/x-pack/test/functional/apps/ml/anomaly_detection_jobs/population_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/population_job.ts index 1dd7801fa334c..9ef7aea22bb6b 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection_jobs/population_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/population_job.ts @@ -259,7 +259,7 @@ export default function ({ getService }: FtrProviderContext) { ); await ml.jobTable.assertJobRowFields(jobId, getExpectedRow(jobId, jobGroups)); - await ml.jobTable.assertJobRowDetailsCounts( + await ml.jobExpandedDetails.assertJobRowDetailsCounts( jobId, getExpectedCounts(jobId), getExpectedModelSizeStats(jobId) @@ -402,7 +402,7 @@ export default function ({ getService }: FtrProviderContext) { ); await ml.jobTable.assertJobRowFields(jobIdClone, getExpectedRow(jobIdClone, jobGroupsClone)); - await ml.jobTable.assertJobRowDetailsCounts( + await ml.jobExpandedDetails.assertJobRowDetailsCounts( jobIdClone, getExpectedCounts(jobIdClone), getExpectedModelSizeStats(jobIdClone) diff --git a/x-pack/test/functional/apps/ml/anomaly_detection_jobs/saved_search_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/saved_search_job.ts index 414230b0b73a1..342a8a13eebbe 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection_jobs/saved_search_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/saved_search_job.ts @@ -424,7 +424,7 @@ export default function ({ getService }: FtrProviderContext) { ...testData.expected.row, }); - await ml.jobTable.assertJobRowDetailsCounts( + await ml.jobExpandedDetails.assertJobRowDetailsCounts( testData.jobId, { job_id: testData.jobId, diff --git a/x-pack/test/functional/apps/ml/anomaly_detection_jobs/single_metric_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/single_metric_job.ts index 957ac090e1ade..411b013deb64c 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection_jobs/single_metric_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/single_metric_job.ts @@ -219,7 +219,7 @@ export default function ({ getService }: FtrProviderContext) { ); await ml.jobTable.assertJobRowFields(jobId, getExpectedRow(jobId, jobGroups)); - await ml.jobTable.assertJobRowDetailsCounts( + await ml.jobExpandedDetails.assertJobRowDetailsCounts( jobId, getExpectedCounts(jobId), getExpectedModelSizeStats(jobId) @@ -357,7 +357,7 @@ export default function ({ getService }: FtrProviderContext) { ); await ml.jobTable.assertJobRowFields(jobIdClone, getExpectedRow(jobIdClone, jobGroupsClone)); - await ml.jobTable.assertJobRowDetailsCounts( + await ml.jobExpandedDetails.assertJobRowDetailsCounts( jobIdClone, getExpectedCounts(jobIdClone), getExpectedModelSizeStats(jobIdClone) diff --git a/x-pack/test/functional/apps/ml/anomaly_detection_jobs/single_metric_job_without_datafeed_start.ts b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/single_metric_job_without_datafeed_start.ts index e137f366628e7..89fbd1213e6e8 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection_jobs/single_metric_job_without_datafeed_start.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection_jobs/single_metric_job_without_datafeed_start.ts @@ -143,7 +143,7 @@ export default function ({ getService }: FtrProviderContext) { ); await ml.jobTable.assertJobRowFields(jobId, getExpectedRow(jobId)); - await ml.jobTable.assertJobRowDetailsCounts( + await ml.jobExpandedDetails.assertJobRowDetailsCounts( jobId, getExpectedCounts(jobId), getExpectedModelSizeStats(jobId) diff --git a/x-pack/test/functional/apps/ml/anomaly_detection_result_views/annotations.ts b/x-pack/test/functional/apps/ml/anomaly_detection_result_views/annotations.ts index ab1177d2dbc84..acae757510aa4 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection_result_views/annotations.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection_result_views/annotations.ts @@ -90,7 +90,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('should display created annotation in job list'); await ml.navigation.navigateToJobManagement(); await ml.jobTable.filterWithSearchString(jobId, 1); - await ml.jobTable.openAnnotationsTab(jobId); + await ml.jobExpandedDetails.openAnnotationsTab(jobId); await ml.jobAnnotations.assertAnnotationExists({ annotation: newText, event: 'user', @@ -124,7 +124,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.jobTable.filterWithSearchString(jobId, 1); - await ml.jobTable.openAnnotationsTab(jobId); + await ml.jobExpandedDetails.openAnnotationsTab(jobId); await ml.jobAnnotations.assertAnnotationContentById( annotationId, expectedOriginalAnnotation @@ -177,7 +177,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('should display edited annotation in job list'); await ml.navigation.navigateToJobManagement(); await ml.jobTable.filterWithSearchString(jobId, 1); - await ml.jobTable.openAnnotationsTab(jobId); + await ml.jobExpandedDetails.openAnnotationsTab(jobId); await ml.jobAnnotations.assertAnnotationContentById(annotationId, expectedEditedAnnotation); }); }); @@ -197,7 +197,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToMl(); await ml.navigation.navigateToJobManagement(); await ml.jobTable.filterWithSearchString(jobId, 1); - await ml.jobTable.openAnnotationsTab(jobId); + await ml.jobExpandedDetails.openAnnotationsTab(jobId); await ml.jobAnnotations.openDatafeedChartFlyout(annotationId, jobId); await ml.jobAnnotations.assertDelayedDataChartExists(); @@ -252,7 +252,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('does not show the deleted annotation in job list'); await ml.navigation.navigateToJobManagement(); await ml.jobTable.filterWithSearchString(jobId, 1); - await ml.jobTable.openAnnotationsTab(jobId); + await ml.jobExpandedDetails.openAnnotationsTab(jobId); await ml.jobAnnotations.assertAnnotationsRowMissing(annotationId); }); }); diff --git a/x-pack/test/functional/apps/ml/short_tests/settings/calendar_creation.ts b/x-pack/test/functional/apps/ml/short_tests/settings/calendar_creation.ts index 15eac59357928..78a15a64ce0bd 100644 --- a/x-pack/test/functional/apps/ml/short_tests/settings/calendar_creation.ts +++ b/x-pack/test/functional/apps/ml/short_tests/settings/calendar_creation.ts @@ -146,8 +146,11 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToAnomalyDetection(); - await ml.jobTable.assertJobRowCalendars('test_calendar_ad_1', [calendarId]); - await ml.jobTable.clickJobRowCalendarWithAssertion('test_calendar_ad_1', calendarId); + await ml.jobExpandedDetails.assertJobRowCalendars('test_calendar_ad_1', [calendarId]); + await ml.jobExpandedDetails.clickJobRowCalendarWithAssertion( + 'test_calendar_ad_1', + calendarId + ); await ml.testExecution.logTestStep( 'created calendars can be connected to job groups after creation' @@ -161,8 +164,8 @@ export default function ({ getService }: FtrProviderContext) { 'multi-metric', ]); await ml.navigation.navigateToAnomalyDetection(); - await ml.jobTable.assertJobRowCalendars('test_calendar_ad_4', [calendarId]); - await ml.jobTable.assertJobRowCalendars('test_calendar_ad_3', [calendarId], false); + await ml.jobExpandedDetails.assertJobRowCalendars('test_calendar_ad_4', [calendarId]); + await ml.jobExpandedDetails.assertJobRowCalendars('test_calendar_ad_3', [calendarId], false); }); async function assignJobToCalendar( diff --git a/x-pack/test/functional/services/ml/job_expanded_details.ts b/x-pack/test/functional/services/ml/job_expanded_details.ts index d9c82d72eabc4..bf5c7b2c87b5b 100644 --- a/x-pack/test/functional/services/ml/job_expanded_details.ts +++ b/x-pack/test/functional/services/ml/job_expanded_details.ts @@ -21,6 +21,14 @@ export function MachineLearningJobExpandedDetailsProvider( const headerPage = getPageObject('header'); return { + async openAnnotationsTab(jobId: string) { + await retry.tryForTime(10000, async () => { + await jobTable.ensureDetailsOpen(jobId); + await testSubjects.click(jobTable.detailsSelector(jobId, 'mlJobListTab-annotations')); + await testSubjects.existOrFail('mlAnnotationsTable'); + }); + }, + async clickEditAnnotationAction(jobId: string, annotationId: string) { await jobAnnotationsTable.ensureAnnotationsActionsMenuOpen(annotationId); await testSubjects.click('mlAnnotationsActionEdit'); @@ -77,7 +85,7 @@ export function MachineLearningJobExpandedDetailsProvider( const { _id: annotationId }: { _id: string } = annotationsFromApi[0]; await jobTable.ensureDetailsOpen(jobId); - await jobTable.openAnnotationsTab(jobId); + await this.openAnnotationsTab(jobId); await this.clearSearchButton(); await jobAnnotationsTable.ensureAnnotationsActionsMenuOpen(annotationId); await testSubjects.click('mlAnnotationsActionOpenInSingleMetricViewer'); @@ -92,7 +100,7 @@ export function MachineLearningJobExpandedDetailsProvider( await this.assertAnnotationsFromApi(annotationsFromApi); await jobTable.ensureDetailsOpen(jobId); - await jobTable.openAnnotationsTab(jobId); + await this.openAnnotationsTab(jobId); await this.clearSearchButton(); const { _id: annotationId }: { _id: string } = annotationsFromApi[0]; @@ -107,7 +115,7 @@ export function MachineLearningJobExpandedDetailsProvider( await jobTable.ensureDetailsClosed(jobId); await jobTable.withDetailsOpen(jobId, async () => { - await jobTable.openAnnotationsTab(jobId); + await this.openAnnotationsTab(jobId); await this.clearSearchButton(); const visibleText = await testSubjects.getVisibleText( jobTable.detailsSelector(jobId, 'mlAnnotationsColumnAnnotation') @@ -118,7 +126,7 @@ export function MachineLearningJobExpandedDetailsProvider( async assertDataFeedFlyout(jobId: string): Promise { await jobTable.withDetailsOpen(jobId, async () => { - await jobTable.openAnnotationsTab(jobId); + await this.openAnnotationsTab(jobId); await this.clearSearchButton(); await testSubjects.click(jobTable.detailsSelector(jobId, 'euiCollapsedItemActionsButton')); await testSubjects.click('mlAnnotationsActionViewDatafeed'); @@ -162,9 +170,46 @@ export function MachineLearningJobExpandedDetailsProvider( }); }, - async assertJobListMultiSelectionText(expectedMsg: string): Promise { - const visibleText = await testSubjects.getVisibleText('~mlADJobListMultiSelectActionsArea'); - expect(visibleText).to.be(expectedMsg); + async clickJobRowCalendarWithAssertion(jobId: string, calendarId: string): Promise { + await jobTable.ensureDetailsOpen(jobId); + const calendarSelector = `mlJobDetailsCalendar-${calendarId}`; + await testSubjects.existOrFail(calendarSelector, { + timeout: 3_000, + }); + await testSubjects.click(calendarSelector, 3_000); + await testSubjects.existOrFail('mlPageCalendarEdit > mlCalendarFormEdit', { + timeout: 3_000, + }); + const calendarTitleVisibleText = await testSubjects.getVisibleText('mlCalendarTitle'); + expect(calendarTitleVisibleText).to.contain( + calendarId, + `Calendar page title should contain [${calendarId}], got [${calendarTitleVisibleText}]` + ); + }, + + async assertJobRowDetailsCounts( + jobId: string, + expectedCounts: object, + expectedModelSizeStats: object + ) { + const { counts, modelSizeStats } = await jobTable.parseJobCounts(jobId); + + // Only check for expected keys / values, ignore additional properties + // This way the tests stay stable when new properties are added on the ES side + for (const [key, value] of Object.entries(expectedCounts)) { + expect(counts) + .to.have.property(key) + .eql(value, `Expected counts property '${key}' to exist with value '${value}'`); + } + + for (const [key, value] of Object.entries(expectedModelSizeStats)) { + expect(modelSizeStats) + .to.have.property(key) + .eql( + value, + `Expected model size stats property '${key}' to exist with value '${value}')` + ); + } }, }; } diff --git a/x-pack/test/functional/services/ml/job_table.ts b/x-pack/test/functional/services/ml/job_table.ts index 97ce1858bc2f1..bd19a31f62b54 100644 --- a/x-pack/test/functional/services/ml/job_table.ts +++ b/x-pack/test/functional/services/ml/job_table.ts @@ -55,21 +55,21 @@ export function MachineLearningJobTableProvider( const testSubjects = getService('testSubjects'); const retry = getService('retry'); - return new (class MlJobTable { - public async selectAllJobs(): Promise { + return { + async selectAllJobs(): Promise { await testSubjects.click('checkboxSelectAll'); - } + }, - public async assertJobsInTable(expectedJobIds: string[]) { + async assertJobsInTable(expectedJobIds: string[]) { const sortedExpectedIds = expectedJobIds.sort(); const sortedActualJobIds = (await this.parseJobTable()).map((row) => row.id).sort(); expect(sortedActualJobIds).to.eql( sortedExpectedIds, `Expected jobs in table to be [${sortedExpectedIds}], got [${sortedActualJobIds}]` ); - } + }, - public async filterByState(quickFilterButton: QuickFilterButtonTypes): Promise { + async filterByState(quickFilterButton: QuickFilterButtonTypes): Promise { const searchBar: WebElementWrapper = await testSubjects.find('mlJobListSearchBar'); const quickFilter: WebElementWrapper = await searchBar.findByCssSelector( `span[data-text="${quickFilterButton}"]` @@ -86,46 +86,9 @@ export function MachineLearningJobTableProvider( quickFilterButton, `Expected visible text of pressed quick filter button to equal [${quickFilterButton}], but got [${pressedBttnText}]` ); - } + }, - public async clickJobRowCalendarWithAssertion( - jobId: string, - calendarId: string - ): Promise { - await this.ensureDetailsOpen(jobId); - const calendarSelector = `mlJobDetailsCalendar-${calendarId}`; - await testSubjects.existOrFail(calendarSelector, { - timeout: 3_000, - }); - await testSubjects.click(calendarSelector, 3_000); - await testSubjects.existOrFail('mlPageCalendarEdit > mlCalendarFormEdit', { - timeout: 3_000, - }); - const calendarTitleVisibleText = await testSubjects.getVisibleText('mlCalendarTitle'); - expect(calendarTitleVisibleText).to.contain( - calendarId, - `Calendar page title should contain [${calendarId}], got [${calendarTitleVisibleText}]` - ); - } - - public async assertJobRowCalendars( - jobId: string, - expectedCalendars: string[], - checkForExists: boolean = true - ): Promise { - await this.withDetailsOpen(jobId, async function verifyJobRowCalendars(): Promise { - for await (const expectedCalendar of expectedCalendars) { - const calendarSelector = `mlJobDetailsCalendar-${expectedCalendar}`; - await testSubjects[checkForExists ? 'existOrFail' : 'missingOrFail'](calendarSelector, { - timeout: 3_000, - }); - if (checkForExists) - expect(await testSubjects.getVisibleText(calendarSelector)).to.be(expectedCalendar); - } - }); - } - - public async parseJobTable( + async parseJobTable( tableEnvironment: 'mlAnomalyDetection' | 'stackMgmtJobList' = 'mlAnomalyDetection' ) { const table = await testSubjects.find('~mlJobListTable'); @@ -215,9 +178,10 @@ export function MachineLearningJobTableProvider( } return rows; - } + }, - public async parseJobCounts(jobId: string) { + // TODO: Mv this fn over too + async parseJobCounts(jobId: string) { return await this.withDetailsOpen(jobId, async () => { // click counts tab await testSubjects.click(this.detailsSelector(jobId, 'mlJobListTab-counts')); @@ -248,59 +212,51 @@ export function MachineLearningJobTableProvider( modelSizeStats: await parseTable(modelSizeStatsTable), }; }); - } + }, - public rowSelector(jobId: string, subSelector?: string) { + rowSelector(jobId: string, subSelector?: string) { const row = `~mlJobListTable > ~row-${jobId}`; return !subSelector ? row : `${row} > ${subSelector}`; - } + }, - public detailsSelector(jobId: string, subSelector?: string) { + detailsSelector(jobId: string, subSelector?: string) { const row = `~mlJobListTable > ~details-${jobId}`; return !subSelector ? row : `${row} > ${subSelector}`; - } + }, - public async withDetailsOpen(jobId: string, block: () => Promise): Promise { + async withDetailsOpen(jobId: string, block: () => Promise): Promise { await this.ensureDetailsOpen(jobId); try { return await block(); } finally { await this.ensureDetailsClosed(jobId); } - } + }, - public async ensureDetailsOpen(jobId: string) { + async ensureDetailsOpen(jobId: string) { await retry.tryForTime(10000, async () => { if (!(await testSubjects.exists(this.detailsSelector(jobId)))) { await testSubjects.click(this.rowSelector(jobId, 'mlJobListRowDetailsToggle')); await testSubjects.existOrFail(this.detailsSelector(jobId), { timeout: 1000 }); } }); - } + }, - public async ensureDetailsClosed(jobId: string) { + async ensureDetailsClosed(jobId: string) { await retry.tryForTime(10000, async () => { if (await testSubjects.exists(this.detailsSelector(jobId))) { await testSubjects.click(this.rowSelector(jobId, 'mlJobListRowDetailsToggle')); await testSubjects.missingOrFail(this.detailsSelector(jobId), { timeout: 1000 }); } }); - } - - public async openAnnotationsTab(jobId: string) { - await retry.tryForTime(10000, async () => { - await this.ensureDetailsOpen(jobId); - await testSubjects.click(this.detailsSelector(jobId, 'mlJobListTab-annotations')); - await testSubjects.existOrFail('mlAnnotationsTable'); - }); - } + }, - public async waitForRefreshButtonLoaded(buttonTestSubj: string) { + async waitForRefreshButtonLoaded(buttonTestSubj: string) { await testSubjects.existOrFail(`~${buttonTestSubj}`, { timeout: 10 * 1000 }); await testSubjects.existOrFail(`${buttonTestSubj} loaded`, { timeout: 30 * 1000 }); - } + }, - public async refreshJobList( + async refreshJobList( tableEnvironment: 'mlAnomalyDetection' | 'stackMgmtJobList' = 'mlAnomalyDetection' ) { const testSubjStr = @@ -312,14 +268,14 @@ export function MachineLearningJobTableProvider( await testSubjects.click(`~${testSubjStr}`); await this.waitForRefreshButtonLoaded(testSubjStr); await this.waitForJobsToLoad(); - } + }, - public async waitForJobsToLoad() { + async waitForJobsToLoad() { await testSubjects.existOrFail('~mlJobListTable', { timeout: 60 * 1000 }); await testSubjects.existOrFail('mlJobListTable loaded', { timeout: 30 * 1000 }); - } + }, - public async filterWithSearchString( + async filterWithSearchString( filter: string, expectedRowCount: number = 1, tableEnvironment: 'mlAnomalyDetection' | 'stackMgmtJobList' = 'mlAnomalyDetection' @@ -339,9 +295,9 @@ export function MachineLearningJobTableProvider( filteredRows )}')` ); - } + }, - public async assertJobRowFields(jobId: string, expectedRow: object) { + async assertJobRowFields(jobId: string, expectedRow: object) { await retry.tryForTime(5000, async () => { await this.refreshJobList(); const rows = await this.parseJobTable(); @@ -353,46 +309,18 @@ export function MachineLearningJobTableProvider( )}')` ); }); - } + }, - public async assertJobRowJobId(jobId: string) { + async assertJobRowJobId(jobId: string) { await retry.tryForTime(5000, async () => { await this.refreshJobList(); const rows = await this.parseJobTable(); const jobRowMatch = rows.find((row) => row.id === jobId); expect(jobRowMatch).to.not.eql(undefined, `Expected row with job ID ${jobId} to exist`); }); - } + }, - public async assertJobRowDetailsCounts( - jobId: string, - expectedCounts: object, - expectedModelSizeStats: object - ) { - const { counts, modelSizeStats } = await this.parseJobCounts(jobId); - - // Only check for expected keys / values, ignore additional properties - // This way the tests stay stable when new properties are added on the ES side - for (const [key, value] of Object.entries(expectedCounts)) { - expect(counts) - .to.have.property(key) - .eql(value, `Expected counts property '${key}' to exist with value '${value}'`); - } - - for (const [key, value] of Object.entries(expectedModelSizeStats)) { - expect(modelSizeStats) - .to.have.property(key) - .eql( - value, - `Expected model size stats property '${key}' to exist with value '${value}')` - ); - } - } - - public async assertJobActionSingleMetricViewerButtonEnabled( - jobId: string, - expectedValue: boolean - ) { + async assertJobActionSingleMetricViewerButtonEnabled(jobId: string, expectedValue: boolean) { const isEnabled = await testSubjects.isEnabled( this.rowSelector(jobId, 'mlOpenJobsInSingleMetricViewerButton') ); @@ -402,12 +330,9 @@ export function MachineLearningJobTableProvider( expectedValue ? 'enabled' : 'disabled' }' (got '${isEnabled ? 'enabled' : 'disabled'}')` ); - } + }, - public async assertJobActionAnomalyExplorerButtonEnabled( - jobId: string, - expectedValue: boolean - ) { + async assertJobActionAnomalyExplorerButtonEnabled(jobId: string, expectedValue: boolean) { const isEnabled = await testSubjects.isEnabled( this.rowSelector(jobId, 'mlOpenJobsInAnomalyExplorerButton') ); @@ -417,9 +342,9 @@ export function MachineLearningJobTableProvider( expectedValue ? 'enabled' : 'disabled' }' (got '${isEnabled ? 'enabled' : 'disabled'}')` ); - } + }, - public async assertJobActionsMenuButtonEnabled(jobId: string, expectedValue: boolean) { + async assertJobActionsMenuButtonEnabled(jobId: string, expectedValue: boolean) { const isEnabled = await testSubjects.isEnabled( this.rowSelector(jobId, 'euiCollapsedItemActionsButton') ); @@ -429,9 +354,9 @@ export function MachineLearningJobTableProvider( expectedValue ? 'enabled' : 'disabled' }' (got '${isEnabled ? 'enabled' : 'disabled'}')` ); - } + }, - public async assertJobActionStartDatafeedButtonEnabled(jobId: string, expectedValue: boolean) { + async assertJobActionStartDatafeedButtonEnabled(jobId: string, expectedValue: boolean) { await this.ensureJobActionsMenuOpen(jobId); const isEnabled = await testSubjects.isEnabled('mlActionButtonStartDatafeed'); expect(isEnabled).to.eql( @@ -440,9 +365,9 @@ export function MachineLearningJobTableProvider( expectedValue ? 'enabled' : 'disabled' }' (got '${isEnabled ? 'enabled' : 'disabled'}')` ); - } + }, - public async assertJobActionResetJobButtonEnabled(jobId: string, expectedValue: boolean) { + async assertJobActionResetJobButtonEnabled(jobId: string, expectedValue: boolean) { await this.ensureJobActionsMenuOpen(jobId); const isEnabled = await testSubjects.isEnabled('mlActionButtonResetJob'); expect(isEnabled).to.eql( @@ -451,9 +376,9 @@ export function MachineLearningJobTableProvider( expectedValue ? 'enabled' : 'disabled' }' (got '${isEnabled ? 'enabled' : 'disabled'}')` ); - } + }, - public async assertJobActionCloneJobButtonEnabled(jobId: string, expectedValue: boolean) { + async assertJobActionCloneJobButtonEnabled(jobId: string, expectedValue: boolean) { await this.ensureJobActionsMenuOpen(jobId); const isEnabled = await testSubjects.isEnabled('mlActionButtonCloneJob'); expect(isEnabled).to.eql( @@ -462,12 +387,9 @@ export function MachineLearningJobTableProvider( expectedValue ? 'enabled' : 'disabled' }' (got '${isEnabled ? 'enabled' : 'disabled'}')` ); - } + }, - public async assertJobActionViewDatafeedCountsButtonEnabled( - jobId: string, - expectedValue: boolean - ) { + async assertJobActionViewDatafeedCountsButtonEnabled(jobId: string, expectedValue: boolean) { await this.ensureJobActionsMenuOpen(jobId); const isEnabled = await testSubjects.isEnabled('mlActionButtonViewDatafeedChart'); expect(isEnabled).to.eql( @@ -476,9 +398,9 @@ export function MachineLearningJobTableProvider( expectedValue ? 'enabled' : 'disabled' }' (got '${isEnabled ? 'enabled' : 'disabled'}')` ); - } + }, - public async assertJobActionEditJobButtonEnabled(jobId: string, expectedValue: boolean) { + async assertJobActionEditJobButtonEnabled(jobId: string, expectedValue: boolean) { await this.ensureJobActionsMenuOpen(jobId); const isEnabled = await testSubjects.isEnabled('mlActionButtonEditJob'); expect(isEnabled).to.eql( @@ -487,9 +409,9 @@ export function MachineLearningJobTableProvider( expectedValue ? 'enabled' : 'disabled' }' (got '${isEnabled ? 'enabled' : 'disabled'}')` ); - } + }, - public async assertJobActionDeleteJobButtonEnabled(jobId: string, expectedValue: boolean) { + async assertJobActionDeleteJobButtonEnabled(jobId: string, expectedValue: boolean) { await this.ensureJobActionsMenuOpen(jobId); const isEnabled = await testSubjects.isEnabled('mlActionButtonDeleteJob'); expect(isEnabled).to.eql( @@ -498,51 +420,51 @@ export function MachineLearningJobTableProvider( expectedValue ? 'enabled' : 'disabled' }' (got '${isEnabled ? 'enabled' : 'disabled'}')` ); - } + }, - public async ensureJobActionsMenuOpen(jobId: string) { + async ensureJobActionsMenuOpen(jobId: string) { await retry.tryForTime(30 * 1000, async () => { if (!(await testSubjects.exists('mlActionButtonDeleteJob'))) { await testSubjects.click(this.rowSelector(jobId, 'euiCollapsedItemActionsButton')); await testSubjects.existOrFail('mlActionButtonDeleteJob', { timeout: 5000 }); } }); - } + }, - public async clickCloneJobAction(jobId: string) { + async clickCloneJobAction(jobId: string) { await this.ensureJobActionsMenuOpen(jobId); await testSubjects.click('mlActionButtonCloneJob'); await testSubjects.existOrFail('~mlPageJobWizard'); - } + }, - public async clickCloneJobActionWhenNoDataViewExists(jobId: string) { + async clickCloneJobActionWhenNoDataViewExists(jobId: string) { await this.ensureJobActionsMenuOpen(jobId); await testSubjects.click('mlActionButtonCloneJob'); await this.assertNoDataViewForCloneJobWarningToastExist(); - } + }, - public async assertNoDataViewForCloneJobWarningToastExist() { + async assertNoDataViewForCloneJobWarningToastExist() { await testSubjects.existOrFail('mlCloneJobNoDataViewExistsWarningToast', { timeout: 5000 }); - } + }, - public async clickEditJobAction(jobId: string) { + async clickEditJobAction(jobId: string) { await this.ensureJobActionsMenuOpen(jobId); await testSubjects.click('mlActionButtonEditJob'); await testSubjects.existOrFail('mlJobEditFlyout'); - } + }, - public async clickDeleteJobAction(jobId: string) { + async clickDeleteJobAction(jobId: string) { await this.ensureJobActionsMenuOpen(jobId); await testSubjects.click('mlActionButtonDeleteJob'); await testSubjects.existOrFail('mlDeleteJobConfirmModal'); - } + }, - public async confirmDeleteJobModal() { + async confirmDeleteJobModal() { await testSubjects.click('mlDeleteJobConfirmModal > mlDeleteJobConfirmModalButton'); await testSubjects.missingOrFail('mlDeleteJobConfirmModal', { timeout: 30 * 1000 }); - } + }, - public async clickDeleteAnnotationsInDeleteJobModal(checked: boolean) { + async clickDeleteAnnotationsInDeleteJobModal(checked: boolean) { await testSubjects.setEuiSwitch( 'mlDeleteJobConfirmModal > mlDeleteJobConfirmModalDeleteAnnotationsSwitch', checked ? 'check' : 'uncheck' @@ -552,23 +474,23 @@ export function MachineLearningJobTableProvider( ); expect(isChecked).to.eql(checked, `Expected delete annotations switch to be ${checked}`); - } + }, - public async clickOpenJobInSingleMetricViewerButton(jobId: string) { + async clickOpenJobInSingleMetricViewerButton(jobId: string) { await testSubjects.click(this.rowSelector(jobId, 'mlOpenJobsInSingleMetricViewerButton')); await testSubjects.existOrFail('~mlPageSingleMetricViewer'); - } + }, - public async clickOpenJobInAnomalyExplorerButton(jobId: string) { + async clickOpenJobInAnomalyExplorerButton(jobId: string) { await testSubjects.click(this.rowSelector(jobId, 'mlOpenJobsInAnomalyExplorerButton')); await testSubjects.existOrFail('~mlPageAnomalyExplorer'); - } + }, - public async isJobRowSelected(jobId: string): Promise { + async isJobRowSelected(jobId: string): Promise { return await testSubjects.isChecked(this.rowSelector(jobId, `checkboxSelectRow-${jobId}`)); - } + }, - public async assertJobRowSelected(jobId: string, expectedValue: boolean) { + async assertJobRowSelected(jobId: string, expectedValue: boolean) { const isSelected = await this.isJobRowSelected(jobId); expect(isSelected).to.eql( expectedValue, @@ -576,37 +498,37 @@ export function MachineLearningJobTableProvider( expectedValue ? 'selected' : 'deselected' }' (got '${isSelected ? 'selected' : 'deselected'}')` ); - } + }, - public async selectJobRow(jobId: string) { + async selectJobRow(jobId: string) { if ((await this.isJobRowSelected(jobId)) === false) { await testSubjects.click(this.rowSelector(jobId, `checkboxSelectRow-${jobId}`)); } await this.assertJobRowSelected(jobId, true); await this.assertMultiSelectActionsAreaActive(); - } + }, - public async deselectJobRow(jobId: string) { + async deselectJobRow(jobId: string) { if ((await this.isJobRowSelected(jobId)) === true) { await testSubjects.click(this.rowSelector(jobId, `checkboxSelectRow-${jobId}`)); } await this.assertJobRowSelected(jobId, false); await this.assertMultiSelectActionsAreaInactive(); - } + }, - public async assertMultiSelectActionsAreaActive() { + async assertMultiSelectActionsAreaActive() { await testSubjects.existOrFail('mlADJobListMultiSelectActionsArea active'); - } + }, - public async assertMultiSelectActionsAreaInactive() { + async assertMultiSelectActionsAreaInactive() { await testSubjects.existOrFail('mlADJobListMultiSelectActionsArea inactive', { allowHidden: true, }); - } + }, - public async assertMultiSelectActionSingleMetricViewerButtonEnabled(expectedValue: boolean) { + async assertMultiSelectActionSingleMetricViewerButtonEnabled(expectedValue: boolean) { const isEnabled = await testSubjects.isEnabled( '~mlADJobListMultiSelectActionsArea > mlOpenJobsInSingleMetricViewerButton' ); @@ -616,9 +538,9 @@ export function MachineLearningJobTableProvider( expectedValue ? 'enabled' : 'disabled' }' (got '${isEnabled ? 'enabled' : 'disabled'}')` ); - } + }, - public async assertMultiSelectActionAnomalyExplorerButtonEnabled(expectedValue: boolean) { + async assertMultiSelectActionAnomalyExplorerButtonEnabled(expectedValue: boolean) { const isEnabled = await testSubjects.isEnabled( '~mlADJobListMultiSelectActionsArea > mlOpenJobsInAnomalyExplorerButton' ); @@ -628,9 +550,9 @@ export function MachineLearningJobTableProvider( expectedValue ? 'enabled' : 'disabled' }' (got '${isEnabled ? 'enabled' : 'disabled'}')` ); - } + }, - public async assertMultiSelectActionEditJobGroupsButtonEnabled(expectedValue: boolean) { + async assertMultiSelectActionEditJobGroupsButtonEnabled(expectedValue: boolean) { const isEnabled = await testSubjects.isEnabled( '~mlADJobListMultiSelectActionsArea > mlADJobListMultiSelectEditJobGroupsButton' ); @@ -640,9 +562,9 @@ export function MachineLearningJobTableProvider( expectedValue ? 'enabled' : 'disabled' }' (got '${isEnabled ? 'enabled' : 'disabled'}')` ); - } + }, - public async assertMultiSelectManagementActionsButtonEnabled(expectedValue: boolean) { + async assertMultiSelectManagementActionsButtonEnabled(expectedValue: boolean) { const isEnabled = await testSubjects.isEnabled( '~mlADJobListMultiSelectActionsArea > mlADJobListMultiSelectManagementActionsButton' ); @@ -652,9 +574,9 @@ export function MachineLearningJobTableProvider( expectedValue ? 'enabled' : 'disabled' }' (got '${isEnabled ? 'enabled' : 'disabled'}')` ); - } + }, - public async assertMultiSelectStartDatafeedActionButtonEnabled(expectedValue: boolean) { + async assertMultiSelectStartDatafeedActionButtonEnabled(expectedValue: boolean) { await this.ensureMultiSelectManagementActionsMenuOpen(); const isEnabled = await testSubjects.isEnabled( 'mlADJobListMultiSelectStartDatafeedActionButton' @@ -665,9 +587,9 @@ export function MachineLearningJobTableProvider( expectedValue ? 'enabled' : 'disabled' }' (got '${isEnabled ? 'enabled' : 'disabled'}')` ); - } + }, - public async assertMultiSelectDeleteJobActionButtonEnabled(expectedValue: boolean) { + async assertMultiSelectDeleteJobActionButtonEnabled(expectedValue: boolean) { await this.ensureMultiSelectManagementActionsMenuOpen(); const isEnabled = await testSubjects.isEnabled('mlADJobListMultiSelectDeleteJobActionButton'); expect(isEnabled).to.eql( @@ -676,9 +598,9 @@ export function MachineLearningJobTableProvider( expectedValue ? 'enabled' : 'disabled' }' (got '${isEnabled ? 'enabled' : 'disabled'}')` ); - } + }, - public async ensureMultiSelectManagementActionsMenuOpen() { + async ensureMultiSelectManagementActionsMenuOpen() { await retry.tryForTime(30 * 1000, async () => { if (!(await testSubjects.exists('mlADJobListMultiSelectDeleteJobActionButton'))) { await testSubjects.click('mlADJobListMultiSelectManagementActionsButton'); @@ -687,48 +609,44 @@ export function MachineLearningJobTableProvider( }); } }); - } + }, - public async openEditCustomUrlsForJobTab(jobId: string) { + async openEditCustomUrlsForJobTab(jobId: string) { await this.clickEditJobAction(jobId); // click Custom URLs tab await testSubjects.click('mlEditJobFlyout-customUrls'); await this.ensureEditCustomUrlTabOpen(); await headerPage.waitUntilLoadingHasFinished(); - } + }, - public async ensureEditCustomUrlTabOpen() { + async ensureEditCustomUrlTabOpen() { await testSubjects.existOrFail('mlJobOpenCustomUrlFormButton', { timeout: 5000 }); - } + }, - public async closeEditJobFlyout() { + async closeEditJobFlyout() { if (await testSubjects.exists('mlEditJobFlyoutCloseButton')) { await testSubjects.click('mlEditJobFlyoutCloseButton'); await testSubjects.missingOrFail('mlJobEditFlyout'); } - } + }, - public async saveEditJobFlyoutChanges() { + async saveEditJobFlyoutChanges() { await testSubjects.click('mlEditJobFlyoutSaveButton'); await testSubjects.missingOrFail('mlJobEditFlyout', { timeout: 5000 }); - } + }, - public async clickOpenCustomUrlEditor() { + async clickOpenCustomUrlEditor() { await this.ensureEditCustomUrlTabOpen(); await testSubjects.click('mlJobOpenCustomUrlFormButton'); await testSubjects.existOrFail('mlJobCustomUrlForm'); - } + }, - public async getExistingCustomUrlCount(): Promise { + async getExistingCustomUrlCount(): Promise { const existingCustomUrls = await testSubjects.findAll('mlJobEditCustomUrlItemLabel'); return existingCustomUrls.length; - } + }, - public async saveCustomUrl( - expectedLabel: string, - expectedIndex: number, - expectedValue?: string - ) { + async saveCustomUrl(expectedLabel: string, expectedIndex: number, expectedValue?: string) { await retry.tryForTime(5000, async () => { await testSubjects.click('mlJobAddCustomUrl'); await customUrls.assertCustomUrlLabel(expectedIndex, expectedLabel); @@ -737,9 +655,9 @@ export function MachineLearningJobTableProvider( if (expectedValue !== undefined) { await customUrls.assertCustomUrlUrlValue(expectedIndex, expectedValue); } - } + }, - public async fillInDiscoverUrlForm(customUrl: DiscoverUrlConfig) { + async fillInDiscoverUrlForm(customUrl: DiscoverUrlConfig) { await this.clickOpenCustomUrlEditor(); await customUrls.setCustomUrlLabel(customUrl.label); await mlCommonUI.selectRadioGroupValue( @@ -758,9 +676,9 @@ export function MachineLearningJobTableProvider( if (customUrl.timeRange === TIME_RANGE_TYPE.INTERVAL) { await customUrls.setCustomUrlTimeRangeInterval(customUrl.timeRangeInterval!); } - } + }, - public async fillInDashboardUrlForm(customUrl: DashboardUrlConfig) { + async fillInDashboardUrlForm(customUrl: DashboardUrlConfig) { await this.clickOpenCustomUrlEditor(); await customUrls.setCustomUrlLabel(customUrl.label); await mlCommonUI.selectRadioGroupValue( @@ -779,16 +697,16 @@ export function MachineLearningJobTableProvider( if (customUrl.timeRange === TIME_RANGE_TYPE.INTERVAL) { await customUrls.setCustomUrlTimeRangeInterval(customUrl.timeRangeInterval!); } - } + }, - public async fillInOtherUrlForm(customUrl: OtherUrlConfig) { + async fillInOtherUrlForm(customUrl: OtherUrlConfig) { await this.clickOpenCustomUrlEditor(); await customUrls.setCustomUrlLabel(customUrl.label); await mlCommonUI.selectRadioGroupValue(`mlJobCustomUrlLinkToTypeInput`, URL_TYPE.OTHER); await customUrls.setCustomUrlOtherTypeUrl(customUrl.url); - } + }, - public async addDiscoverCustomUrl(jobId: string, customUrl: DiscoverUrlConfig) { + async addDiscoverCustomUrl(jobId: string, customUrl: DiscoverUrlConfig) { await retry.tryForTime(30 * 1000, async () => { await this.closeEditJobFlyout(); await this.openEditCustomUrlsForJobTab(jobId); @@ -800,9 +718,9 @@ export function MachineLearningJobTableProvider( // Save the job await this.saveEditJobFlyoutChanges(); - } + }, - public async addDashboardCustomUrl( + async addDashboardCustomUrl( jobId: string, customUrl: DashboardUrlConfig, expectedResult: { index: number; url: string } @@ -816,9 +734,9 @@ export function MachineLearningJobTableProvider( // Save the job await this.saveEditJobFlyoutChanges(); - } + }, - public async addOtherTypeCustomUrl(jobId: string, customUrl: OtherUrlConfig) { + async addOtherTypeCustomUrl(jobId: string, customUrl: OtherUrlConfig) { await retry.tryForTime(30 * 1000, async () => { await this.closeEditJobFlyout(); await this.openEditCustomUrlsForJobTab(jobId); @@ -830,9 +748,9 @@ export function MachineLearningJobTableProvider( // Save the job await this.saveEditJobFlyoutChanges(); - } + }, - public async editCustomUrl( + async editCustomUrl( jobId: string, indexInList: number, customUrl: { label: string; url: string } @@ -843,9 +761,9 @@ export function MachineLearningJobTableProvider( // Save the edit await this.saveEditJobFlyoutChanges(); - } + }, - public async deleteCustomUrl(jobId: string, indexInList: number) { + async deleteCustomUrl(jobId: string, indexInList: number) { await this.openEditCustomUrlsForJobTab(jobId); const beforeCustomUrls = await testSubjects.findAll('mlJobEditCustomUrlItemLabel'); await customUrls.deleteCustomUrl(indexInList); @@ -855,30 +773,31 @@ export function MachineLearningJobTableProvider( await this.openEditCustomUrlsForJobTab(jobId); await customUrls.assertCustomUrlsLength(beforeCustomUrls.length - 1); await this.closeEditJobFlyout(); - } + }, - public async openTestCustomUrl(jobId: string, indexInList: number) { + async openTestCustomUrl(jobId: string, indexInList: number) { await this.openEditCustomUrlsForJobTab(jobId); await customUrls.clickTestCustomUrl(indexInList); - } + }, - public async testDiscoverCustomUrlAction(expectedHitCountFormatted: string) { + async testDiscoverCustomUrlAction(expectedHitCountFormatted: string) { await customUrls.assertDiscoverCustomUrlAction(expectedHitCountFormatted); - } + }, - public async testDashboardCustomUrlAction(expectedPanelCount: number) { + async testDashboardCustomUrlAction(expectedPanelCount: number) { await customUrls.assertDashboardCustomUrlAction(expectedPanelCount); - } + }, - public async testOtherTypeCustomUrlAction( - jobId: string, - indexInList: number, - expectedUrl: string - ) { + async testOtherTypeCustomUrlAction(jobId: string, indexInList: number, expectedUrl: string) { // Can't test the contents of the external page, so just check the expected URL. await this.openEditCustomUrlsForJobTab(jobId); await customUrls.assertCustomUrlUrlValue(indexInList, expectedUrl); await this.closeEditJobFlyout(); - } - })(); + }, + + async assertJobListMultiSelectionText(expectedMsg: string): Promise { + const visibleText = await testSubjects.getVisibleText('~mlADJobListMultiSelectActionsArea'); + expect(visibleText).to.be(expectedMsg); + }, + }; } diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/large_prebuilt_rules_package/trial_license_complete_tier/install_large_prebuilt_rules_package.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/large_prebuilt_rules_package/trial_license_complete_tier/install_large_prebuilt_rules_package.ts index 10a5adaf83346..29ca3eea30239 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/large_prebuilt_rules_package/trial_license_complete_tier/install_large_prebuilt_rules_package.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/large_prebuilt_rules_package/trial_license_complete_tier/install_large_prebuilt_rules_package.ts @@ -18,8 +18,7 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const log = getService('log'); - // Failing: See https://github.com/elastic/kibana/issues/192479 - describe.skip('@ess @serverless @skipInServerlessMKI install_large_prebuilt_rules_package', () => { + describe('@ess @serverless @skipInServerlessMKI install_large_prebuilt_rules_package', () => { beforeEach(async () => { await deleteAllRules(supertest, log); await deleteAllPrebuiltRuleAssets(es, log); @@ -36,9 +35,12 @@ export default ({ getService }: FtrProviderContext): void => { es, supertest ); - expect(statusBeforePackageInstallation.rules_installed).toBe(0); - expect(statusBeforePackageInstallation.rules_not_installed).toBe(0); - expect(statusBeforePackageInstallation.rules_not_updated).toBe(0); + + expect(statusBeforePackageInstallation).toMatchObject({ + rules_installed: 0, + rules_not_installed: 0, + rules_not_updated: 0, + }); // Install the package with 15000 prebuilt historical version of rules rules and 750 unique rules await installPrebuiltRulesAndTimelines(es, supertest); @@ -48,9 +50,12 @@ export default ({ getService }: FtrProviderContext): void => { es, supertest ); - expect(statusAfterPackageInstallation.rules_installed).toBe(750); - expect(statusAfterPackageInstallation.rules_not_installed).toBe(0); - expect(statusAfterPackageInstallation.rules_not_updated).toBe(0); + + expect(statusAfterPackageInstallation).toMatchObject({ + rules_installed: 750, + rules_not_installed: 0, + rules_not_updated: 0, + }); }); }); }; diff --git a/x-pack/test_serverless/api_integration/services/alerting_api.ts b/x-pack/test_serverless/api_integration/services/alerting_api.ts deleted file mode 100644 index 6000e9d8bdc88..0000000000000 --- a/x-pack/test_serverless/api_integration/services/alerting_api.ts +++ /dev/null @@ -1,176 +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 { - AggregationsAggregate, - SearchResponse, -} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import { MetricThresholdParams } from '@kbn/infra-plugin/common/alerting/metrics'; -import { ThresholdParams } from '@kbn/observability-plugin/common/custom_threshold_rule/types'; -import { RoleCredentials } from '../../shared/services'; -import { SloBurnRateRuleParams } from './slo_api'; -import { FtrProviderContext } from '../ftr_provider_context'; - -export function AlertingApiProvider({ getService }: FtrProviderContext) { - const retry = getService('retry'); - const es = getService('es'); - const requestTimeout = 30 * 1000; - const retryTimeout = 120 * 1000; - const logger = getService('log'); - const svlCommonApi = getService('svlCommonApi'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - return { - async waitForRuleStatus({ - roleAuthc, - ruleId, - expectedStatus, - }: { - roleAuthc: RoleCredentials; - ruleId: string; - expectedStatus: string; - }) { - if (!ruleId) { - throw new Error(`'ruleId' is undefined`); - } - return await retry.tryForTime(retryTimeout, async () => { - const response = await supertestWithoutAuth - .get(`/api/alerting/rule/${ruleId}`) - .set(svlCommonApi.getInternalRequestHeader()) - .set(roleAuthc.apiKeyHeader) - .timeout(requestTimeout); - const { execution_status: executionStatus } = response.body || {}; - const { status } = executionStatus || {}; - if (status !== expectedStatus) { - throw new Error(`waitForStatus(${expectedStatus}): got ${status}`); - } - return executionStatus?.status; - }); - }, - - async waitForDocumentInIndex({ - indexName, - docCountTarget = 1, - }: { - indexName: string; - docCountTarget?: number; - }): Promise>> { - return await retry.tryForTime(retryTimeout, async () => { - const response = await es.search({ - index: indexName, - rest_total_hits_as_int: true, - }); - logger.debug(`Found ${response.hits.total} docs, looking for atleast ${docCountTarget}.`); - if (!response.hits.total || (response.hits.total as number) < docCountTarget) { - throw new Error('No hits found'); - } - return response; - }); - }, - - async waitForAlertInIndex({ - indexName, - ruleId, - }: { - indexName: string; - ruleId: string; - }): Promise>> { - if (!ruleId) { - throw new Error(`'ruleId' is undefined`); - } - return await retry.tryForTime(retryTimeout, async () => { - const response = await es.search({ - index: indexName, - body: { - query: { - term: { - 'kibana.alert.rule.uuid': ruleId, - }, - }, - }, - }); - if (response.hits.hits.length === 0) { - throw new Error('No hits found'); - } - return response; - }); - }, - - async createIndexConnector({ - roleAuthc, - name, - indexName, - }: { - roleAuthc: RoleCredentials; - name: string; - indexName: string; - }) { - const { body } = await supertestWithoutAuth - .post(`/api/actions/connector`) - .set(svlCommonApi.getInternalRequestHeader()) - .set(roleAuthc.apiKeyHeader) - .send({ - name, - config: { - index: indexName, - refresh: true, - }, - connector_type_id: '.index', - }); - return body.id as string; - }, - - async createRule({ - roleAuthc, - name, - ruleTypeId, - params, - actions = [], - tags = [], - schedule, - consumer, - }: { - roleAuthc: RoleCredentials; - ruleTypeId: string; - name: string; - params: MetricThresholdParams | ThresholdParams | SloBurnRateRuleParams; - actions?: any[]; - tags?: any[]; - schedule?: { interval: string }; - consumer: string; - }) { - const { body } = await supertestWithoutAuth - .post(`/api/alerting/rule`) - .set(svlCommonApi.getInternalRequestHeader()) - .set(roleAuthc.apiKeyHeader) - .send({ - params, - consumer, - schedule: schedule || { - interval: '5m', - }, - tags, - name, - rule_type_id: ruleTypeId, - actions, - }); - return body; - }, - - async findRule(roleAuthc: RoleCredentials, ruleId: string) { - if (!ruleId) { - throw new Error(`'ruleId' is undefined`); - } - const response = await supertestWithoutAuth - .get('/api/alerting/rules/_find') - .set(svlCommonApi.getInternalRequestHeader()) - .set(roleAuthc.apiKeyHeader); - return response.body.data.find((obj: any) => obj.id === ruleId); - }, - }; -} diff --git a/x-pack/test_serverless/api_integration/services/index.ts b/x-pack/test_serverless/api_integration/services/index.ts index 347fc1f68b0ca..22ce9b3bb4794 100644 --- a/x-pack/test_serverless/api_integration/services/index.ts +++ b/x-pack/test_serverless/api_integration/services/index.ts @@ -9,7 +9,6 @@ import { GenericFtrProviderContext } from '@kbn/test'; import { services as deploymentAgnosticSharedServices } from '../../shared/services/deployment_agnostic_services'; import { services as svlSharedServices } from '../../shared/services'; -import { AlertingApiProvider } from './alerting_api'; import { SamlToolsProvider } from './saml_tools'; import { SvlCasesServiceProvider } from './svl_cases'; import { SloApiProvider } from './slo_api'; @@ -35,7 +34,6 @@ export const services = { // serverless FTR services ...svlSharedServices, - alertingApi: AlertingApiProvider, samlTools: SamlToolsProvider, svlCases: SvlCasesServiceProvider, sloApi: SloApiProvider, diff --git a/x-pack/test_serverless/api_integration/test_suites/common/alerting/alert_documents.ts b/x-pack/test_serverless/api_integration/test_suites/common/alerting/alert_documents.ts index 93dd4e5565db5..cf727fd9fd1bc 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/alerting/alert_documents.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/alerting/alert_documents.ts @@ -42,22 +42,18 @@ import { ALERT_PREVIOUS_ACTION_GROUP, } from '@kbn/rule-data-utils'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { createEsQueryRule } from './helpers/alerting_api_helper'; -import { waitForAlertInIndex, waitForNumRuleRuns } from './helpers/alerting_wait_for_helpers'; import { ObjectRemover } from '../../../../shared/lib'; -import { InternalRequestHeader, RoleCredentials } from '../../../../shared/services'; +import { RoleCredentials } from '../../../../shared/services'; const OPEN_OR_ACTIVE = new Set(['open', 'active']); export default function ({ getService }: FtrProviderContext) { - const svlCommonApi = getService('svlCommonApi'); const svlUserManager = getService('svlUserManager'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); let roleAdmin: RoleCredentials; - let internalReqHeader: InternalRequestHeader; const supertest = getService('supertest'); const esClient = getService('es'); const objectRemover = new ObjectRemover(supertest); + const alertingApi = getService('alertingApi'); describe('Alert documents', function () { // Timeout of 360000ms exceeded @@ -68,7 +64,6 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { roleAdmin = await svlUserManager.createM2mApiKeyWithRoleScope('admin'); - internalReqHeader = svlCommonApi.getInternalRequestHeader(); }); afterEach(async () => { @@ -80,10 +75,8 @@ export default function ({ getService }: FtrProviderContext) { }); it('should generate an alert document for an active alert', async () => { - const createdRule = await createEsQueryRule({ - supertestWithoutAuth, + const createdRule = await alertingApi.helpers.createEsQueryRule({ roleAuthc: roleAdmin, - internalReqHeader, consumer: 'alerts', name: 'always fire', ruleTypeId: RULE_TYPE_ID, @@ -103,17 +96,15 @@ export default function ({ getService }: FtrProviderContext) { // get the first alert document written const testStart1 = new Date(); - await waitForNumRuleRuns({ - supertestWithoutAuth, + await alertingApi.helpers.waitForNumRuleRuns({ roleAuthc: roleAdmin, - internalReqHeader, numOfRuns: 1, ruleId, esClient, testStart: testStart1, }); - const alResp1 = await waitForAlertInIndex({ + const alResp1 = await alertingApi.helpers.waitForAlertInIndex({ esClient, filter: testStart1, indexName: ALERT_INDEX, @@ -206,10 +197,8 @@ export default function ({ getService }: FtrProviderContext) { }); it('should update an alert document for an ongoing alert', async () => { - const createdRule = await createEsQueryRule({ - supertestWithoutAuth, + const createdRule = await alertingApi.helpers.createEsQueryRule({ roleAuthc: roleAdmin, - internalReqHeader, consumer: 'alerts', name: 'always fire', ruleTypeId: RULE_TYPE_ID, @@ -229,17 +218,15 @@ export default function ({ getService }: FtrProviderContext) { // get the first alert document written const testStart1 = new Date(); - await waitForNumRuleRuns({ - supertestWithoutAuth, + await alertingApi.helpers.waitForNumRuleRuns({ roleAuthc: roleAdmin, - internalReqHeader, numOfRuns: 1, ruleId, esClient, testStart: testStart1, }); - const alResp1 = await waitForAlertInIndex({ + const alResp1 = await alertingApi.helpers.waitForAlertInIndex({ esClient, filter: testStart1, indexName: ALERT_INDEX, @@ -249,17 +236,15 @@ export default function ({ getService }: FtrProviderContext) { // wait for another run, get the updated alert document const testStart2 = new Date(); - await waitForNumRuleRuns({ - supertestWithoutAuth, + await alertingApi.helpers.waitForNumRuleRuns({ roleAuthc: roleAdmin, - internalReqHeader, numOfRuns: 1, ruleId, esClient, testStart: testStart2, }); - const alResp2 = await waitForAlertInIndex({ + const alResp2 = await alertingApi.helpers.waitForAlertInIndex({ esClient, filter: testStart2, indexName: ALERT_INDEX, diff --git a/x-pack/test_serverless/api_integration/test_suites/common/alerting/helpers/alerting_api_helper.ts b/x-pack/test_serverless/api_integration/test_suites/common/alerting/helpers/alerting_api_helper.ts deleted file mode 100644 index f7a909c688d0e..0000000000000 --- a/x-pack/test_serverless/api_integration/test_suites/common/alerting/helpers/alerting_api_helper.ts +++ /dev/null @@ -1,478 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment'; -import { v4 as uuidv4 } from 'uuid'; -import type { Agent as SuperTestAgent } from 'supertest'; -import { - InternalRequestHeader, - RoleCredentials, - SupertestWithoutAuthProviderType, -} from '../../../../../shared/services'; - -interface CreateEsQueryRuleParams { - size: number; - thresholdComparator: string; - threshold: number[]; - timeWindowSize?: number; - timeWindowUnit?: string; - esQuery?: string; - timeField?: string; - searchConfiguration?: unknown; - indexName?: string; - excludeHitsFromPreviousRun?: boolean; - aggType?: string; - aggField?: string; - groupBy?: string; - termField?: string; - termSize?: number; - index?: string[]; -} - -export async function createIndexConnector({ - supertestWithoutAuth, - roleAuthc, - internalReqHeader, - name, - indexName, -}: { - supertestWithoutAuth: SupertestWithoutAuthProviderType; - roleAuthc: RoleCredentials; - internalReqHeader: InternalRequestHeader; - name: string; - indexName: string; -}) { - const { body } = await supertestWithoutAuth - .post(`/api/actions/connector`) - .set(internalReqHeader) - .set(roleAuthc.apiKeyHeader) - .send({ - name, - config: { - index: indexName, - refresh: true, - }, - connector_type_id: '.index', - }) - .expect(200); - return body; -} - -export async function createSlackConnector({ - supertestWithoutAuth, - roleAuthc, - internalReqHeader, - name, -}: { - supertestWithoutAuth: SupertestWithoutAuthProviderType; - roleAuthc: RoleCredentials; - internalReqHeader: InternalRequestHeader; - name: string; -}) { - const { body } = await supertestWithoutAuth - .post(`/api/actions/connector`) - .set(internalReqHeader) - .set(roleAuthc.apiKeyHeader) - .send({ - name, - config: {}, - secrets: { - webhookUrl: 'http://test', - }, - connector_type_id: '.slack', - }) - .expect(200); - return body; -} - -export async function createEsQueryRule({ - supertestWithoutAuth, - roleAuthc, - internalReqHeader, - name, - ruleTypeId, - params, - actions = [], - tags = [], - schedule, - consumer, - notifyWhen, - enabled = true, -}: { - supertestWithoutAuth: SupertestWithoutAuthProviderType; - roleAuthc: RoleCredentials; - internalReqHeader: InternalRequestHeader; - ruleTypeId: string; - name: string; - params: CreateEsQueryRuleParams; - consumer: string; - actions?: any[]; - tags?: any[]; - schedule?: { interval: string }; - notifyWhen?: string; - enabled?: boolean; -}) { - const { body } = await supertestWithoutAuth - .post(`/api/alerting/rule`) - .set(internalReqHeader) - .set(roleAuthc.apiKeyHeader) - .send({ - enabled, - params, - consumer, - schedule: schedule || { - interval: '1h', - }, - tags, - name, - rule_type_id: ruleTypeId, - actions, - ...(notifyWhen ? { notify_when: notifyWhen, throttle: '5m' } : {}), - }) - .expect(200); - return body; -} - -export const generateUniqueKey = () => uuidv4().replace(/-/g, ''); - -export async function createAnomalyRule({ - supertest, - name = generateUniqueKey(), - actions = [], - tags = ['foo', 'bar'], - schedule, - consumer = 'alerts', - notifyWhen, - enabled = true, - ruleTypeId = 'apm.anomaly', - params, -}: { - supertest: SuperTestAgent; - name?: string; - consumer?: string; - actions?: any[]; - tags?: any[]; - schedule?: { interval: string }; - notifyWhen?: string; - enabled?: boolean; - ruleTypeId?: string; - params?: any; -}) { - const { body } = await supertest - .post(`/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .set('x-elastic-internal-origin', 'foo') - .send({ - enabled, - params: params || { - anomalySeverityType: 'critical', - anomalyDetectorTypes: ['txLatency'], - environment: 'ENVIRONMENT_ALL', - windowSize: 30, - windowUnit: 'm', - }, - consumer, - schedule: schedule || { - interval: '1m', - }, - tags, - name, - rule_type_id: ruleTypeId, - actions, - ...(notifyWhen ? { notify_when: notifyWhen, throttle: '5m' } : {}), - }) - .expect(200); - return body; -} - -export async function createLatencyThresholdRule({ - supertest, - name = generateUniqueKey(), - actions = [], - tags = ['foo', 'bar'], - schedule, - consumer = 'apm', - notifyWhen, - enabled = true, - ruleTypeId = 'apm.transaction_duration', - params, -}: { - supertest: SuperTestAgent; - name?: string; - consumer?: string; - actions?: any[]; - tags?: any[]; - schedule?: { interval: string }; - notifyWhen?: string; - enabled?: boolean; - ruleTypeId?: string; - params?: any; -}) { - const { body } = await supertest - .post(`/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .set('x-elastic-internal-origin', 'foo') - .send({ - enabled, - params: params || { - aggregationType: 'avg', - environment: 'ENVIRONMENT_ALL', - threshold: 1500, - windowSize: 5, - windowUnit: 'm', - }, - consumer, - schedule: schedule || { - interval: '1m', - }, - tags, - name, - rule_type_id: ruleTypeId, - actions, - ...(notifyWhen ? { notify_when: notifyWhen, throttle: '5m' } : {}), - }); - return body; -} - -export async function createInventoryRule({ - supertest, - name = generateUniqueKey(), - actions = [], - tags = ['foo', 'bar'], - schedule, - consumer = 'alerts', - notifyWhen, - enabled = true, - ruleTypeId = 'metrics.alert.inventory.threshold', - params, -}: { - supertest: SuperTestAgent; - name?: string; - consumer?: string; - actions?: any[]; - tags?: any[]; - schedule?: { interval: string }; - notifyWhen?: string; - enabled?: boolean; - ruleTypeId?: string; - params?: any; -}) { - const { body } = await supertest - .post(`/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .set('x-elastic-internal-origin', 'foo') - .send({ - enabled, - params: params || { - nodeType: 'host', - criteria: [ - { - metric: 'cpu', - comparator: '>', - threshold: [5], - timeSize: 1, - timeUnit: 'm', - customMetric: { - type: 'custom', - id: 'alert-custom-metric', - field: '', - aggregation: 'avg', - }, - }, - ], - sourceId: 'default', - }, - consumer, - schedule: schedule || { - interval: '1m', - }, - tags, - name, - rule_type_id: ruleTypeId, - actions, - ...(notifyWhen ? { notify_when: notifyWhen, throttle: '5m' } : {}), - }) - .expect(200); - return body; -} - -export async function disableRule({ - supertest, - ruleId, -}: { - supertest: SuperTestAgent; - ruleId: string; -}) { - const { body } = await supertest - .post(`/api/alerting/rule/${ruleId}/_disable`) - .set('kbn-xsrf', 'foo') - .set('x-elastic-internal-origin', 'foo') - .expect(204); - return body; -} - -export async function updateEsQueryRule({ - supertest, - ruleId, - updates, -}: { - supertest: SuperTestAgent; - ruleId: string; - updates: any; -}) { - const { body: r } = await supertest - .get(`/api/alerting/rule/${ruleId}`) - .set('kbn-xsrf', 'foo') - .set('x-elastic-internal-origin', 'foo') - .expect(200); - const body = await supertest - .put(`/api/alerting/rule/${ruleId}`) - .set('kbn-xsrf', 'foo') - .set('x-elastic-internal-origin', 'foo') - .send({ - ...{ - name: r.name, - schedule: r.schedule, - throttle: r.throttle, - tags: r.tags, - params: r.params, - notify_when: r.notifyWhen, - actions: r.actions.map((action: any) => ({ - group: action.group, - params: action.params, - id: action.id, - frequency: action.frequency, - })), - }, - ...updates, - }) - .expect(200); - return body; -} - -export async function runRule({ - supertestWithoutAuth, - roleAuthc, - internalReqHeader, - ruleId, -}: { - supertestWithoutAuth: SupertestWithoutAuthProviderType; - roleAuthc: RoleCredentials; - internalReqHeader: InternalRequestHeader; - ruleId: string; -}) { - const response = await supertestWithoutAuth - .post(`/internal/alerting/rule/${ruleId}/_run_soon`) - .set(internalReqHeader) - .set(roleAuthc.apiKeyHeader) - .expect(204); - return response; -} - -export async function muteRule({ - supertest, - ruleId, -}: { - supertest: SuperTestAgent; - ruleId: string; -}) { - const { body } = await supertest - .post(`/api/alerting/rule/${ruleId}/_mute_all`) - .set('kbn-xsrf', 'foo') - .set('x-elastic-internal-origin', 'foo') - .expect(204); - return body; -} - -export async function enableRule({ - supertest, - ruleId, -}: { - supertest: SuperTestAgent; - ruleId: string; -}) { - const { body } = await supertest - .post(`/api/alerting/rule/${ruleId}/_enable`) - .set('kbn-xsrf', 'foo') - .set('x-elastic-internal-origin', 'foo') - .expect(204); - return body; -} - -export async function muteAlert({ - supertest, - ruleId, - alertId, -}: { - supertest: SuperTestAgent; - ruleId: string; - alertId: string; -}) { - const { body } = await supertest - .post(`/api/alerting/rule/${ruleId}/alert/${alertId}/_mute`) - .set('kbn-xsrf', 'foo') - .set('x-elastic-internal-origin', 'foo') - .expect(204); - return body; -} - -export async function unmuteRule({ - supertest, - ruleId, -}: { - supertest: SuperTestAgent; - ruleId: string; -}) { - const { body } = await supertest - .post(`/api/alerting/rule/${ruleId}/_unmute_all`) - .set('kbn-xsrf', 'foo') - .set('x-elastic-internal-origin', 'foo') - .expect(204); - return body; -} - -export async function snoozeRule({ - supertest, - ruleId, -}: { - supertest: SuperTestAgent; - ruleId: string; -}) { - const { body } = await supertest - .post(`/internal/alerting/rule/${ruleId}/_snooze`) - .set('kbn-xsrf', 'foo') - .set('x-elastic-internal-origin', 'foo') - .send({ - snooze_schedule: { - duration: 100000000, - rRule: { - count: 1, - dtstart: moment().format(), - tzid: 'UTC', - }, - }, - }) - .expect(204); - return body; -} - -export async function findRule({ - supertest, - ruleId, -}: { - supertest: SuperTestAgent; - ruleId: string; -}) { - if (!ruleId) { - throw new Error(`'ruleId' is undefined`); - } - const response = await supertest - .get(`/api/alerting/rule/${ruleId}`) - .set('kbn-xsrf', 'foo') - .set('x-elastic-internal-origin', 'foo'); - return response.body || {}; -} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/alerting/helpers/alerting_wait_for_helpers.ts b/x-pack/test_serverless/api_integration/test_suites/common/alerting/helpers/alerting_wait_for_helpers.ts deleted file mode 100644 index c7f2ac357e4a2..0000000000000 --- a/x-pack/test_serverless/api_integration/test_suites/common/alerting/helpers/alerting_wait_for_helpers.ts +++ /dev/null @@ -1,402 +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 pRetry from 'p-retry'; -import type { Client } from '@elastic/elasticsearch'; -import type { - AggregationsAggregate, - SearchResponse, -} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { runRule } from './alerting_api_helper'; -import type { SupertestWithoutAuthProviderType } from '../../../../../shared/services'; -import { RoleCredentials } from '../../../../../shared/services'; -import { InternalRequestHeader } from '../../../../../shared/services'; - -export async function waitForDocumentInIndex({ - esClient, - indexName, - ruleId, - num = 1, - sort = 'desc', -}: { - esClient: Client; - indexName: string; - ruleId: string; - num?: number; - sort?: 'asc' | 'desc'; -}): Promise { - return await pRetry( - async () => { - const response = await esClient.search({ - index: indexName, - sort: `date:${sort}`, - body: { - query: { - bool: { - must: [ - { - term: { - 'ruleId.keyword': ruleId, - }, - }, - ], - }, - }, - }, - }); - if (response.hits.hits.length < num) { - throw new Error(`Only found ${response.hits.hits.length} / ${num} documents`); - } - return response; - }, - { retries: 10 } - ); -} - -export async function getDocumentsInIndex({ - esClient, - indexName, - ruleId, -}: { - esClient: Client; - indexName: string; - ruleId: string; -}): Promise { - return await esClient.search({ - index: indexName, - body: { - query: { - bool: { - must: [ - { - term: { - 'ruleId.keyword': ruleId, - }, - }, - ], - }, - }, - }, - }); -} - -export async function createIndex({ - esClient, - indexName, -}: { - esClient: Client; - indexName: string; -}) { - return await esClient.indices.create( - { - index: indexName, - body: {}, - }, - { meta: true } - ); -} - -export async function waitForAlertInIndex({ - esClient, - filter, - indexName, - ruleId, - num = 1, -}: { - esClient: Client; - filter: Date; - indexName: string; - ruleId: string; - num: number; -}): Promise>> { - return await pRetry( - async () => { - const response = await esClient.search({ - index: indexName, - body: { - query: { - bool: { - must: [ - { - term: { - 'kibana.alert.rule.uuid': ruleId, - }, - }, - { - range: { - '@timestamp': { - gte: filter.getTime().toString(), - }, - }, - }, - ], - }, - }, - }, - }); - if (response.hits.hits.length < num) { - throw new Error(`Only found ${response.hits.hits.length} / ${num} documents`); - } - return response; - }, - { retries: 10 } - ); -} - -export async function waitForAllTasksIdle({ - esClient, - filter, -}: { - esClient: Client; - filter: Date; -}): Promise { - return await pRetry( - async () => { - const response = await esClient.search({ - index: '.kibana_task_manager', - body: { - query: { - bool: { - must: [ - { - terms: { - 'task.scope': ['actions', 'alerting'], - }, - }, - { - range: { - 'task.scheduledAt': { - gte: filter.getTime().toString(), - }, - }, - }, - ], - must_not: [ - { - term: { - 'task.status': 'idle', - }, - }, - ], - }, - }, - }, - }); - if (response.hits.hits.length !== 0) { - throw new Error(`Expected 0 hits but received ${response.hits.hits.length}`); - } - return response; - }, - { retries: 10 } - ); -} - -export async function waitForAllTasks({ - esClient, - filter, - taskType, - attempts, -}: { - esClient: Client; - filter: Date; - taskType: string; - attempts: number; -}): Promise { - return await pRetry( - async () => { - const response = await esClient.search({ - index: '.kibana_task_manager', - body: { - query: { - bool: { - must: [ - { - term: { - 'task.status': 'idle', - }, - }, - { - term: { - 'task.attempts': attempts, - }, - }, - { - terms: { - 'task.scope': ['actions', 'alerting'], - }, - }, - { - term: { - 'task.taskType': taskType, - }, - }, - { - range: { - 'task.scheduledAt': { - gte: filter.getTime().toString(), - }, - }, - }, - ], - }, - }, - }, - }); - if (response.hits.hits.length === 0) { - throw new Error('No hits found'); - } - return response; - }, - { retries: 10 } - ); -} - -export async function waitForDisabled({ - esClient, - ruleId, - filter, -}: { - esClient: Client; - ruleId: string; - filter: Date; -}): Promise { - return await pRetry( - async () => { - const response = await esClient.search({ - index: '.kibana_task_manager', - body: { - query: { - bool: { - must: [ - { - term: { - 'task.id': `task:${ruleId}`, - }, - }, - { - terms: { - 'task.scope': ['actions', 'alerting'], - }, - }, - { - range: { - 'task.scheduledAt': { - gte: filter.getTime().toString(), - }, - }, - }, - { - term: { - 'task.enabled': true, - }, - }, - ], - }, - }, - }, - }); - if (response.hits.hits.length !== 0) { - throw new Error(`Expected 0 hits but received ${response.hits.hits.length}`); - } - return response; - }, - { retries: 10 } - ); -} - -export async function waitForExecutionEventLog({ - esClient, - filter, - ruleId, - num = 1, -}: { - esClient: Client; - filter: Date; - ruleId: string; - num?: number; -}): Promise { - return await pRetry( - async () => { - const response = await esClient.search({ - index: '.kibana-event-log*', - body: { - query: { - bool: { - filter: [ - { - term: { - 'rule.id': { - value: ruleId, - }, - }, - }, - { - term: { - 'event.provider': { - value: 'alerting', - }, - }, - }, - { - term: { - 'event.action': 'execute', - }, - }, - { - range: { - '@timestamp': { - gte: filter.getTime().toString(), - }, - }, - }, - ], - }, - }, - }, - }); - if (response.hits.hits.length < num) { - throw new Error('No hits found'); - } - return response; - }, - { retries: 10 } - ); -} - -export async function waitForNumRuleRuns({ - supertestWithoutAuth, - roleAuthc, - internalReqHeader, - numOfRuns, - ruleId, - esClient, - testStart, -}: { - supertestWithoutAuth: SupertestWithoutAuthProviderType; - roleAuthc: RoleCredentials; - internalReqHeader: InternalRequestHeader; - numOfRuns: number; - ruleId: string; - esClient: Client; - testStart: Date; -}) { - for (let i = 0; i < numOfRuns; i++) { - await pRetry( - async () => { - await runRule({ supertestWithoutAuth, roleAuthc, internalReqHeader, ruleId }); - await waitForExecutionEventLog({ - esClient, - filter: testStart, - ruleId, - num: i + 1, - }); - await waitForAllTasksIdle({ esClient, filter: testStart }); - }, - { retries: 10 } - ); - } -} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/alerting/rules.ts b/x-pack/test_serverless/api_integration/test_suites/common/alerting/rules.ts index 37b78a5e1b36f..593c10f371f09 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/alerting/rules.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/alerting/rules.ts @@ -9,28 +9,6 @@ import { RULE_SAVED_OBJECT_TYPE } from '@kbn/alerting-plugin/server'; import expect from '@kbn/expect'; import { omit } from 'lodash'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { - createIndexConnector, - createEsQueryRule, - disableRule, - updateEsQueryRule, - runRule, - muteRule, - enableRule, - muteAlert, - unmuteRule, - createSlackConnector, -} from './helpers/alerting_api_helper'; -import { - createIndex, - getDocumentsInIndex, - waitForAllTasks, - waitForAllTasksIdle, - waitForDisabled, - waitForDocumentInIndex, - waitForExecutionEventLog, - waitForNumRuleRuns, -} from './helpers/alerting_wait_for_helpers'; import type { InternalRequestHeader, RoleCredentials } from '../../../../shared/services'; export default function ({ getService }: FtrProviderContext) { @@ -39,7 +17,8 @@ export default function ({ getService }: FtrProviderContext) { const esDeleteAllIndices = getService('esDeleteAllIndices'); const svlCommonApi = getService('svlCommonApi'); const svlUserManager = getService('svlUserManager'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); + const alertingApi = getService('alertingApi'); + let roleAdmin: RoleCredentials; let internalReqHeader: InternalRequestHeader; @@ -73,19 +52,15 @@ export default function ({ getService }: FtrProviderContext) { it('should schedule task, run rule and schedule actions when appropriate', async () => { const testStart = new Date(); - const createdConnector = await createIndexConnector({ - supertestWithoutAuth, + const createdConnector = await alertingApi.helpers.createIndexConnector({ roleAuthc: roleAdmin, - internalReqHeader, name: 'Index Connector: Alerting API test', indexName: ALERT_ACTION_INDEX, }); connectorId = createdConnector.id; - const createdRule = await createEsQueryRule({ - supertestWithoutAuth, + const createdRule = await alertingApi.helpers.createEsQueryRule({ roleAuthc: roleAdmin, - internalReqHeader, consumer: 'alerts', name: 'always fire', ruleTypeId: RULE_TYPE_ID, @@ -130,10 +105,14 @@ export default function ({ getService }: FtrProviderContext) { ruleId = createdRule.id; // Wait for the action to index a document before disabling the alert and waiting for tasks to finish - const resp = await waitForDocumentInIndex({ + const resp = await alertingApi.helpers.waitForDocumentInIndex({ esClient, indexName: ALERT_ACTION_INDEX, ruleId, + retryOptions: { + retryCount: 12, + retryDelay: 2000, + }, }); expect(resp.hits.hits.length).to.be(1); @@ -151,7 +130,7 @@ export default function ({ getService }: FtrProviderContext) { tags: '', }); - const eventLogResp = await waitForExecutionEventLog({ + const eventLogResp = await alertingApi.helpers.waiting.waitForExecutionEventLog({ esClient, filter: testStart, ruleId, @@ -171,19 +150,15 @@ export default function ({ getService }: FtrProviderContext) { it('should pass updated rule params to executor', async () => { const testStart = new Date(); - const createdConnector = await createIndexConnector({ - supertestWithoutAuth, + const createdConnector = await alertingApi.helpers.createIndexConnector({ roleAuthc: roleAdmin, - internalReqHeader, name: 'Index Connector: Alerting API test', indexName: ALERT_ACTION_INDEX, }); connectorId = createdConnector.id; - const createdRule = await createEsQueryRule({ - supertestWithoutAuth, + const createdRule = await alertingApi.helpers.createEsQueryRule({ roleAuthc: roleAdmin, - internalReqHeader, consumer: 'alerts', name: 'always fire', ruleTypeId: RULE_TYPE_ID, @@ -228,10 +203,11 @@ export default function ({ getService }: FtrProviderContext) { ruleId = createdRule.id; // Wait for the action to index a document before disabling the alert and waiting for tasks to finish - const resp = await waitForDocumentInIndex({ + const resp = await alertingApi.helpers.waitForDocumentInIndex({ esClient, indexName: ALERT_ACTION_INDEX, ruleId, + retryOptions: { retryDelay: 800, retryCount: 10 }, }); expect(resp.hits.hits.length).to.be(1); @@ -249,13 +225,13 @@ export default function ({ getService }: FtrProviderContext) { tags: '', }); - await waitForAllTasksIdle({ + await alertingApi.helpers.waiting.waitForAllTasksIdle({ esClient, filter: testStart, }); - await updateEsQueryRule({ - supertest, + await alertingApi.helpers.updateEsQueryRule({ + roleAuthc: roleAdmin, ruleId, updates: { name: 'def', @@ -263,15 +239,13 @@ export default function ({ getService }: FtrProviderContext) { }, }); - await runRule({ - supertestWithoutAuth, + await alertingApi.helpers.runRule({ roleAuthc: roleAdmin, - internalReqHeader, ruleId, }); // make sure alert info passed to executor is correct - const resp2 = await waitForDocumentInIndex({ + const resp2 = await alertingApi.helpers.waitForDocumentInIndex({ esClient, indexName: ALERT_ACTION_INDEX, ruleId, @@ -298,18 +272,14 @@ export default function ({ getService }: FtrProviderContext) { const testStart = new Date(); // Should fail - const createdConnector = await createSlackConnector({ - supertestWithoutAuth, + const createdConnector = await alertingApi.helpers.createSlackConnector({ roleAuthc: roleAdmin, - internalReqHeader, name: 'Slack Connector: Alerting API test', }); connectorId = createdConnector.id; - const createdRule = await createEsQueryRule({ - supertestWithoutAuth, + const createdRule = await alertingApi.helpers.createEsQueryRule({ roleAuthc: roleAdmin, - internalReqHeader, consumer: 'alerts', name: 'always fire', ruleTypeId: RULE_TYPE_ID, @@ -341,7 +311,7 @@ export default function ({ getService }: FtrProviderContext) { ruleId = createdRule.id; // Should retry when the the action fails - const resp = await waitForAllTasks({ + const resp = await alertingApi.helpers.waiting.waitForAllTasks({ esClient, filter: testStart, taskType: 'actions:.slack', @@ -353,19 +323,15 @@ export default function ({ getService }: FtrProviderContext) { it('should throttle alerts when appropriate', async () => { const testStart = new Date(); - const createdConnector = await createIndexConnector({ - supertestWithoutAuth, + const createdConnector = await alertingApi.helpers.createIndexConnector({ roleAuthc: roleAdmin, - internalReqHeader, name: 'Index Connector: Alerting API test', indexName: ALERT_ACTION_INDEX, }); connectorId = createdConnector.id; - const createdRule = await createEsQueryRule({ - supertestWithoutAuth, + const createdRule = await alertingApi.helpers.createEsQueryRule({ roleAuthc: roleAdmin, - internalReqHeader, consumer: 'alerts', name: 'always fire', ruleTypeId: RULE_TYPE_ID, @@ -407,29 +373,27 @@ export default function ({ getService }: FtrProviderContext) { ruleId = createdRule.id; // Wait until alerts ran at least 3 times before disabling the alert and waiting for tasks to finish - await waitForNumRuleRuns({ - supertestWithoutAuth, + await alertingApi.helpers.waitForNumRuleRuns({ roleAuthc: roleAdmin, - internalReqHeader, numOfRuns: 3, ruleId, esClient, testStart, }); - await disableRule({ - supertest, + await alertingApi.helpers.disableRule({ + roleAuthc: roleAdmin, ruleId, }); - await waitForDisabled({ + await alertingApi.helpers.waiting.waitForDisabled({ esClient, ruleId, filter: testStart, }); // Ensure actions only executed once - const resp = await waitForDocumentInIndex({ + const resp = await alertingApi.helpers.waitForDocumentInIndex({ esClient, indexName: ALERT_ACTION_INDEX, ruleId, @@ -440,19 +404,15 @@ export default function ({ getService }: FtrProviderContext) { it('should throttle alerts with throttled action when appropriate', async () => { const testStart = new Date(); - const createdConnector = await createIndexConnector({ - supertestWithoutAuth, + const createdConnector = await alertingApi.helpers.createIndexConnector({ roleAuthc: roleAdmin, - internalReqHeader, name: 'Index Connector: Alerting API test', indexName: ALERT_ACTION_INDEX, }); connectorId = createdConnector.id; - const createdRule = await createEsQueryRule({ - supertestWithoutAuth, + const createdRule = await alertingApi.helpers.createEsQueryRule({ roleAuthc: roleAdmin, - internalReqHeader, consumer: 'alerts', name: 'always fire', ruleTypeId: RULE_TYPE_ID, @@ -498,29 +458,27 @@ export default function ({ getService }: FtrProviderContext) { ruleId = createdRule.id; // Wait until alerts ran at least 3 times before disabling the alert and waiting for tasks to finish - await waitForNumRuleRuns({ - supertestWithoutAuth, + await alertingApi.helpers.waitForNumRuleRuns({ roleAuthc: roleAdmin, - internalReqHeader, numOfRuns: 3, ruleId, esClient, testStart, }); - await disableRule({ - supertest, + await alertingApi.helpers.disableRule({ + roleAuthc: roleAdmin, ruleId, }); - await waitForDisabled({ + await alertingApi.helpers.waiting.waitForDisabled({ esClient, ruleId, filter: testStart, }); // Ensure actions only executed once - const resp = await waitForDocumentInIndex({ + const resp = await alertingApi.helpers.waitForDocumentInIndex({ esClient, indexName: ALERT_ACTION_INDEX, ruleId, @@ -531,19 +489,15 @@ export default function ({ getService }: FtrProviderContext) { it('should reset throttle window when not firing and should not throttle when changing groups', async () => { const testStart = new Date(); - const createdConnector = await createIndexConnector({ - supertestWithoutAuth, + const createdConnector = await alertingApi.helpers.createIndexConnector({ roleAuthc: roleAdmin, - internalReqHeader, name: 'Index Connector: Alerting API test', indexName: ALERT_ACTION_INDEX, }); connectorId = createdConnector.id; - const createdRule = await createEsQueryRule({ - supertestWithoutAuth, + const createdRule = await alertingApi.helpers.createEsQueryRule({ roleAuthc: roleAdmin, - internalReqHeader, consumer: 'alerts', name: 'always fire', ruleTypeId: RULE_TYPE_ID, @@ -614,21 +568,21 @@ export default function ({ getService }: FtrProviderContext) { ruleId = createdRule.id; // Wait for the action to index a document - const resp = await waitForDocumentInIndex({ + const resp = await alertingApi.helpers.waiting.waitForDocumentInIndex({ esClient, indexName: ALERT_ACTION_INDEX, ruleId, }); expect(resp.hits.hits.length).to.be(1); - await waitForAllTasksIdle({ + await alertingApi.helpers.waiting.waitForAllTasksIdle({ esClient, filter: testStart, }); // Update the rule to recover - await updateEsQueryRule({ - supertest, + await alertingApi.helpers.updateEsQueryRule({ + roleAuthc: roleAdmin, ruleId, updates: { name: 'never fire', @@ -645,34 +599,36 @@ export default function ({ getService }: FtrProviderContext) { }, }); - await runRule({ - supertestWithoutAuth, + await alertingApi.helpers.runRule({ roleAuthc: roleAdmin, - internalReqHeader, ruleId, }); - const eventLogResp = await waitForExecutionEventLog({ + const eventLogResp = await alertingApi.helpers.waiting.waitForExecutionEventLog({ esClient, filter: testStart, ruleId, num: 2, + retryOptions: { + retryCount: 12, + retryDelay: 2000, + }, }); expect(eventLogResp.hits.hits.length).to.be(2); - await disableRule({ - supertest, + await alertingApi.helpers.disableRule({ + roleAuthc: roleAdmin, ruleId, }); - await waitForDisabled({ + await alertingApi.helpers.waiting.waitForDisabled({ esClient, ruleId, filter: testStart, }); // Ensure only 2 actions are executed - const resp2 = await waitForDocumentInIndex({ + const resp2 = await alertingApi.helpers.waitForDocumentInIndex({ esClient, indexName: ALERT_ACTION_INDEX, ruleId, @@ -683,21 +639,17 @@ export default function ({ getService }: FtrProviderContext) { it(`shouldn't schedule actions when alert is muted`, async () => { const testStart = new Date(); - await createIndex({ esClient, indexName: ALERT_ACTION_INDEX }); + await alertingApi.helpers.waiting.createIndex({ esClient, indexName: ALERT_ACTION_INDEX }); - const createdConnector = await createIndexConnector({ - supertestWithoutAuth, + const createdConnector = await alertingApi.helpers.createIndexConnector({ roleAuthc: roleAdmin, - internalReqHeader, name: 'Index Connector: Alerting API test', indexName: ALERT_ACTION_INDEX, }); connectorId = createdConnector.id; - const createdRule = await createEsQueryRule({ - supertestWithoutAuth, + const createdRule = await alertingApi.helpers.createEsQueryRule({ roleAuthc: roleAdmin, - internalReqHeader, enabled: false, consumer: 'alerts', name: 'always fire', @@ -742,41 +694,39 @@ export default function ({ getService }: FtrProviderContext) { }); ruleId = createdRule.id; - await muteRule({ - supertest, + await alertingApi.helpers.muteRule({ + roleAuthc: roleAdmin, ruleId, }); - await enableRule({ - supertest, + await alertingApi.helpers.enableRule({ + roleAuthc: roleAdmin, ruleId, }); // Wait until alerts schedule actions twice to ensure actions had a chance to skip // execution once before disabling the alert and waiting for tasks to finish - await waitForNumRuleRuns({ - supertestWithoutAuth, + await alertingApi.helpers.waitForNumRuleRuns({ roleAuthc: roleAdmin, - internalReqHeader, numOfRuns: 2, ruleId, esClient, testStart, }); - await disableRule({ - supertest, + await alertingApi.helpers.disableRule({ + roleAuthc: roleAdmin, ruleId, }); - await waitForDisabled({ + await alertingApi.helpers.waiting.waitForDisabled({ esClient, ruleId, filter: testStart, }); // Should not have executed any action - const resp2 = await getDocumentsInIndex({ + const resp2 = await alertingApi.helpers.waiting.getDocumentsInIndex({ esClient, indexName: ALERT_ACTION_INDEX, ruleId, @@ -786,21 +736,17 @@ export default function ({ getService }: FtrProviderContext) { it(`shouldn't schedule actions when alert instance is muted`, async () => { const testStart = new Date(); - await createIndex({ esClient, indexName: ALERT_ACTION_INDEX }); + await alertingApi.helpers.waiting.createIndex({ esClient, indexName: ALERT_ACTION_INDEX }); - const createdConnector = await createIndexConnector({ - supertestWithoutAuth, + const createdConnector = await alertingApi.helpers.createIndexConnector({ roleAuthc: roleAdmin, - internalReqHeader, name: 'Index Connector: Alerting API test', indexName: ALERT_ACTION_INDEX, }); connectorId = createdConnector.id; - const createdRule = await createEsQueryRule({ - supertestWithoutAuth, + const createdRule = await alertingApi.helpers.createEsQueryRule({ roleAuthc: roleAdmin, - internalReqHeader, enabled: false, consumer: 'alerts', name: 'always fire', @@ -845,42 +791,40 @@ export default function ({ getService }: FtrProviderContext) { }); ruleId = createdRule.id; - await muteAlert({ - supertest, + await alertingApi.helpers.muteAlert({ + roleAuthc: roleAdmin, ruleId, alertId: 'query matched', }); - await enableRule({ - supertest, + await alertingApi.helpers.enableRule({ + roleAuthc: roleAdmin, ruleId, }); // Wait until alerts schedule actions twice to ensure actions had a chance to skip // execution once before disabling the alert and waiting for tasks to finish - await waitForNumRuleRuns({ - supertestWithoutAuth, + await alertingApi.helpers.waitForNumRuleRuns({ roleAuthc: roleAdmin, - internalReqHeader, numOfRuns: 2, ruleId, esClient, testStart, }); - await disableRule({ - supertest, + await alertingApi.helpers.disableRule({ + roleAuthc: roleAdmin, ruleId, }); - await waitForDisabled({ + await alertingApi.helpers.waiting.waitForDisabled({ esClient, ruleId, filter: testStart, }); // Should not have executed any action - const resp2 = await getDocumentsInIndex({ + const resp2 = await alertingApi.helpers.waiting.getDocumentsInIndex({ esClient, indexName: ALERT_ACTION_INDEX, ruleId, @@ -889,19 +833,15 @@ export default function ({ getService }: FtrProviderContext) { }); it(`should unmute all instances when unmuting an alert`, async () => { - const createdConnector = await createIndexConnector({ - supertestWithoutAuth, + const createdConnector = await alertingApi.helpers.createIndexConnector({ roleAuthc: roleAdmin, - internalReqHeader, name: 'Index Connector: Alerting API test', indexName: ALERT_ACTION_INDEX, }); connectorId = createdConnector.id; - const createdRule = await createEsQueryRule({ - supertestWithoutAuth, + const createdRule = await alertingApi.helpers.createEsQueryRule({ roleAuthc: roleAdmin, - internalReqHeader, enabled: false, consumer: 'alerts', name: 'always fire', @@ -946,29 +886,29 @@ export default function ({ getService }: FtrProviderContext) { }); ruleId = createdRule.id; - await muteAlert({ - supertest, + await alertingApi.helpers.muteAlert({ + roleAuthc: roleAdmin, ruleId, alertId: 'query matched', }); - await muteRule({ - supertest, + await alertingApi.helpers.muteRule({ + roleAuthc: roleAdmin, ruleId, }); - await unmuteRule({ - supertest, + await alertingApi.helpers.unmuteRule({ + roleAuthc: roleAdmin, ruleId, }); - await enableRule({ - supertest, + await alertingApi.helpers.enableRule({ + roleAuthc: roleAdmin, ruleId, }); // Should have one document indexed by the action - const resp = await waitForDocumentInIndex({ + const resp = await alertingApi.helpers.waitForDocumentInIndex({ esClient, indexName: ALERT_ACTION_INDEX, ruleId, diff --git a/x-pack/test_serverless/api_integration/test_suites/common/alerting/summary_actions.ts b/x-pack/test_serverless/api_integration/test_suites/common/alerting/summary_actions.ts index 995a7ee197610..ec63653bef7c7 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/alerting/summary_actions.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/alerting/summary_actions.ts @@ -29,27 +29,18 @@ import { } from '@kbn/rule-data-utils'; import { omit, padStart } from 'lodash'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { createIndexConnector, createEsQueryRule } from './helpers/alerting_api_helper'; -import { - createIndex, - getDocumentsInIndex, - waitForAlertInIndex, - waitForDocumentInIndex, -} from './helpers/alerting_wait_for_helpers'; -import { InternalRequestHeader, RoleCredentials } from '../../../../shared/services'; +import { RoleCredentials } from '../../../../shared/services'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esClient = getService('es'); const esDeleteAllIndices = getService('esDeleteAllIndices'); - - const svlCommonApi = getService('svlCommonApi'); const svlUserManager = getService('svlUserManager'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); + const alertingApi = getService('alertingApi'); let roleAdmin: RoleCredentials; - let internalReqHeader: InternalRequestHeader; - describe('Summary actions', function () { + // Failing: See https://github.com/elastic/kibana/issues/193061 + describe.skip('Summary actions', function () { const RULE_TYPE_ID = '.es-query'; const ALERT_ACTION_INDEX = 'alert-action-es-query'; const ALERT_INDEX = '.alerts-stack.alerts-default'; @@ -75,7 +66,6 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { roleAdmin = await svlUserManager.createM2mApiKeyWithRoleScope('admin'); - internalReqHeader = svlCommonApi.getInternalRequestHeader(); }); afterEach(async () => { @@ -98,19 +88,15 @@ export default function ({ getService }: FtrProviderContext) { it('should schedule actions for summary of alerts per rule run', async () => { const testStart = new Date(); - const createdConnector = await createIndexConnector({ - supertestWithoutAuth, + const createdConnector = await alertingApi.helpers.createIndexConnector({ roleAuthc: roleAdmin, - internalReqHeader, name: 'Index Connector: Alerting API test', indexName: ALERT_ACTION_INDEX, }); connectorId = createdConnector.id; - const createdRule = await createEsQueryRule({ - supertestWithoutAuth, + const createdRule = await alertingApi.helpers.createEsQueryRule({ roleAuthc: roleAdmin, - internalReqHeader, consumer: 'alerts', name: 'always fire', ruleTypeId: RULE_TYPE_ID, @@ -158,19 +144,27 @@ export default function ({ getService }: FtrProviderContext) { }); ruleId = createdRule.id; - const resp = await waitForDocumentInIndex({ + const resp = await alertingApi.helpers.waitForDocumentInIndex({ esClient, indexName: ALERT_ACTION_INDEX, ruleId, + retryOptions: { + retryCount: 20, + retryDelay: 15_000, + }, }); expect(resp.hits.hits.length).to.be(1); - const resp2 = await waitForAlertInIndex({ + const resp2 = await alertingApi.helpers.waitForAlertInIndex({ esClient, filter: testStart, indexName: ALERT_INDEX, ruleId, num: 1, + retryOptions: { + retryCount: 20, + retryDelay: 15_000, + }, }); expect(resp2.hits.hits.length).to.be(1); @@ -228,19 +222,15 @@ export default function ({ getService }: FtrProviderContext) { it('should filter alerts by kql', async () => { const testStart = new Date(); - const createdConnector = await createIndexConnector({ - supertestWithoutAuth, + const createdConnector = await alertingApi.helpers.createIndexConnector({ roleAuthc: roleAdmin, - internalReqHeader, name: 'Index Connector: Alerting API test', indexName: ALERT_ACTION_INDEX, }); connectorId = createdConnector.id; - const createdRule = await createEsQueryRule({ - supertestWithoutAuth, + const createdRule = await alertingApi.helpers.createEsQueryRule({ roleAuthc: roleAdmin, - internalReqHeader, consumer: 'alerts', name: 'always fire', ruleTypeId: RULE_TYPE_ID, @@ -288,19 +278,27 @@ export default function ({ getService }: FtrProviderContext) { }); ruleId = createdRule.id; - const resp = await waitForDocumentInIndex({ + const resp = await alertingApi.helpers.waitForDocumentInIndex({ esClient, indexName: ALERT_ACTION_INDEX, ruleId, + retryOptions: { + retryCount: 20, + retryDelay: 15_000, + }, }); expect(resp.hits.hits.length).to.be(1); - const resp2 = await waitForAlertInIndex({ + const resp2 = await alertingApi.helpers.waitForAlertInIndex({ esClient, filter: testStart, indexName: ALERT_INDEX, ruleId, num: 1, + retryOptions: { + retryCount: 20, + retryDelay: 15_000, + }, }); expect(resp2.hits.hits.length).to.be(1); @@ -365,21 +363,17 @@ export default function ({ getService }: FtrProviderContext) { const start = `${hour}:${minutes}`; const end = `${hour}:${minutes}`; - await createIndex({ esClient, indexName: ALERT_ACTION_INDEX }); + await alertingApi.helpers.waiting.createIndex({ esClient, indexName: ALERT_ACTION_INDEX }); - const createdConnector = await createIndexConnector({ - supertestWithoutAuth, + const createdConnector = await alertingApi.helpers.createIndexConnector({ roleAuthc: roleAdmin, - internalReqHeader, name: 'Index Connector: Alerting API test', indexName: ALERT_ACTION_INDEX, }); connectorId = createdConnector.id; - const createdRule = await createEsQueryRule({ - supertestWithoutAuth, + const createdRule = await alertingApi.helpers.createEsQueryRule({ roleAuthc: roleAdmin, - internalReqHeader, consumer: 'alerts', name: 'always fire', ruleTypeId: RULE_TYPE_ID, @@ -433,7 +427,7 @@ export default function ({ getService }: FtrProviderContext) { ruleId = createdRule.id; // Should not have executed any action - const resp = await getDocumentsInIndex({ + const resp = await alertingApi.helpers.waiting.getDocumentsInIndex({ esClient, indexName: ALERT_ACTION_INDEX, ruleId, @@ -443,19 +437,15 @@ export default function ({ getService }: FtrProviderContext) { it('should schedule actions for summary of alerts on a custom interval', async () => { const testStart = new Date(); - const createdConnector = await createIndexConnector({ - supertestWithoutAuth, + const createdConnector = await alertingApi.helpers.createIndexConnector({ roleAuthc: roleAdmin, - internalReqHeader, name: 'Index Connector: Alerting API test', indexName: ALERT_ACTION_INDEX, }); connectorId = createdConnector.id; - const createdRule = await createEsQueryRule({ - supertestWithoutAuth, + const createdRule = await alertingApi.helpers.createEsQueryRule({ roleAuthc: roleAdmin, - internalReqHeader, consumer: 'alerts', name: 'always fire', ruleTypeId: RULE_TYPE_ID, @@ -501,20 +491,28 @@ export default function ({ getService }: FtrProviderContext) { }); ruleId = createdRule.id; - const resp = await waitForDocumentInIndex({ + const resp = await alertingApi.helpers.waitForDocumentInIndex({ esClient, indexName: ALERT_ACTION_INDEX, ruleId, num: 2, sort: 'asc', + retryOptions: { + retryCount: 20, + retryDelay: 10_000, + }, }); - const resp2 = await waitForAlertInIndex({ + const resp2 = await alertingApi.helpers.waitForAlertInIndex({ esClient, filter: testStart, indexName: ALERT_INDEX, ruleId, num: 1, + retryOptions: { + retryCount: 20, + retryDelay: 15_000, + }, }); expect(resp2.hits.hits.length).to.be(1); diff --git a/x-pack/test_serverless/api_integration/test_suites/common/index_management/inference_endpoints.ts b/x-pack/test_serverless/api_integration/test_suites/common/index_management/inference_endpoints.ts index f5f712fc7d5a1..83ac02779b5f0 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/index_management/inference_endpoints.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/index_management/inference_endpoints.ts @@ -26,7 +26,8 @@ export default function ({ getService }: FtrProviderContext) { let roleAuthc: RoleCredentials; let internalReqHeader: InternalRequestHeader; - describe('Inference endpoints', function () { + // Failing: See https://github.com/elastic/kibana/issues/193036 + describe.skip('Inference endpoints', function () { before(async () => { roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin'); internalReqHeader = svlCommonApi.getInternalRequestHeader(); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/es_query_rule/es_query_rule.ts b/x-pack/test_serverless/api_integration/test_suites/observability/es_query_rule/es_query_rule.ts index 39edd9ba01eb9..8d627413ecbc0 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/es_query_rule/es_query_rule.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/es_query_rule/es_query_rule.ts @@ -12,7 +12,6 @@ */ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { createEsQueryRule } from '../../common/alerting/helpers/alerting_api_helper'; import { InternalRequestHeader, RoleCredentials } from '../../../../shared/services'; export default function ({ getService }: FtrProviderContext) { @@ -22,7 +21,6 @@ export default function ({ getService }: FtrProviderContext) { const alertingApi = getService('alertingApi'); const svlCommonApi = getService('svlCommonApi'); const svlUserManager = getService('svlUserManager'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); let roleAuthc: RoleCredentials; let internalReqHeader: InternalRequestHeader; @@ -58,10 +56,8 @@ export default function ({ getService }: FtrProviderContext) { indexName: ALERT_ACTION_INDEX, }); - const createdRule = await createEsQueryRule({ - supertestWithoutAuth, + const createdRule = await alertingApi.helpers.createEsQueryRule({ roleAuthc, - internalReqHeader, consumer: 'observability', name: 'always fire', ruleTypeId: RULE_TYPE_ID, diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts b/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts index 329b9be0de561..2e41125e8265b 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts @@ -3666,6 +3666,18 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:ingest-agent-policies/delete", "saved_object:ingest-agent-policies/bulk_delete", "saved_object:ingest-agent-policies/share_to_space", + "saved_object:fleet-agent-policies/bulk_get", + "saved_object:fleet-agent-policies/get", + "saved_object:fleet-agent-policies/find", + "saved_object:fleet-agent-policies/open_point_in_time", + "saved_object:fleet-agent-policies/close_point_in_time", + "saved_object:fleet-agent-policies/create", + "saved_object:fleet-agent-policies/bulk_create", + "saved_object:fleet-agent-policies/update", + "saved_object:fleet-agent-policies/bulk_update", + "saved_object:fleet-agent-policies/delete", + "saved_object:fleet-agent-policies/bulk_delete", + "saved_object:fleet-agent-policies/share_to_space", "saved_object:ingest-package-policies/bulk_get", "saved_object:ingest-package-policies/get", "saved_object:ingest-package-policies/find", @@ -3678,6 +3690,18 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:ingest-package-policies/delete", "saved_object:ingest-package-policies/bulk_delete", "saved_object:ingest-package-policies/share_to_space", + "saved_object:fleet-package-policies/bulk_get", + "saved_object:fleet-package-policies/get", + "saved_object:fleet-package-policies/find", + "saved_object:fleet-package-policies/open_point_in_time", + "saved_object:fleet-package-policies/close_point_in_time", + "saved_object:fleet-package-policies/create", + "saved_object:fleet-package-policies/bulk_create", + "saved_object:fleet-package-policies/update", + "saved_object:fleet-package-policies/bulk_update", + "saved_object:fleet-package-policies/delete", + "saved_object:fleet-package-policies/bulk_delete", + "saved_object:fleet-package-policies/share_to_space", "saved_object:epm-packages/bulk_get", "saved_object:epm-packages/get", "saved_object:epm-packages/find", @@ -3993,6 +4017,18 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:ingest-agent-policies/delete", "saved_object:ingest-agent-policies/bulk_delete", "saved_object:ingest-agent-policies/share_to_space", + "saved_object:fleet-agent-policies/bulk_get", + "saved_object:fleet-agent-policies/get", + "saved_object:fleet-agent-policies/find", + "saved_object:fleet-agent-policies/open_point_in_time", + "saved_object:fleet-agent-policies/close_point_in_time", + "saved_object:fleet-agent-policies/create", + "saved_object:fleet-agent-policies/bulk_create", + "saved_object:fleet-agent-policies/update", + "saved_object:fleet-agent-policies/bulk_update", + "saved_object:fleet-agent-policies/delete", + "saved_object:fleet-agent-policies/bulk_delete", + "saved_object:fleet-agent-policies/share_to_space", "saved_object:ingest-package-policies/bulk_get", "saved_object:ingest-package-policies/get", "saved_object:ingest-package-policies/find", @@ -4005,6 +4041,18 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:ingest-package-policies/delete", "saved_object:ingest-package-policies/bulk_delete", "saved_object:ingest-package-policies/share_to_space", + "saved_object:fleet-package-policies/bulk_get", + "saved_object:fleet-package-policies/get", + "saved_object:fleet-package-policies/find", + "saved_object:fleet-package-policies/open_point_in_time", + "saved_object:fleet-package-policies/close_point_in_time", + "saved_object:fleet-package-policies/create", + "saved_object:fleet-package-policies/bulk_create", + "saved_object:fleet-package-policies/update", + "saved_object:fleet-package-policies/bulk_update", + "saved_object:fleet-package-policies/delete", + "saved_object:fleet-package-policies/bulk_delete", + "saved_object:fleet-package-policies/share_to_space", "saved_object:epm-packages/bulk_get", "saved_object:epm-packages/get", "saved_object:epm-packages/find", @@ -4305,11 +4353,21 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:ingest-agent-policies/find", "saved_object:ingest-agent-policies/open_point_in_time", "saved_object:ingest-agent-policies/close_point_in_time", + "saved_object:fleet-agent-policies/bulk_get", + "saved_object:fleet-agent-policies/get", + "saved_object:fleet-agent-policies/find", + "saved_object:fleet-agent-policies/open_point_in_time", + "saved_object:fleet-agent-policies/close_point_in_time", "saved_object:ingest-package-policies/bulk_get", "saved_object:ingest-package-policies/get", "saved_object:ingest-package-policies/find", "saved_object:ingest-package-policies/open_point_in_time", "saved_object:ingest-package-policies/close_point_in_time", + "saved_object:fleet-package-policies/bulk_get", + "saved_object:fleet-package-policies/get", + "saved_object:fleet-package-policies/find", + "saved_object:fleet-package-policies/open_point_in_time", + "saved_object:fleet-package-policies/close_point_in_time", "saved_object:epm-packages/bulk_get", "saved_object:epm-packages/get", "saved_object:epm-packages/find", @@ -4457,11 +4515,21 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:ingest-agent-policies/find", "saved_object:ingest-agent-policies/open_point_in_time", "saved_object:ingest-agent-policies/close_point_in_time", + "saved_object:fleet-agent-policies/bulk_get", + "saved_object:fleet-agent-policies/get", + "saved_object:fleet-agent-policies/find", + "saved_object:fleet-agent-policies/open_point_in_time", + "saved_object:fleet-agent-policies/close_point_in_time", "saved_object:ingest-package-policies/bulk_get", "saved_object:ingest-package-policies/get", "saved_object:ingest-package-policies/find", "saved_object:ingest-package-policies/open_point_in_time", "saved_object:ingest-package-policies/close_point_in_time", + "saved_object:fleet-package-policies/bulk_get", + "saved_object:fleet-package-policies/get", + "saved_object:fleet-package-policies/find", + "saved_object:fleet-package-policies/open_point_in_time", + "saved_object:fleet-package-policies/close_point_in_time", "saved_object:epm-packages/bulk_get", "saved_object:epm-packages/get", "saved_object:epm-packages/find", diff --git a/x-pack/test_serverless/api_integration/test_suites/security/platform_security/authorization.ts b/x-pack/test_serverless/api_integration/test_suites/security/platform_security/authorization.ts index 07dbcf7ded031..5cf491188ba96 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/platform_security/authorization.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/platform_security/authorization.ts @@ -349,6 +349,18 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:risk-engine-configuration/delete", "saved_object:risk-engine-configuration/bulk_delete", "saved_object:risk-engine-configuration/share_to_space", + "saved_object:entity-engine-status/bulk_get", + "saved_object:entity-engine-status/get", + "saved_object:entity-engine-status/find", + "saved_object:entity-engine-status/open_point_in_time", + "saved_object:entity-engine-status/close_point_in_time", + "saved_object:entity-engine-status/create", + "saved_object:entity-engine-status/bulk_create", + "saved_object:entity-engine-status/update", + "saved_object:entity-engine-status/bulk_update", + "saved_object:entity-engine-status/delete", + "saved_object:entity-engine-status/bulk_delete", + "saved_object:entity-engine-status/share_to_space", "saved_object:policy-settings-protection-updates-note/bulk_get", "saved_object:policy-settings-protection-updates-note/get", "saved_object:policy-settings-protection-updates-note/find", @@ -1182,6 +1194,18 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:risk-engine-configuration/delete", "saved_object:risk-engine-configuration/bulk_delete", "saved_object:risk-engine-configuration/share_to_space", + "saved_object:entity-engine-status/bulk_get", + "saved_object:entity-engine-status/get", + "saved_object:entity-engine-status/find", + "saved_object:entity-engine-status/open_point_in_time", + "saved_object:entity-engine-status/close_point_in_time", + "saved_object:entity-engine-status/create", + "saved_object:entity-engine-status/bulk_create", + "saved_object:entity-engine-status/update", + "saved_object:entity-engine-status/bulk_update", + "saved_object:entity-engine-status/delete", + "saved_object:entity-engine-status/bulk_delete", + "saved_object:entity-engine-status/share_to_space", "saved_object:policy-settings-protection-updates-note/bulk_get", "saved_object:policy-settings-protection-updates-note/get", "saved_object:policy-settings-protection-updates-note/find", @@ -1779,6 +1803,11 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:risk-engine-configuration/find", "saved_object:risk-engine-configuration/open_point_in_time", "saved_object:risk-engine-configuration/close_point_in_time", + "saved_object:entity-engine-status/bulk_get", + "saved_object:entity-engine-status/get", + "saved_object:entity-engine-status/find", + "saved_object:entity-engine-status/open_point_in_time", + "saved_object:entity-engine-status/close_point_in_time", "saved_object:policy-settings-protection-updates-note/bulk_get", "saved_object:policy-settings-protection-updates-note/get", "saved_object:policy-settings-protection-updates-note/find", @@ -2135,6 +2164,11 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:risk-engine-configuration/find", "saved_object:risk-engine-configuration/open_point_in_time", "saved_object:risk-engine-configuration/close_point_in_time", + "saved_object:entity-engine-status/bulk_get", + "saved_object:entity-engine-status/get", + "saved_object:entity-engine-status/find", + "saved_object:entity-engine-status/open_point_in_time", + "saved_object:entity-engine-status/close_point_in_time", "saved_object:policy-settings-protection-updates-note/bulk_get", "saved_object:policy-settings-protection-updates-note/get", "saved_object:policy-settings-protection-updates-note/find", diff --git a/x-pack/test_serverless/functional/page_objects/svl_search_elasticsearch_start_page.ts b/x-pack/test_serverless/functional/page_objects/svl_search_elasticsearch_start_page.ts index 64852a7da8943..33dbc6f693ea8 100644 --- a/x-pack/test_serverless/functional/page_objects/svl_search_elasticsearch_start_page.ts +++ b/x-pack/test_serverless/functional/page_objects/svl_search_elasticsearch_start_page.ts @@ -20,9 +20,7 @@ export function SvlSearchElasticsearchStartPageProvider({ getService }: FtrProvi }, async expectToBeOnIndexDetailsPage() { await retry.tryForTime(60 * 1000, async () => { - expect(await browser.getCurrentUrl()).contain( - '/app/management/data/index_management/indices/index_details' - ); + expect(await browser.getCurrentUrl()).contain('/app/elasticsearch/indices/index_details'); }); }, async expectToBeOnIndexListPage() { diff --git a/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts b/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts index 5ac440ce6c4f4..09b69aaed5332 100644 --- a/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts +++ b/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts @@ -32,6 +32,40 @@ export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderCont async expectBackToIndicesButtonRedirectsToListPage() { await testSubjects.existOrFail('indicesList'); }, + async expectConnectionDetails() { + await testSubjects.existOrFail('connectionDetailsEndpoint', { timeout: 2000 }); + expect(await (await testSubjects.find('connectionDetailsEndpoint')).getVisibleText()).to.be( + 'https://fakeprojectid.es.fake-domain.cld.elstc.co:443' + ); + }, + async expectQuickStats() { + await testSubjects.existOrFail('quickStats', { timeout: 2000 }); + const quickStatsElem = await testSubjects.find('quickStats'); + const quickStatsDocumentElem = await quickStatsElem.findByTestSubject( + 'QuickStatsDocumentCount' + ); + expect(await quickStatsDocumentElem.getVisibleText()).to.contain('Document count\n0'); + expect(await quickStatsDocumentElem.getVisibleText()).not.to.contain('Index Size\n0b'); + await quickStatsDocumentElem.click(); + expect(await quickStatsDocumentElem.getVisibleText()).to.contain('Index Size\n0b'); + }, + async expectQuickStatsAIMappings() { + await testSubjects.existOrFail('quickStats', { timeout: 2000 }); + const quickStatsElem = await testSubjects.find('quickStats'); + const quickStatsAIMappingsElem = await quickStatsElem.findByTestSubject( + 'QuickStatsAIMappings' + ); + await quickStatsAIMappingsElem.click(); + await testSubjects.existOrFail('setupAISearchButton', { timeout: 2000 }); + }, + + async expectQuickStatsAIMappingsToHaveVectorFields() { + const quickStatsDocumentElem = await testSubjects.find('QuickStatsAIMappings'); + await quickStatsDocumentElem.click(); + expect(await quickStatsDocumentElem.getVisibleText()).to.contain('AI Search\n1 Field'); + await testSubjects.missingOrFail('setupAISearchButton', { timeout: 2000 }); + }, + async expectMoreOptionsActionButtonExists() { await testSubjects.existOrFail('moreOptionsActionButton'); }, diff --git a/x-pack/test_serverless/functional/services/index.ts b/x-pack/test_serverless/functional/services/index.ts index c63a16b4402f1..770cdbb88c97a 100644 --- a/x-pack/test_serverless/functional/services/index.ts +++ b/x-pack/test_serverless/functional/services/index.ts @@ -7,7 +7,6 @@ import { services as deploymentAgnosticFunctionalServices } from './deployment_agnostic_services'; import { services as svlSharedServices } from '../../shared/services'; - import { SvlCommonNavigationServiceProvider } from './svl_common_navigation'; import { SvlObltNavigationServiceProvider } from './svl_oblt_navigation'; import { SvlSearchNavigationServiceProvider } from './svl_search_navigation'; @@ -17,6 +16,7 @@ import { SvlCasesServiceProvider } from '../../api_integration/services/svl_case import { MachineLearningProvider } from './ml'; import { LogsSynthtraceProvider } from './log'; import { UISettingsServiceProvider } from './ui_settings'; +import { services as SvlApiIntegrationSvcs } from '../../api_integration/services'; export const services = { // deployment agnostic FTR services @@ -34,4 +34,5 @@ export const services = { uiSettings: UISettingsServiceProvider, // log services svlLogsSynthtraceClient: LogsSynthtraceProvider, + alertingApi: SvlApiIntegrationSvcs.alertingApi, }; diff --git a/x-pack/test_serverless/functional/test_suites/observability/rules/rules_list.ts b/x-pack/test_serverless/functional/test_suites/observability/rules/rules_list.ts index c2b878511a4dd..6a0d515afd232 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/rules/rules_list.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/rules/rules_list.ts @@ -7,17 +7,7 @@ import { expect } from 'expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { - createAnomalyRule as createRule, - disableRule, - enableRule, - runRule, - createIndexConnector, - snoozeRule, - createLatencyThresholdRule, - createEsQueryRule, -} from '../../../../api_integration/test_suites/common/alerting/helpers/alerting_api_helper'; -import { InternalRequestHeader, RoleCredentials } from '../../../../shared/services'; +import { RoleCredentials } from '../../../../shared/services'; export default ({ getPageObject, getService }: FtrProviderContext) => { const svlCommonPage = getPageObject('svlCommonPage'); @@ -31,11 +21,9 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const retry = getService('retry'); const toasts = getService('toasts'); const log = getService('log'); - const svlCommonApi = getService('svlCommonApi'); const svlUserManager = getService('svlUserManager'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); + const alertingApi = getService('alertingApi'); let roleAuthc: RoleCredentials; - let internalReqHeader: InternalRequestHeader; async function refreshRulesList() { const existsClearFilter = await testSubjects.exists('rules-list-clear-filter'); @@ -57,10 +45,13 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { numAttempts: number; }) { for (let i = 0; i < numAttempts; i++) { - await runRule({ supertestWithoutAuth, roleAuthc, internalReqHeader, ruleId }); + await alertingApi.helpers.runRule({ roleAuthc, ruleId }); await new Promise((resolve) => setTimeout(resolve, intervalMilliseconds)); - await disableRule({ supertest, ruleId }); + await alertingApi.helpers.disableRule({ + roleAuthc, + ruleId, + }); await new Promise((resolve) => setTimeout(resolve, intervalMilliseconds)); await refreshRulesList(); @@ -68,7 +59,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const rulesStatuses = result.map((item: { status: string }) => item.status); if (rulesStatuses.includes('Failed')) return; - await enableRule({ supertest, ruleId }); + await alertingApi.helpers.enableRule({ roleAuthc, ruleId }); } } @@ -84,7 +75,6 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { before(async () => { roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin'); - internalReqHeader = svlCommonApi.getInternalRequestHeader(); await svlCommonPage.loginWithPrivilegedRole(); await svlObltNavigation.navigateToLandingPage(); await svlCommonNavigation.sidenav.clickLink({ text: 'Alerts' }); @@ -107,10 +97,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('should create an ES Query Rule and display it when consumer is observability', async () => { - const esQuery = await createEsQueryRule({ - supertestWithoutAuth, + const esQuery = await alertingApi.helpers.createEsQueryRule({ roleAuthc, - internalReqHeader, name: 'ES Query', consumer: 'observability', ruleTypeId: '.es-query', @@ -134,10 +122,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('should create an ES Query rule but not display it when consumer is stackAlerts', async () => { - const esQuery = await createEsQueryRule({ - supertestWithoutAuth, + const esQuery = await alertingApi.helpers.createEsQueryRule({ roleAuthc, - internalReqHeader, name: 'ES Query', consumer: 'stackAlerts', ruleTypeId: '.es-query', @@ -159,7 +145,10 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('should create and display an APM latency rule', async () => { - const apmLatency = await createLatencyThresholdRule({ supertest, name: 'Apm latency' }); + const apmLatency = await alertingApi.helpers.createLatencyThresholdRule({ + roleAuthc, + name: 'Apm latency', + }); ruleIdList = [apmLatency.id]; await refreshRulesList(); @@ -169,16 +158,16 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('should display rules in alphabetical order', async () => { - const rule1 = await createRule({ - supertest, + const rule1 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, name: 'b', }); - const rule2 = await createRule({ - supertest, + const rule2 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, name: 'c', }); - const rule3 = await createRule({ - supertest, + const rule3 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, name: 'a', }); @@ -194,8 +183,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('should search for rule', async () => { - const rule1 = await createRule({ - supertest, + const rule1 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, }); ruleIdList = [rule1.id]; @@ -215,13 +204,13 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('should update rule list on the search clear button click', async () => { - const rule1 = await createRule({ - supertest, + const rule1 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, name: 'a', }); - const rule2 = await createRule({ - supertest, + const rule2 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, name: 'b', tags: [], }); @@ -266,8 +255,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('should search for tags', async () => { - const rule1 = await createRule({ - supertest, + const rule1 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, name: 'a', tags: ['tag', 'tagtag', 'taggity tag'], }); @@ -289,8 +278,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('should display an empty list when search did not return any rules', async () => { - const rule1 = await createRule({ - supertest, + const rule1 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, }); ruleIdList = [rule1.id]; @@ -301,8 +290,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('should disable single rule', async () => { - const rule1 = await createRule({ - supertest, + const rule1 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, }); ruleIdList = [rule1.id]; @@ -329,14 +318,17 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('should re-enable single rule', async () => { - const rule1 = await createRule({ - supertest, + const rule1 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, name: 'a', }); ruleIdList = [rule1.id]; - await disableRule({ supertest, ruleId: rule1.id }); + await alertingApi.helpers.disableRule({ + roleAuthc, + ruleId: rule1.id, + }); await refreshRulesList(); await svlTriggersActionsUI.searchRules(rule1.name); @@ -360,13 +352,13 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('should delete single rule', async () => { - const rule1 = await createRule({ - supertest, + const rule1 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, name: 'a', }); - const rule2 = await createRule({ - supertest, + const rule2 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, name: 'b', }); @@ -392,8 +384,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('should disable all selection', async () => { - const createdRule1 = await createRule({ - supertest, + const createdRule1 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, }); ruleIdList = [createdRule1.id]; @@ -422,13 +414,16 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('should enable all selection', async () => { - const rule1 = await createRule({ - supertest, + const rule1 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, }); ruleIdList = [rule1.id]; - await disableRule({ supertest, ruleId: rule1.id }); + await alertingApi.helpers.disableRule({ + roleAuthc, + ruleId: rule1.id, + }); await refreshRulesList(); await svlTriggersActionsUI.searchRules(rule1.name); @@ -445,8 +440,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('should render percentile column and cells correctly', async () => { - const rule1 = await createRule({ - supertest, + const rule1 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, }); ruleIdList = [rule1.id]; @@ -481,8 +476,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('should delete all selection', async () => { - const createdRule1 = await createRule({ - supertest, + const createdRule1 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, }); ruleIdList = [createdRule1.id]; @@ -508,12 +503,12 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it.skip('should filter rules by the status', async () => { - const rule1 = await createRule({ - supertest, + const rule1 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, }); - const failedRule = await createRule({ - supertest, + const failedRule = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, }); ruleIdList = [rule1.id, failedRule.id]; @@ -558,8 +553,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it.skip('should display total rules by status and error banner only when exists rules with status error', async () => { - const rule1 = await createRule({ - supertest, + const rule1 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, }); await refreshRulesList(); @@ -582,8 +577,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { ); expect(alertsErrorBannerWhenNoErrors).toHaveLength(0); - const failedRule = await createRule({ - supertest, + const failedRule = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, }); ruleIdList = [rule1.id, failedRule.id]; @@ -617,8 +612,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it.skip('Expand error in rules table when there is rule with an error associated', async () => { - const rule1 = await createRule({ - supertest, + const rule1 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, name: 'a', }); @@ -639,8 +634,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { let expandRulesErrorLink = await find.allByCssSelector('[data-test-subj="expandRulesError"]'); expect(expandRulesErrorLink).toHaveLength(0); - const failedRule = await createRule({ - supertest, + const failedRule = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, }); ruleIdList = [rule1.id, failedRule.id]; @@ -666,12 +661,12 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('should filter rules by the rule type', async () => { - const rule1 = await createRule({ - supertest, + const rule1 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, }); - const rule2 = await createLatencyThresholdRule({ - supertest, + const rule2 = await alertingApi.helpers.createLatencyThresholdRule({ + roleAuthc, }); ruleIdList = [rule1.id, rule2.id]; @@ -730,36 +725,36 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }; // Enabled alert - const rule1 = await createRule({ - supertest, + const rule1 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, }); - const disabledRule = await createRule({ - supertest, + const disabledRule = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, }); - await disableRule({ - supertest, + await alertingApi.helpers.disableRule({ + roleAuthc, ruleId: disabledRule.id, }); - const snoozedRule = await createRule({ - supertest, + const snoozedRule = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, }); - await snoozeRule({ - supertest, + await alertingApi.helpers.snoozeRule({ + roleAuthc, ruleId: snoozedRule.id, }); - const snoozedAndDisabledRule = await createRule({ - supertest, + const snoozedAndDisabledRule = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, }); - await snoozeRule({ - supertest, + await alertingApi.helpers.snoozeRule({ + roleAuthc, ruleId: snoozedAndDisabledRule.id, }); - await disableRule({ - supertest, + await alertingApi.helpers.disableRule({ + roleAuthc, ruleId: snoozedAndDisabledRule.id, }); @@ -801,28 +796,28 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('should filter rules by the tag', async () => { - const rule1 = await createRule({ - supertest, + const rule1 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, tags: ['a'], }); - const rule2 = await createRule({ - supertest, + const rule2 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, tags: ['b'], }); - const rule3 = await createRule({ - supertest, + const rule3 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, tags: ['a', 'b'], }); - const rule4 = await createRule({ - supertest, + const rule4 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, tags: ['b', 'c'], }); - const rule5 = await createRule({ - supertest, + const rule5 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, tags: ['c'], }); @@ -864,17 +859,15 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('should not prevent rules with action execution capabilities from being edited', async () => { - const action = await createIndexConnector({ - supertestWithoutAuth, + const action = await alertingApi.helpers.createIndexConnector({ roleAuthc, - internalReqHeader, name: 'Index Connector: Alerting API test', indexName: '.alerts-observability.apm.alerts-default', }); expect(action).not.toBe(undefined); - const rule1 = await createRule({ - supertest, + const rule1 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, actions: [ { group: 'threshold_met', @@ -902,8 +895,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('should allow rules to be snoozed using the right side dropdown', async () => { - const rule1 = await createRule({ - supertest, + const rule1 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, }); ruleIdList = [rule1.id]; @@ -922,8 +915,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('should allow rules to be snoozed indefinitely using the right side dropdown', async () => { - const rule1 = await createRule({ - supertest, + const rule1 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, }); ruleIdList = [rule1.id]; @@ -942,14 +935,14 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('should allow snoozed rules to be unsnoozed using the right side dropdown', async () => { - const rule1 = await createRule({ - supertest, + const rule1 = await alertingApi.helpers.createAnomalyRule({ + roleAuthc, }); ruleIdList = [rule1.id]; - await snoozeRule({ - supertest, + await alertingApi.helpers.snoozeRule({ + roleAuthc, ruleId: rule1.id, }); diff --git a/x-pack/test_serverless/functional/test_suites/search/rules/rule_details.ts b/x-pack/test_serverless/functional/test_suites/search/rules/rule_details.ts index 40d57101693bc..00363f21299de 100644 --- a/x-pack/test_serverless/functional/test_suites/search/rules/rule_details.ts +++ b/x-pack/test_serverless/functional/test_suites/search/rules/rule_details.ts @@ -7,13 +7,8 @@ import { expect } from 'expect'; import { v4 as uuidv4 } from 'uuid'; -import { InternalRequestHeader, RoleCredentials } from '../../../../shared/services'; +import { RoleCredentials } from '../../../../shared/services'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { - createEsQueryRule as createRule, - createSlackConnector, - createIndexConnector, -} from '../../../../api_integration/test_suites/common/alerting/helpers/alerting_api_helper'; export enum RuleNotifyWhen { CHANGE = 'onActionGroupChange', @@ -34,6 +29,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const toasts = getService('toasts'); const comboBox = getService('comboBox'); const config = getService('config'); + const alertingApi = getService('alertingApi'); const openFirstRule = async (ruleName: string) => { await svlTriggersActionsUI.searchRules(ruleName); @@ -66,15 +62,11 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { let ruleIdList: string[]; let connectorIdList: string[]; - const svlCommonApi = getService('svlCommonApi'); const svlUserManager = getService('svlUserManager'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); let roleAuthc: RoleCredentials; - let internalReqHeader: InternalRequestHeader; before(async () => { roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin'); - internalReqHeader = svlCommonApi.getInternalRequestHeader(); await svlCommonPage.loginAsViewer(); }); @@ -88,10 +80,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const RULE_TYPE_ID = '.es-query'; before(async () => { - const rule = await createRule({ - supertestWithoutAuth, + const rule = await alertingApi.helpers.createEsQueryRule({ roleAuthc, - internalReqHeader, consumer: 'alerts', name: ruleName, ruleTypeId: RULE_TYPE_ID, @@ -261,10 +251,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const RULE_TYPE_ID = '.es-query'; before(async () => { - const rule = await createRule({ - supertestWithoutAuth, + const rule = await alertingApi.helpers.createEsQueryRule({ roleAuthc, - internalReqHeader, consumer: 'alerts', name: ruleName, ruleTypeId: RULE_TYPE_ID, @@ -369,26 +357,20 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { it('should show and update deleted connectors when there are existing connectors of the same type', async () => { const testRunUuid = uuidv4(); - const connector1 = await createSlackConnector({ - supertestWithoutAuth, + const connector1 = await alertingApi.helpers.createSlackConnector({ roleAuthc, - internalReqHeader, name: `slack-${testRunUuid}-${0}`, }); - const connector2 = await createSlackConnector({ - supertestWithoutAuth, + const connector2 = await alertingApi.helpers.createSlackConnector({ roleAuthc, - internalReqHeader, name: `slack-${testRunUuid}-${1}`, }); connectorIdList = [connector2.id]; - const rule = await createRule({ - supertestWithoutAuth, + const rule = await alertingApi.helpers.createEsQueryRule({ roleAuthc, - internalReqHeader, consumer: 'alerts', name: testRunUuid, ruleTypeId: RULE_TYPE_ID, @@ -450,18 +432,14 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { it('should show and update deleted connectors when there are no existing connectors of the same type', async () => { const testRunUuid = uuidv4(); - const connector = await createIndexConnector({ - supertestWithoutAuth, + const connector = await alertingApi.helpers.createIndexConnector({ roleAuthc, - internalReqHeader, name: `index-${testRunUuid}-${2}`, indexName: ALERT_ACTION_INDEX, }); - const rule = await createRule({ - supertestWithoutAuth, + const rule = await alertingApi.helpers.createEsQueryRule({ roleAuthc, - internalReqHeader, consumer: 'alerts', name: testRunUuid, ruleTypeId: RULE_TYPE_ID, @@ -576,26 +554,20 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const testRunUuid = uuidv4(); const RULE_TYPE_ID = '.es-query'; - const connector1 = await createSlackConnector({ - supertestWithoutAuth, + const connector1 = await alertingApi.helpers.createSlackConnector({ roleAuthc, - internalReqHeader, name: `slack-${testRunUuid}-${0}`, }); - const connector2 = await createSlackConnector({ - supertestWithoutAuth, + const connector2 = await alertingApi.helpers.createSlackConnector({ roleAuthc, - internalReqHeader, name: `slack-${testRunUuid}-${1}`, }); connectorIdList = [connector1.id, connector2.id]; - const rule = await createRule({ - supertestWithoutAuth, + const rule = await alertingApi.helpers.createEsQueryRule({ roleAuthc, - internalReqHeader, consumer: 'alerts', name: `test-rule-${testRunUuid}`, ruleTypeId: RULE_TYPE_ID, @@ -670,10 +642,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('renders a disabled rule details view in app button', async () => { - const rule = await createRule({ - supertestWithoutAuth, + const rule = await alertingApi.helpers.createEsQueryRule({ roleAuthc, - internalReqHeader, consumer: 'alerts', name: ruleName, ruleTypeId: RULE_TYPE_ID, diff --git a/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts b/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts index 9450dca44df57..cd39079274d0a 100644 --- a/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts +++ b/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts @@ -26,6 +26,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { after(async () => { await esDeleteAllIndices(indexName); }); + describe('index details page overview', () => { before(async () => { await es.indices.create({ index: indexName }); @@ -41,11 +42,35 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('should have embedded dev console', async () => { await testHasEmbeddedConsole(pageObjects); }); + it('should have connection details', async () => { + await pageObjects.svlSearchIndexDetailPage.expectConnectionDetails(); + }); + + it('should have quick stats', async () => { + await pageObjects.svlSearchIndexDetailPage.expectQuickStats(); + await pageObjects.svlSearchIndexDetailPage.expectQuickStatsAIMappings(); + await es.indices.putMapping({ + index: indexName, + body: { + properties: { + my_field: { + type: 'dense_vector', + dims: 3, + }, + }, + }, + }); + await svlSearchNavigation.navigateToIndexDetailPage(indexName); + + await pageObjects.svlSearchIndexDetailPage.expectQuickStatsAIMappingsToHaveVectorFields(); + }); + it('back to indices button should redirect to list page', async () => { await pageObjects.svlSearchIndexDetailPage.expectBackToIndicesButtonExists(); await pageObjects.svlSearchIndexDetailPage.clickBackToIndicesButton(); await pageObjects.svlSearchIndexDetailPage.expectBackToIndicesButtonRedirectsToListPage(); }); + describe('page loading error', () => { before(async () => { await svlSearchNavigation.navigateToIndexDetailPage(indexName); diff --git a/x-pack/test_serverless/shared/services/alerting_api.ts b/x-pack/test_serverless/shared/services/alerting_api.ts new file mode 100644 index 0000000000000..afed22fbe2c9a --- /dev/null +++ b/x-pack/test_serverless/shared/services/alerting_api.ts @@ -0,0 +1,1032 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import type { + AggregationsAggregate, + SearchResponse, +} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { Client } from '@elastic/elasticsearch'; +import { MetricThresholdParams } from '@kbn/infra-plugin/common/alerting/metrics'; +import { ThresholdParams } from '@kbn/observability-plugin/common/custom_threshold_rule/types'; +import { v4 as uuidv4 } from 'uuid'; +import type { TryWithRetriesOptions } from '@kbn/ftr-common-functional-services'; +import { RoleCredentials } from '.'; +import type { SloBurnRateRuleParams } from '../../api_integration/services/slo_api'; +import { FtrProviderContext } from '../../functional/ftr_provider_context'; + +interface CreateEsQueryRuleParams { + size: number; + thresholdComparator: string; + threshold: number[]; + timeWindowSize?: number; + timeWindowUnit?: string; + esQuery?: string; + timeField?: string; + searchConfiguration?: unknown; + indexName?: string; + excludeHitsFromPreviousRun?: boolean; + aggType?: string; + aggField?: string; + groupBy?: string; + termField?: string; + termSize?: number; + index?: string[]; +} +const RETRY_COUNT = 10; +const RETRY_DELAY = 1000; + +export function AlertingApiProvider({ getService }: FtrProviderContext) { + const retry = getService('retry'); + const es = getService('es'); + const requestTimeout = 30 * 1000; + const retryTimeout = 120 * 1000; + const logger = getService('log'); + const svlCommonApi = getService('svlCommonApi'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + const generateUniqueKey = () => uuidv4().replace(/-/g, ''); + + const helpers = { + async waitForAlertInIndex({ + esClient, + filter, + indexName, + ruleId, + num = 1, + retryOptions = { retryCount: RETRY_COUNT, retryDelay: RETRY_DELAY }, + }: { + esClient: Client; + filter: Date; + indexName: string; + ruleId: string; + num: number; + retryOptions?: TryWithRetriesOptions; + }): Promise>> { + return await retry.tryWithRetries( + `Alerting API - waitForAlertInIndex, retryOptions: ${JSON.stringify(retryOptions)}`, + async () => { + const response = await esClient.search({ + index: indexName, + body: { + query: { + bool: { + must: [ + { + term: { + 'kibana.alert.rule.uuid': ruleId, + }, + }, + { + range: { + '@timestamp': { + gte: filter.getTime().toString(), + }, + }, + }, + ], + }, + }, + }, + }); + if (response.hits.hits.length < num) + throw new Error(`Only found ${response.hits.hits.length} / ${num} documents`); + + return response; + }, + retryOptions + ); + }, + + async waitForDocumentInIndex({ + esClient, + indexName, + ruleId, + num = 1, + sort = 'desc', + retryOptions = { retryCount: RETRY_COUNT, retryDelay: RETRY_DELAY }, + }: { + esClient: Client; + indexName: string; + ruleId: string; + num?: number; + sort?: 'asc' | 'desc'; + retryOptions?: TryWithRetriesOptions; + }): Promise { + return await retry.tryWithRetries( + `Alerting API - waitForDocumentInIndex, retryOptions: ${JSON.stringify(retryOptions)}`, + async () => { + const response = await esClient.search({ + index: indexName, + sort: `date:${sort}`, + body: { + query: { + bool: { + must: [ + { + term: { + 'ruleId.keyword': ruleId, + }, + }, + ], + }, + }, + }, + }); + if (response.hits.hits.length < num) { + throw new Error(`Only found ${response.hits.hits.length} / ${num} documents`); + } + return response; + }, + retryOptions + ); + }, + + async createIndexConnector({ + roleAuthc, + name, + indexName, + }: { + roleAuthc: RoleCredentials; + name: string; + indexName: string; + }) { + const { body } = await supertestWithoutAuth + .post(`/api/actions/connector`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .send({ + name, + config: { + index: indexName, + refresh: true, + }, + connector_type_id: '.index', + }) + .expect(200); + return body; + }, + + async createSlackConnector({ roleAuthc, name }: { roleAuthc: RoleCredentials; name: string }) { + const { body } = await supertestWithoutAuth + .post(`/api/actions/connector`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .send({ + name, + config: {}, + secrets: { + webhookUrl: 'http://test', + }, + connector_type_id: '.slack', + }) + .expect(200); + return body; + }, + + async createEsQueryRule({ + roleAuthc, + name, + ruleTypeId, + params, + actions = [], + tags = [], + schedule, + consumer, + notifyWhen, + enabled = true, + }: { + roleAuthc: RoleCredentials; + ruleTypeId: string; + name: string; + params: CreateEsQueryRuleParams; + consumer: string; + actions?: any[]; + tags?: any[]; + schedule?: { interval: string }; + notifyWhen?: string; + enabled?: boolean; + }) { + const { body } = await supertestWithoutAuth + .post(`/api/alerting/rule`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .send({ + enabled, + params, + consumer, + schedule: schedule || { + interval: '1h', + }, + tags, + name, + rule_type_id: ruleTypeId, + actions, + ...(notifyWhen ? { notify_when: notifyWhen, throttle: '5m' } : {}), + }) + .expect(200); + return body; + }, + + async createAnomalyRule({ + roleAuthc, + name = generateUniqueKey(), + actions = [], + tags = ['foo', 'bar'], + schedule, + consumer = 'alerts', + notifyWhen, + enabled = true, + ruleTypeId = 'apm.anomaly', + params, + }: { + roleAuthc: RoleCredentials; + name?: string; + consumer?: string; + actions?: any[]; + tags?: any[]; + schedule?: { interval: string }; + notifyWhen?: string; + enabled?: boolean; + ruleTypeId?: string; + params?: any; + }) { + const { body } = await supertestWithoutAuth + .post(`/api/alerting/rule`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .send({ + enabled, + params: params || { + anomalySeverityType: 'critical', + anomalyDetectorTypes: ['txLatency'], + environment: 'ENVIRONMENT_ALL', + windowSize: 30, + windowUnit: 'm', + }, + consumer, + schedule: schedule || { + interval: '1m', + }, + tags, + name, + rule_type_id: ruleTypeId, + actions, + ...(notifyWhen ? { notify_when: notifyWhen, throttle: '5m' } : {}), + }) + .expect(200); + return body; + }, + + async createLatencyThresholdRule({ + roleAuthc, + name = generateUniqueKey(), + actions = [], + tags = ['foo', 'bar'], + schedule, + consumer = 'apm', + notifyWhen, + enabled = true, + ruleTypeId = 'apm.transaction_duration', + params, + }: { + roleAuthc: RoleCredentials; + name?: string; + consumer?: string; + actions?: any[]; + tags?: any[]; + schedule?: { interval: string }; + notifyWhen?: string; + enabled?: boolean; + ruleTypeId?: string; + params?: any; + }) { + const { body } = await supertestWithoutAuth + .post(`/api/alerting/rule`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .send({ + enabled, + params: params || { + aggregationType: 'avg', + environment: 'ENVIRONMENT_ALL', + threshold: 1500, + windowSize: 5, + windowUnit: 'm', + }, + consumer, + schedule: schedule || { + interval: '1m', + }, + tags, + name, + rule_type_id: ruleTypeId, + actions, + ...(notifyWhen ? { notify_when: notifyWhen, throttle: '5m' } : {}), + }); + return body; + }, + + async createInventoryRule({ + roleAuthc, + name = generateUniqueKey(), + actions = [], + tags = ['foo', 'bar'], + schedule, + consumer = 'alerts', + notifyWhen, + enabled = true, + ruleTypeId = 'metrics.alert.inventory.threshold', + params, + }: { + roleAuthc: RoleCredentials; + name?: string; + consumer?: string; + actions?: any[]; + tags?: any[]; + schedule?: { interval: string }; + notifyWhen?: string; + enabled?: boolean; + ruleTypeId?: string; + params?: any; + }) { + const { body } = await supertestWithoutAuth + .post(`/api/alerting/rule`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .send({ + enabled, + params: params || { + nodeType: 'host', + criteria: [ + { + metric: 'cpu', + comparator: '>', + threshold: [5], + timeSize: 1, + timeUnit: 'm', + customMetric: { + type: 'custom', + id: 'alert-custom-metric', + field: '', + aggregation: 'avg', + }, + }, + ], + sourceId: 'default', + }, + consumer, + schedule: schedule || { + interval: '1m', + }, + tags, + name, + rule_type_id: ruleTypeId, + actions, + ...(notifyWhen ? { notify_when: notifyWhen, throttle: '5m' } : {}), + }) + .expect(200); + return body; + }, + + async disableRule({ roleAuthc, ruleId }: { roleAuthc: RoleCredentials; ruleId: string }) { + const { body } = await supertestWithoutAuth + .post(`/api/alerting/rule/${ruleId}/_disable`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .expect(204); + return body; + }, + + async updateEsQueryRule({ + roleAuthc, + ruleId, + updates, + }: { + roleAuthc: RoleCredentials; + ruleId: string; + updates: any; + }) { + const { body: r } = await supertestWithoutAuth + .get(`/api/alerting/rule/${ruleId}`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .expect(200); + const body = await supertestWithoutAuth + .put(`/api/alerting/rule/${ruleId}`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .send({ + ...{ + name: r.name, + schedule: r.schedule, + throttle: r.throttle, + tags: r.tags, + params: r.params, + notify_when: r.notifyWhen, + actions: r.actions.map((action: any) => ({ + group: action.group, + params: action.params, + id: action.id, + frequency: action.frequency, + })), + }, + ...updates, + }) + .expect(200); + return body; + }, + + async runRule({ roleAuthc, ruleId }: { roleAuthc: RoleCredentials; ruleId: string }) { + const response = await supertestWithoutAuth + .post(`/internal/alerting/rule/${ruleId}/_run_soon`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .expect(204); + return response; + }, + + async waitForNumRuleRuns({ + roleAuthc, + numOfRuns, + ruleId, + esClient, + testStart, + retryOptions = { retryCount: RETRY_COUNT, retryDelay: RETRY_DELAY }, + }: { + roleAuthc: RoleCredentials; + numOfRuns: number; + ruleId: string; + esClient: Client; + testStart: Date; + retryOptions?: TryWithRetriesOptions; + }) { + for (let i = 0; i < numOfRuns; i++) { + await retry.tryWithRetries( + `Alerting API - waitForNumRuleRuns, retryOptions: ${JSON.stringify(retryOptions)}`, + async () => { + await this.runRule({ roleAuthc, ruleId }); + await this.waiting.waitForExecutionEventLog({ + esClient, + filter: testStart, + ruleId, + num: i + 1, + }); + await this.waiting.waitForAllTasksIdle({ esClient, filter: testStart }); + }, + retryOptions + ); + } + }, + + async muteRule({ roleAuthc, ruleId }: { roleAuthc: RoleCredentials; ruleId: string }) { + const { body } = await supertestWithoutAuth + .post(`/api/alerting/rule/${ruleId}/_mute_all`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .expect(204); + return body; + }, + + async enableRule({ roleAuthc, ruleId }: { roleAuthc: RoleCredentials; ruleId: string }) { + const { body } = await supertestWithoutAuth + .post(`/api/alerting/rule/${ruleId}/_enable`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .expect(204); + return body; + }, + + async muteAlert({ + roleAuthc, + ruleId, + alertId, + }: { + roleAuthc: RoleCredentials; + ruleId: string; + alertId: string; + }) { + const { body } = await supertestWithoutAuth + .post(`/api/alerting/rule/${ruleId}/alert/${alertId}/_mute`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .expect(204); + return body; + }, + + async unmuteRule({ roleAuthc, ruleId }: { roleAuthc: RoleCredentials; ruleId: string }) { + const { body } = await supertestWithoutAuth + .post(`/api/alerting/rule/${ruleId}/_unmute_all`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .expect(204); + return body; + }, + + async snoozeRule({ roleAuthc, ruleId }: { roleAuthc: RoleCredentials; ruleId: string }) { + const { body } = await supertestWithoutAuth + .post(`/internal/alerting/rule/${ruleId}/_snooze`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .send({ + snooze_schedule: { + duration: 100000000, + rRule: { + count: 1, + dtstart: moment().format(), + tzid: 'UTC', + }, + }, + }) + .expect(204); + return body; + }, + + async findRule({ roleAuthc, ruleId }: { roleAuthc: RoleCredentials; ruleId: string }) { + if (!ruleId) { + throw new Error(`'ruleId' is undefined`); + } + const response = await supertestWithoutAuth + .get(`/api/alerting/rule/${ruleId}`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader); + return response.body || {}; + }, + + waiting: { + async waitForDocumentInIndex({ + esClient, + indexName, + ruleId, + num = 1, + sort = 'desc', + retryOptions = { retryCount: RETRY_COUNT, retryDelay: RETRY_DELAY }, + }: { + esClient: Client; + indexName: string; + ruleId: string; + num?: number; + sort?: 'asc' | 'desc'; + retryOptions?: TryWithRetriesOptions; + }): Promise { + return await retry.tryWithRetries( + `Alerting API - waiting.waitForDocumentInIndex, retryOptions: ${JSON.stringify( + retryOptions + )}`, + async () => { + const response = await esClient.search({ + index: indexName, + sort: `date:${sort}`, + body: { + query: { + bool: { + must: [ + { + term: { + 'ruleId.keyword': ruleId, + }, + }, + ], + }, + }, + }, + }); + if (response.hits.hits.length < num) { + throw new Error(`Only found ${response.hits.hits.length} / ${num} documents`); + } + return response; + }, + retryOptions + ); + }, + + async getDocumentsInIndex({ + esClient, + indexName, + ruleId, + }: { + esClient: Client; + indexName: string; + ruleId: string; + }): Promise { + return await esClient.search({ + index: indexName, + body: { + query: { + bool: { + must: [ + { + term: { + 'ruleId.keyword': ruleId, + }, + }, + ], + }, + }, + }, + }); + }, + + async waitForAllTasksIdle({ + esClient, + filter, + retryOptions = { retryCount: RETRY_COUNT, retryDelay: RETRY_DELAY }, + }: { + esClient: Client; + filter: Date; + retryOptions?: TryWithRetriesOptions; + }): Promise { + return await retry.tryWithRetries( + `Alerting API - waiting.waitForAllTasksIdle, retryOptions: ${JSON.stringify( + retryOptions + )}`, + async () => { + const response = await esClient.search({ + index: '.kibana_task_manager', + body: { + query: { + bool: { + must: [ + { + terms: { + 'task.scope': ['actions', 'alerting'], + }, + }, + { + range: { + 'task.scheduledAt': { + gte: filter.getTime().toString(), + }, + }, + }, + ], + must_not: [ + { + term: { + 'task.status': 'idle', + }, + }, + ], + }, + }, + }, + }); + if (response.hits.hits.length !== 0) { + throw new Error(`Expected 0 hits but received ${response.hits.hits.length}`); + } + return response; + }, + retryOptions + ); + }, + + async waitForExecutionEventLog({ + esClient, + filter, + ruleId, + num = 1, + retryOptions = { retryCount: RETRY_COUNT, retryDelay: RETRY_DELAY }, + }: { + esClient: Client; + filter: Date; + ruleId: string; + num?: number; + retryOptions?: TryWithRetriesOptions; + }): Promise { + return await retry.tryWithRetries( + `Alerting API - waiting.waitForExecutionEventLog, retryOptions: ${JSON.stringify( + retryOptions + )}`, + async () => { + const response = await esClient.search({ + index: '.kibana-event-log*', + body: { + query: { + bool: { + filter: [ + { + term: { + 'rule.id': { + value: ruleId, + }, + }, + }, + { + term: { + 'event.provider': { + value: 'alerting', + }, + }, + }, + { + term: { + 'event.action': 'execute', + }, + }, + { + range: { + '@timestamp': { + gte: filter.getTime().toString(), + }, + }, + }, + ], + }, + }, + }, + }); + if (response.hits.hits.length < num) { + throw new Error('No hits found'); + } + return response; + }, + retryOptions + ); + }, + + async createIndex({ esClient, indexName }: { esClient: Client; indexName: string }) { + return await esClient.indices.create( + { + index: indexName, + body: {}, + }, + { meta: true } + ); + }, + + async waitForAllTasks({ + esClient, + filter, + taskType, + attempts, + retryOptions = { retryCount: RETRY_COUNT, retryDelay: RETRY_DELAY }, + }: { + esClient: Client; + filter: Date; + taskType: string; + attempts: number; + retryOptions?: TryWithRetriesOptions; + }): Promise { + return await retry.tryWithRetries( + `Alerting API - waiting.waitForAllTasks, retryOptions: ${JSON.stringify(retryOptions)}`, + async () => { + const response = await esClient.search({ + index: '.kibana_task_manager', + body: { + query: { + bool: { + must: [ + { + term: { + 'task.status': 'idle', + }, + }, + { + term: { + 'task.attempts': attempts, + }, + }, + { + terms: { + 'task.scope': ['actions', 'alerting'], + }, + }, + { + term: { + 'task.taskType': taskType, + }, + }, + { + range: { + 'task.scheduledAt': { + gte: filter.getTime().toString(), + }, + }, + }, + ], + }, + }, + }, + }); + if (response.hits.hits.length === 0) { + throw new Error('No hits found'); + } + return response; + }, + retryOptions + ); + }, + + async waitForDisabled({ + esClient, + ruleId, + filter, + retryOptions = { retryCount: RETRY_COUNT, retryDelay: RETRY_DELAY }, + }: { + esClient: Client; + ruleId: string; + filter: Date; + retryOptions?: TryWithRetriesOptions; + }): Promise { + return await retry.tryWithRetries( + `Alerting API - waiting.waitForDisabled, retryOptions: ${JSON.stringify(retryOptions)}`, + async () => { + const response = await esClient.search({ + index: '.kibana_task_manager', + body: { + query: { + bool: { + must: [ + { + term: { + 'task.id': `task:${ruleId}`, + }, + }, + { + terms: { + 'task.scope': ['actions', 'alerting'], + }, + }, + { + range: { + 'task.scheduledAt': { + gte: filter.getTime().toString(), + }, + }, + }, + { + term: { + 'task.enabled': true, + }, + }, + ], + }, + }, + }, + }); + if (response.hits.hits.length !== 0) { + throw new Error(`Expected 0 hits but received ${response.hits.hits.length}`); + } + return response; + }, + retryOptions + ); + }, + }, + }; + + return { + helpers, + + async waitForRuleStatus({ + roleAuthc, + ruleId, + expectedStatus, + }: { + roleAuthc: RoleCredentials; + ruleId: string; + expectedStatus: string; + }) { + if (!ruleId) { + throw new Error(`'ruleId' is undefined`); + } + return await retry.tryForTime(retryTimeout, async () => { + const response = await supertestWithoutAuth + .get(`/api/alerting/rule/${ruleId}`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .timeout(requestTimeout); + const { execution_status: executionStatus } = response.body || {}; + const { status } = executionStatus || {}; + if (status !== expectedStatus) { + throw new Error(`waitForStatus(${expectedStatus}): got ${status}`); + } + return executionStatus?.status; + }); + }, + + async waitForDocumentInIndex({ + indexName, + docCountTarget = 1, + }: { + indexName: string; + docCountTarget?: number; + }): Promise>> { + return await retry.tryForTime(retryTimeout, async () => { + const response = await es.search({ + index: indexName, + rest_total_hits_as_int: true, + }); + logger.debug(`Found ${response.hits.total} docs, looking for at least ${docCountTarget}.`); + if (!response.hits.total || (response.hits.total as number) < docCountTarget) { + throw new Error('No hits found'); + } + return response; + }); + }, + + async waitForAlertInIndex({ + indexName, + ruleId, + }: { + indexName: string; + ruleId: string; + }): Promise>> { + if (!ruleId) { + throw new Error(`'ruleId' is undefined`); + } + return await retry.tryForTime(retryTimeout, async () => { + const response = await es.search({ + index: indexName, + body: { + query: { + term: { + 'kibana.alert.rule.uuid': ruleId, + }, + }, + }, + }); + if (response.hits.hits.length === 0) { + throw new Error('No hits found'); + } + return response; + }); + }, + + async createIndexConnector({ + roleAuthc, + name, + indexName, + }: { + roleAuthc: RoleCredentials; + name: string; + indexName: string; + }) { + const { body } = await supertestWithoutAuth + .post(`/api/actions/connector`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .send({ + name, + config: { + index: indexName, + refresh: true, + }, + connector_type_id: '.index', + }); + return body.id as string; + }, + + async createRule({ + roleAuthc, + name, + ruleTypeId, + params, + actions = [], + tags = [], + schedule, + consumer, + }: { + roleAuthc: RoleCredentials; + ruleTypeId: string; + name: string; + params: MetricThresholdParams | ThresholdParams | SloBurnRateRuleParams; + actions?: any[]; + tags?: any[]; + schedule?: { interval: string }; + consumer: string; + }) { + const { body } = await supertestWithoutAuth + .post(`/api/alerting/rule`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .send({ + params, + consumer, + schedule: schedule || { + interval: '5m', + }, + tags, + name, + rule_type_id: ruleTypeId, + actions, + }); + return body; + }, + + async findRule(roleAuthc: RoleCredentials, ruleId: string) { + if (!ruleId) { + throw new Error(`'ruleId' is undefined`); + } + const response = await supertestWithoutAuth + .get('/api/alerting/rules/_find') + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader); + return response.body.data.find((obj: any) => obj.id === ruleId); + }, + }; +} diff --git a/x-pack/test_serverless/shared/services/deployment_agnostic_services.ts b/x-pack/test_serverless/shared/services/deployment_agnostic_services.ts index 97a5963bd9e3b..2272890e52eb4 100644 --- a/x-pack/test_serverless/shared/services/deployment_agnostic_services.ts +++ b/x-pack/test_serverless/shared/services/deployment_agnostic_services.ts @@ -8,7 +8,7 @@ import _ from 'lodash'; import { services as apiIntegrationServices } from '@kbn/test-suites-xpack/api_integration/services'; - +import { AlertingApiProvider } from './alerting_api'; /* * Some FTR services from api integration stateful tests are compatible with serverless environment * While adding a new one, make sure to verify that it works on both Kibana CI and MKI @@ -35,4 +35,5 @@ const deploymentAgnosticApiIntegrationServices = _.pick(apiIntegrationServices, export const services = { // deployment agnostic FTR services ...deploymentAgnosticApiIntegrationServices, + alertingApi: AlertingApiProvider, };