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,
};