From f2b9348f976b96c296b3dc89949115a10c9b19f9 Mon Sep 17 00:00:00 2001
From: Ignacio Rivas
Date: Wed, 9 Oct 2024 17:40:10 +0200
Subject: [PATCH 01/87] [Search profiler] Migrate ace to monaco (#195343)
---
.../public/application/components/_index.scss | 1 -
.../_profile_query_editor.scss | 25 -----
.../editor/editor.test.tsx | 10 +-
.../profile_query_editor/editor/editor.tsx | 97 ++++++++-----------
.../profile_query_editor/editor/index.ts | 3 +-
.../editor/init_editor.ts | 36 -------
.../profile_query_editor.tsx | 30 +++---
.../searchprofiler/public/shared_imports.ts | 2 -
x-pack/plugins/searchprofiler/tsconfig.json | 3 +-
.../apps/group1/search_profiler.ts | 6 +-
.../apps/dev_tools/searchprofiler_editor.ts | 2 +-
.../page_objects/search_profiler_page.ts | 9 +-
.../common/dev_tools/search_profiler.ts | 14 +--
13 files changed, 76 insertions(+), 162 deletions(-)
delete mode 100644 x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/_profile_query_editor.scss
delete mode 100644 x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/init_editor.ts
diff --git a/x-pack/plugins/searchprofiler/public/application/components/_index.scss b/x-pack/plugins/searchprofiler/public/application/components/_index.scss
index 9d6688a2d4d98..ee36c5e8e6567 100644
--- a/x-pack/plugins/searchprofiler/public/application/components/_index.scss
+++ b/x-pack/plugins/searchprofiler/public/application/components/_index.scss
@@ -3,5 +3,4 @@ $badgeSize: $euiSize * 5.5;
@import 'highlight_details_flyout/highlight_details_flyout';
@import 'license_warning_notice/license_warning_notice';
@import 'percentage_badge/percentage_badge';
-@import 'profile_query_editor/profile_query_editor';
@import 'profile_tree/index';
diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/_profile_query_editor.scss b/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/_profile_query_editor.scss
deleted file mode 100644
index 035ff16c990bb..0000000000000
--- a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/_profile_query_editor.scss
+++ /dev/null
@@ -1,25 +0,0 @@
-
-.prfDevTool__sense {
- order: 1;
- // To anchor ace editor
- position: relative;
-
- // Ace Editor overrides
- .ace_editor {
- min-height: $euiSize * 10;
- flex-grow: 1;
- margin-bottom: $euiSize;
- margin-top: $euiSize;
- outline: solid 1px $euiColorLightShade;
- }
-
- .errorMarker {
- position: absolute;
- background: rgba($euiColorDanger, .5);
- z-index: 20;
- }
-}
-
-.prfDevTool__profileButtonContainer {
- flex-shrink: 1;
-}
diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/editor.test.tsx b/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/editor.test.tsx
index 34e0867df8ec6..483f0ef7f6106 100644
--- a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/editor.test.tsx
+++ b/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/editor.test.tsx
@@ -5,20 +5,14 @@
* 2.0.
*/
-import 'brace';
-import 'brace/mode/json';
-
-import { coreMock } from '@kbn/core/public/mocks';
import { registerTestBed } from '@kbn/test-jest-helpers';
import { Editor, Props } from './editor';
-const coreStart = coreMock.createStart();
-
describe('Editor Component', () => {
it('renders', async () => {
const props: Props = {
- ...coreStart,
- initialValue: '',
+ editorValue: '',
+ setEditorValue: () => {},
licenseEnabled: true,
onEditorReady: (e: any) => {},
};
diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/editor.tsx b/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/editor.tsx
index 068673d4ce4c1..3701323d414c2 100644
--- a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/editor.tsx
+++ b/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/editor.tsx
@@ -5,67 +5,37 @@
* 2.0.
*/
-import React, { memo, useRef, useEffect, useState } from 'react';
+import React, { memo, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiScreenReaderOnly } from '@elastic/eui';
-import { Editor as AceEditor } from 'brace';
+import { EuiScreenReaderOnly, EuiSpacer } from '@elastic/eui';
+import { CodeEditor } from '@kbn/code-editor';
+import { monaco, XJsonLang } from '@kbn/monaco';
-import { SearchProfilerStartServices } from '../../../../types';
-import { ace } from '../../../../shared_imports';
-import { initializeEditor } from './init_editor';
-
-const { useUIAceKeyboardMode } = ace;
-
-type EditorShim = ReturnType;
-
-export type EditorInstance = EditorShim;
-
-export interface Props extends SearchProfilerStartServices {
+export interface Props {
licenseEnabled: boolean;
- initialValue: string;
- onEditorReady: (editor: EditorShim) => void;
+ editorValue: string;
+ setEditorValue: (value: string) => void;
+ onEditorReady: (props: EditorProps) => void;
}
-const createEditorShim = (aceEditor: AceEditor) => {
- return {
- getValue() {
- return aceEditor.getValue();
- },
- focus() {
- aceEditor.focus();
- },
- };
-};
-
const EDITOR_INPUT_ID = 'SearchProfilerTextArea';
-export const Editor = memo(
- ({ licenseEnabled, initialValue, onEditorReady, ...startServices }: Props) => {
- const containerRef = useRef(null as any);
- const editorInstanceRef = useRef(null as any);
-
- const [textArea, setTextArea] = useState(null);
-
- useUIAceKeyboardMode(textArea, startServices);
-
- useEffect(() => {
- const divEl = containerRef.current;
- editorInstanceRef.current = initializeEditor({ el: divEl, licenseEnabled });
- editorInstanceRef.current.setValue(initialValue, 1);
- const textarea = divEl.querySelector('textarea');
- if (textarea) {
- textarea.setAttribute('id', EDITOR_INPUT_ID);
- }
- setTextArea(licenseEnabled ? containerRef.current!.querySelector('textarea') : null);
-
- onEditorReady(createEditorShim(editorInstanceRef.current));
+export interface EditorProps {
+ focus: () => void;
+}
- return () => {
- if (editorInstanceRef.current) {
- editorInstanceRef.current.destroy();
- }
- };
- }, [initialValue, onEditorReady, licenseEnabled]);
+export const Editor = memo(
+ ({ licenseEnabled, editorValue, setEditorValue, onEditorReady }: Props) => {
+ const editorDidMountCallback = useCallback(
+ (editor: monaco.editor.IStandaloneCodeEditor) => {
+ onEditorReady({
+ focus: () => {
+ editor.focus();
+ },
+ } as EditorProps);
+ },
+ [onEditorReady]
+ );
return (
<>
@@ -76,7 +46,26 @@ export const Editor = memo(
})}
-
+
+
+
+
>
);
}
diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/index.ts b/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/index.ts
index 5d8be48041176..1ac3ec704bc5d 100644
--- a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/index.ts
+++ b/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/index.ts
@@ -5,5 +5,4 @@
* 2.0.
*/
-export type { EditorInstance } from './editor';
-export { Editor } from './editor';
+export { Editor, type EditorProps } from './editor';
diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/init_editor.ts b/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/init_editor.ts
deleted file mode 100644
index 24d119254db78..0000000000000
--- a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/editor/init_editor.ts
+++ /dev/null
@@ -1,36 +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 ace from 'brace';
-import { installXJsonMode } from '@kbn/ace';
-
-export function initializeEditor({
- el,
- licenseEnabled,
-}: {
- el: HTMLDivElement;
- licenseEnabled: boolean;
-}) {
- const editor: ace.Editor = ace.acequire('ace/ace').edit(el);
-
- installXJsonMode(editor);
- editor.$blockScrolling = Infinity;
-
- if (!licenseEnabled) {
- editor.setReadOnly(true);
- editor.container.style.pointerEvents = 'none';
- editor.container.style.opacity = '0.5';
- const textArea = editor.container.querySelector('textarea');
- if (textArea) {
- textArea.setAttribute('tabindex', '-1');
- }
- editor.renderer.setStyle('disabled');
- editor.blur();
- }
-
- return editor;
-}
diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/profile_query_editor.tsx b/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/profile_query_editor.tsx
index 577c3e530e8cc..a88f1040caa3a 100644
--- a/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/profile_query_editor.tsx
+++ b/x-pack/plugins/searchprofiler/public/application/components/profile_query_editor/profile_query_editor.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { useRef, memo, useCallback } from 'react';
+import React, { useRef, memo, useCallback, useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiForm,
@@ -23,7 +23,7 @@ import { decompressFromEncodedURIComponent } from 'lz-string';
import { useRequestProfile } from '../../hooks';
import { useAppContext } from '../../contexts/app_context';
import { useProfilerActionContext } from '../../contexts/profiler_context';
-import { Editor, EditorInstance } from './editor';
+import { Editor, type EditorProps } from './editor';
const DEFAULT_INDEX_VALUE = '_all';
@@ -39,33 +39,36 @@ const INITIAL_EDITOR_VALUE = `{
* Drives state changes for mine via profiler action context.
*/
export const ProfileQueryEditor = memo(() => {
- const editorRef = useRef(null as any);
+ const editorPropsRef = useRef(null as any);
const indexInputRef = useRef(null as any);
const dispatch = useProfilerActionContext();
- const { getLicenseStatus, notifications, location, ...startServices } = useAppContext();
+ const { getLicenseStatus, notifications, location } = useAppContext();
const queryParams = new URLSearchParams(location.search);
const indexName = queryParams.get('index');
const searchProfilerQueryURI = queryParams.get('load_from');
+
const searchProfilerQuery =
searchProfilerQueryURI &&
decompressFromEncodedURIComponent(searchProfilerQueryURI.replace(/^data:text\/plain,/, ''));
+ const [editorValue, setEditorValue] = useState(
+ searchProfilerQuery ? searchProfilerQuery : INITIAL_EDITOR_VALUE
+ );
const requestProfile = useRequestProfile();
const handleProfileClick = async () => {
dispatch({ type: 'setProfiling', value: true });
try {
- const { current: editor } = editorRef;
const { data: result, error } = await requestProfile({
- query: editorRef.current.getValue(),
+ query: editorValue,
index: indexInputRef.current.value,
});
if (error) {
notifications.addDanger(error);
- editor.focus();
+ editorPropsRef.current.focus();
return;
}
if (result === null) {
@@ -78,18 +81,13 @@ export const ProfileQueryEditor = memo(() => {
};
const onEditorReady = useCallback(
- (editorInstance: any) => (editorRef.current = editorInstance),
+ (editorPropsInstance: EditorProps) => (editorPropsRef.current = editorPropsInstance),
[]
);
const licenseEnabled = getLicenseStatus().valid;
return (
-
+
{/* Form */}
@@ -120,9 +118,9 @@ export const ProfileQueryEditor = memo(() => {
diff --git a/x-pack/plugins/searchprofiler/public/shared_imports.ts b/x-pack/plugins/searchprofiler/public/shared_imports.ts
index b1af4bab9e62d..3daab65e28db8 100644
--- a/x-pack/plugins/searchprofiler/public/shared_imports.ts
+++ b/x-pack/plugins/searchprofiler/public/shared_imports.ts
@@ -5,6 +5,4 @@
* 2.0.
*/
-export { ace } from '@kbn/es-ui-shared-plugin/public';
-
export { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
diff --git a/x-pack/plugins/searchprofiler/tsconfig.json b/x-pack/plugins/searchprofiler/tsconfig.json
index b99b0962e39fc..063b7dfa63ce6 100644
--- a/x-pack/plugins/searchprofiler/tsconfig.json
+++ b/x-pack/plugins/searchprofiler/tsconfig.json
@@ -20,9 +20,10 @@
"@kbn/expect",
"@kbn/test-jest-helpers",
"@kbn/i18n-react",
- "@kbn/ace",
"@kbn/config-schema",
"@kbn/react-kibana-context-render",
+ "@kbn/code-editor",
+ "@kbn/monaco",
],
"exclude": [
"target/**/*",
diff --git a/x-pack/test/accessibility/apps/group1/search_profiler.ts b/x-pack/test/accessibility/apps/group1/search_profiler.ts
index 522c5e4cf730e..fbd3649120ea1 100644
--- a/x-pack/test/accessibility/apps/group1/search_profiler.ts
+++ b/x-pack/test/accessibility/apps/group1/search_profiler.ts
@@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['common', 'security']);
const testSubjects = getService('testSubjects');
- const aceEditor = getService('aceEditor');
+ const monacoEditor = getService('monacoEditor');
const a11y = getService('a11y');
const esArchiver = getService('esArchiver');
@@ -27,7 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional');
});
- it('input the JSON in the aceeditor', async () => {
+ it('input the JSON in the editor', async () => {
const input = {
query: {
bool: {
@@ -54,7 +54,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
},
};
- await aceEditor.setValue('searchProfilerEditor', JSON.stringify(input));
+ await monacoEditor.setCodeEditorValue(JSON.stringify(input), 0);
await a11y.testAppSnapshot();
});
diff --git a/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts b/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts
index 174f3d4527178..87c36de62bba6 100644
--- a/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts
+++ b/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts
@@ -67,7 +67,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
`parser errors to match expectation: HAS ${expectation ? 'ERRORS' : 'NO ERRORS'}`,
async () => {
const actual = await PageObjects.searchProfiler.editorHasParseErrors();
- return expectation === actual;
+ return expectation === actual?.length > 0;
}
);
}
diff --git a/x-pack/test/functional/page_objects/search_profiler_page.ts b/x-pack/test/functional/page_objects/search_profiler_page.ts
index a110bd16eeafe..151b9a613c356 100644
--- a/x-pack/test/functional/page_objects/search_profiler_page.ts
+++ b/x-pack/test/functional/page_objects/search_profiler_page.ts
@@ -11,7 +11,7 @@ import { FtrProviderContext } from '../ftr_provider_context';
export function SearchProfilerPageProvider({ getService }: FtrProviderContext) {
const find = getService('find');
const testSubjects = getService('testSubjects');
- const aceEditor = getService('aceEditor');
+ const monacoEditor = getService('monacoEditor');
const editorTestSubjectSelector = 'searchProfilerEditor';
return {
@@ -19,10 +19,10 @@ export function SearchProfilerPageProvider({ getService }: FtrProviderContext) {
return await testSubjects.exists(editorTestSubjectSelector);
},
async setQuery(query: any) {
- await aceEditor.setValue(editorTestSubjectSelector, JSON.stringify(query));
+ await monacoEditor.setCodeEditorValue(JSON.stringify(query), 0);
},
async getQuery() {
- return JSON.parse(await aceEditor.getValue(editorTestSubjectSelector));
+ return JSON.parse(await monacoEditor.getCodeEditorValue(0));
},
async setIndexName(indexName: string) {
await testSubjects.setValue('indexName', indexName);
@@ -36,6 +36,7 @@ export function SearchProfilerPageProvider({ getService }: FtrProviderContext) {
},
async getProfileContent() {
const profileTree = await find.byClassName('prfDevTool__main__profiletree');
+ // const profileTree = await find.byClassName('prfDevTool__page');
return profileTree.getVisibleText();
},
getUrlWithIndexAndQuery({ indexName, query }: { indexName: string; query: any }) {
@@ -43,7 +44,7 @@ export function SearchProfilerPageProvider({ getService }: FtrProviderContext) {
return `/searchprofiler?index=${indexName}&load_from=${searchQueryURI}`;
},
async editorHasParseErrors() {
- return await aceEditor.hasParseErrors(editorTestSubjectSelector);
+ return await monacoEditor.getCurrentMarkers(editorTestSubjectSelector);
},
async editorHasErrorNotification() {
const notification = await testSubjects.find('noShardsNotification');
diff --git a/x-pack/test_serverless/functional/test_suites/common/dev_tools/search_profiler.ts b/x-pack/test_serverless/functional/test_suites/common/dev_tools/search_profiler.ts
index 6a908ce4e0fe8..979943ffa602c 100644
--- a/x-pack/test_serverless/functional/test_suites/common/dev_tools/search_profiler.ts
+++ b/x-pack/test_serverless/functional/test_suites/common/dev_tools/search_profiler.ts
@@ -8,7 +8,7 @@
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
-const testIndex = 'test-index';
+const indexName = 'my_index';
const testQuery = {
query: {
match_all: {},
@@ -53,10 +53,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
},
};
- // Since we're not actually running the query in the test,
- // this index name is just an input placeholder and does not exist
- const indexName = 'my_index';
-
await PageObjects.common.navigateToUrl(
'searchProfiler',
PageObjects.searchProfiler.getUrlWithIndexAndQuery({ indexName, query }),
@@ -77,21 +73,21 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
describe('With a test index', () => {
before(async () => {
- await es.indices.create({ index: testIndex });
+ await es.indices.create({ index: indexName });
});
after(async () => {
- await es.indices.delete({ index: testIndex });
+ await es.indices.delete({ index: indexName });
});
it('profiles a simple query', async () => {
- await PageObjects.searchProfiler.setIndexName(testIndex);
+ await PageObjects.searchProfiler.setIndexName(indexName);
await PageObjects.searchProfiler.setQuery(testQuery);
await PageObjects.searchProfiler.clickProfileButton();
const content = await PageObjects.searchProfiler.getProfileContent();
- expect(content).to.contain(testIndex);
+ expect(content).to.contain(indexName);
});
});
});
From d273c07edcc1d0ab00f73309a2fb385b43f6221b Mon Sep 17 00:00:00 2001
From: Ignacio Rivas
Date: Wed, 9 Oct 2024 17:54:43 +0200
Subject: [PATCH 02/87] [Console] Delete unused sense models and unused files
(#195344)
---
NOTICE.txt | 31 -
src/plugins/console/README.md | 8 +-
.../editor_context/editor_registry.ts | 5 +-
.../console/public/application/hooks/index.ts | 3 +-
.../use_restore_request_from_history/index.ts | 10 -
.../restore_request_from_history.ts | 48 -
.../restore_request_from_history_to_monaco.ts | 25 -
.../use_restore_request_from_history.ts | 21 -
.../hooks/use_send_current_request/index.ts | 1 -
.../hooks/use_send_current_request/track.ts | 34 -
.../use_send_current_request.test.tsx | 130 -
.../use_send_current_request.ts | 148 -
.../application/hooks/use_set_input_editor.ts | 3 +-
.../public/application/models/index.ts | 11 -
.../models/legacy_core_editor/create.ts | 20 -
.../legacy_core_editor/create_readonly.ts | 81 -
.../models/legacy_core_editor/index.ts | 18 -
.../models/legacy_core_editor/input.test.js | 559 ----
.../legacy_core_editor.test.mocks.ts | 29 -
.../legacy_core_editor/legacy_core_editor.ts | 511 ----
.../models/legacy_core_editor/mode/input.ts | 79 -
.../mode/input_highlight_rules.ts | 180 --
.../models/legacy_core_editor/mode/output.ts | 37 -
.../mode/output_highlight_rules.test.ts | 56 -
.../mode/output_highlight_rules.ts | 64 -
.../models/legacy_core_editor/mode/script.ts | 48 -
.../legacy_core_editor/mode/worker/index.d.ts | 10 -
.../legacy_core_editor/mode/worker/index.js | 15 -
.../legacy_core_editor/mode/worker/worker.js | 2392 -----------------
.../output_tokenization.test.js | 91 -
.../models/legacy_core_editor/smart_resize.ts | 27 -
.../legacy_core_editor/theme_sense_dark.js | 123 -
.../__fixtures__/editor_input1.txt | 37 -
.../application/models/sense_editor/create.ts | 22 -
.../application/models/sense_editor/curl.ts | 194 --
.../application/models/sense_editor/index.ts | 14 -
.../models/sense_editor/integration.test.js | 1279 ---------
.../models/sense_editor/sense_editor.test.js | 641 -----
.../sense_editor/sense_editor.test.mocks.ts | 20 -
.../models/sense_editor/sense_editor.ts | 534 ----
.../public/application/stores/editor.ts | 3 +-
.../public/lib/ace_token_provider/index.ts | 10 -
.../ace_token_provider/token_provider.test.ts | 223 --
.../lib/ace_token_provider/token_provider.ts | 84 -
.../public/lib/autocomplete/autocomplete.ts | 1316 ---------
.../get_endpoint_from_position.ts | 33 -
.../autocomplete/looks_like_typing_in.test.ts | 224 --
.../lib/autocomplete/looks_like_typing_in.ts | 109 -
.../autocomplete_entities.test.js | 1 -
.../__fixtures__/curl_parsing.txt | 146 -
.../console/public/lib/curl_parsing/curl.js | 194 --
.../lib/curl_parsing/curl_parsing.test.js | 37 -
src/plugins/console/public/lib/kb/kb.test.js | 1 -
.../console/public/lib/row_parser.test.ts | 107 -
src/plugins/console/public/lib/row_parser.ts | 161 --
src/plugins/console/public/styles/_app.scss | 53 -
.../console/public/types/core_editor.ts | 5 +-
src/plugins/console/tsconfig.json | 2 -
.../translations/translations/fr-FR.json | 6 -
.../translations/translations/ja-JP.json | 6 -
.../translations/translations/zh-CN.json | 6 -
61 files changed, 9 insertions(+), 10277 deletions(-)
delete mode 100644 src/plugins/console/public/application/hooks/use_restore_request_from_history/index.ts
delete mode 100644 src/plugins/console/public/application/hooks/use_restore_request_from_history/restore_request_from_history.ts
delete mode 100644 src/plugins/console/public/application/hooks/use_restore_request_from_history/restore_request_from_history_to_monaco.ts
delete mode 100644 src/plugins/console/public/application/hooks/use_restore_request_from_history/use_restore_request_from_history.ts
delete mode 100644 src/plugins/console/public/application/hooks/use_send_current_request/track.ts
delete mode 100644 src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx
delete mode 100644 src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts
delete mode 100644 src/plugins/console/public/application/models/index.ts
delete mode 100644 src/plugins/console/public/application/models/legacy_core_editor/create.ts
delete mode 100644 src/plugins/console/public/application/models/legacy_core_editor/create_readonly.ts
delete mode 100644 src/plugins/console/public/application/models/legacy_core_editor/index.ts
delete mode 100644 src/plugins/console/public/application/models/legacy_core_editor/input.test.js
delete mode 100644 src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.test.mocks.ts
delete mode 100644 src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts
delete mode 100644 src/plugins/console/public/application/models/legacy_core_editor/mode/input.ts
delete mode 100644 src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.ts
delete mode 100644 src/plugins/console/public/application/models/legacy_core_editor/mode/output.ts
delete mode 100644 src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.test.ts
delete mode 100644 src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.ts
delete mode 100644 src/plugins/console/public/application/models/legacy_core_editor/mode/script.ts
delete mode 100644 src/plugins/console/public/application/models/legacy_core_editor/mode/worker/index.d.ts
delete mode 100644 src/plugins/console/public/application/models/legacy_core_editor/mode/worker/index.js
delete mode 100644 src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js
delete mode 100644 src/plugins/console/public/application/models/legacy_core_editor/output_tokenization.test.js
delete mode 100644 src/plugins/console/public/application/models/legacy_core_editor/smart_resize.ts
delete mode 100644 src/plugins/console/public/application/models/legacy_core_editor/theme_sense_dark.js
delete mode 100644 src/plugins/console/public/application/models/sense_editor/__fixtures__/editor_input1.txt
delete mode 100644 src/plugins/console/public/application/models/sense_editor/create.ts
delete mode 100644 src/plugins/console/public/application/models/sense_editor/curl.ts
delete mode 100644 src/plugins/console/public/application/models/sense_editor/index.ts
delete mode 100644 src/plugins/console/public/application/models/sense_editor/integration.test.js
delete mode 100644 src/plugins/console/public/application/models/sense_editor/sense_editor.test.js
delete mode 100644 src/plugins/console/public/application/models/sense_editor/sense_editor.test.mocks.ts
delete mode 100644 src/plugins/console/public/application/models/sense_editor/sense_editor.ts
delete mode 100644 src/plugins/console/public/lib/ace_token_provider/index.ts
delete mode 100644 src/plugins/console/public/lib/ace_token_provider/token_provider.test.ts
delete mode 100644 src/plugins/console/public/lib/ace_token_provider/token_provider.ts
delete mode 100644 src/plugins/console/public/lib/autocomplete/autocomplete.ts
delete mode 100644 src/plugins/console/public/lib/autocomplete/get_endpoint_from_position.ts
delete mode 100644 src/plugins/console/public/lib/autocomplete/looks_like_typing_in.test.ts
delete mode 100644 src/plugins/console/public/lib/autocomplete/looks_like_typing_in.ts
delete mode 100644 src/plugins/console/public/lib/curl_parsing/__fixtures__/curl_parsing.txt
delete mode 100644 src/plugins/console/public/lib/curl_parsing/curl.js
delete mode 100644 src/plugins/console/public/lib/curl_parsing/curl_parsing.test.js
delete mode 100644 src/plugins/console/public/lib/row_parser.test.ts
delete mode 100644 src/plugins/console/public/lib/row_parser.ts
diff --git a/NOTICE.txt b/NOTICE.txt
index 3cee52c089cb4..80d49de19e5db 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -74,37 +74,6 @@ under a "BSD" license.
Distributed under the BSD license:
-Copyright (c) 2010, Ajax.org B.V.
-All rights reserved.
-
- Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
- * Redistributions of source code must retain the above copyright
- notice, this list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above copyright
- notice, this list of conditions and the following disclaimer in the
- documentation and/or other materials provided with the distribution.
- * Neither the name of Ajax.org B.V. nor the
- names of its contributors may be used to endorse or promote products
- derived from this software without specific prior written permission.
-
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
-DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
-ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
----
-This product includes code that is based on Ace editor, which was available
-under a "BSD" license.
-
-Distributed under the BSD license:
-
Copyright (c) 2010, Ajax.org B.V.
All rights reserved.
diff --git a/src/plugins/console/README.md b/src/plugins/console/README.md
index 02da27229286a..35921de334380 100644
--- a/src/plugins/console/README.md
+++ b/src/plugins/console/README.md
@@ -44,7 +44,7 @@ POST /_some_endpoint
```
## Architecture
-Console uses Ace editor that is wrapped with [`CoreEditor`](https://github.com/elastic/kibana/blob/main/src/plugins/console/public/types/core_editor.ts), so that if needed it can easily be replaced with another editor, for example Monaco.
+Console uses Monaco editor that is wrapped with [`kbn-monaco`](https://github.com/elastic/kibana/blob/main/packages/kbn-monaco/index.ts), so that if needed it can easily be replaced with another editor.
The autocomplete logic is located in [`autocomplete`](https://github.com/elastic/kibana/blob/main/src/plugins/console/public/lib/autocomplete) folder. Autocomplete rules are computed by classes in `components` sub-folder.
## Autocomplete definitions
@@ -317,8 +317,4 @@ Another change is replacing jQuery with the core http client to communicate with
### Outstanding issues
#### Autocomplete suggestions for Kibana API endpoints
Console currently supports autocomplete suggestions for Elasticsearch API endpoints. The autocomplete suggestions for Kibana API endpoints are not supported yet.
-Related issue: [#130661](https://github.com/elastic/kibana/issues/130661)
-
-#### Migration to Monaco Editor
-Console plugin is currently using Ace Editor and it is planned to migrate to Monaco Editor in the future.
-Related issue: [#57435](https://github.com/elastic/kibana/issues/57435)
\ No newline at end of file
+Related issue: [#130661](https://github.com/elastic/kibana/issues/130661)
\ No newline at end of file
diff --git a/src/plugins/console/public/application/contexts/editor_context/editor_registry.ts b/src/plugins/console/public/application/contexts/editor_context/editor_registry.ts
index 8197ff0460e86..dc7b58ecbd267 100644
--- a/src/plugins/console/public/application/contexts/editor_context/editor_registry.ts
+++ b/src/plugins/console/public/application/contexts/editor_context/editor_registry.ts
@@ -8,12 +8,11 @@
*/
import { MonacoEditorActionsProvider } from '../../containers/editor/monaco_editor_actions_provider';
-import { SenseEditor } from '../../models/sense_editor';
export class EditorRegistry {
- private inputEditor: SenseEditor | MonacoEditorActionsProvider | undefined;
+ private inputEditor: MonacoEditorActionsProvider | undefined;
- setInputEditor(inputEditor: SenseEditor | MonacoEditorActionsProvider) {
+ setInputEditor(inputEditor: MonacoEditorActionsProvider) {
this.inputEditor = inputEditor;
}
diff --git a/src/plugins/console/public/application/hooks/index.ts b/src/plugins/console/public/application/hooks/index.ts
index b6b7211a940e4..29c554771dad0 100644
--- a/src/plugins/console/public/application/hooks/index.ts
+++ b/src/plugins/console/public/application/hooks/index.ts
@@ -8,7 +8,6 @@
*/
export { useSetInputEditor } from './use_set_input_editor';
-export { useRestoreRequestFromHistory } from './use_restore_request_from_history';
-export { useSendCurrentRequest, sendRequest } from './use_send_current_request';
+export { sendRequest } from './use_send_current_request';
export { useSaveCurrentTextObject } from './use_save_current_text_object';
export { useDataInit } from './use_data_init';
diff --git a/src/plugins/console/public/application/hooks/use_restore_request_from_history/index.ts b/src/plugins/console/public/application/hooks/use_restore_request_from_history/index.ts
deleted file mode 100644
index 47f12868d9bc6..0000000000000
--- a/src/plugins/console/public/application/hooks/use_restore_request_from_history/index.ts
+++ /dev/null
@@ -1,10 +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", 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".
- */
-
-export { useRestoreRequestFromHistory } from './use_restore_request_from_history';
diff --git a/src/plugins/console/public/application/hooks/use_restore_request_from_history/restore_request_from_history.ts b/src/plugins/console/public/application/hooks/use_restore_request_from_history/restore_request_from_history.ts
deleted file mode 100644
index 897e499dc481e..0000000000000
--- a/src/plugins/console/public/application/hooks/use_restore_request_from_history/restore_request_from_history.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", 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 RowParser from '../../../lib/row_parser';
-import { ESRequest } from '../../../types';
-import { SenseEditor } from '../../models/sense_editor';
-import { formatRequestBodyDoc } from '../../../lib/utils';
-
-export function restoreRequestFromHistory(editor: SenseEditor, req: ESRequest) {
- const coreEditor = editor.getCoreEditor();
- let pos = coreEditor.getCurrentPosition();
- let prefix = '';
- let suffix = '\n';
- const parser = new RowParser(coreEditor);
- if (parser.isStartRequestRow(pos.lineNumber)) {
- pos.column = 1;
- suffix += '\n';
- } else if (parser.isEndRequestRow(pos.lineNumber)) {
- const line = coreEditor.getLineValue(pos.lineNumber);
- pos.column = line.length + 1;
- prefix = '\n\n';
- } else if (parser.isInBetweenRequestsRow(pos.lineNumber)) {
- pos.column = 1;
- } else {
- pos = editor.nextRequestEnd(pos);
- prefix = '\n\n';
- }
-
- let s = prefix + req.method + ' ' + req.endpoint;
- if (req.data) {
- const indent = true;
- const formattedData = formatRequestBodyDoc([req.data], indent);
- s += '\n' + formattedData.data;
- }
-
- s += suffix;
-
- coreEditor.insert(pos, s);
- coreEditor.moveCursorToPosition({ lineNumber: pos.lineNumber + prefix.length, column: 1 });
- coreEditor.clearSelection();
- coreEditor.getContainer().focus();
-}
diff --git a/src/plugins/console/public/application/hooks/use_restore_request_from_history/restore_request_from_history_to_monaco.ts b/src/plugins/console/public/application/hooks/use_restore_request_from_history/restore_request_from_history_to_monaco.ts
deleted file mode 100644
index 08c2bc6af86a3..0000000000000
--- a/src/plugins/console/public/application/hooks/use_restore_request_from_history/restore_request_from_history_to_monaco.ts
+++ /dev/null
@@ -1,25 +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", 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 { formatRequestBodyDoc } from '../../../lib/utils';
-import { MonacoEditorActionsProvider } from '../../containers/editor/monaco_editor_actions_provider';
-import { ESRequest } from '../../../types';
-
-export async function restoreRequestFromHistoryToMonaco(
- provider: MonacoEditorActionsProvider,
- req: ESRequest
-) {
- let s = req.method + ' ' + req.endpoint;
- if (req.data) {
- const indent = true;
- const formattedData = formatRequestBodyDoc([req.data], indent);
- s += '\n' + formattedData.data;
- }
- await provider.restoreRequestFromHistory(s);
-}
diff --git a/src/plugins/console/public/application/hooks/use_restore_request_from_history/use_restore_request_from_history.ts b/src/plugins/console/public/application/hooks/use_restore_request_from_history/use_restore_request_from_history.ts
deleted file mode 100644
index 5ee0d185923c2..0000000000000
--- a/src/plugins/console/public/application/hooks/use_restore_request_from_history/use_restore_request_from_history.ts
+++ /dev/null
@@ -1,21 +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", 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 { useCallback } from 'react';
-import { instance as registry } from '../../contexts/editor_context/editor_registry';
-import { ESRequest } from '../../../types';
-import { restoreRequestFromHistoryToMonaco } from './restore_request_from_history_to_monaco';
-import { MonacoEditorActionsProvider } from '../../containers/editor/monaco_editor_actions_provider';
-
-export const useRestoreRequestFromHistory = () => {
- return useCallback(async (req: ESRequest) => {
- const editor = registry.getInputEditor();
- await restoreRequestFromHistoryToMonaco(editor as MonacoEditorActionsProvider, req);
- }, []);
-};
diff --git a/src/plugins/console/public/application/hooks/use_send_current_request/index.ts b/src/plugins/console/public/application/hooks/use_send_current_request/index.ts
index 753184f67e998..656c0b939cf5b 100644
--- a/src/plugins/console/public/application/hooks/use_send_current_request/index.ts
+++ b/src/plugins/console/public/application/hooks/use_send_current_request/index.ts
@@ -7,5 +7,4 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-export { useSendCurrentRequest } from './use_send_current_request';
export { sendRequest } from './send_request';
diff --git a/src/plugins/console/public/application/hooks/use_send_current_request/track.ts b/src/plugins/console/public/application/hooks/use_send_current_request/track.ts
deleted file mode 100644
index e663c0b8354c1..0000000000000
--- a/src/plugins/console/public/application/hooks/use_send_current_request/track.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", 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 { SenseEditor } from '../../models/sense_editor';
-import { getEndpointFromPosition } from '../../../lib/autocomplete/get_endpoint_from_position';
-import { MetricsTracker } from '../../../types';
-
-export const track = (
- requests: Array<{ method: string }>,
- editor: SenseEditor,
- trackUiMetric: MetricsTracker
-) => {
- const coreEditor = editor.getCoreEditor();
- // `getEndpointFromPosition` gets values from the server-side generated JSON files which
- // are a combination of JS, automatically generated JSON and manual overrides. That means
- // the metrics reported from here will be tied to the definitions in those files.
- // See src/legacy/core_plugins/console/server/api_server/spec
- const endpointDescription = getEndpointFromPosition(
- coreEditor,
- coreEditor.getCurrentPosition(),
- editor.parser
- );
-
- if (requests[0] && endpointDescription) {
- const eventName = `${requests[0].method}_${endpointDescription.id ?? 'unknown'}`;
- trackUiMetric.count(eventName);
- }
-};
diff --git a/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx b/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx
deleted file mode 100644
index 7f3082d5ef3dc..0000000000000
--- a/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx
+++ /dev/null
@@ -1,130 +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", 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".
- */
-
-jest.mock('./send_request', () => ({ sendRequest: jest.fn() }));
-jest.mock('../../contexts/editor_context/editor_registry', () => ({
- instance: { getInputEditor: jest.fn() },
-}));
-jest.mock('./track', () => ({ track: jest.fn() }));
-jest.mock('../../contexts/request_context', () => ({ useRequestActionContext: jest.fn() }));
-jest.mock('../../../lib/utils', () => ({ replaceVariables: jest.fn() }));
-
-import React from 'react';
-import { renderHook, act } from '@testing-library/react-hooks';
-
-import { ContextValue, ServicesContextProvider } from '../../contexts';
-import { serviceContextMock } from '../../contexts/services_context.mock';
-import { useRequestActionContext } from '../../contexts/request_context';
-import { instance as editorRegistry } from '../../contexts/editor_context/editor_registry';
-import * as utils from '../../../lib/utils';
-
-import { sendRequest } from './send_request';
-import { useSendCurrentRequest } from './use_send_current_request';
-
-describe('useSendCurrentRequest', () => {
- let mockContextValue: ContextValue;
- let dispatch: (...args: unknown[]) => void;
- const contexts = ({ children }: { children: JSX.Element }) => (
- {children}
- );
-
- beforeEach(() => {
- mockContextValue = serviceContextMock.create();
- dispatch = jest.fn();
- (useRequestActionContext as jest.Mock).mockReturnValue(dispatch);
- (utils.replaceVariables as jest.Mock).mockReturnValue(['test']);
- });
-
- afterEach(() => {
- jest.resetAllMocks();
- });
-
- it('calls send request', async () => {
- // Set up mocks
- (mockContextValue.services.settings.toJSON as jest.Mock).mockReturnValue({});
- // This request should succeed
- (sendRequest as jest.Mock).mockResolvedValue([]);
- (editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({
- getRequestsInRange: () => ['test'],
- }));
-
- const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts });
- await act(() => result.current());
- expect(sendRequest).toHaveBeenCalledWith({
- http: mockContextValue.services.http,
- requests: ['test'],
- });
-
- // Second call should be the request success
- const [, [requestSucceededCall]] = (dispatch as jest.Mock).mock.calls;
- expect(requestSucceededCall).toEqual({ type: 'requestSuccess', payload: { data: [] } });
- });
-
- it('handles known errors', async () => {
- // Set up mocks
- (sendRequest as jest.Mock).mockRejectedValue({ response: 'nada' });
- (editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({
- getRequestsInRange: () => ['test'],
- }));
-
- const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts });
- await act(() => result.current());
- // Second call should be the request failure
- const [, [requestFailedCall]] = (dispatch as jest.Mock).mock.calls;
-
- // The request must have concluded
- expect(requestFailedCall).toEqual({ type: 'requestFail', payload: { response: 'nada' } });
- });
-
- it('handles unknown errors', async () => {
- // Set up mocks
- (sendRequest as jest.Mock).mockRejectedValue(NaN /* unexpected error value */);
- (editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({
- getRequestsInRange: () => ['test'],
- }));
-
- const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts });
- await act(() => result.current());
- // Second call should be the request failure
- const [, [requestFailedCall]] = (dispatch as jest.Mock).mock.calls;
-
- // The request must have concluded
- expect(requestFailedCall).toEqual({ type: 'requestFail', payload: undefined });
- // It also notified the user
- expect(mockContextValue.services.notifications.toasts.addError).toHaveBeenCalledWith(NaN, {
- title: 'Unknown Request Error',
- });
- });
-
- it('notifies the user about save to history errors once only', async () => {
- // Set up mocks
- (sendRequest as jest.Mock).mockReturnValue(
- [{ request: {} }, { request: {} }] /* two responses to save history */
- );
- (mockContextValue.services.settings.toJSON as jest.Mock).mockReturnValue({
- isHistoryEnabled: true,
- });
- (mockContextValue.services.history.addToHistory as jest.Mock).mockImplementation(() => {
- // Mock throwing
- throw new Error('cannot save!');
- });
- (editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({
- getRequestsInRange: () => ['test', 'test'],
- }));
-
- const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts });
- await act(() => result.current());
-
- expect(dispatch).toHaveBeenCalledTimes(2);
-
- expect(mockContextValue.services.history.addToHistory).toHaveBeenCalledTimes(2);
- // It only called notification once
- expect(mockContextValue.services.notifications.toasts.addError).toHaveBeenCalledTimes(1);
- });
-});
diff --git a/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts b/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts
deleted file mode 100644
index afdd5358432e9..0000000000000
--- a/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts
+++ /dev/null
@@ -1,148 +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", 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 { i18n } from '@kbn/i18n';
-import { useCallback } from 'react';
-
-import { toMountPoint } from '../../../shared_imports';
-import { isQuotaExceededError } from '../../../services/history';
-import { instance as registry } from '../../contexts/editor_context/editor_registry';
-import { useRequestActionContext, useServicesContext } from '../../contexts';
-import { StorageQuotaError } from '../../components/storage_quota_error';
-import { sendRequest } from './send_request';
-import { track } from './track';
-import { replaceVariables } from '../../../lib/utils';
-import { StorageKeys } from '../../../services';
-import { DEFAULT_VARIABLES } from '../../../../common/constants';
-import { SenseEditor } from '../../models';
-
-export const useSendCurrentRequest = () => {
- const {
- services: { history, settings, notifications, trackUiMetric, http, autocompleteInfo, storage },
- ...startServices
- } = useServicesContext();
-
- const dispatch = useRequestActionContext();
-
- return useCallback(async () => {
- try {
- const editor = registry.getInputEditor() as SenseEditor;
- const variables = storage.get(StorageKeys.VARIABLES, DEFAULT_VARIABLES);
- let requests = await editor.getRequestsInRange();
- requests = replaceVariables(requests, variables);
- if (!requests.length) {
- notifications.toasts.add(
- i18n.translate('console.notification.error.noRequestSelectedTitle', {
- defaultMessage:
- 'No request selected. Select a request by placing the cursor inside it.',
- })
- );
- return;
- }
-
- dispatch({ type: 'sendRequest', payload: undefined });
-
- // Fire and forget
- setTimeout(() => track(requests, editor as SenseEditor, trackUiMetric), 0);
-
- const results = await sendRequest({ http, requests });
-
- let saveToHistoryError: undefined | Error;
- const { isHistoryEnabled } = settings.toJSON();
-
- if (isHistoryEnabled) {
- results.forEach(({ request: { path, method, data } }) => {
- try {
- history.addToHistory(path, method, data);
- } catch (e) {
- // Grab only the first error
- if (!saveToHistoryError) {
- saveToHistoryError = e;
- }
- }
- });
- }
-
- if (saveToHistoryError) {
- const errorTitle = i18n.translate('console.notification.error.couldNotSaveRequestTitle', {
- defaultMessage: 'Could not save request to Console history.',
- });
- if (isQuotaExceededError(saveToHistoryError)) {
- const toast = notifications.toasts.addWarning({
- title: i18n.translate('console.notification.error.historyQuotaReachedMessage', {
- defaultMessage:
- 'Request history is full. Clear the console history or disable saving new requests.',
- }),
- text: toMountPoint(
- StorageQuotaError({
- onClearHistory: () => {
- history.clearHistory();
- notifications.toasts.remove(toast);
- },
- onDisableSavingToHistory: () => {
- settings.setIsHistoryEnabled(false);
- notifications.toasts.remove(toast);
- },
- }),
- startServices
- ),
- });
- } else {
- // Best effort, but still notify the user.
- notifications.toasts.addError(saveToHistoryError, {
- title: errorTitle,
- });
- }
- }
-
- const { polling } = settings.toJSON();
- if (polling) {
- // If the user has submitted a request against ES, something in the fields, indices, aliases,
- // or templates may have changed, so we'll need to update this data. Assume that if
- // the user disables polling they're trying to optimize performance or otherwise
- // preserve resources, so they won't want this request sent either.
- autocompleteInfo.retrieve(settings, settings.getAutocomplete());
- }
-
- dispatch({
- type: 'requestSuccess',
- payload: {
- data: results,
- },
- });
- } catch (e) {
- if (e?.response) {
- dispatch({
- type: 'requestFail',
- payload: e,
- });
- } else {
- dispatch({
- type: 'requestFail',
- payload: undefined,
- });
- notifications.toasts.addError(e, {
- title: i18n.translate('console.notification.error.unknownErrorTitle', {
- defaultMessage: 'Unknown Request Error',
- }),
- });
- }
- }
- }, [
- storage,
- dispatch,
- http,
- settings,
- notifications.toasts,
- trackUiMetric,
- history,
- autocompleteInfo,
- startServices,
- ]);
-};
diff --git a/src/plugins/console/public/application/hooks/use_set_input_editor.ts b/src/plugins/console/public/application/hooks/use_set_input_editor.ts
index d6029420a1772..148ede97520ea 100644
--- a/src/plugins/console/public/application/hooks/use_set_input_editor.ts
+++ b/src/plugins/console/public/application/hooks/use_set_input_editor.ts
@@ -10,14 +10,13 @@
import { useCallback } from 'react';
import { useEditorActionContext } from '../contexts/editor_context';
import { instance as registry } from '../contexts/editor_context/editor_registry';
-import { SenseEditor } from '../models';
import { MonacoEditorActionsProvider } from '../containers/editor/monaco_editor_actions_provider';
export const useSetInputEditor = () => {
const dispatch = useEditorActionContext();
return useCallback(
- (editor: SenseEditor | MonacoEditorActionsProvider) => {
+ (editor: MonacoEditorActionsProvider) => {
dispatch({ type: 'setInputEditor', payload: editor });
registry.setInputEditor(editor);
},
diff --git a/src/plugins/console/public/application/models/index.ts b/src/plugins/console/public/application/models/index.ts
deleted file mode 100644
index 0d4a8f474daee..0000000000000
--- a/src/plugins/console/public/application/models/index.ts
+++ /dev/null
@@ -1,11 +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", 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".
- */
-
-export * from './legacy_core_editor/legacy_core_editor';
-export * from './sense_editor';
diff --git a/src/plugins/console/public/application/models/legacy_core_editor/create.ts b/src/plugins/console/public/application/models/legacy_core_editor/create.ts
deleted file mode 100644
index b2631e8d6712b..0000000000000
--- a/src/plugins/console/public/application/models/legacy_core_editor/create.ts
+++ /dev/null
@@ -1,20 +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", 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 ace from 'brace';
-import { LegacyCoreEditor } from './legacy_core_editor';
-
-export const create = (el: HTMLElement) => {
- const actions = document.querySelector('#ConAppEditorActions');
- if (!actions) {
- throw new Error('Could not find ConAppEditorActions element!');
- }
- const aceEditor = ace.edit(el);
- return new LegacyCoreEditor(aceEditor, actions);
-};
diff --git a/src/plugins/console/public/application/models/legacy_core_editor/create_readonly.ts b/src/plugins/console/public/application/models/legacy_core_editor/create_readonly.ts
deleted file mode 100644
index dc0a95c224395..0000000000000
--- a/src/plugins/console/public/application/models/legacy_core_editor/create_readonly.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", 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 _ from 'lodash';
-import ace from 'brace';
-import { Mode } from './mode/output';
-import smartResize from './smart_resize';
-
-export interface CustomAceEditor extends ace.Editor {
- update: (text: string, mode?: string | Mode, cb?: () => void) => void;
- append: (text: string, foldPrevious?: boolean, cb?: () => void) => void;
-}
-
-/**
- * Note: using read-only ace editor leaks the Ace editor API - use this as sparingly as possible or
- * create an interface for it so that we don't rely directly on vendor APIs.
- */
-export function createReadOnlyAceEditor(element: HTMLElement): CustomAceEditor {
- const output: CustomAceEditor = ace.acequire('ace/ace').edit(element);
-
- const outputMode = new Mode();
-
- output.$blockScrolling = Infinity;
- output.resize = smartResize(output);
- output.update = (val, mode, cb) => {
- if (typeof mode === 'function') {
- cb = mode as () => void;
- mode = void 0;
- }
-
- const session = output.getSession();
- const currentMode = val ? mode || outputMode : 'ace/mode/text';
-
- // @ts-ignore
- // ignore ts error here due to type definition mistake in brace for setMode(mode: string): void;
- // this method accepts string or SyntaxMode which is an object. See https://github.com/ajaxorg/ace/blob/13dc911dbc0ea31ca343d5744b3f472767458fc3/ace.d.ts#L467
- session.setMode(currentMode);
- session.setValue(val);
- if (typeof cb === 'function') {
- setTimeout(cb);
- }
- };
-
- output.append = (val: string, foldPrevious?: boolean, cb?: () => void) => {
- if (typeof foldPrevious === 'function') {
- cb = foldPrevious;
- foldPrevious = true;
- }
- if (_.isUndefined(foldPrevious)) {
- foldPrevious = true;
- }
- const session = output.getSession();
- const lastLine = session.getLength();
- if (foldPrevious) {
- output.moveCursorTo(Math.max(0, lastLine - 1), 0);
- }
- session.insert({ row: lastLine, column: 0 }, '\n' + val);
- output.moveCursorTo(lastLine + 1, 0);
- if (typeof cb === 'function') {
- setTimeout(cb);
- }
- };
-
- (function setupSession(session) {
- session.setMode('ace/mode/text');
- (session as unknown as { setFoldStyle: (v: string) => void }).setFoldStyle('markbeginend');
- session.setTabSize(2);
- session.setUseWrapMode(true);
- })(output.getSession());
-
- output.setShowPrintMargin(false);
- output.setReadOnly(true);
-
- return output;
-}
diff --git a/src/plugins/console/public/application/models/legacy_core_editor/index.ts b/src/plugins/console/public/application/models/legacy_core_editor/index.ts
deleted file mode 100644
index e885257520245..0000000000000
--- a/src/plugins/console/public/application/models/legacy_core_editor/index.ts
+++ /dev/null
@@ -1,18 +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", 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 'brace';
-import 'brace/ext/language_tools';
-import 'brace/ext/searchbox';
-import 'brace/mode/json';
-import 'brace/mode/text';
-
-export * from './legacy_core_editor';
-export * from './create_readonly';
-export * from './create';
diff --git a/src/plugins/console/public/application/models/legacy_core_editor/input.test.js b/src/plugins/console/public/application/models/legacy_core_editor/input.test.js
deleted file mode 100644
index e472edc1af125..0000000000000
--- a/src/plugins/console/public/application/models/legacy_core_editor/input.test.js
+++ /dev/null
@@ -1,559 +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", 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 './legacy_core_editor.test.mocks';
-import RowParser from '../../../lib/row_parser';
-import { createTokenIterator } from '../../factories';
-import $ from 'jquery';
-import { create } from './create';
-
-describe('Input', () => {
- let coreEditor;
- beforeEach(() => {
- // Set up our document body
- document.body.innerHTML = ``;
-
- coreEditor = create(document.querySelector('#ConAppEditor'));
-
- $(coreEditor.getContainer()).show();
- });
- afterEach(() => {
- $(coreEditor.getContainer()).hide();
- });
-
- describe('.getLineCount', () => {
- it('returns the correct line length', async () => {
- await coreEditor.setValue('1\n2\n3\n4', true);
- expect(coreEditor.getLineCount()).toBe(4);
- });
- });
-
- describe('Tokenization', () => {
- function tokensAsList() {
- const iter = createTokenIterator({
- editor: coreEditor,
- position: { lineNumber: 1, column: 1 },
- });
- const ret = [];
- let t = iter.getCurrentToken();
- const parser = new RowParser(coreEditor);
- if (parser.isEmptyToken(t)) {
- t = parser.nextNonEmptyToken(iter);
- }
- while (t) {
- ret.push({ value: t.value, type: t.type });
- t = parser.nextNonEmptyToken(iter);
- }
-
- return ret;
- }
-
- let testCount = 0;
-
- function tokenTest(tokenList, prefix, data) {
- if (data && typeof data !== 'string') {
- data = JSON.stringify(data, null, 3);
- }
- if (data) {
- if (prefix) {
- data = prefix + '\n' + data;
- }
- } else {
- data = prefix;
- }
-
- test('Token test ' + testCount++ + ' prefix: ' + prefix, async function () {
- await coreEditor.setValue(data, true);
- const tokens = tokensAsList();
- const normTokenList = [];
- for (let i = 0; i < tokenList.length; i++) {
- normTokenList.push({ type: tokenList[i++], value: tokenList[i] });
- }
-
- expect(tokens).toEqual(normTokenList);
- });
- }
-
- tokenTest(['method', 'GET', 'url.part', '_search'], 'GET _search');
-
- tokenTest(['method', 'GET', 'url.slash', '/', 'url.part', '_search'], 'GET /_search');
-
- tokenTest(
- [
- 'method',
- 'GET',
- 'url.protocol_host',
- 'http://somehost',
- 'url.slash',
- '/',
- 'url.part',
- '_search',
- ],
- 'GET http://somehost/_search'
- );
-
- tokenTest(['method', 'GET', 'url.protocol_host', 'http://somehost'], 'GET http://somehost');
-
- tokenTest(
- ['method', 'GET', 'url.protocol_host', 'http://somehost', 'url.slash', '/'],
- 'GET http://somehost/'
- );
-
- tokenTest(
- ['method', 'GET', 'url.protocol_host', 'http://test:user@somehost', 'url.slash', '/'],
- 'GET http://test:user@somehost/'
- );
-
- tokenTest(
- ['method', 'GET', 'url.part', '_cluster', 'url.slash', '/', 'url.part', 'nodes'],
- 'GET _cluster/nodes'
- );
-
- tokenTest(
- [
- 'method',
- 'GET',
- 'url.slash',
- '/',
- 'url.part',
- '_cluster',
- 'url.slash',
- '/',
- 'url.part',
- 'nodes',
- ],
- 'GET /_cluster/nodes'
- );
-
- tokenTest(
- ['method', 'GET', 'url.part', 'index', 'url.slash', '/', 'url.part', '_search'],
- 'GET index/_search'
- );
-
- tokenTest(['method', 'GET', 'url.part', 'index'], 'GET index');
-
- tokenTest(
- ['method', 'GET', 'url.part', 'index', 'url.slash', '/', 'url.part', 'type'],
- 'GET index/type'
- );
-
- tokenTest(
- [
- 'method',
- 'GET',
- 'url.slash',
- '/',
- 'url.part',
- 'index',
- 'url.slash',
- '/',
- 'url.part',
- 'type',
- 'url.slash',
- '/',
- ],
- 'GET /index/type/'
- );
-
- tokenTest(
- [
- 'method',
- 'GET',
- 'url.part',
- 'index',
- 'url.slash',
- '/',
- 'url.part',
- 'type',
- 'url.slash',
- '/',
- 'url.part',
- '_search',
- ],
- 'GET index/type/_search'
- );
-
- tokenTest(
- [
- 'method',
- 'GET',
- 'url.part',
- 'index',
- 'url.slash',
- '/',
- 'url.part',
- 'type',
- 'url.slash',
- '/',
- 'url.part',
- '_search',
- 'url.questionmark',
- '?',
- 'url.param',
- 'value',
- 'url.equal',
- '=',
- 'url.value',
- '1',
- ],
- 'GET index/type/_search?value=1'
- );
-
- tokenTest(
- [
- 'method',
- 'GET',
- 'url.part',
- 'index',
- 'url.slash',
- '/',
- 'url.part',
- 'type',
- 'url.slash',
- '/',
- 'url.part',
- '1',
- ],
- 'GET index/type/1'
- );
-
- tokenTest(
- [
- 'method',
- 'GET',
- 'url.slash',
- '/',
- 'url.part',
- 'index1',
- 'url.comma',
- ',',
- 'url.part',
- 'index2',
- 'url.slash',
- '/',
- ],
- 'GET /index1,index2/'
- );
-
- tokenTest(
- [
- 'method',
- 'GET',
- 'url.slash',
- '/',
- 'url.part',
- 'index1',
- 'url.comma',
- ',',
- 'url.part',
- 'index2',
- 'url.slash',
- '/',
- 'url.part',
- '_search',
- ],
- 'GET /index1,index2/_search'
- );
-
- tokenTest(
- [
- 'method',
- 'GET',
- 'url.part',
- 'index1',
- 'url.comma',
- ',',
- 'url.part',
- 'index2',
- 'url.slash',
- '/',
- 'url.part',
- '_search',
- ],
- 'GET index1,index2/_search'
- );
-
- tokenTest(
- [
- 'method',
- 'GET',
- 'url.slash',
- '/',
- 'url.part',
- 'index1',
- 'url.comma',
- ',',
- 'url.part',
- 'index2',
- ],
- 'GET /index1,index2'
- );
-
- tokenTest(
- ['method', 'GET', 'url.part', 'index1', 'url.comma', ',', 'url.part', 'index2'],
- 'GET index1,index2'
- );
-
- tokenTest(
- ['method', 'GET', 'url.slash', '/', 'url.part', 'index1', 'url.comma', ','],
- 'GET /index1,'
- );
-
- tokenTest(
- ['method', 'PUT', 'url.slash', '/', 'url.part', 'index', 'url.slash', '/'],
- 'PUT /index/'
- );
-
- tokenTest(
- ['method', 'GET', 'url.part', 'index', 'url.slash', '/', 'url.part', '_search'],
- 'GET index/_search '
- );
-
- tokenTest(['method', 'PUT', 'url.slash', '/', 'url.part', 'index'], 'PUT /index');
-
- tokenTest(
- [
- 'method',
- 'PUT',
- 'url.slash',
- '/',
- 'url.part',
- 'index1',
- 'url.comma',
- ',',
- 'url.part',
- 'index2',
- 'url.slash',
- '/',
- 'url.part',
- 'type1',
- 'url.comma',
- ',',
- 'url.part',
- 'type2',
- ],
- 'PUT /index1,index2/type1,type2'
- );
-
- tokenTest(
- [
- 'method',
- 'PUT',
- 'url.slash',
- '/',
- 'url.part',
- 'index1',
- 'url.slash',
- '/',
- 'url.part',
- 'type1',
- 'url.comma',
- ',',
- 'url.part',
- 'type2',
- 'url.comma',
- ',',
- ],
- 'PUT /index1/type1,type2,'
- );
-
- tokenTest(
- [
- 'method',
- 'PUT',
- 'url.part',
- 'index1',
- 'url.comma',
- ',',
- 'url.part',
- 'index2',
- 'url.slash',
- '/',
- 'url.part',
- 'type1',
- 'url.comma',
- ',',
- 'url.part',
- 'type2',
- 'url.slash',
- '/',
- 'url.part',
- '1234',
- ],
- 'PUT index1,index2/type1,type2/1234'
- );
-
- tokenTest(
- [
- 'method',
- 'POST',
- 'url.part',
- '_search',
- 'paren.lparen',
- '{',
- 'variable',
- '"q"',
- 'punctuation.colon',
- ':',
- 'paren.lparen',
- '{',
- 'paren.rparen',
- '}',
- 'paren.rparen',
- '}',
- ],
- 'POST _search\n' + '{\n' + ' "q": {}\n' + ' \n' + '}'
- );
-
- tokenTest(
- [
- 'method',
- 'POST',
- 'url.part',
- '_search',
- 'paren.lparen',
- '{',
- 'variable',
- '"q"',
- 'punctuation.colon',
- ':',
- 'paren.lparen',
- '{',
- 'variable',
- '"s"',
- 'punctuation.colon',
- ':',
- 'paren.lparen',
- '{',
- 'paren.rparen',
- '}',
- 'paren.rparen',
- '}',
- 'paren.rparen',
- '}',
- ],
- 'POST _search\n' + '{\n' + ' "q": { "s": {}}\n' + ' \n' + '}'
- );
-
- function statesAsList() {
- const ret = [];
- const maxLine = coreEditor.getLineCount();
- for (let line = 1; line <= maxLine; line++) ret.push(coreEditor.getLineState(line));
- return ret;
- }
-
- function statesTest(statesList, prefix, data) {
- if (data && typeof data !== 'string') {
- data = JSON.stringify(data, null, 3);
- }
- if (data) {
- if (prefix) {
- data = prefix + '\n' + data;
- }
- } else {
- data = prefix;
- }
-
- test('States test ' + testCount++ + ' prefix: ' + prefix, async function () {
- await coreEditor.setValue(data, true);
- const modes = statesAsList();
- expect(modes).toEqual(statesList);
- });
- }
-
- statesTest(
- ['start', 'json', 'json', 'start'],
- 'POST _search\n' + '{\n' + ' "query": { "match_all": {} }\n' + '}'
- );
-
- statesTest(
- ['start', 'json', ['json', 'json'], ['json', 'json'], 'json', 'start'],
- 'POST _search\n' + '{\n' + ' "query": { \n' + ' "match_all": {} \n' + ' }\n' + '}'
- );
-
- statesTest(
- ['start', 'json', 'json', 'start'],
- 'POST _search\n' + '{\n' + ' "script": { "source": "" }\n' + '}'
- );
-
- statesTest(
- ['start', 'json', 'json', 'start'],
- 'POST _search\n' + '{\n' + ' "script": ""\n' + '}'
- );
-
- statesTest(
- ['start', 'json', ['json', 'json'], 'json', 'start'],
- 'POST _search\n' + '{\n' + ' "script": {\n' + ' }\n' + '}'
- );
-
- statesTest(
- [
- 'start',
- 'json',
- ['script-start', 'json', 'json', 'json'],
- ['script-start', 'json', 'json', 'json'],
- ['json', 'json'],
- 'json',
- 'start',
- ],
- 'POST _search\n' +
- '{\n' +
- ' "test": { "script": """\n' +
- ' test script\n' +
- ' """\n' +
- ' }\n' +
- '}'
- );
-
- statesTest(
- ['start', 'json', ['script-start', 'json'], ['script-start', 'json'], 'json', 'start'],
- 'POST _search\n' + '{\n' + ' "script": """\n' + ' test script\n' + ' """,\n' + '}'
- );
-
- statesTest(
- ['start', 'json', 'json', 'start'],
- 'POST _search\n' + '{\n' + ' "script": """test script""",\n' + '}'
- );
-
- statesTest(
- ['start', 'json', ['string_literal', 'json'], ['string_literal', 'json'], 'json', 'start'],
- 'POST _search\n' + '{\n' + ' "something": """\n' + ' test script\n' + ' """,\n' + '}'
- );
-
- statesTest(
- [
- 'start',
- 'json',
- ['string_literal', 'json', 'json', 'json'],
- ['string_literal', 'json', 'json', 'json'],
- ['json', 'json'],
- ['json', 'json'],
- 'json',
- 'start',
- ],
- 'POST _search\n' +
- '{\n' +
- ' "something": { "f" : """\n' +
- ' test script\n' +
- ' """,\n' +
- ' "g": 1\n' +
- ' }\n' +
- '}'
- );
-
- statesTest(
- ['start', 'json', 'json', 'start'],
- 'POST _search\n' + '{\n' + ' "something": """test script""",\n' + '}'
- );
- });
-});
diff --git a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.test.mocks.ts b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.test.mocks.ts
deleted file mode 100644
index 2ef5551e893d1..0000000000000
--- a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.test.mocks.ts
+++ /dev/null
@@ -1,29 +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", 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".
- */
-
-jest.mock('./mode/worker', () => {
- return { workerModule: { id: 'sense_editor/mode/worker', src: '' } };
-});
-
-import '@kbn/web-worker-stub';
-
-// @ts-ignore
-window.URL = {
- createObjectURL: () => {
- return '';
- },
-};
-
-import 'brace';
-import 'brace/ext/language_tools';
-import 'brace/ext/searchbox';
-import 'brace/mode/json';
-import 'brace/mode/text';
-
-document.queryCommandSupported = () => true;
diff --git a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts
deleted file mode 100644
index edeb64104be7f..0000000000000
--- a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts
+++ /dev/null
@@ -1,511 +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", 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 ace, { type Annotation } from 'brace';
-import { Editor as IAceEditor, IEditSession as IAceEditSession } from 'brace';
-import $ from 'jquery';
-import {
- CoreEditor,
- Position,
- Range,
- Token,
- TokensProvider,
- EditorEvent,
- AutoCompleterFunction,
-} from '../../../types';
-import { AceTokensProvider } from '../../../lib/ace_token_provider';
-import * as curl from '../sense_editor/curl';
-import smartResize from './smart_resize';
-import * as InputMode from './mode/input';
-
-const _AceRange = ace.acequire('ace/range').Range;
-
-const rangeToAceRange = ({ start, end }: Range) =>
- new _AceRange(start.lineNumber - 1, start.column - 1, end.lineNumber - 1, end.column - 1);
-
-export class LegacyCoreEditor implements CoreEditor {
- private _aceOnPaste: Function;
- $actions: JQuery;
- resize: () => void;
-
- constructor(private readonly editor: IAceEditor, actions: HTMLElement) {
- this.$actions = $(actions);
- this.editor.setShowPrintMargin(false);
-
- const session = this.editor.getSession();
- // @ts-expect-error
- // ignore ts error here due to type definition mistake in brace for setMode(mode: string): void;
- // this method accepts string or SyntaxMode which is an object. See https://github.com/ajaxorg/ace/blob/13dc911dbc0ea31ca343d5744b3f472767458fc3/ace.d.ts#L467
- session.setMode(new InputMode.Mode());
- (session as unknown as { setFoldStyle: (style: string) => void }).setFoldStyle('markbeginend');
- session.setTabSize(2);
- session.setUseWrapMode(true);
-
- this.resize = smartResize(this.editor);
-
- // Intercept ace on paste handler.
- this._aceOnPaste = this.editor.onPaste;
- this.editor.onPaste = this.DO_NOT_USE_onPaste.bind(this);
-
- this.editor.setOptions({
- enableBasicAutocompletion: true,
- });
-
- this.editor.$blockScrolling = Infinity;
- this.hideActionsBar();
- this.editor.focus();
- }
-
- // dirty check for tokenizer state, uses a lot less cycles
- // than listening for tokenizerUpdate
- waitForLatestTokens(): Promise {
- return new Promise((resolve) => {
- const session = this.editor.getSession();
- const checkInterval = 25;
-
- const check = () => {
- // If the bgTokenizer doesn't exist, we can assume that the underlying editor has been
- // torn down, e.g. by closing the History tab, and we don't need to do anything further.
- if (session.bgTokenizer) {
- // Wait until the bgTokenizer is done running before executing the callback.
- if ((session.bgTokenizer as unknown as { running: boolean }).running) {
- setTimeout(check, checkInterval);
- } else {
- resolve();
- }
- }
- };
-
- setTimeout(check, 0);
- });
- }
-
- getLineState(lineNumber: number) {
- const session = this.editor.getSession();
- return session.getState(lineNumber - 1);
- }
-
- getValueInRange(range: Range): string {
- return this.editor.getSession().getTextRange(rangeToAceRange(range));
- }
-
- getTokenProvider(): TokensProvider {
- return new AceTokensProvider(this.editor.getSession());
- }
-
- getValue(): string {
- return this.editor.getValue();
- }
-
- async setValue(text: string, forceRetokenize: boolean): Promise {
- const session = this.editor.getSession();
- session.setValue(text);
- if (forceRetokenize) {
- await this.forceRetokenize();
- }
- }
-
- getLineValue(lineNumber: number): string {
- const session = this.editor.getSession();
- return session.getLine(lineNumber - 1);
- }
-
- getCurrentPosition(): Position {
- const cursorPosition = this.editor.getCursorPosition();
- return {
- lineNumber: cursorPosition.row + 1,
- column: cursorPosition.column + 1,
- };
- }
-
- clearSelection(): void {
- this.editor.clearSelection();
- }
-
- getTokenAt(pos: Position): Token | null {
- const provider = this.getTokenProvider();
- return provider.getTokenAt(pos);
- }
-
- insert(valueOrPos: string | Position, value?: string): void {
- if (typeof valueOrPos === 'string') {
- this.editor.insert(valueOrPos);
- return;
- }
- const document = this.editor.getSession().getDocument();
- document.insert(
- {
- column: valueOrPos.column - 1,
- row: valueOrPos.lineNumber - 1,
- },
- value || ''
- );
- }
-
- moveCursorToPosition(pos: Position): void {
- this.editor.moveCursorToPosition({ row: pos.lineNumber - 1, column: pos.column - 1 });
- }
-
- replace(range: Range, value: string): void {
- const session = this.editor.getSession();
- session.replace(rangeToAceRange(range), value);
- }
-
- getLines(startLine: number, endLine: number): string[] {
- const session = this.editor.getSession();
- return session.getLines(startLine - 1, endLine - 1);
- }
-
- replaceRange(range: Range, value: string) {
- const pos = this.editor.getCursorPosition();
- this.editor.getSession().replace(rangeToAceRange(range), value);
-
- const maxRow = Math.max(range.start.lineNumber - 1 + value.split('\n').length - 1, 1);
- pos.row = Math.min(pos.row, maxRow);
- this.editor.moveCursorToPosition(pos);
- // ACE UPGRADE - check if needed - at the moment the above may trigger a selection.
- this.editor.clearSelection();
- }
-
- getSelectionRange() {
- const result = this.editor.getSelectionRange();
- return {
- start: {
- lineNumber: result.start.row + 1,
- column: result.start.column + 1,
- },
- end: {
- lineNumber: result.end.row + 1,
- column: result.end.column + 1,
- },
- };
- }
-
- getLineCount() {
- // Only use this function to return line count as it uses
- // a cache.
- return this.editor.getSession().getLength();
- }
-
- addMarker(range: Range) {
- return this.editor
- .getSession()
- .addMarker(rangeToAceRange(range), 'ace_snippet-marker', 'fullLine', false);
- }
-
- removeMarker(ref: number) {
- this.editor.getSession().removeMarker(ref);
- }
-
- getWrapLimit(): number {
- return this.editor.getSession().getWrapLimit();
- }
-
- on(event: EditorEvent, listener: () => void) {
- if (event === 'changeCursor') {
- this.editor.getSession().selection.on(event, listener);
- } else if (event === 'changeSelection') {
- this.editor.on(event, listener);
- } else {
- this.editor.getSession().on(event, listener);
- }
- }
-
- off(event: EditorEvent, listener: () => void) {
- if (event === 'changeSelection') {
- this.editor.off(event, listener);
- }
- }
-
- isCompleterActive() {
- return Boolean(
- (this.editor as unknown as { completer: { activated: unknown } }).completer &&
- (this.editor as unknown as { completer: { activated: unknown } }).completer.activated
- );
- }
-
- detachCompleter() {
- // In some situations we need to detach the autocomplete suggestions element manually,
- // such as when navigating away from Console when the suggestions list is open.
- const completer = (this.editor as unknown as { completer: { detach(): void } }).completer;
- return completer?.detach();
- }
-
- private forceRetokenize() {
- const session = this.editor.getSession();
- return new Promise((resolve) => {
- // force update of tokens, but not on this thread to allow for ace rendering.
- setTimeout(function () {
- let i;
- for (i = 0; i < session.getLength(); i++) {
- session.getTokens(i);
- }
- resolve();
- });
- });
- }
-
- // eslint-disable-next-line @typescript-eslint/naming-convention
- private DO_NOT_USE_onPaste(text: string) {
- if (text && curl.detectCURL(text)) {
- const curlInput = curl.parseCURL(text);
- this.editor.insert(curlInput);
- return;
- }
- this._aceOnPaste.call(this.editor, text);
- }
-
- private setActionsBar = (value: number | null, topOrBottom: 'top' | 'bottom' = 'top') => {
- if (value === null) {
- this.$actions.css('visibility', 'hidden');
- } else {
- if (topOrBottom === 'top') {
- this.$actions.css({
- bottom: 'auto',
- top: value,
- visibility: 'visible',
- });
- } else {
- this.$actions.css({
- top: 'auto',
- bottom: value,
- visibility: 'visible',
- });
- }
- }
- };
-
- private hideActionsBar = () => {
- this.setActionsBar(null);
- };
-
- execCommand(cmd: string) {
- this.editor.execCommand(cmd);
- }
-
- getContainer(): HTMLDivElement {
- return this.editor.container as HTMLDivElement;
- }
-
- setStyles(styles: { wrapLines: boolean; fontSize: string }) {
- this.editor.getSession().setUseWrapMode(styles.wrapLines);
- this.editor.container.style.fontSize = styles.fontSize;
- }
-
- registerKeyboardShortcut(opts: { keys: string; fn: () => void; name: string }): void {
- this.editor.commands.addCommand({
- exec: opts.fn,
- name: opts.name,
- bindKey: opts.keys,
- });
- }
-
- unregisterKeyboardShortcut(command: string) {
- // @ts-ignore
- this.editor.commands.removeCommand(command);
- }
-
- legacyUpdateUI(range: Range) {
- if (!this.$actions) {
- return;
- }
- if (range) {
- // elements are positioned relative to the editor's container
- // pageY is relative to page, so subtract the offset
- // from pageY to get the new top value
- const offsetFromPage = $(this.editor.container).offset()!.top;
- const startLine = range.start.lineNumber;
- const startColumn = range.start.column;
- const firstLine = this.getLineValue(startLine);
- const maxLineLength = this.getWrapLimit() - 5;
- const isWrapping = firstLine.length > maxLineLength;
- const totalOffset = offsetFromPage - (window.pageYOffset || 0);
- const getScreenCoords = (line: number) =>
- this.editor.renderer.textToScreenCoordinates(line - 1, startColumn).pageY - totalOffset;
- const topOfReq = getScreenCoords(startLine);
-
- if (topOfReq >= 0) {
- const { bottom: maxBottom } = this.editor.container.getBoundingClientRect();
- if (topOfReq > maxBottom - totalOffset) {
- this.setActionsBar(0, 'bottom');
- return;
- }
- let offset = 0;
- if (isWrapping) {
- // Try get the line height of the text area in pixels.
- const textArea = $(this.editor.container.querySelector('textArea')!);
- const hasRoomOnNextLine = this.getLineValue(startLine).length < maxLineLength;
- if (textArea && hasRoomOnNextLine) {
- // Line height + the number of wraps we have on a line.
- offset += this.getLineValue(startLine).length * textArea.height()!;
- } else {
- if (startLine > 1) {
- this.setActionsBar(getScreenCoords(startLine - 1));
- return;
- }
- this.setActionsBar(getScreenCoords(startLine + 1));
- return;
- }
- }
- this.setActionsBar(topOfReq + offset);
- return;
- }
-
- const bottomOfReq =
- this.editor.renderer.textToScreenCoordinates(range.end.lineNumber, range.end.column).pageY -
- offsetFromPage;
-
- if (bottomOfReq >= 0) {
- this.setActionsBar(0);
- return;
- }
- }
- }
-
- registerAutocompleter(autocompleter: AutoCompleterFunction): void {
- // Hook into Ace
-
- // disable standard context based autocompletion.
- // @ts-ignore
- ace.define(
- 'ace/autocomplete/text_completer',
- ['require', 'exports', 'module'],
- function (
- require: unknown,
- exports: {
- getCompletions: (
- innerEditor: unknown,
- session: unknown,
- pos: unknown,
- prefix: unknown,
- callback: (e: null | Error, values: string[]) => void
- ) => void;
- }
- ) {
- exports.getCompletions = function (innerEditor, session, pos, prefix, callback) {
- callback(null, []);
- };
- }
- );
-
- const langTools = ace.acequire('ace/ext/language_tools');
-
- langTools.setCompleters([
- {
- identifierRegexps: [
- /[a-zA-Z_0-9\.\$\-\u00A2-\uFFFF]/, // adds support for dot character
- ],
- getCompletions: (
- // eslint-disable-next-line @typescript-eslint/naming-convention
- DO_NOT_USE_1: IAceEditor,
- aceEditSession: IAceEditSession,
- pos: { row: number; column: number },
- prefix: string,
- callback: (...args: unknown[]) => void
- ) => {
- const position: Position = {
- lineNumber: pos.row + 1,
- column: pos.column + 1,
- };
-
- const getAnnotationControls = () => {
- let customAnnotation: Annotation;
- return {
- setAnnotation(text: string) {
- const annotations = aceEditSession.getAnnotations();
- customAnnotation = {
- text,
- row: pos.row,
- column: pos.column,
- type: 'warning',
- };
-
- aceEditSession.setAnnotations([...annotations, customAnnotation]);
- },
- removeAnnotation() {
- aceEditSession.setAnnotations(
- aceEditSession.getAnnotations().filter((a: Annotation) => a !== customAnnotation)
- );
- },
- };
- };
-
- autocompleter(position, prefix, callback, getAnnotationControls());
- },
- },
- ]);
- }
-
- destroy() {
- this.editor.destroy();
- }
-
- /**
- * Formats body of the request in the editor by removing the extra whitespaces at the beginning of lines,
- * And adds the correct indentation for each line
- * @param reqRange request range to indent
- */
- autoIndent(reqRange: Range) {
- const session = this.editor.getSession();
- const mode = session.getMode();
- const startRow = reqRange.start.lineNumber;
- const endRow = reqRange.end.lineNumber;
- const tab = session.getTabString();
-
- for (let row = startRow; row <= endRow; row++) {
- let prevLineState = '';
- let prevLineIndent = '';
- if (row > 0) {
- prevLineState = session.getState(row - 1);
- const prevLine = session.getLine(row - 1);
- prevLineIndent = mode.getNextLineIndent(prevLineState, prevLine, tab);
- }
-
- const line = session.getLine(row);
- // @ts-ignore
- // Brace does not expose type definition for mode.$getIndent, though we have access to this method provided by the underlying Ace editor.
- // See https://github.com/ajaxorg/ace/blob/87ce087ed1cf20eeabe56fb0894e048d9bc9c481/lib/ace/mode/text.js#L259
- const currLineIndent = mode.$getIndent(line);
- if (prevLineIndent !== currLineIndent) {
- if (currLineIndent.length > 0) {
- // If current line has indentation, remove it.
- // Next we will add the correct indentation by looking at the previous line
- const range = new _AceRange(row, 0, row, currLineIndent.length);
- session.remove(range);
- }
- if (prevLineIndent.length > 0) {
- // If previous line has indentation, add indentation at the current line
- session.insert({ row, column: 0 }, prevLineIndent);
- }
- }
-
- // Lastly outdent any closing braces
- mode.autoOutdent(prevLineState, session, row);
- }
- }
-
- getAllFoldRanges(): Range[] {
- const session = this.editor.getSession();
- // @ts-ignore
- // Brace does not expose type definition for session.getAllFolds, though we have access to this method provided by the underlying Ace editor.
- // See https://github.com/ajaxorg/ace/blob/13dc911dbc0ea31ca343d5744b3f472767458fc3/ace.d.ts#L82
- return session.getAllFolds().map((fold) => fold.range);
- }
-
- addFoldsAtRanges(foldRanges: Range[]) {
- const session = this.editor.getSession();
- foldRanges.forEach((range) => {
- try {
- session.addFold('...', _AceRange.fromPoints(range.start, range.end));
- } catch (e) {
- // ignore the error if a fold fails
- }
- });
- }
-}
diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/input.ts b/src/plugins/console/public/application/models/legacy_core_editor/mode/input.ts
deleted file mode 100644
index 450feec6e9c3d..0000000000000
--- a/src/plugins/console/public/application/models/legacy_core_editor/mode/input.ts
+++ /dev/null
@@ -1,79 +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", 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 ace from 'brace';
-import { workerModule } from './worker';
-import { ScriptMode } from './script';
-
-const TextMode = ace.acequire('ace/mode/text').Mode;
-
-const MatchingBraceOutdent = ace.acequire('ace/mode/matching_brace_outdent').MatchingBraceOutdent;
-const CstyleBehaviour = ace.acequire('ace/mode/behaviour/cstyle').CstyleBehaviour;
-const CStyleFoldMode = ace.acequire('ace/mode/folding/cstyle').FoldMode;
-const WorkerClient = ace.acequire('ace/worker/worker_client').WorkerClient;
-const AceTokenizer = ace.acequire('ace/tokenizer').Tokenizer;
-
-import { InputHighlightRules } from './input_highlight_rules';
-
-export class Mode extends TextMode {
- constructor() {
- super();
- this.$tokenizer = new AceTokenizer(new InputHighlightRules().getRules());
- this.$outdent = new MatchingBraceOutdent();
- this.$behaviour = new CstyleBehaviour();
- this.foldingRules = new CStyleFoldMode();
- this.createModeDelegates({
- 'script-': ScriptMode,
- });
- }
-}
-
-(function (this: Mode) {
- this.getCompletions = function () {
- // autocomplete is done by the autocomplete module.
- return [];
- };
-
- this.getNextLineIndent = function (state: string, line: string, tab: string) {
- let indent = this.$getIndent(line);
-
- if (state !== 'string_literal') {
- const match = line.match(/^.*[\{\(\[]\s*$/);
- if (match) {
- indent += tab;
- }
- }
-
- return indent;
- };
-
- this.checkOutdent = function (state: unknown, line: string, input: string) {
- return this.$outdent.checkOutdent(line, input);
- };
-
- this.autoOutdent = function (state: unknown, doc: string, row: string) {
- this.$outdent.autoOutdent(doc, row);
- };
- this.createWorker = function (session: {
- getDocument: () => string;
- setAnnotations: (arg0: unknown) => void;
- }) {
- const worker = new WorkerClient(['ace', 'sense_editor'], workerModule, 'SenseWorker');
- worker.attachToDocument(session.getDocument());
- worker.on('error', function (e: { data: unknown }) {
- session.setAnnotations([e.data]);
- });
-
- worker.on('ok', function (anno: { data: unknown }) {
- session.setAnnotations(anno.data);
- });
-
- return worker;
- };
-}).call(Mode.prototype);
diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.ts b/src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.ts
deleted file mode 100644
index 8a2f64b3c71f4..0000000000000
--- a/src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.ts
+++ /dev/null
@@ -1,180 +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", 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 ace from 'brace';
-import { addXJsonToRules } from '@kbn/ace';
-
-type Token =
- | string
- | { token?: string; regex?: string; next?: string; push?: boolean; include?: string };
-
-export function addEOL(
- tokens: Token[],
- reg: string | RegExp,
- nextIfEOL: string,
- normalNext?: string
-) {
- if (typeof reg === 'object') {
- reg = reg.source;
- }
- return [
- { token: tokens.concat(['whitespace']), regex: reg + '(\\s*)$', next: nextIfEOL },
- { token: tokens, regex: reg, next: normalNext },
- ];
-}
-
-export const mergeTokens = (...args: any[]) => [].concat.apply([], args);
-
-const TextHighlightRules = ace.acequire('ace/mode/text_highlight_rules').TextHighlightRules;
-// translating this to monaco
-export class InputHighlightRules extends TextHighlightRules {
- constructor() {
- super();
- this.$rules = {
- // TODO
- 'start-sql': [
- { token: 'whitespace', regex: '\\s+' },
- { token: 'paren.lparen', regex: '{', next: 'json-sql', push: true },
- { regex: '', next: 'start' },
- ],
- start: mergeTokens(
- [
- // done
- { token: 'warning', regex: '#!.*$' },
- // done
- { include: 'comments' },
- // done
- { token: 'paren.lparen', regex: '{', next: 'json', push: true },
- ],
- // done
- addEOL(['method'], /([a-zA-Z]+)/, 'start', 'method_sep'),
- [
- // done
- {
- token: 'whitespace',
- regex: '\\s+',
- },
- // done
- {
- token: 'text',
- regex: '.+?',
- },
- ]
- ),
- method_sep: mergeTokens(
- // done
- addEOL(
- ['whitespace', 'url.protocol_host', 'url.slash'],
- /(\s+)(https?:\/\/[^?\/,]+)(\/)/,
- 'start',
- 'url'
- ),
- // done
- addEOL(['whitespace', 'variable.template'], /(\s+)(\${\w+})/, 'start', 'url'),
- // done
- addEOL(['whitespace', 'url.protocol_host'], /(\s+)(https?:\/\/[^?\/,]+)/, 'start', 'url'),
- // done
- addEOL(['whitespace', 'url.slash'], /(\s+)(\/)/, 'start', 'url'),
- // done
- addEOL(['whitespace'], /(\s+)/, 'start', 'url')
- ),
- url: mergeTokens(
- // done
- addEOL(['variable.template'], /(\${\w+})/, 'start'),
- // TODO
- addEOL(['url.part'], /(_sql)/, 'start-sql', 'url-sql'),
- // done
- addEOL(['url.part'], /([^?\/,\s]+)/, 'start'),
- // done
- addEOL(['url.comma'], /(,)/, 'start'),
- // done
- addEOL(['url.slash'], /(\/)/, 'start'),
- // done
- addEOL(['url.questionmark'], /(\?)/, 'start', 'urlParams'),
- // done
- addEOL(['whitespace', 'comment.punctuation', 'comment.line'], /(\s+)(\/\/)(.*$)/, 'start')
- ),
- urlParams: mergeTokens(
- // done
- addEOL(['url.param', 'url.equal', 'variable.template'], /([^&=]+)(=)(\${\w+})/, 'start'),
- // done
- addEOL(['url.param', 'url.equal', 'url.value'], /([^&=]+)(=)([^&]*)/, 'start'),
- // done
- addEOL(['url.param'], /([^&=]+)/, 'start'),
- // done
- addEOL(['url.amp'], /(&)/, 'start'),
- // done
- addEOL(['whitespace', 'comment.punctuation', 'comment.line'], /(\s+)(\/\/)(.*$)/, 'start')
- ),
- // TODO
- 'url-sql': mergeTokens(
- addEOL(['url.part'], /([^?\/,\s]+)/, 'start-sql'),
- addEOL(['url.comma'], /(,)/, 'start-sql'),
- addEOL(['url.slash'], /(\/)/, 'start-sql'),
- addEOL(['url.questionmark'], /(\?)/, 'start-sql', 'urlParams-sql')
- ),
- // TODO
- 'urlParams-sql': mergeTokens(
- addEOL(['url.param', 'url.equal', 'url.value'], /([^&=]+)(=)([^&]*)/, 'start-sql'),
- addEOL(['url.param'], /([^&=]+)/, 'start-sql'),
- addEOL(['url.amp'], /(&)/, 'start-sql')
- ),
- /**
- * Each key in this.$rules considered to be a state in state machine. Regular expressions define the tokens for the current state, as well as the transitions into another state.
- * See for more details https://cloud9-sdk.readme.io/docs/highlighting-rules#section-defining-states
- * *
- * Define a state for comments, these comment rules then can be included in other states. E.g. in 'start' and 'json' states by including { include: 'comments' }
- * This will avoid duplicating the same rules in other states
- */
- comments: [
- {
- // Capture a line comment, indicated by #
- // done
- token: ['comment.punctuation', 'comment.line'],
- regex: /(#)(.*$)/,
- },
- {
- // Begin capturing a block comment, indicated by /*
- // done
- token: 'comment.punctuation',
- regex: /\/\*/,
- push: [
- {
- // Finish capturing a block comment, indicated by */
- // done
- token: 'comment.punctuation',
- regex: /\*\//,
- next: 'pop',
- },
- {
- // done
- defaultToken: 'comment.block',
- },
- ],
- },
- {
- // Capture a line comment, indicated by //
- // done
- token: ['comment.punctuation', 'comment.line'],
- regex: /(\/\/)(.*$)/,
- },
- ],
- };
-
- addXJsonToRules(this, 'json');
- // Add comment rules to json rule set
- this.$rules.json.unshift({ include: 'comments' });
-
- this.$rules.json.unshift({ token: 'variable.template', regex: /("\${\w+}")/ });
-
- if (this instanceof InputHighlightRules) {
- this.normalizeRules();
- }
- }
-}
diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/output.ts b/src/plugins/console/public/application/models/legacy_core_editor/mode/output.ts
deleted file mode 100644
index df7f3c37d55ec..0000000000000
--- a/src/plugins/console/public/application/models/legacy_core_editor/mode/output.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", 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 ace from 'brace';
-
-import { OutputJsonHighlightRules } from './output_highlight_rules';
-
-const JSONMode = ace.acequire('ace/mode/json').Mode;
-const MatchingBraceOutdent = ace.acequire('ace/mode/matching_brace_outdent').MatchingBraceOutdent;
-const CstyleBehaviour = ace.acequire('ace/mode/behaviour/cstyle').CstyleBehaviour;
-const CStyleFoldMode = ace.acequire('ace/mode/folding/cstyle').FoldMode;
-ace.acequire('ace/worker/worker_client');
-const AceTokenizer = ace.acequire('ace/tokenizer').Tokenizer;
-
-export class Mode extends JSONMode {
- constructor() {
- super();
- this.$tokenizer = new AceTokenizer(new OutputJsonHighlightRules().getRules());
- this.$outdent = new MatchingBraceOutdent();
- this.$behaviour = new CstyleBehaviour();
- this.foldingRules = new CStyleFoldMode();
- }
-}
-
-(function (this: Mode) {
- this.createWorker = function () {
- return null;
- };
-
- this.$id = 'sense/mode/input';
-}).call(Mode.prototype);
diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.test.ts b/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.test.ts
deleted file mode 100644
index a18841aa4dc17..0000000000000
--- a/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.test.ts
+++ /dev/null
@@ -1,56 +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", 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 { mapStatusCodeToBadge } from './output_highlight_rules';
-
-describe('mapStatusCodeToBadge', () => {
- const testCases = [
- {
- description: 'treats 100 as as default',
- value: '# PUT test-index 100 Continue',
- badge: 'badge.badge--default',
- },
- {
- description: 'treats 200 as success',
- value: '# PUT test-index 200 OK',
- badge: 'badge.badge--success',
- },
- {
- description: 'treats 301 as primary',
- value: '# PUT test-index 301 Moved Permanently',
- badge: 'badge.badge--primary',
- },
- {
- description: 'treats 400 as warning',
- value: '# PUT test-index 404 Not Found',
- badge: 'badge.badge--warning',
- },
- {
- description: 'treats 502 as danger',
- value: '# PUT test-index 502 Bad Gateway',
- badge: 'badge.badge--danger',
- },
- {
- description: 'treats unexpected numbers as danger',
- value: '# PUT test-index 666 Demonic Invasion',
- badge: 'badge.badge--danger',
- },
- {
- description: 'treats no numbers as undefined',
- value: '# PUT test-index',
- badge: undefined,
- },
- ];
-
- testCases.forEach(({ description, value, badge }) => {
- test(description, () => {
- expect(mapStatusCodeToBadge(value)).toBe(badge);
- });
- });
-});
diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.ts b/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.ts
deleted file mode 100644
index 765ba3e263f22..0000000000000
--- a/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.ts
+++ /dev/null
@@ -1,64 +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", 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 ace from 'brace';
-import 'brace/mode/json';
-import { addXJsonToRules } from '@kbn/ace';
-
-const JsonHighlightRules = ace.acequire('ace/mode/json_highlight_rules').JsonHighlightRules;
-
-export const mapStatusCodeToBadge = (value?: string) => {
- const regExpMatchArray = value?.match(/\d+/);
- if (regExpMatchArray) {
- const status = parseInt(regExpMatchArray[0], 10);
- if (status <= 199) {
- return 'badge.badge--default';
- }
- if (status <= 299) {
- return 'badge.badge--success';
- }
- if (status <= 399) {
- return 'badge.badge--primary';
- }
- if (status <= 499) {
- return 'badge.badge--warning';
- }
- return 'badge.badge--danger';
- }
-};
-
-export class OutputJsonHighlightRules extends JsonHighlightRules {
- constructor() {
- super();
- this.$rules = {};
- addXJsonToRules(this, 'start');
- this.$rules.start.unshift(
- {
- token: 'warning',
- regex: '#!.*$',
- },
- {
- token: 'comment',
- // match a comment starting with a hash at the start of the line
- // ignore status codes and status texts at the end of the line (e.g. # GET _search/foo 200, # GET _search/foo 200 OK)
- regex: /#(.*?)(?=[1-5][0-9][0-9]\s(?:[\sA-Za-z]+)|(?:[1-5][0-9][0-9])|$)/,
- },
- {
- token: mapStatusCodeToBadge,
- // match status codes and status texts at the end of the line (e.g. # GET _search/foo 200, # GET _search/foo 200 OK)
- // this rule allows us to highlight them with the corresponding badge color (e.g. 200 OK -> badge.badge--success)
- regex: /([1-5][0-9][0-9]\s?[\sA-Za-z]+$)/,
- }
- );
-
- if (this instanceof OutputJsonHighlightRules) {
- this.normalizeRules();
- }
- }
-}
diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/script.ts b/src/plugins/console/public/application/models/legacy_core_editor/mode/script.ts
deleted file mode 100644
index f50b6d3abe8ab..0000000000000
--- a/src/plugins/console/public/application/models/legacy_core_editor/mode/script.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", 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 ace from 'brace';
-import { ScriptHighlightRules } from '@kbn/ace';
-
-const TextMode = ace.acequire('ace/mode/text').Mode;
-const MatchingBraceOutdent = ace.acequire('ace/mode/matching_brace_outdent').MatchingBraceOutdent;
-const CstyleBehaviour = ace.acequire('ace/mode/behaviour/cstyle').CstyleBehaviour;
-const CStyleFoldMode = ace.acequire('ace/mode/folding/cstyle').FoldMode;
-ace.acequire('ace/tokenizer');
-
-export class ScriptMode extends TextMode {
- constructor() {
- super();
- this.$outdent = new MatchingBraceOutdent();
- this.$behaviour = new CstyleBehaviour();
- this.foldingRules = new CStyleFoldMode();
- }
-}
-
-(function (this: ScriptMode) {
- this.HighlightRules = ScriptHighlightRules;
-
- this.getNextLineIndent = function (state: unknown, line: string, tab: string) {
- let indent = this.$getIndent(line);
- const match = line.match(/^.*[\{\[]\s*$/);
- if (match) {
- indent += tab;
- }
-
- return indent;
- };
-
- this.checkOutdent = function (state: unknown, line: string, input: string) {
- return this.$outdent.checkOutdent(line, input);
- };
-
- this.autoOutdent = function (state: unknown, doc: string, row: string) {
- this.$outdent.autoOutdent(doc, row);
- };
-}).call(ScriptMode.prototype);
diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/index.d.ts b/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/index.d.ts
deleted file mode 100644
index 8067bec3556ae..0000000000000
--- a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/index.d.ts
+++ /dev/null
@@ -1,10 +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", 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".
- */
-
-export declare const workerModule: { id: string; src: string };
diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/index.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/index.js
deleted file mode 100644
index 23f636b79e1a6..0000000000000
--- a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/index.js
+++ /dev/null
@@ -1,15 +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", 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 src from '!!raw-loader!./worker';
-
-export const workerModule = {
- id: 'sense_editor/mode/worker',
- src,
-};
diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js
deleted file mode 100644
index 65567f377cc52..0000000000000
--- a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js
+++ /dev/null
@@ -1,2392 +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", 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".
- */
-
-/* @notice
- *
- * This product includes code that is based on Ace editor, which was available
- * under a "BSD" license.
- *
- * Distributed under the BSD license:
- *
- * Copyright (c) 2010, Ajax.org B.V.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- * * Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- * * Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- * * Neither the name of Ajax.org B.V. nor the
- * names of its contributors may be used to endorse or promote products
- * derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-/* eslint-disable prettier/prettier,prefer-const,eqeqeq,import/no-commonjs,no-undef,no-sequences,
- block-scoped-var,no-use-before-define,no-var,one-var,guard-for-in,new-cap,no-nested-ternary,no-redeclare,
- no-unused-vars,no-extend-native,no-empty,camelcase,no-proto,@kbn/imports/no_unresolvable_imports */
-/*
- This file is loaded up as a blob by Brace to hand to Ace to load as Jsonp
- (hence the redefining of everything). It is based on the javascript
- mode from the brace distro.
-*/
-function init(window) {
- function resolveModuleId(id, paths) {
- for (let testPath = id, tail = ''; testPath;) {
- let alias = paths[testPath];
- if ('string' === typeof alias) return alias + tail;
- if (alias)
- {return (alias.location.replace(/\/*$/, '/') + (tail || alias.main || alias.name));}
- if (alias === !1) return '';
- let i = testPath.lastIndexOf('/');
- if (-1 === i) break;
- (tail = testPath.substr(i) + tail), (testPath = testPath.slice(0, i));
- }
- return id;
- }
- if (
- !(
- (void 0 !== window.window && window.document) ||
- (window.acequire && window.define)
- )
- ) {
- window.console ||
- ((window.console = function () {
- let msgs = Array.prototype.slice.call(arguments, 0);
- postMessage({ type: 'log', data: msgs });
- }),
- (window.console.error = window.console.warn = window.console.log = window.console.trace =
- window.console)),
- (window.window = window),
- (window.ace = window),
- (window.onerror = function (message, file, line, col, err) {
- postMessage({
- type: 'error',
- data: {
- message: message,
- data: err.data,
- file: file,
- line: line,
- col: col,
- stack: err.stack,
- },
- });
- }),
- (window.normalizeModule = function (parentId, moduleName) {
- if (-1 !== moduleName.indexOf('!')) {
- let chunks = moduleName.split('!');
- return (
- window.normalizeModule(parentId, chunks[0]) +
- '!' +
- window.normalizeModule(parentId, chunks[1])
- );
- }
- if ('.' == moduleName.charAt(0)) {
- let base = parentId
- .split('/')
- .slice(0, -1)
- .join('/');
- for (
- moduleName = (base ? base + '/' : '') + moduleName;
- -1 !== moduleName.indexOf('.') && previous != moduleName;
-
- ) {
- var previous = moduleName;
- moduleName = moduleName
- .replace(/^\.\//, '')
- .replace(/\/\.\//, '/')
- .replace(/[^\/]+\/\.\.\//, '');
- }
- }
- return moduleName;
- }),
- (window.acequire = function acequire(parentId, id) {
- if ((id || ((id = parentId), (parentId = null)), !id.charAt))
- {throw Error(
- 'worker.js acequire() accepts only (parentId, id) as arguments'
- );}
- id = window.normalizeModule(parentId, id);
- let module = window.acequire.modules[id];
- if (module)
- {return (
- module.initialized ||
- ((module.initialized = !0),
- (module.exports = module.factory().exports)),
- module.exports
- );}
- if (!window.acequire.tlns) return console.log('unable to load ' + id);
- let path = resolveModuleId(id, window.acequire.tlns);
- return (
- '.js' != path.slice(-3) && (path += '.js'),
- (window.acequire.id = id),
- (window.acequire.modules[id] = {}),
- importScripts(path),
- window.acequire(parentId, id)
- );
- }),
- (window.acequire.modules = {}),
- (window.acequire.tlns = {}),
- (window.define = function (id, deps, factory) {
- if (
- (2 == arguments.length
- ? ((factory = deps),
- 'string' !== typeof id && ((deps = id), (id = window.acequire.id)))
- : 1 == arguments.length &&
- ((factory = id), (deps = []), (id = window.acequire.id)),
- 'function' !== typeof factory)
- )
- {return (
- (window.acequire.modules[id] = {
- exports: factory,
- initialized: !0,
- }),
- void 0
- );}
- deps.length || (deps = ['require', 'exports', 'module']);
- let req = function (childId) {
- return window.acequire(id, childId);
- };
- window.acequire.modules[id] = {
- exports: {},
- factory: function () {
- let module = this,
- returnExports = factory.apply(
- this,
- deps.map(function (dep) {
- switch (dep) {
- case 'require':
- return req;
- case 'exports':
- return module.exports;
- case 'module':
- return module;
- default:
- return req(dep);
- }
- })
- );
- return returnExports && (module.exports = returnExports), module;
- },
- };
- }),
- (window.define.amd = {}),
- (acequire.tlns = {}),
- (window.initBaseUrls = function (topLevelNamespaces) {
- for (let i in topLevelNamespaces)
- {acequire.tlns[i] = topLevelNamespaces[i];}
- }),
- (window.initSender = function () {
- let EventEmitter = window.acequire('ace/lib/event_emitter')
- .EventEmitter,
- oop = window.acequire('ace/lib/oop'),
- Sender = function () {};
- return (
- function () {
- oop.implement(this, EventEmitter),
- (this.callback = function (data, callbackId) {
- postMessage({ type: 'call', id: callbackId, data: data });
- }),
- (this.emit = function (name, data) {
- postMessage({ type: 'event', name: name, data: data });
- });
- }.call(Sender.prototype),
- new Sender()
- );
- });
- let main = (window.main = null),
- sender = (window.sender = null);
- window.onmessage = function (e) {
- let msg = e.data;
- if (msg.event && sender) sender._signal(msg.event, msg.data);
- else if (msg.command)
- {if (main[msg.command]) main[msg.command].apply(main, msg.args);
- else {
- if (!window[msg.command])
- throw Error('Unknown command:' + msg.command);
- window[msg.command].apply(window, msg.args);
- }}
- else if (msg.init) {
- window.initBaseUrls(msg.tlns),
- acequire('ace/lib/es5-shim'),
- (sender = window.sender = window.initSender());
- let clazz = acequire(msg.module)[msg.classname];
- main = window.main = new clazz(sender);
- }
- };
- }
-}
-init(this);
-ace.define('ace/lib/oop', ['require', 'exports', 'module'], function (
- acequire,
- exports
-) {
- (exports.inherits = function (ctor, superCtor) {
- (ctor.super_ = superCtor),
- (ctor.prototype = Object.create(superCtor.prototype, {
- constructor: {
- value: ctor,
- enumerable: !1,
- writable: !0,
- configurable: !0,
- },
- }));
- }),
- (exports.mixin = function (obj, mixin) {
- for (let key in mixin) obj[key] = mixin[key];
- return obj;
- }),
- (exports.implement = function (proto, mixin) {
- exports.mixin(proto, mixin);
- });
-}),
-ace.define('ace/range', ['require', 'exports', 'module'], function (
- acequire,
- exports
-) {
- let comparePoints = function (p1, p2) {
- return p1.row - p2.row || p1.column - p2.column;
- },
- Range = function (startRow, startColumn, endRow, endColumn) {
- (this.start = { row: startRow, column: startColumn }),
- (this.end = { row: endRow, column: endColumn });
- };
- (function () {
- (this.isEqual = function (range) {
- return (
- this.start.row === range.start.row &&
- this.end.row === range.end.row &&
- this.start.column === range.start.column &&
- this.end.column === range.end.column
- );
- }),
- (this.toString = function () {
- return (
- 'Range: [' +
- this.start.row +
- '/' +
- this.start.column +
- '] -> [' +
- this.end.row +
- '/' +
- this.end.column +
- ']'
- );
- }),
- (this.contains = function (row, column) {
- return 0 == this.compare(row, column);
- }),
- (this.compareRange = function (range) {
- let cmp,
- end = range.end,
- start = range.start;
- return (
- (cmp = this.compare(end.row, end.column)),
- 1 == cmp
- ? ((cmp = this.compare(start.row, start.column)),
- 1 == cmp ? 2 : 0 == cmp ? 1 : 0)
- : -1 == cmp
- ? -2
- : ((cmp = this.compare(start.row, start.column)),
- -1 == cmp ? -1 : 1 == cmp ? 42 : 0)
- );
- }),
- (this.comparePoint = function (p) {
- return this.compare(p.row, p.column);
- }),
- (this.containsRange = function (range) {
- return (
- 0 == this.comparePoint(range.start) &&
- 0 == this.comparePoint(range.end)
- );
- }),
- (this.intersects = function (range) {
- let cmp = this.compareRange(range);
- return -1 == cmp || 0 == cmp || 1 == cmp;
- }),
- (this.isEnd = function (row, column) {
- return this.end.row == row && this.end.column == column;
- }),
- (this.isStart = function (row, column) {
- return this.start.row == row && this.start.column == column;
- }),
- (this.setStart = function (row, column) {
- 'object' === typeof row
- ? ((this.start.column = row.column), (this.start.row = row.row))
- : ((this.start.row = row), (this.start.column = column));
- }),
- (this.setEnd = function (row, column) {
- 'object' === typeof row
- ? ((this.end.column = row.column), (this.end.row = row.row))
- : ((this.end.row = row), (this.end.column = column));
- }),
- (this.inside = function (row, column) {
- return 0 == this.compare(row, column)
- ? this.isEnd(row, column) || this.isStart(row, column)
- ? !1
- : !0
- : !1;
- }),
- (this.insideStart = function (row, column) {
- return 0 == this.compare(row, column)
- ? this.isEnd(row, column)
- ? !1
- : !0
- : !1;
- }),
- (this.insideEnd = function (row, column) {
- return 0 == this.compare(row, column)
- ? this.isStart(row, column)
- ? !1
- : !0
- : !1;
- }),
- (this.compare = function (row, column) {
- return this.isMultiLine() || row !== this.start.row
- ? this.start.row > row
- ? -1
- : row > this.end.row
- ? 1
- : this.start.row === row
- ? column >= this.start.column
- ? 0
- : -1
- : this.end.row === row
- ? this.end.column >= column
- ? 0
- : 1
- : 0
- : this.start.column > column
- ? -1
- : column > this.end.column
- ? 1
- : 0;
- }),
- (this.compareStart = function (row, column) {
- return this.start.row == row && this.start.column == column
- ? -1
- : this.compare(row, column);
- }),
- (this.compareEnd = function (row, column) {
- return this.end.row == row && this.end.column == column
- ? 1
- : this.compare(row, column);
- }),
- (this.compareInside = function (row, column) {
- return this.end.row == row && this.end.column == column
- ? 1
- : this.start.row == row && this.start.column == column
- ? -1
- : this.compare(row, column);
- }),
- (this.clipRows = function (firstRow, lastRow) {
- if (this.end.row > lastRow) var end = { row: lastRow + 1, column: 0 };
- else if (firstRow > this.end.row)
- {var end = { row: firstRow, column: 0 };}
- if (this.start.row > lastRow)
- {var start = { row: lastRow + 1, column: 0 };}
- else if (firstRow > this.start.row)
- {var start = { row: firstRow, column: 0 };}
- return Range.fromPoints(start || this.start, end || this.end);
- }),
- (this.extend = function (row, column) {
- let cmp = this.compare(row, column);
- if (0 == cmp) return this;
- if (-1 == cmp) var start = { row: row, column: column };
- else var end = { row: row, column: column };
- return Range.fromPoints(start || this.start, end || this.end);
- }),
- (this.isEmpty = function () {
- return (
- this.start.row === this.end.row &&
- this.start.column === this.end.column
- );
- }),
- (this.isMultiLine = function () {
- return this.start.row !== this.end.row;
- }),
- (this.clone = function () {
- return Range.fromPoints(this.start, this.end);
- }),
- (this.collapseRows = function () {
- return 0 == this.end.column
- ? new Range(
- this.start.row,
- 0,
- Math.max(this.start.row, this.end.row - 1),
- 0
- )
- : new Range(this.start.row, 0, this.end.row, 0);
- }),
- (this.toScreenRange = function (session) {
- let screenPosStart = session.documentToScreenPosition(this.start),
- screenPosEnd = session.documentToScreenPosition(this.end);
- return new Range(
- screenPosStart.row,
- screenPosStart.column,
- screenPosEnd.row,
- screenPosEnd.column
- );
- }),
- (this.moveBy = function (row, column) {
- (this.start.row += row),
- (this.start.column += column),
- (this.end.row += row),
- (this.end.column += column);
- });
- }.call(Range.prototype),
- (Range.fromPoints = function (start, end) {
- return new Range(start.row, start.column, end.row, end.column);
- }),
- (Range.comparePoints = comparePoints),
- (Range.comparePoints = function (p1, p2) {
- return p1.row - p2.row || p1.column - p2.column;
- }),
- (exports.Range = Range));
-}),
-ace.define('ace/apply_delta', ['require', 'exports', 'module'], function (
- acequire,
- exports
-) {
- exports.applyDelta = function (docLines, delta) {
- let row = delta.start.row,
- startColumn = delta.start.column,
- line = docLines[row] || '';
- switch (delta.action) {
- case 'insert':
- var lines = delta.lines;
- if (1 === lines.length)
- {docLines[row] =
- line.substring(0, startColumn) +
- delta.lines[0] +
- line.substring(startColumn);}
- else {
- let args = [row, 1].concat(delta.lines);
- docLines.splice.apply(docLines, args),
- (docLines[row] = line.substring(0, startColumn) + docLines[row]),
- (docLines[row + delta.lines.length - 1] += line.substring(
- startColumn
- ));
- }
- break;
- case 'remove':
- var endColumn = delta.end.column,
- endRow = delta.end.row;
- row === endRow
- ? (docLines[row] =
- line.substring(0, startColumn) + line.substring(endColumn))
- : docLines.splice(
- row,
- endRow - row + 1,
- line.substring(0, startColumn) +
- docLines[endRow].substring(endColumn)
- );
- }
- };
-}),
-ace.define(
- 'ace/lib/event_emitter',
- ['require', 'exports', 'module'],
- function (acequire, exports) {
- let EventEmitter = {},
- stopPropagation = function () {
- this.propagationStopped = !0;
- },
- preventDefault = function () {
- this.defaultPrevented = !0;
- };
- (EventEmitter._emit = EventEmitter._dispatchEvent = function (
- eventName,
- e
- ) {
- this._eventRegistry || (this._eventRegistry = {}),
- this._defaultHandlers || (this._defaultHandlers = {});
- let listeners = this._eventRegistry[eventName] || [],
- defaultHandler = this._defaultHandlers[eventName];
- if (listeners.length || defaultHandler) {
- ('object' === typeof e && e) || (e = {}),
- e.type || (e.type = eventName),
- e.stopPropagation || (e.stopPropagation = stopPropagation),
- e.preventDefault || (e.preventDefault = preventDefault),
- (listeners = listeners.slice());
- for (
- let i = 0;
- listeners.length > i &&
- (listeners[i](e, this), !e.propagationStopped);
- i++
- );
- return defaultHandler && !e.defaultPrevented
- ? defaultHandler(e, this)
- : void 0;
- }
- }),
- (EventEmitter._signal = function (eventName, e) {
- let listeners = (this._eventRegistry || {})[eventName];
- if (listeners) {
- listeners = listeners.slice();
- for (let i = 0; listeners.length > i; i++) listeners[i](e, this);
- }
- }),
- (EventEmitter.once = function (eventName, callback) {
- let _self = this;
- callback &&
- this.addEventListener(eventName, function newCallback() {
- _self.removeEventListener(eventName, newCallback),
- callback.apply(null, arguments);
- });
- }),
- (EventEmitter.setDefaultHandler = function (eventName, callback) {
- let handlers = this._defaultHandlers;
- if (
- (handlers ||
- (handlers = this._defaultHandlers = { _disabled_: {} }),
- handlers[eventName])
- ) {
- let old = handlers[eventName],
- disabled = handlers._disabled_[eventName];
- disabled || (handlers._disabled_[eventName] = disabled = []),
- disabled.push(old);
- let i = disabled.indexOf(callback);
- -1 != i && disabled.splice(i, 1);
- }
- handlers[eventName] = callback;
- }),
- (EventEmitter.removeDefaultHandler = function (eventName, callback) {
- let handlers = this._defaultHandlers;
- if (handlers) {
- let disabled = handlers._disabled_[eventName];
- if (handlers[eventName] == callback)
- {handlers[eventName],
- disabled && this.setDefaultHandler(eventName, disabled.pop());}
- else if (disabled) {
- let i = disabled.indexOf(callback);
- -1 != i && disabled.splice(i, 1);
- }
- }
- }),
- (EventEmitter.on = EventEmitter.addEventListener = function (
- eventName,
- callback,
- capturing
- ) {
- this._eventRegistry = this._eventRegistry || {};
- let listeners = this._eventRegistry[eventName];
- return (
- listeners || (listeners = this._eventRegistry[eventName] = []),
- -1 == listeners.indexOf(callback) &&
- listeners[capturing ? 'unshift' : 'push'](callback),
- callback
- );
- }),
- (EventEmitter.off = EventEmitter.removeListener = EventEmitter.removeEventListener = function (
- eventName,
- callback
- ) {
- this._eventRegistry = this._eventRegistry || {};
- let listeners = this._eventRegistry[eventName];
- if (listeners) {
- let index = listeners.indexOf(callback);
- -1 !== index && listeners.splice(index, 1);
- }
- }),
- (EventEmitter.removeAllListeners = function (eventName) {
- this._eventRegistry && (this._eventRegistry[eventName] = []);
- }),
- (exports.EventEmitter = EventEmitter);
- }
-),
-ace.define(
- 'ace/anchor',
- ['require', 'exports', 'module', 'ace/lib/oop', 'ace/lib/event_emitter'],
- function (acequire, exports) {
- let oop = acequire('./lib/oop'),
- EventEmitter = acequire('./lib/event_emitter').EventEmitter,
- Anchor = (exports.Anchor = function (doc, row, column) {
- (this.$onChange = this.onChange.bind(this)),
- this.attach(doc),
- column === void 0
- ? this.setPosition(row.row, row.column)
- : this.setPosition(row, column);
- });
- (function () {
- function $pointsInOrder(point1, point2, equalPointsInOrder) {
- let bColIsAfter = equalPointsInOrder
- ? point1.column <= point2.column
- : point1.column < point2.column;
- return (
- point1.row < point2.row || (point1.row == point2.row && bColIsAfter)
- );
- }
- function $getTransformedPoint(delta, point, moveIfEqual) {
- let deltaIsInsert = 'insert' == delta.action,
- deltaRowShift =
- (deltaIsInsert ? 1 : -1) * (delta.end.row - delta.start.row),
- deltaColShift =
- (deltaIsInsert ? 1 : -1) *
- (delta.end.column - delta.start.column),
- deltaStart = delta.start,
- deltaEnd = deltaIsInsert ? deltaStart : delta.end;
- return $pointsInOrder(point, deltaStart, moveIfEqual)
- ? { row: point.row, column: point.column }
- : $pointsInOrder(deltaEnd, point, !moveIfEqual)
- ? {
- row: point.row + deltaRowShift,
- column:
- point.column +
- (point.row == deltaEnd.row ? deltaColShift : 0),
- }
- : { row: deltaStart.row, column: deltaStart.column };
- }
- oop.implement(this, EventEmitter),
- (this.getPosition = function () {
- return this.$clipPositionToDocument(this.row, this.column);
- }),
- (this.getDocument = function () {
- return this.document;
- }),
- (this.$insertRight = !1),
- (this.onChange = function (delta) {
- if (
- !(
- (delta.start.row == delta.end.row &&
- delta.start.row != this.row) ||
- delta.start.row > this.row
- )
- ) {
- let point = $getTransformedPoint(
- delta,
- { row: this.row, column: this.column },
- this.$insertRight
- );
- this.setPosition(point.row, point.column, !0);
- }
- }),
- (this.setPosition = function (row, column, noClip) {
- let pos;
- if (
- ((pos = noClip
- ? { row: row, column: column }
- : this.$clipPositionToDocument(row, column)),
- this.row != pos.row || this.column != pos.column)
- ) {
- let old = { row: this.row, column: this.column };
- (this.row = pos.row),
- (this.column = pos.column),
- this._signal('change', { old: old, value: pos });
- }
- }),
- (this.detach = function () {
- this.document.removeEventListener('change', this.$onChange);
- }),
- (this.attach = function (doc) {
- (this.document = doc || this.document),
- this.document.on('change', this.$onChange);
- }),
- (this.$clipPositionToDocument = function (row, column) {
- let pos = {};
- return (
- row >= this.document.getLength()
- ? ((pos.row = Math.max(0, this.document.getLength() - 1)),
- (pos.column = this.document.getLine(pos.row).length))
- : 0 > row
- ? ((pos.row = 0), (pos.column = 0))
- : ((pos.row = row),
- (pos.column = Math.min(
- this.document.getLine(pos.row).length,
- Math.max(0, column)
- ))),
- 0 > column && (pos.column = 0),
- pos
- );
- });
- }.call(Anchor.prototype));
- }
-),
-ace.define(
- 'ace/document',
- [
- 'require',
- 'exports',
- 'module',
- 'ace/lib/oop',
- 'ace/apply_delta',
- 'ace/lib/event_emitter',
- 'ace/range',
- 'ace/anchor',
- ],
- function (acequire, exports) {
- let oop = acequire('./lib/oop'),
- applyDelta = acequire('./apply_delta').applyDelta,
- EventEmitter = acequire('./lib/event_emitter').EventEmitter,
- Range = acequire('./range').Range,
- Anchor = acequire('./anchor').Anchor,
- Document = function (textOrLines) {
- (this.$lines = ['']),
- 0 === textOrLines.length
- ? (this.$lines = [''])
- : Array.isArray(textOrLines)
- ? this.insertMergedLines({ row: 0, column: 0 }, textOrLines)
- : this.insert({ row: 0, column: 0 }, textOrLines);
- };
- (function () {
- oop.implement(this, EventEmitter),
- (this.setValue = function (text) {
- let len = this.getLength() - 1;
- this.remove(new Range(0, 0, len, this.getLine(len).length)),
- this.insert({ row: 0, column: 0 }, text);
- }),
- (this.getValue = function () {
- return this.getAllLines().join(this.getNewLineCharacter());
- }),
- (this.createAnchor = function (row, column) {
- return new Anchor(this, row, column);
- }),
- (this.$split =
- 0 === 'aaa'.split(/a/).length
- ? function (text) {
- return text.replace(/\r\n|\r/g, '\n').split('\n');
- }
- : function (text) {
- return text.split(/\r\n|\r|\n/);
- }),
- (this.$detectNewLine = function (text) {
- let match = text.match(/^.*?(\r\n|\r|\n)/m);
- (this.$autoNewLine = match ? match[1] : '\n'),
- this._signal('changeNewLineMode');
- }),
- (this.getNewLineCharacter = function () {
- switch (this.$newLineMode) {
- case 'windows':
- return '\r\n';
- case 'unix':
- return '\n';
- default:
- return this.$autoNewLine || '\n';
- }
- }),
- (this.$autoNewLine = ''),
- (this.$newLineMode = 'auto'),
- (this.setNewLineMode = function (newLineMode) {
- this.$newLineMode !== newLineMode &&
- ((this.$newLineMode = newLineMode),
- this._signal('changeNewLineMode'));
- }),
- (this.getNewLineMode = function () {
- return this.$newLineMode;
- }),
- (this.isNewLine = function (text) {
- return '\r\n' == text || '\r' == text || '\n' == text;
- }),
- (this.getLine = function (row) {
- return this.$lines[row] || '';
- }),
- (this.getLines = function (firstRow, lastRow) {
- return this.$lines.slice(firstRow, lastRow + 1);
- }),
- (this.getAllLines = function () {
- return this.getLines(0, this.getLength());
- }),
- (this.getLength = function () {
- return this.$lines.length;
- }),
- (this.getTextRange = function (range) {
- return this.getLinesForRange(range).join(
- this.getNewLineCharacter()
- );
- }),
- (this.getLinesForRange = function (range) {
- let lines;
- if (range.start.row === range.end.row)
- {lines = [
- this.getLine(range.start.row).substring(
- range.start.column,
- range.end.column
- ),
- ];}
- else {
- (lines = this.getLines(range.start.row, range.end.row)),
- (lines[0] = (lines[0] || '').substring(range.start.column));
- let l = lines.length - 1;
- range.end.row - range.start.row == l &&
- (lines[l] = lines[l].substring(0, range.end.column));
- }
- return lines;
- }),
- (this.insertLines = function (row, lines) {
- return (
- console.warn(
- 'Use of document.insertLines is deprecated. Use the insertFullLines method instead.'
- ),
- this.insertFullLines(row, lines)
- );
- }),
- (this.removeLines = function (firstRow, lastRow) {
- return (
- console.warn(
- 'Use of document.removeLines is deprecated. Use the removeFullLines method instead.'
- ),
- this.removeFullLines(firstRow, lastRow)
- );
- }),
- (this.insertNewLine = function (position) {
- return (
- console.warn(
- 'Use of document.insertNewLine is deprecated. Use insertMergedLines(position, [\'\', \'\']) instead.'
- ),
- this.insertMergedLines(position, ['', ''])
- );
- }),
- (this.insert = function (position, text) {
- return (
- 1 >= this.getLength() && this.$detectNewLine(text),
- this.insertMergedLines(position, this.$split(text))
- );
- }),
- (this.insertInLine = function (position, text) {
- let start = this.clippedPos(position.row, position.column),
- end = this.pos(position.row, position.column + text.length);
- return (
- this.applyDelta(
- { start: start, end: end, action: 'insert', lines: [text] },
- !0
- ),
- this.clonePos(end)
- );
- }),
- (this.clippedPos = function (row, column) {
- let length = this.getLength();
- void 0 === row
- ? (row = length)
- : 0 > row
- ? (row = 0)
- : row >= length && ((row = length - 1), (column = void 0));
- let line = this.getLine(row);
- return (
- void 0 == column && (column = line.length),
- (column = Math.min(Math.max(column, 0), line.length)),
- { row: row, column: column }
- );
- }),
- (this.clonePos = function (pos) {
- return { row: pos.row, column: pos.column };
- }),
- (this.pos = function (row, column) {
- return { row: row, column: column };
- }),
- (this.$clipPosition = function (position) {
- let length = this.getLength();
- return (
- position.row >= length
- ? ((position.row = Math.max(0, length - 1)),
- (position.column = this.getLine(length - 1).length))
- : ((position.row = Math.max(0, position.row)),
- (position.column = Math.min(
- Math.max(position.column, 0),
- this.getLine(position.row).length
- ))),
- position
- );
- }),
- (this.insertFullLines = function (row, lines) {
- row = Math.min(Math.max(row, 0), this.getLength());
- let column = 0;
- this.getLength() > row
- ? ((lines = lines.concat([''])), (column = 0))
- : ((lines = [''].concat(lines)),
- row--,
- (column = this.$lines[row].length)),
- this.insertMergedLines({ row: row, column: column }, lines);
- }),
- (this.insertMergedLines = function (position, lines) {
- let start = this.clippedPos(position.row, position.column),
- end = {
- row: start.row + lines.length - 1,
- column:
- (1 == lines.length ? start.column : 0) +
- lines[lines.length - 1].length,
- };
- return (
- this.applyDelta({
- start: start,
- end: end,
- action: 'insert',
- lines: lines,
- }),
- this.clonePos(end)
- );
- }),
- (this.remove = function (range) {
- let start = this.clippedPos(range.start.row, range.start.column),
- end = this.clippedPos(range.end.row, range.end.column);
- return (
- this.applyDelta({
- start: start,
- end: end,
- action: 'remove',
- lines: this.getLinesForRange({ start: start, end: end }),
- }),
- this.clonePos(start)
- );
- }),
- (this.removeInLine = function (row, startColumn, endColumn) {
- let start = this.clippedPos(row, startColumn),
- end = this.clippedPos(row, endColumn);
- return (
- this.applyDelta(
- {
- start: start,
- end: end,
- action: 'remove',
- lines: this.getLinesForRange({ start: start, end: end }),
- },
- !0
- ),
- this.clonePos(start)
- );
- }),
- (this.removeFullLines = function (firstRow, lastRow) {
- (firstRow = Math.min(Math.max(0, firstRow), this.getLength() - 1)),
- (lastRow = Math.min(Math.max(0, lastRow), this.getLength() - 1));
- let deleteFirstNewLine =
- lastRow == this.getLength() - 1 && firstRow > 0,
- deleteLastNewLine = this.getLength() - 1 > lastRow,
- startRow = deleteFirstNewLine ? firstRow - 1 : firstRow,
- startCol = deleteFirstNewLine ? this.getLine(startRow).length : 0,
- endRow = deleteLastNewLine ? lastRow + 1 : lastRow,
- endCol = deleteLastNewLine ? 0 : this.getLine(endRow).length,
- range = new Range(startRow, startCol, endRow, endCol),
- deletedLines = this.$lines.slice(firstRow, lastRow + 1);
- return (
- this.applyDelta({
- start: range.start,
- end: range.end,
- action: 'remove',
- lines: this.getLinesForRange(range),
- }),
- deletedLines
- );
- }),
- (this.removeNewLine = function (row) {
- this.getLength() - 1 > row &&
- row >= 0 &&
- this.applyDelta({
- start: this.pos(row, this.getLine(row).length),
- end: this.pos(row + 1, 0),
- action: 'remove',
- lines: ['', ''],
- });
- }),
- (this.replace = function (range, text) {
- if (
- (range instanceof Range ||
- (range = Range.fromPoints(range.start, range.end)),
- 0 === text.length && range.isEmpty())
- )
- {return range.start;}
- if (text == this.getTextRange(range)) return range.end;
- this.remove(range);
- let end;
- return (end = text ? this.insert(range.start, text) : range.start);
- }),
- (this.applyDeltas = function (deltas) {
- for (let i = 0; deltas.length > i; i++) this.applyDelta(deltas[i]);
- }),
- (this.revertDeltas = function (deltas) {
- for (let i = deltas.length - 1; i >= 0; i--)
- {this.revertDelta(deltas[i]);}
- }),
- (this.applyDelta = function (delta, doNotValidate) {
- let isInsert = 'insert' == delta.action;
- (isInsert
- ? 1 >= delta.lines.length && !delta.lines[0]
- : !Range.comparePoints(delta.start, delta.end)) ||
- (isInsert &&
- delta.lines.length > 2e4 &&
- this.$splitAndapplyLargeDelta(delta, 2e4),
- applyDelta(this.$lines, delta, doNotValidate),
- this._signal('change', delta));
- }),
- (this.$splitAndapplyLargeDelta = function (delta, MAX) {
- for (
- let lines = delta.lines,
- l = lines.length,
- row = delta.start.row,
- column = delta.start.column,
- from = 0,
- to = 0;
- ;
-
- ) {
- (from = to), (to += MAX - 1);
- let chunk = lines.slice(from, to);
- if (to > l) {
- (delta.lines = chunk),
- (delta.start.row = row + from),
- (delta.start.column = column);
- break;
- }
- chunk.push(''),
- this.applyDelta(
- {
- start: this.pos(row + from, column),
- end: this.pos(row + to, (column = 0)),
- action: delta.action,
- lines: chunk,
- },
- !0
- );
- }
- }),
- (this.revertDelta = function (delta) {
- this.applyDelta({
- start: this.clonePos(delta.start),
- end: this.clonePos(delta.end),
- action: 'insert' == delta.action ? 'remove' : 'insert',
- lines: delta.lines.slice(),
- });
- }),
- (this.indexToPosition = function (index, startRow) {
- for (
- var lines = this.$lines || this.getAllLines(),
- newlineLength = this.getNewLineCharacter().length,
- i = startRow || 0,
- l = lines.length;
- l > i;
- i++
- )
- {if (((index -= lines[i].length + newlineLength), 0 > index))
- return {
- row: i,
- column: index + lines[i].length + newlineLength,
- };}
- return { row: l - 1, column: lines[l - 1].length };
- }),
- (this.positionToIndex = function (pos, startRow) {
- for (
- var lines = this.$lines || this.getAllLines(),
- newlineLength = this.getNewLineCharacter().length,
- index = 0,
- row = Math.min(pos.row, lines.length),
- i = startRow || 0;
- row > i;
- ++i
- )
- {index += lines[i].length + newlineLength;}
- return index + pos.column;
- });
- }.call(Document.prototype),
- (exports.Document = Document));
- }
-),
-ace.define('ace/lib/lang', ['require', 'exports', 'module'], function (
- acequire,
- exports
-) {
- (exports.last = function (a) {
- return a[a.length - 1];
- }),
- (exports.stringReverse = function (string) {
- return string
- .split('')
- .reverse()
- .join('');
- }),
- (exports.stringRepeat = function (string, count) {
- for (var result = ''; count > 0;)
- {1 & count && (result += string), (count >>= 1) && (string += string);}
- return result;
- });
- let trimBeginRegexp = /^\s\s*/,
- trimEndRegexp = /\s\s*$/;
- (exports.stringTrimLeft = function (string) {
- return string.replace(trimBeginRegexp, '');
- }),
- (exports.stringTrimRight = function (string) {
- return string.replace(trimEndRegexp, '');
- }),
- (exports.copyObject = function (obj) {
- let copy = {};
- for (let key in obj) copy[key] = obj[key];
- return copy;
- }),
- (exports.copyArray = function (array) {
- for (var copy = [], i = 0, l = array.length; l > i; i++)
- {copy[i] =
- array[i] && 'object' == typeof array[i]
- ? this.copyObject(array[i])
- : array[i];}
- return copy;
- }),
- (exports.deepCopy = function deepCopy(obj) {
- if ('object' !== typeof obj || !obj) return obj;
- let copy;
- if (Array.isArray(obj)) {
- copy = [];
- for (var key = 0; obj.length > key; key++)
- {copy[key] = deepCopy(obj[key]);}
- return copy;
- }
- if ('[object Object]' !== Object.prototype.toString.call(obj))
- {return obj;}
- copy = {};
- for (var key in obj) copy[key] = deepCopy(obj[key]);
- return copy;
- }),
- (exports.arrayToMap = function (arr) {
- for (var map = {}, i = 0; arr.length > i; i++) map[arr[i]] = 1;
- return map;
- }),
- (exports.createMap = function (props) {
- let map = Object.create(null);
- for (let i in props) map[i] = props[i];
- return map;
- }),
- (exports.arrayRemove = function (array, value) {
- for (let i = 0; array.length >= i; i++)
- {value === array[i] && array.splice(i, 1);}
- }),
- (exports.escapeRegExp = function (str) {
- return str.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1');
- }),
- (exports.escapeHTML = function (str) {
- return str
- .replace(/&/g, '&')
- .replace(/"/g, '"')
- .replace(/'/g, ''')
- .replace(/ i; i += 2) {
- if (Array.isArray(data[i + 1]))
- var d = {
- action: 'insert',
- start: data[i],
- lines: data[i + 1],
- };
- else
- var d = {
- action: 'remove',
- start: data[i],
- end: data[i + 1],
- };
- doc.applyDelta(d, !0);
- }}
- return _self.$timeout
- ? deferredUpdate.schedule(_self.$timeout)
- : (_self.onUpdate(), void 0);
- });
- });
- (function () {
- (this.$timeout = 500),
- (this.setTimeout = function (timeout) {
- this.$timeout = timeout;
- }),
- (this.setValue = function (value) {
- this.doc.setValue(value),
- this.deferredUpdate.schedule(this.$timeout);
- }),
- (this.getValue = function (callbackId) {
- this.sender.callback(this.doc.getValue(), callbackId);
- }),
- (this.onUpdate = function () {}),
- (this.isPending = function () {
- return this.deferredUpdate.isPending();
- });
- }.call(Mirror.prototype));
- }
-),
-ace.define('ace/lib/es5-shim', ['require', 'exports', 'module'], function () {
- function Empty() {}
- function doesDefinePropertyWork(object) {
- try {
- return (
- Object.defineProperty(object, 'sentinel', {}), 'sentinel' in object
- );
- } catch (exception) {}
- }
- function toInteger(n) {
- return (
- (n = +n),
- n !== n
- ? (n = 0)
- : 0 !== n &&
- n !== 1 / 0 &&
- n !== -(1 / 0) &&
- (n = (n > 0 || -1) * Math.floor(Math.abs(n))),
- n
- );
- }
- Function.prototype.bind ||
- (Function.prototype.bind = function (that) {
- let target = this;
- if ('function' !== typeof target)
- {throw new TypeError(
- 'Function.prototype.bind called on incompatible ' + target
- );}
- var args = slice.call(arguments, 1),
- bound = function () {
- if (this instanceof bound) {
- let result = target.apply(
- this,
- args.concat(slice.call(arguments))
- );
- return Object(result) === result ? result : this;
- }
- return target.apply(that, args.concat(slice.call(arguments)));
- };
- return (
- target.prototype &&
- ((Empty.prototype = target.prototype),
- (bound.prototype = new Empty()),
- (Empty.prototype = null)),
- bound
- );
- });
- var defineGetter,
- defineSetter,
- lookupGetter,
- lookupSetter,
- supportsAccessors,
- call = Function.prototype.call,
- prototypeOfArray = Array.prototype,
- prototypeOfObject = Object.prototype,
- slice = prototypeOfArray.slice,
- _toString = call.bind(prototypeOfObject.toString),
- owns = call.bind(prototypeOfObject.hasOwnProperty);
- if (
- ((supportsAccessors = owns(prototypeOfObject, '__defineGetter__')) &&
- ((defineGetter = call.bind(prototypeOfObject.__defineGetter__)),
- (defineSetter = call.bind(prototypeOfObject.__defineSetter__)),
- (lookupGetter = call.bind(prototypeOfObject.__lookupGetter__)),
- (lookupSetter = call.bind(prototypeOfObject.__lookupSetter__))),
- 2 != [1, 2].splice(0).length)
- )
- {if (
- (function() {
- function makeArray(l) {
- var a = Array(l + 2);
- return (a[0] = a[1] = 0), a;
- }
- var lengthBefore,
- array = [];
- return (
- array.splice.apply(array, makeArray(20)),
- array.splice.apply(array, makeArray(26)),
- (lengthBefore = array.length),
- array.splice(5, 0, 'XXX'),
- lengthBefore + 1 == array.length,
- lengthBefore + 1 == array.length ? !0 : void 0
- );
- })()
- ) {
- var array_splice = Array.prototype.splice;
- Array.prototype.splice = function(start, deleteCount) {
- return arguments.length
- ? array_splice.apply(
- this,
- [
- void 0 === start ? 0 : start,
- void 0 === deleteCount ? this.length - start : deleteCount,
- ].concat(slice.call(arguments, 2))
- )
- : [];
- };
- } else
- Array.prototype.splice = function(pos, removeCount) {
- var length = this.length;
- pos > 0
- ? pos > length && (pos = length)
- : void 0 == pos
- ? (pos = 0)
- : 0 > pos && (pos = Math.max(length + pos, 0)),
- length > pos + removeCount || (removeCount = length - pos);
- var removed = this.slice(pos, pos + removeCount),
- insert = slice.call(arguments, 2),
- add = insert.length;
- if (pos === length) add && this.push.apply(this, insert);
- else {
- var remove = Math.min(removeCount, length - pos),
- tailOldPos = pos + remove,
- tailNewPos = tailOldPos + add - remove,
- tailCount = length - tailOldPos,
- lengthAfterRemove = length - remove;
- if (tailOldPos > tailNewPos)
- for (var i = 0; tailCount > i; ++i)
- this[tailNewPos + i] = this[tailOldPos + i];
- else if (tailNewPos > tailOldPos)
- for (i = tailCount; i--; )
- this[tailNewPos + i] = this[tailOldPos + i];
- if (add && pos === lengthAfterRemove)
- (this.length = lengthAfterRemove), this.push.apply(this, insert);
- else
- for (this.length = lengthAfterRemove + add, i = 0; add > i; ++i)
- this[pos + i] = insert[i];
- }
- return removed;
- };}
- Array.isArray ||
- (Array.isArray = function (obj) {
- return '[object Array]' == _toString(obj);
- });
- let boxedString = Object('a'),
- splitString = 'a' != boxedString[0] || !(0 in boxedString);
- if (
- (Array.prototype.forEach ||
- (Array.prototype.forEach = function (fun) {
- let object = toObject(this),
- self =
- splitString && '[object String]' == _toString(this)
- ? this.split('')
- : object,
- thisp = arguments[1],
- i = -1,
- length = self.length >>> 0;
- if ('[object Function]' != _toString(fun)) throw new TypeError();
- for (; length > ++i;)
- {i in self && fun.call(thisp, self[i], i, object);}
- }),
- Array.prototype.map ||
- (Array.prototype.map = function (fun) {
- let object = toObject(this),
- self =
- splitString && '[object String]' == _toString(this)
- ? this.split('')
- : object,
- length = self.length >>> 0,
- result = Array(length),
- thisp = arguments[1];
- if ('[object Function]' != _toString(fun))
- {throw new TypeError(fun + ' is not a function');}
- for (let i = 0; length > i; i++)
- {i in self && (result[i] = fun.call(thisp, self[i], i, object));}
- return result;
- }),
- Array.prototype.filter ||
- (Array.prototype.filter = function (fun) {
- let value,
- object = toObject(this),
- self =
- splitString && '[object String]' == _toString(this)
- ? this.split('')
- : object,
- length = self.length >>> 0,
- result = [],
- thisp = arguments[1];
- if ('[object Function]' != _toString(fun))
- {throw new TypeError(fun + ' is not a function');}
- for (let i = 0; length > i; i++)
- {i in self &&
- ((value = self[i]),
- fun.call(thisp, value, i, object) && result.push(value));}
- return result;
- }),
- Array.prototype.every ||
- (Array.prototype.every = function (fun) {
- let object = toObject(this),
- self =
- splitString && '[object String]' == _toString(this)
- ? this.split('')
- : object,
- length = self.length >>> 0,
- thisp = arguments[1];
- if ('[object Function]' != _toString(fun))
- {throw new TypeError(fun + ' is not a function');}
- for (let i = 0; length > i; i++)
- {if (i in self && !fun.call(thisp, self[i], i, object)) return !1;}
- return !0;
- }),
- Array.prototype.some ||
- (Array.prototype.some = function (fun) {
- let object = toObject(this),
- self =
- splitString && '[object String]' == _toString(this)
- ? this.split('')
- : object,
- length = self.length >>> 0,
- thisp = arguments[1];
- if ('[object Function]' != _toString(fun))
- {throw new TypeError(fun + ' is not a function');}
- for (let i = 0; length > i; i++)
- {if (i in self && fun.call(thisp, self[i], i, object)) return !0;}
- return !1;
- }),
- Array.prototype.reduce ||
- (Array.prototype.reduce = function (fun) {
- let object = toObject(this),
- self =
- splitString && '[object String]' == _toString(this)
- ? this.split('')
- : object,
- length = self.length >>> 0;
- if ('[object Function]' != _toString(fun))
- {throw new TypeError(fun + ' is not a function');}
- if (!length && 1 == arguments.length)
- {throw new TypeError('reduce of empty array with no initial value');}
- let result,
- i = 0;
- if (arguments.length >= 2) result = arguments[1];
- else
- {for (;;) {
- if (i in self) {
- result = self[i++];
- break;
- }
- if (++i >= length)
- throw new TypeError(
- 'reduce of empty array with no initial value'
- );
- }}
- for (; length > i; i++)
- {i in self &&
- (result = fun.call(void 0, result, self[i], i, object));}
- return result;
- }),
- Array.prototype.reduceRight ||
- (Array.prototype.reduceRight = function (fun) {
- let object = toObject(this),
- self =
- splitString && '[object String]' == _toString(this)
- ? this.split('')
- : object,
- length = self.length >>> 0;
- if ('[object Function]' != _toString(fun))
- {throw new TypeError(fun + ' is not a function');}
- if (!length && 1 == arguments.length)
- {throw new TypeError(
- 'reduceRight of empty array with no initial value'
- );}
- let result,
- i = length - 1;
- if (arguments.length >= 2) result = arguments[1];
- else
- {for (;;) {
- if (i in self) {
- result = self[i--];
- break;
- }
- if (0 > --i)
- throw new TypeError(
- 'reduceRight of empty array with no initial value'
- );
- }}
- do
- {i in this &&
- (result = fun.call(void 0, result, self[i], i, object));}
- while (i--);
- return result;
- }),
- (Array.prototype.indexOf && -1 == [0, 1].indexOf(1, 2)) ||
- (Array.prototype.indexOf = function (sought) {
- let self =
- splitString && '[object String]' == _toString(this)
- ? this.split('')
- : toObject(this),
- length = self.length >>> 0;
- if (!length) return -1;
- let i = 0;
- for (
- arguments.length > 1 && (i = toInteger(arguments[1])),
- i = i >= 0 ? i : Math.max(0, length + i);
- length > i;
- i++
- )
- {if (i in self && self[i] === sought) return i;}
- return -1;
- }),
- (Array.prototype.lastIndexOf && -1 == [0, 1].lastIndexOf(0, -3)) ||
- (Array.prototype.lastIndexOf = function (sought) {
- let self =
- splitString && '[object String]' == _toString(this)
- ? this.split('')
- : toObject(this),
- length = self.length >>> 0;
- if (!length) return -1;
- let i = length - 1;
- for (
- arguments.length > 1 && (i = Math.min(i, toInteger(arguments[1]))),
- i = i >= 0 ? i : length - Math.abs(i);
- i >= 0;
- i--
- )
- {if (i in self && sought === self[i]) return i;}
- return -1;
- }),
- Object.getPrototypeOf ||
- (Object.getPrototypeOf = function (object) {
- return (
- object.__proto__ ||
- (object.constructor
- ? object.constructor.prototype
- : prototypeOfObject)
- );
- }),
- !Object.getOwnPropertyDescriptor)
- ) {
- let ERR_NON_OBJECT =
- 'Object.getOwnPropertyDescriptor called on a non-object: ';
- Object.getOwnPropertyDescriptor = function (object, property) {
- if (
- ('object' !== typeof object && 'function' !== typeof object) ||
- null === object
- )
- {throw new TypeError(ERR_NON_OBJECT + object);}
- if (owns(object, property)) {
- var descriptor, getter, setter;
- if (
- ((descriptor = { enumerable: !0, configurable: !0 }),
- supportsAccessors)
- ) {
- let prototype = object.__proto__;
- object.__proto__ = prototypeOfObject;
- var getter = lookupGetter(object, property),
- setter = lookupSetter(object, property);
- if (((object.__proto__ = prototype), getter || setter))
- {return (
- getter && (descriptor.get = getter),
- setter && (descriptor.set = setter),
- descriptor
- );}
- }
- return (descriptor.value = object[property]), descriptor;
- }
- };
- }
- if (
- (Object.getOwnPropertyNames ||
- (Object.getOwnPropertyNames = function (object) {
- return Object.keys(object);
- }),
- !Object.create)
- ) {
- let createEmpty;
- (createEmpty =
- null === Object.prototype.__proto__
- ? function () {
- return { __proto__: null };
- }
- : function () {
- let empty = {};
- for (let i in empty) empty[i] = null;
- return (
- (empty.constructor = empty.hasOwnProperty = empty.propertyIsEnumerable = empty.isPrototypeOf = empty.toLocaleString = empty.toString = empty.valueOf = empty.__proto__ = null),
- empty
- );
- }),
- (Object.create = function (prototype, properties) {
- let object;
- if (null === prototype) object = createEmpty();
- else {
- if ('object' !== typeof prototype)
- {throw new TypeError(
- 'typeof prototype[' + typeof prototype + "] != 'object'"
- );}
- let Type = function () {};
- (Type.prototype = prototype),
- (object = new Type()),
- (object.__proto__ = prototype);
- }
- return (
- void 0 !== properties &&
- Object.defineProperties(object, properties),
- object
- );
- });
- }
- if (Object.defineProperty) {
- let definePropertyWorksOnObject = doesDefinePropertyWork({}),
- definePropertyWorksOnDom =
- 'undefined' === typeof document ||
- doesDefinePropertyWork(document.createElement('div'));
- if (!definePropertyWorksOnObject || !definePropertyWorksOnDom)
- {var definePropertyFallback = Object.defineProperty;}
- }
- if (!Object.defineProperty || definePropertyFallback) {
- let ERR_NON_OBJECT_DESCRIPTOR =
- 'Property description must be an object: ',
- ERR_NON_OBJECT_TARGET = 'Object.defineProperty called on non-object: ',
- ERR_ACCESSORS_NOT_SUPPORTED =
- 'getters & setters can not be defined on this javascript engine';
- Object.defineProperty = function (object, property, descriptor) {
- if (
- ('object' !== typeof object && 'function' !== typeof object) ||
- null === object
- )
- {throw new TypeError(ERR_NON_OBJECT_TARGET + object);}
- if (
- ('object' !== typeof descriptor && 'function' !== typeof descriptor) ||
- null === descriptor
- )
- {throw new TypeError(ERR_NON_OBJECT_DESCRIPTOR + descriptor);}
- if (definePropertyFallback)
- {try {
- return definePropertyFallback.call(
- Object,
- object,
- property,
- descriptor
- );
- } catch (exception) {}}
- if (owns(descriptor, 'value'))
- {if (
- supportsAccessors &&
- (lookupGetter(object, property) || lookupSetter(object, property))
- ) {
- var prototype = object.__proto__;
- (object.__proto__ = prototypeOfObject),
- delete object[property],
- (object[property] = descriptor.value),
- (object.__proto__ = prototype);
- } else object[property] = descriptor.value;}
- else {
- if (!supportsAccessors)
- {throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED);}
- owns(descriptor, 'get') &&
- defineGetter(object, property, descriptor.get),
- owns(descriptor, 'set') &&
- defineSetter(object, property, descriptor.set);
- }
- return object;
- };
- }
- Object.defineProperties ||
- (Object.defineProperties = function (object, properties) {
- for (let property in properties)
- {owns(properties, property) &&
- Object.defineProperty(object, property, properties[property]);}
- return object;
- }),
- Object.seal ||
- (Object.seal = function (object) {
- return object;
- }),
- Object.freeze ||
- (Object.freeze = function (object) {
- return object;
- });
- try {
- Object.freeze(function () {});
- } catch (exception) {
- Object.freeze = (function (freezeObject) {
- return function (object) {
- return 'function' === typeof object ? object : freezeObject(object);
- };
- }(Object.freeze));
- }
- if (
- (Object.preventExtensions ||
- (Object.preventExtensions = function (object) {
- return object;
- }),
- Object.isSealed ||
- (Object.isSealed = function () {
- return !1;
- }),
- Object.isFrozen ||
- (Object.isFrozen = function () {
- return !1;
- }),
- Object.isExtensible ||
- (Object.isExtensible = function (object) {
- if (Object(object) === object) throw new TypeError();
- for (var name = ''; owns(object, name);) name += '?';
- object[name] = !0;
- let returnValue = owns(object, name);
- return delete object[name], returnValue;
- }),
- !Object.keys)
- ) {
- let hasDontEnumBug = !0,
- dontEnums = [
- 'toString',
- 'toLocaleString',
- 'valueOf',
- 'hasOwnProperty',
- 'isPrototypeOf',
- 'propertyIsEnumerable',
- 'constructor',
- ],
- dontEnumsLength = dontEnums.length;
- for (let key in { toString: null }) hasDontEnumBug = !1;
- Object.keys = function (object) {
- if (
- ('object' !== typeof object && 'function' !== typeof object) ||
- null === object
- )
- {throw new TypeError('Object.keys called on a non-object');}
- let keys = [];
- for (let name in object) owns(object, name) && keys.push(name);
- if (hasDontEnumBug)
- {for (var i = 0, ii = dontEnumsLength; ii > i; i++) {
- var dontEnum = dontEnums[i];
- owns(object, dontEnum) && keys.push(dontEnum);
- }}
- return keys;
- };
- }
- Date.now ||
- (Date.now = function () {
- return new Date().getTime();
- });
- let ws = ' \nv\f\r \u2028\u2029';
- if (!String.prototype.trim || ws.trim()) {
- ws = '[' + ws + ']';
- let trimBeginRegexp = RegExp('^' + ws + ws + '*'),
- trimEndRegexp = RegExp(ws + ws + '*$');
- String.prototype.trim = function () {
- return (this + '')
- .replace(trimBeginRegexp, '')
- .replace(trimEndRegexp, '');
- };
- }
- var toObject = function (o) {
- if (null == o) throw new TypeError('can\'t convert ' + o + ' to object');
- return Object(o);
- };
-});
-ace.define(
- 'sense_editor/mode/worker_parser',
- ['require', 'exports', 'module'],
- function () {
- let at, // The index of the current character
- ch, // The current character
- annos, // annotations
- escapee = {
- '"': '"',
- '\\': '\\',
- '/': '/',
- b: '\b',
- f: '\f',
- n: '\n',
- r: '\r',
- t: '\t',
- },
- text,
- annotate = function (type, text) {
- annos.push({ type: type, text: text, at: at });
- },
- error = function (m) {
- throw {
- name: 'SyntaxError',
- message: m,
- at: at,
- text: text,
- };
- },
- reset = function (newAt) {
- ch = text.charAt(newAt);
- at = newAt + 1;
- },
- next = function (c) {
- if (c && c !== ch) {
- error('Expected \'' + c + '\' instead of \'' + ch + '\'');
- }
-
- ch = text.charAt(at);
- at += 1;
- return ch;
- },
- nextUpTo = function (upTo, errorMessage) {
- let currentAt = at,
- i = text.indexOf(upTo, currentAt);
- if (i < 0) {
- error(errorMessage || 'Expected \'' + upTo + '\'');
- }
- reset(i + upTo.length);
- return text.substring(currentAt, i);
- },
- peek = function (offset) {
- return text.charAt(at + offset);
- },
- number = function () {
- let number,
- string = '';
-
- if (ch === '-') {
- string = '-';
- next('-');
- }
- while (ch >= '0' && ch <= '9') {
- string += ch;
- next();
- }
- if (ch === '.') {
- string += '.';
- while (next() && ch >= '0' && ch <= '9') {
- string += ch;
- }
- }
- if (ch === 'e' || ch === 'E') {
- string += ch;
- next();
- if (ch === '-' || ch === '+') {
- string += ch;
- next();
- }
- while (ch >= '0' && ch <= '9') {
- string += ch;
- next();
- }
- }
- number = +string;
- if (isNaN(number)) {
- error('Bad number');
- } else {
- return number;
- }
- },
- string = function () {
- let hex,
- i,
- string = '',
- uffff;
-
- if (ch === '"') {
- // If the current and the next characters are equal to "", empty string or start of triple quoted strings
- if (peek(0) === '"' && peek(1) === '"') {
- // literal
- next('"');
- next('"');
- return nextUpTo('"""', 'failed to find closing \'"""\'');
- } else {
- while (next()) {
- if (ch === '"') {
- next();
- return string;
- } else if (ch === '\\') {
- next();
- if (ch === 'u') {
- uffff = 0;
- for (i = 0; i < 4; i += 1) {
- hex = parseInt(next(), 16);
- if (!isFinite(hex)) {
- break;
- }
- uffff = uffff * 16 + hex;
- }
- string += String.fromCharCode(uffff);
- } else if (typeof escapee[ch] === 'string') {
- string += escapee[ch];
- } else {
- break;
- }
- } else {
- string += ch;
- }
- }
- }
- }
- error('Bad string');
- },
- white = function () {
- while (ch) {
- // Skip whitespace.
- while (ch && ch <= ' ') {
- next();
- }
- // if the current char in iteration is '#' or the char and the next char is equal to '//'
- // we are on the single line comment
- if (ch === '#' || ch === '/' && peek(0) === '/') {
- // Until we are on the new line, skip to the next char
- while (ch && ch !== '\n') {
- next();
- }
- } else if (ch === '/' && peek(0) === '*') {
- // If the chars starts with '/*', we are on the multiline comment
- next();
- next();
- while (ch && !(ch === '*' && peek(0) === '/')) {
- // Until we have closing tags '*/', skip to the next char
- next();
- }
- if (ch) {
- next();
- next();
- }
- } else break;
- }
- },
- strictWhite = function () {
- while (ch && (ch == ' ' || ch == '\t')) {
- next();
- }
- },
- newLine = function () {
- if (ch == '\n') next();
- },
- word = function () {
- switch (ch) {
- case 't':
- next('t');
- next('r');
- next('u');
- next('e');
- return true;
- case 'f':
- next('f');
- next('a');
- next('l');
- next('s');
- next('e');
- return false;
- case 'n':
- next('n');
- next('u');
- next('l');
- next('l');
- return null;
- }
- error('Unexpected \'' + ch + '\'');
- },
- // parses and returns the method
- method = function () {
- switch (ch) {
- case 'g':
- next('g');
- next('e');
- next('t');
- return 'get';
- case 'G':
- next('G');
- next('E');
- next('T');
- return 'GET';
- case 'h':
- next('h');
- next('e');
- next('a');
- next('d');
- return 'head';
- case 'H':
- next('H');
- next('E');
- next('A');
- next('D');
- return 'HEAD';
- case 'd':
- next('d');
- next('e');
- next('l');
- next('e');
- next('t');
- next('e');
- return 'delete';
- case 'D':
- next('D');
- next('E');
- next('L');
- next('E');
- next('T');
- next('E');
- return 'DELETE';
- case 'p':
- next('p');
- switch (ch) {
- case 'a':
- next('a');
- next('t');
- next('c');
- next('h');
- return 'patch';
- case 'u':
- next('u');
- next('t');
- return 'put';
- case 'o':
- next('o');
- next('s');
- next('t');
- return 'post';
- default:
- error('Unexpected \'' + ch + '\'');
- }
- break;
- case 'P':
- next('P');
- switch (ch) {
- case 'A':
- next('A');
- next('T');
- next('C');
- next('H');
- return 'PATCH';
- case 'U':
- next('U');
- next('T');
- return 'PUT';
- case 'O':
- next('O');
- next('S');
- next('T');
- return 'POST';
- default:
- error('Unexpected \'' + ch + '\'');
- }
- break;
- default:
- error('Expected one of GET/POST/PUT/DELETE/HEAD/PATCH');
- }
- },
- value, // Place holder for the value function.
- array = function () {
- const array = [];
-
- if (ch === '[') {
- next('[');
- white();
- if (ch === ']') {
- next(']');
- return array; // empty array
- }
- while (ch) {
- array.push(value());
- white();
- if (ch === ']') {
- next(']');
- return array;
- }
- next(',');
- white();
- }
- }
- error('Bad array');
- },
- object = function () {
- let key,
- object = {};
-
- if (ch === '{') {
- next('{');
- white();
- if (ch === '}') {
- next('}');
- return object; // empty object
- }
- while (ch) {
- key = string();
- white();
- next(':');
- if (Object.hasOwnProperty.call(object, key)) {
- error('Duplicate key "' + key + '"');
- }
- object[key] = value();
- white();
- if (ch === '}') {
- next('}');
- return object;
- }
- next(',');
- white();
- }
- }
- error('Bad object');
- };
-
- value = function () {
- white();
- switch (ch) {
- case '{':
- return object();
- case '[':
- return array();
- case '"':
- return string();
- case '-':
- return number();
- default:
- return ch >= '0' && ch <= '9' ? number() : word();
- }
- };
-
- let url = function () {
- let url = '';
- while (ch && ch != '\n') {
- url += ch;
- next();
- }
- if (url == '') {
- error('Missing url');
- }
- return url;
- },
- request = function () {
- white();
- method();
- strictWhite();
- url();
- strictWhite(); // advance to one new line
- newLine();
- strictWhite();
- if (ch == '{') {
- object();
- }
- // multi doc request
- strictWhite(); // advance to one new line
- newLine();
- strictWhite();
- while (ch == '{') {
- // another object
- object();
- strictWhite();
- newLine();
- strictWhite();
- }
- },
- comment = function () {
- while (ch == '#') {
- while (ch && ch !== '\n') {
- next();
- }
- white();
- }
- },
- multi_request = function () {
- while (ch && ch != '') {
- white();
- if (!ch) {
- continue;
- }
- try {
- comment();
- white();
- if (!ch) {
- continue;
- }
- request();
- white();
- } catch (e) {
- annotate('error', e.message);
- // snap
- const substring = text.substr(at);
- const nextMatch = substring.search(/^POST|HEAD|GET|PUT|DELETE|PATCH/m);
- if (nextMatch < 1) return;
- reset(at + nextMatch);
- }
- }
- };
-
- return function (source, reviver) {
- let result;
-
- text = source;
- at = 0;
- annos = [];
- next();
- multi_request();
- white();
- if (ch) {
- annotate('error', 'Syntax error');
- }
-
- result = { annotations: annos };
-
- return typeof reviver === 'function'
- ? (function walk(holder, key) {
- let k,
- v,
- value = holder[key];
- if (value && typeof value === 'object') {
- for (k in value) {
- if (Object.hasOwnProperty.call(value, k)) {
- v = walk(value, k);
- if (v !== undefined) {
- value[k] = v;
- } else {
- delete value[k];
- }
- }
- }
- }
- return reviver.call(holder, key, value);
- }({ '': result }, ''))
- : result;
- };
- }
-);
-
-ace.define(
- 'sense_editor/mode/worker',
- [
- 'require',
- 'exports',
- 'module',
- 'ace/lib/oop',
- 'ace/worker/mirror',
- 'sense_editor/mode/worker_parser',
- ],
- function (require, exports) {
- const oop = require('ace/lib/oop');
- const Mirror = require('ace/worker/mirror').Mirror;
- const parse = require('sense_editor/mode/worker_parser');
-
- const SenseWorker = (exports.SenseWorker = function (sender) {
- Mirror.call(this, sender);
- this.setTimeout(200);
- });
-
- oop.inherits(SenseWorker, Mirror);
-
- (function () {
- this.id = 'senseWorker';
- this.onUpdate = function () {
- const value = this.doc.getValue();
- let pos, result;
- try {
- result = parse(value);
- } catch (e) {
- pos = this.charToDocumentPosition(e.at - 1);
- this.sender.emit('error', {
- row: pos.row,
- column: pos.column,
- text: e.message,
- type: 'error',
- });
- return;
- }
- for (let i = 0; i < result.annotations.length; i++) {
- pos = this.charToDocumentPosition(result.annotations[i].at - 1);
- result.annotations[i].row = pos.row;
- result.annotations[i].column = pos.column;
- }
- this.sender.emit('ok', result.annotations);
- };
-
- this.charToDocumentPosition = function (charPos) {
- let i = 0;
- const len = this.doc.getLength();
- const nl = this.doc.getNewLineCharacter().length;
-
- if (!len) {
- return { row: 0, column: 0 };
- }
-
- let lineStart = 0,
- line;
- while (i < len) {
- line = this.doc.getLine(i);
- const lineLength = line.length + nl;
- if (lineStart + lineLength > charPos) {
- return {
- row: i,
- column: charPos - lineStart,
- };
- }
-
- lineStart += lineLength;
- i += 1;
- }
-
- return {
- row: i - 1,
- column: line.length,
- };
- };
- }.call(SenseWorker.prototype));
- }
-);
diff --git a/src/plugins/console/public/application/models/legacy_core_editor/output_tokenization.test.js b/src/plugins/console/public/application/models/legacy_core_editor/output_tokenization.test.js
deleted file mode 100644
index e09bf06e48246..0000000000000
--- a/src/plugins/console/public/application/models/legacy_core_editor/output_tokenization.test.js
+++ /dev/null
@@ -1,91 +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", 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 './legacy_core_editor.test.mocks';
-import $ from 'jquery';
-import RowParser from '../../../lib/row_parser';
-import ace from 'brace';
-import { createReadOnlyAceEditor } from './create_readonly';
-let output;
-const tokenIterator = ace.acequire('ace/token_iterator');
-
-describe('Output Tokenization', () => {
- beforeEach(() => {
- output = createReadOnlyAceEditor(document.querySelector('#ConAppOutput'));
- $(output.container).show();
- });
-
- afterEach(() => {
- $(output.container).hide();
- });
-
- function tokensAsList() {
- const iter = new tokenIterator.TokenIterator(output.getSession(), 0, 0);
- const ret = [];
- let t = iter.getCurrentToken();
- const parser = new RowParser(output);
- if (parser.isEmptyToken(t)) {
- t = parser.nextNonEmptyToken(iter);
- }
- while (t) {
- ret.push({ value: t.value, type: t.type });
- t = parser.nextNonEmptyToken(iter);
- }
-
- return ret;
- }
-
- let testCount = 0;
-
- function tokenTest(tokenList, data) {
- if (data && typeof data !== 'string') {
- data = JSON.stringify(data, null, 3);
- }
-
- test('Token test ' + testCount++, function (done) {
- output.update(data, function () {
- const tokens = tokensAsList();
- const normTokenList = [];
- for (let i = 0; i < tokenList.length; i++) {
- normTokenList.push({ type: tokenList[i++], value: tokenList[i] });
- }
-
- expect(tokens).toEqual(normTokenList);
- done();
- });
- });
- }
-
- tokenTest(
- ['warning', '#! warning', 'comment', '# GET url', 'paren.lparen', '{', 'paren.rparen', '}'],
- '#! warning\n' + '# GET url\n' + '{}'
- );
-
- tokenTest(
- [
- 'comment',
- '# GET url',
- 'paren.lparen',
- '{',
- 'variable',
- '"f"',
- 'punctuation.colon',
- ':',
- 'punctuation.start_triple_quote',
- '"""',
- 'multi_string',
- 'raw',
- 'punctuation.end_triple_quote',
- '"""',
- 'paren.rparen',
- '}',
- ],
- '# GET url\n' + '{ "f": """raw""" }'
- );
-});
diff --git a/src/plugins/console/public/application/models/legacy_core_editor/smart_resize.ts b/src/plugins/console/public/application/models/legacy_core_editor/smart_resize.ts
deleted file mode 100644
index c238e8c6a5da7..0000000000000
--- a/src/plugins/console/public/application/models/legacy_core_editor/smart_resize.ts
+++ /dev/null
@@ -1,27 +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", 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 { get, throttle } from 'lodash';
-import type { Editor } from 'brace';
-
-// eslint-disable-next-line import/no-default-export
-export default function (editor: Editor) {
- const resize = editor.resize;
-
- const throttledResize = throttle(() => {
- resize.call(editor, false);
-
- // Keep current top line in view when resizing to avoid losing user context
- const userRow = get(throttledResize, 'topRow', 0);
- if (userRow !== 0) {
- editor.renderer.scrollToLine(userRow, false, false, () => {});
- }
- }, 35);
- return throttledResize;
-}
diff --git a/src/plugins/console/public/application/models/legacy_core_editor/theme_sense_dark.js b/src/plugins/console/public/application/models/legacy_core_editor/theme_sense_dark.js
deleted file mode 100644
index fd8e12bf1d703..0000000000000
--- a/src/plugins/console/public/application/models/legacy_core_editor/theme_sense_dark.js
+++ /dev/null
@@ -1,123 +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", 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 ace from 'brace';
-
-ace.define('ace/theme/sense-dark', ['require', 'exports', 'module'], function (require, exports) {
- exports.isDark = true;
- exports.cssClass = 'ace-sense-dark';
- exports.cssText =
- '.ace-sense-dark .ace_gutter {\
-background: #2e3236;\
-color: #bbbfc2;\
-}\
-.ace-sense-dark .ace_print-margin {\
-width: 1px;\
-background: #555651\
-}\
-.ace-sense-dark .ace_scroller {\
-background-color: #202328;\
-}\
-.ace-sense-dark .ace_content {\
-}\
-.ace-sense-dark .ace_text-layer {\
-color: #F8F8F2\
-}\
-.ace-sense-dark .ace_cursor {\
-border-left: 2px solid #F8F8F0\
-}\
-.ace-sense-dark .ace_overwrite-cursors .ace_cursor {\
-border-left: 0px;\
-border-bottom: 1px solid #F8F8F0\
-}\
-.ace-sense-dark .ace_marker-layer .ace_selection {\
-background: #222\
-}\
-.ace-sense-dark.ace_multiselect .ace_selection.ace_start {\
-box-shadow: 0 0 3px 0px #272822;\
-border-radius: 2px\
-}\
-.ace-sense-dark .ace_marker-layer .ace_step {\
-background: rgb(102, 82, 0)\
-}\
-.ace-sense-dark .ace_marker-layer .ace_bracket {\
-margin: -1px 0 0 -1px;\
-border: 1px solid #49483E\
-}\
-.ace-sense-dark .ace_marker-layer .ace_active-line {\
-background: #202020\
-}\
-.ace-sense-dark .ace_gutter-active-line {\
-background-color: #272727\
-}\
-.ace-sense-dark .ace_marker-layer .ace_selected-word {\
-border: 1px solid #49483E\
-}\
-.ace-sense-dark .ace_invisible {\
-color: #49483E\
-}\
-.ace-sense-dark .ace_entity.ace_name.ace_tag,\
-.ace-sense-dark .ace_keyword,\
-.ace-sense-dark .ace_meta,\
-.ace-sense-dark .ace_storage {\
-color: #F92672\
-}\
-.ace-sense-dark .ace_constant.ace_character,\
-.ace-sense-dark .ace_constant.ace_language,\
-.ace-sense-dark .ace_constant.ace_numeric,\
-.ace-sense-dark .ace_constant.ace_other {\
-color: #AE81FF\
-}\
-.ace-sense-dark .ace_invalid {\
-color: #F8F8F0;\
-background-color: #F92672\
-}\
-.ace-sense-dark .ace_invalid.ace_deprecated {\
-color: #F8F8F0;\
-background-color: #AE81FF\
-}\
-.ace-sense-dark .ace_support.ace_constant,\
-.ace-sense-dark .ace_support.ace_function {\
-color: #66D9EF\
-}\
-.ace-sense-dark .ace_fold {\
-background-color: #A6E22E;\
-border-color: #F8F8F2\
-}\
-.ace-sense-dark .ace_storage.ace_type,\
-.ace-sense-dark .ace_support.ace_class,\
-.ace-sense-dark .ace_support.ace_type {\
-font-style: italic;\
-color: #66D9EF\
-}\
-.ace-sense-dark .ace_entity.ace_name.ace_function,\
-.ace-sense-dark .ace_entity.ace_other.ace_attribute-name,\
-.ace-sense-dark .ace_variable {\
-color: #A6E22E\
-}\
-.ace-sense-dark .ace_variable.ace_parameter {\
-font-style: italic;\
-color: #FD971F\
-}\
-.ace-sense-dark .ace_string {\
-color: #E6DB74\
-}\
-.ace-sense-dark .ace_comment {\
-color: #629755\
-}\
-.ace-sense-dark .ace_markup.ace_underline {\
-text-decoration: underline\
-}\
-.ace-sense-dark .ace_indent-guide {\
-background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNQ11D6z7Bq1ar/ABCKBG6g04U2AAAAAElFTkSuQmCC) right repeat-y\
-}';
-
- const dom = require('ace/lib/dom');
- dom.importCssString(exports.cssText, exports.cssClass);
-});
diff --git a/src/plugins/console/public/application/models/sense_editor/__fixtures__/editor_input1.txt b/src/plugins/console/public/application/models/sense_editor/__fixtures__/editor_input1.txt
deleted file mode 100644
index 517f22bd8ad6a..0000000000000
--- a/src/plugins/console/public/application/models/sense_editor/__fixtures__/editor_input1.txt
+++ /dev/null
@@ -1,37 +0,0 @@
-GET _search
-{
- "query": { "match_all": {} }
-}
-
-#preceeding comment
-GET _stats?level=shards
-
-#in between comment
-
-PUT index_1/type1/1
-{
- "f": 1
-}
-
-PUT index_1/type1/2
-{
- "f": 2
-}
-
-# comment
-
-
-GET index_1/type1/1/_source?_source_include=f
-
-DELETE index_2
-
-
-POST /_sql?format=txt
-{
- "query": "SELECT prenom FROM claude_index WHERE prenom = 'claude' ",
- "fetch_size": 1
-}
-
-GET ,,/_search?pretty
-
-GET kbn:/api/spaces/space
\ No newline at end of file
diff --git a/src/plugins/console/public/application/models/sense_editor/create.ts b/src/plugins/console/public/application/models/sense_editor/create.ts
deleted file mode 100644
index 9c6c3e38471d5..0000000000000
--- a/src/plugins/console/public/application/models/sense_editor/create.ts
+++ /dev/null
@@ -1,22 +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", 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 { SenseEditor } from './sense_editor';
-import * as core from '../legacy_core_editor';
-
-export function create(element: HTMLElement) {
- const coreEditor = core.create(element);
- const senseEditor = new SenseEditor(coreEditor);
-
- /**
- * Init the editor
- */
- senseEditor.highlightCurrentRequestsAndUpdateActionBar();
- return senseEditor;
-}
diff --git a/src/plugins/console/public/application/models/sense_editor/curl.ts b/src/plugins/console/public/application/models/sense_editor/curl.ts
deleted file mode 100644
index 9080610a0e8c5..0000000000000
--- a/src/plugins/console/public/application/models/sense_editor/curl.ts
+++ /dev/null
@@ -1,194 +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", 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".
- */
-
-function detectCURLinLine(line: string) {
- // returns true if text matches a curl request
- return line.match(/^\s*?curl\s+(-X[A-Z]+)?\s*['"]?.*?['"]?(\s*$|\s+?-d\s*?['"])/);
-}
-
-export function detectCURL(text: string) {
- // returns true if text matches a curl request
- if (!text) return false;
- for (const line of text.split('\n')) {
- if (detectCURLinLine(line)) {
- return true;
- }
- }
- return false;
-}
-
-export function parseCURL(text: string) {
- let state = 'NONE';
- const out = [];
- let body: string[] = [];
- let line = '';
- const lines = text.trim().split('\n');
- let matches;
-
- const EmptyLine = /^\s*$/;
- const Comment = /^\s*(?:#|\/{2,})(.*)\n?$/;
- const ExecutionComment = /^\s*#!/;
- const ClosingSingleQuote = /^([^']*)'/;
- const ClosingDoubleQuote = /^((?:[^\\"]|\\.)*)"/;
- const EscapedQuotes = /^((?:[^\\"']|\\.)+)/;
-
- const LooksLikeCurl = /^\s*curl\s+/;
- const CurlVerb = /-X ?(GET|HEAD|POST|PUT|DELETE|PATCH)/;
-
- const HasProtocol = /[\s"']https?:\/\//;
- const CurlRequestWithProto = /[\s"']https?:\/\/[^\/ ]+\/+([^\s"']+)/;
- const CurlRequestWithoutProto = /[\s"'][^\/ ]+\/+([^\s"']+)/;
- const CurlData = /^.+\s(--data|-d)\s*/;
- const SenseLine = /^\s*(GET|HEAD|POST|PUT|DELETE|PATCH)\s+\/?(.+)/;
-
- if (lines.length > 0 && ExecutionComment.test(lines[0])) {
- lines.shift();
- }
-
- function nextLine() {
- if (line.length > 0) {
- return true;
- }
- if (lines.length === 0) {
- return false;
- }
- line = lines.shift()!.replace(/[\r\n]+/g, '\n') + '\n';
- return true;
- }
-
- function unescapeLastBodyEl() {
- const str = body.pop()!.replace(/\\([\\"'])/g, '$1');
- body.push(str);
- }
-
- // Is the next char a single or double quote?
- // If so remove it
- function detectQuote() {
- if (line.substr(0, 1) === "'") {
- line = line.substr(1);
- state = 'SINGLE_QUOTE';
- } else if (line.substr(0, 1) === '"') {
- line = line.substr(1);
- state = 'DOUBLE_QUOTE';
- } else {
- state = 'UNQUOTED';
- }
- }
-
- // Body is finished - append to output with final LF
- function addBodyToOut() {
- if (body.length > 0) {
- out.push(body.join(''));
- body = [];
- }
- state = 'LF';
- out.push('\n');
- }
-
- // If the pattern matches, then the state is about to change,
- // so add the capture to the body and detect the next state
- // Otherwise add the whole line
- function consumeMatching(pattern: string | RegExp) {
- const result = line.match(pattern);
- if (result) {
- body.push(result[1]);
- line = line.substr(result[0].length);
- detectQuote();
- } else {
- body.push(line);
- line = '';
- }
- }
-
- function parseCurlLine() {
- let verb = 'GET';
- let request = '';
- let result;
- if ((result = line.match(CurlVerb))) {
- verb = result[1];
- }
-
- // JS regexen don't support possessive quantifiers, so
- // we need two distinct patterns
- const pattern = HasProtocol.test(line) ? CurlRequestWithProto : CurlRequestWithoutProto;
-
- if ((result = line.match(pattern))) {
- request = result[1];
- }
-
- out.push(verb + ' /' + request + '\n');
-
- if ((result = line.match(CurlData))) {
- line = line.substr(result[0].length);
- detectQuote();
- if (EmptyLine.test(line)) {
- line = '';
- }
- } else {
- state = 'NONE';
- line = '';
- out.push('');
- }
- }
-
- while (nextLine()) {
- if (state === 'SINGLE_QUOTE') {
- consumeMatching(ClosingSingleQuote);
- } else if (state === 'DOUBLE_QUOTE') {
- consumeMatching(ClosingDoubleQuote);
- unescapeLastBodyEl();
- } else if (state === 'UNQUOTED') {
- consumeMatching(EscapedQuotes);
- if (body.length) {
- unescapeLastBodyEl();
- }
- if (state === 'UNQUOTED') {
- addBodyToOut();
- line = '';
- }
- }
-
- // the BODY state (used to match the body of a Sense request)
- // can be terminated early if it encounters
- // a comment or an empty line
- else if (state === 'BODY') {
- if (Comment.test(line) || EmptyLine.test(line)) {
- addBodyToOut();
- } else {
- body.push(line);
- line = '';
- }
- } else if (EmptyLine.test(line)) {
- if (state !== 'LF') {
- out.push('\n');
- state = 'LF';
- }
- line = '';
- } else if ((matches = line.match(Comment))) {
- out.push('#' + matches[1] + '\n');
- state = 'NONE';
- line = '';
- } else if (LooksLikeCurl.test(line)) {
- parseCurlLine();
- } else if ((matches = line.match(SenseLine))) {
- out.push(matches[1] + ' /' + matches[2] + '\n');
- line = '';
- state = 'BODY';
- }
-
- // Nothing else matches, so output with a prefix of ### for debugging purposes
- else {
- out.push('### ' + line);
- line = '';
- }
- }
-
- addBodyToOut();
- return out.join('').trim();
-}
diff --git a/src/plugins/console/public/application/models/sense_editor/index.ts b/src/plugins/console/public/application/models/sense_editor/index.ts
deleted file mode 100644
index 2bd44988dc02f..0000000000000
--- a/src/plugins/console/public/application/models/sense_editor/index.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", 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".
- */
-
-export * from './create';
-export * from '../legacy_core_editor/create_readonly';
-export { MODE } from '../../../lib/row_parser';
-export { SenseEditor } from './sense_editor';
-export { getEndpointFromPosition } from '../../../lib/autocomplete/get_endpoint_from_position';
diff --git a/src/plugins/console/public/application/models/sense_editor/integration.test.js b/src/plugins/console/public/application/models/sense_editor/integration.test.js
deleted file mode 100644
index bed83293e31d6..0000000000000
--- a/src/plugins/console/public/application/models/sense_editor/integration.test.js
+++ /dev/null
@@ -1,1279 +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", 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 './sense_editor.test.mocks';
-import { create } from './create';
-import _ from 'lodash';
-import $ from 'jquery';
-
-import * as kb from '../../../lib/kb/kb';
-import { AutocompleteInfo, setAutocompleteInfo } from '../../../services';
-import { httpServiceMock } from '@kbn/core-http-browser-mocks';
-import { StorageMock } from '../../../services/storage.mock';
-import { SettingsMock } from '../../../services/settings.mock';
-
-describe('Integration', () => {
- let senseEditor;
- let autocompleteInfo;
-
- beforeEach(() => {
- // Set up our document body
- document.body.innerHTML =
- '';
-
- senseEditor = create(document.querySelector('#ConAppEditor'));
- $(senseEditor.getCoreEditor().getContainer()).show();
- senseEditor.autocomplete._test.removeChangeListener();
- autocompleteInfo = new AutocompleteInfo();
-
- const httpMock = httpServiceMock.createSetupContract();
- const storage = new StorageMock({}, 'test');
- const settingsMock = new SettingsMock(storage);
-
- settingsMock.getAutocomplete.mockReturnValue({ fields: true });
-
- autocompleteInfo.mapping.setup(httpMock, settingsMock);
-
- setAutocompleteInfo(autocompleteInfo);
- });
- afterEach(() => {
- $(senseEditor.getCoreEditor().getContainer()).hide();
- senseEditor.autocomplete._test.addChangeListener();
- autocompleteInfo = null;
- setAutocompleteInfo(null);
- });
-
- function processContextTest(data, mapping, kbSchemes, requestLine, testToRun) {
- test(testToRun.name, function (done) {
- let lineOffset = 0; // add one for the extra method line
- let editorValue = data;
- if (requestLine != null) {
- if (data != null) {
- editorValue = requestLine + '\n' + data;
- lineOffset = 1;
- } else {
- editorValue = requestLine;
- }
- }
-
- testToRun.cursor.lineNumber += lineOffset;
-
- autocompleteInfo.clear();
- autocompleteInfo.mapping.loadMappings(mapping);
- const json = {};
- json[test.name] = kbSchemes || {};
- const testApi = kb._test.loadApisFromJson(json);
- if (kbSchemes) {
- // if (kbSchemes.globals) {
- // $.each(kbSchemes.globals, function (parent, rules) {
- // testApi.addGlobalAutocompleteRules(parent, rules);
- // });
- // }
- if (kbSchemes.endpoints) {
- $.each(kbSchemes.endpoints, function (endpoint, scheme) {
- testApi.addEndpointDescription(endpoint, scheme);
- });
- }
- }
- kb._test.setActiveApi(testApi);
- const { cursor } = testToRun;
- senseEditor.update(editorValue, true).then(() => {
- senseEditor.getCoreEditor().moveCursorToPosition(cursor);
- // allow ace rendering to move cursor so it will be seen during test - handy for debugging.
- //setTimeout(function () {
- senseEditor.completer = {
- base: {},
- changeListener: function () {},
- }; // mimic auto complete
-
- senseEditor.autocomplete._test.getCompletions(
- senseEditor,
- null,
- cursor,
- '',
- function (err, terms) {
- if (testToRun.assertThrows) {
- done();
- return;
- }
-
- if (err) {
- throw err;
- }
-
- if (testToRun.no_context) {
- expect(!terms || terms.length === 0).toBeTruthy();
- } else {
- expect(terms).not.toBeNull();
- expect(terms.length).toBeGreaterThan(0);
- }
-
- if (!terms || terms.length === 0) {
- done();
- return;
- }
-
- if (testToRun.autoCompleteSet) {
- const expectedTerms = _.map(testToRun.autoCompleteSet, function (t) {
- if (typeof t !== 'object') {
- t = { name: t };
- }
- return t;
- });
- if (terms.length !== expectedTerms.length) {
- expect(_.map(terms, 'name')).toEqual(_.map(expectedTerms, 'name'));
- } else {
- const filteredActualTerms = _.map(terms, function (actualTerm, i) {
- const expectedTerm = expectedTerms[i];
- const filteredTerm = {};
- _.each(expectedTerm, function (v, p) {
- filteredTerm[p] = actualTerm[p];
- });
- return filteredTerm;
- });
- expect(filteredActualTerms).toEqual(expectedTerms);
- }
- }
-
- const context = terms[0].context;
- const {
- cursor: { lineNumber, column },
- } = testToRun;
- senseEditor.autocomplete._test.addReplacementInfoToContext(
- context,
- { lineNumber, column },
- terms[0].value
- );
-
- function ac(prop, propTest) {
- if (typeof testToRun[prop] !== 'undefined') {
- if (propTest) {
- propTest(context[prop], testToRun[prop], prop);
- } else {
- expect(context[prop]).toEqual(testToRun[prop]);
- }
- }
- }
-
- function posCompare(actual, expected) {
- expect(actual.lineNumber).toEqual(expected.lineNumber + lineOffset);
- expect(actual.column).toEqual(expected.column);
- }
-
- function rangeCompare(actual, expected, name) {
- posCompare(actual.start, expected.start, name + '.start');
- posCompare(actual.end, expected.end, name + '.end');
- }
-
- ac('prefixToAdd');
- ac('suffixToAdd');
- ac('addTemplate');
- ac('textBoxPosition', posCompare);
- ac('rangeToReplace', rangeCompare);
- done();
- },
- { setAnnotation: () => {}, removeAnnotation: () => {} }
- );
- });
- });
- }
-
- function contextTests(data, mapping, kbSchemes, requestLine, tests) {
- if (data != null && typeof data !== 'string') {
- data = JSON.stringify(data, null, 3);
- }
- for (let t = 0; t < tests.length; t++) {
- processContextTest(data, mapping, kbSchemes, requestLine, tests[t]);
- }
- }
-
- const SEARCH_KB = {
- endpoints: {
- _search: {
- methods: ['GET', 'POST'],
- patterns: ['{index}/_search', '_search'],
- data_autocomplete_rules: {
- query: {
- match_all: {},
- term: { '{field}': { __template: { f: 1 } } },
- },
- size: {},
- facets: {
- __template: {
- FIELD: {},
- },
- '*': { terms: { field: '{field}' } },
- },
- },
- },
- },
- };
-
- const MAPPING = {
- index1: {
- properties: {
- 'field1.1.1': { type: 'string' },
- 'field1.1.2': { type: 'string' },
- },
- },
- index2: {
- properties: {
- 'field2.1.1': { type: 'string' },
- 'field2.1.2': { type: 'string' },
- },
- },
- };
-
- contextTests({}, MAPPING, SEARCH_KB, 'POST _search', [
- {
- name: 'Empty doc',
- cursor: { lineNumber: 1, column: 2 },
- initialValue: '',
- addTemplate: true,
- prefixToAdd: '',
- suffixToAdd: '',
- rangeToReplace: {
- start: { lineNumber: 1, column: 2 },
- end: { lineNumber: 1, column: 2 },
- },
- autoCompleteSet: ['facets', 'query', 'size'],
- },
- ]);
-
- contextTests({}, MAPPING, SEARCH_KB, 'POST _no_context', [
- {
- name: 'Missing KB',
- cursor: { lineNumber: 1, column: 2 },
- no_context: true,
- },
- ]);
-
- contextTests(
- {
- query: {
- f: 1,
- },
- },
- MAPPING,
- {
- globals: {
- query: {
- t1: 2,
- },
- },
- endpoints: {},
- },
- 'POST _no_context',
- [
- {
- name: 'Missing KB - global auto complete',
- cursor: { lineNumber: 3, column: 6 },
- autoCompleteSet: ['t1'],
- },
- ]
- );
-
- contextTests(
- {
- query: {
- field: 'something',
- },
- facets: {},
- size: 20,
- },
- MAPPING,
- SEARCH_KB,
- 'POST _search',
- [
- {
- name: 'existing dictionary key, no template',
- cursor: { lineNumber: 2, column: 6 },
- initialValue: 'query',
- addTemplate: false,
- prefixToAdd: '',
- suffixToAdd: '',
- rangeToReplace: {
- start: { lineNumber: 2, column: 4 },
- end: { lineNumber: 2, column: 11 },
- },
- autoCompleteSet: ['facets', 'query', 'size'],
- },
- {
- name: 'existing inner dictionary key',
- cursor: { lineNumber: 3, column: 8 },
- initialValue: 'field',
- addTemplate: false,
- prefixToAdd: '',
- suffixToAdd: '',
- rangeToReplace: {
- start: { lineNumber: 3, column: 7 },
- end: { lineNumber: 3, column: 14 },
- },
- autoCompleteSet: ['match_all', 'term'],
- },
- {
- name: 'existing dictionary key, yes template',
- cursor: { lineNumber: 5, column: 8 },
- initialValue: 'facets',
- addTemplate: true,
- prefixToAdd: '',
- suffixToAdd: '',
- rangeToReplace: {
- start: { lineNumber: 5, column: 4 },
- end: { lineNumber: 5, column: 16 },
- },
- autoCompleteSet: ['facets', 'query', 'size'],
- },
- {
- name: 'ignoring meta keys',
- cursor: { lineNumber: 5, column: 15 },
- no_context: true,
- },
- ]
- );
-
- contextTests(
- '{\n' +
- ' "query": {\n' +
- ' "field": "something"\n' +
- ' },\n' +
- ' "facets": {},\n' +
- ' "size": 20\n' +
- '}',
- MAPPING,
- SEARCH_KB,
- 'POST _search',
- [
- {
- name: 'trailing comma, end of line',
- cursor: { lineNumber: 5, column: 17 },
- initialValue: '',
- addTemplate: true,
- prefixToAdd: '',
- suffixToAdd: ', ',
- rangeToReplace: {
- start: { lineNumber: 5, column: 17 },
- end: { lineNumber: 5, column: 17 },
- },
- autoCompleteSet: ['facets', 'query', 'size'],
- },
- {
- name: 'trailing comma, beginning of line',
- cursor: { lineNumber: 6, column: 2 },
- initialValue: '',
- addTemplate: true,
- prefixToAdd: '',
- suffixToAdd: ', ',
- rangeToReplace: {
- start: { lineNumber: 6, column: 2 },
- end: { lineNumber: 6, column: 2 },
- },
- autoCompleteSet: ['facets', 'query', 'size'],
- },
- {
- name: 'prefix comma, end of line',
- cursor: { lineNumber: 7, column: 1 },
- initialValue: '',
- addTemplate: true,
- prefixToAdd: ',\n',
- suffixToAdd: '',
- rangeToReplace: {
- start: { lineNumber: 6, column: 14 },
- end: { lineNumber: 7, column: 1 },
- },
- autoCompleteSet: ['facets', 'query', 'size'],
- },
- ]
- );
-
- contextTests(
- {
- object: 1,
- array: 1,
- value_one_of: 1,
- value: 2,
- something_else: 5,
- },
- MAPPING,
- {
- endpoints: {
- _test: {
- patterns: ['_test'],
- data_autocomplete_rules: {
- object: { bla: 1 },
- array: [1],
- value_one_of: { __one_of: [1, 2] },
- value: 3,
- '*': { __one_of: [4, 5] },
- },
- },
- },
- },
- 'GET _test',
- [
- {
- name: 'not matching object when { is not opened',
- cursor: { lineNumber: 2, column: 13 },
- initialValue: '',
- autoCompleteSet: ['{'],
- },
- {
- name: 'not matching array when [ is not opened',
- cursor: { lineNumber: 3, column: 13 },
- initialValue: '',
- autoCompleteSet: ['['],
- },
- {
- name: 'matching value with one_of',
- cursor: { lineNumber: 4, column: 20 },
- initialValue: '',
- autoCompleteSet: [1, 2],
- },
- {
- name: 'matching value',
- cursor: { lineNumber: 5, column: 13 },
- initialValue: '',
- autoCompleteSet: [3],
- },
- {
- name: 'matching any value with one_of',
- cursor: { lineNumber: 6, column: 22 },
- initialValue: '',
- autoCompleteSet: [4, 5],
- },
- ]
- );
-
- contextTests(
- {
- query: {
- field: 'something',
- },
- facets: {
- name: {},
- },
- size: 20,
- },
- MAPPING,
- SEARCH_KB,
- 'GET _search',
- [
- {
- name: '* matching everything',
- cursor: { lineNumber: 6, column: 16 },
- initialValue: '',
- addTemplate: true,
- prefixToAdd: '',
- suffixToAdd: '',
- rangeToReplace: {
- start: { lineNumber: 6, column: 16 },
- end: { lineNumber: 6, column: 16 },
- },
- autoCompleteSet: [{ name: 'terms', meta: 'API' }],
- },
- ]
- );
-
- contextTests(
- {
- index: '123',
- },
- MAPPING,
- {
- endpoints: {
- _test: {
- patterns: ['_test'],
- data_autocomplete_rules: {
- index: '{index}',
- },
- },
- },
- },
- 'GET _test',
- [
- {
- name: '{index} matching',
- cursor: { lineNumber: 2, column: 16 },
- autoCompleteSet: [
- { name: 'index1', meta: 'index' },
- { name: 'index2', meta: 'index' },
- ],
- },
- ]
- );
-
- function tt(term, template, meta) {
- term = { name: term, template: template };
- if (meta) {
- term.meta = meta;
- }
- return term;
- }
-
- contextTests(
- {
- array: ['a'],
- oneof: '1',
- },
- MAPPING,
- {
- endpoints: {
- _endpoint: {
- patterns: ['_endpoint'],
- data_autocomplete_rules: {
- array: ['a', 'b'],
- number: 1,
- object: {},
- fixed: { __template: { a: 1 } },
- oneof: { __one_of: ['o1', 'o2'] },
- },
- },
- },
- },
- 'GET _endpoint',
- [
- {
- name: 'Templates 1',
- cursor: { lineNumber: 2, column: 1 },
- autoCompleteSet: [
- tt('array', []),
- tt('fixed', { a: 1 }),
- tt('number', 1),
- tt('object', {}),
- tt('oneof', 'o1'),
- ],
- },
- {
- name: 'Templates - one off',
- cursor: { lineNumber: 5, column: 13 },
- autoCompleteSet: [tt('o1'), tt('o2')],
- },
- ]
- );
-
- contextTests(
- {
- string: 'value',
- context: {},
- },
- MAPPING,
- {
- endpoints: {
- _endpoint: {
- patterns: ['_endpoint'],
- data_autocomplete_rules: {
- context: {
- __one_of: [
- {
- __condition: {
- lines_regex: 'value',
- },
- match: {},
- },
- {
- __condition: {
- lines_regex: 'other',
- },
- no_match: {},
- },
- { always: {} },
- ],
- },
- },
- },
- },
- },
- 'GET _endpoint',
- [
- {
- name: 'Conditionals',
- cursor: { lineNumber: 3, column: 16 },
- autoCompleteSet: [tt('always', {}), tt('match', {})],
- },
- ]
- );
-
- contextTests(
- {
- any_of_numbers: [1],
- any_of_obj: [
- {
- a: 1,
- },
- ],
- any_of_mixed: [
- {
- a: 1,
- },
- 2,
- ],
- },
- MAPPING,
- {
- endpoints: {
- _endpoint: {
- patterns: ['_endpoint'],
- data_autocomplete_rules: {
- any_of_numbers: { __template: [1, 2], __any_of: [1, 2, 3] },
- any_of_obj: {
- __template: [{ c: 1 }],
- __any_of: [{ a: 1, b: 2 }, { c: 1 }],
- },
- any_of_mixed: {
- __any_of: [{ a: 1 }, 3],
- },
- },
- },
- },
- },
- 'GET _endpoint',
- [
- {
- name: 'Any of - templates',
- cursor: { lineNumber: 2, column: 1 },
- autoCompleteSet: [
- tt('any_of_mixed', []),
- tt('any_of_numbers', [1, 2]),
- tt('any_of_obj', [{ c: 1 }]),
- ],
- },
- {
- name: 'Any of - numbers',
- cursor: { lineNumber: 3, column: 3 },
- autoCompleteSet: [1, 2, 3],
- },
- {
- name: 'Any of - object',
- cursor: { lineNumber: 7, column: 3 },
- autoCompleteSet: [tt('a', 1), tt('b', 2), tt('c', 1)],
- },
- {
- name: 'Any of - mixed - obj',
- cursor: { lineNumber: 12, column: 3 },
- autoCompleteSet: [tt('a', 1)],
- },
- {
- name: 'Any of - mixed - both',
- cursor: { lineNumber: 14, column: 3 },
- autoCompleteSet: [tt(3), tt('{')],
- },
- ]
- );
-
- contextTests(
- {},
- MAPPING,
- {
- endpoints: {
- _endpoint: {
- patterns: ['_endpoint'],
- data_autocomplete_rules: {
- query: '',
- },
- },
- },
- },
- 'GET _endpoint',
- [
- {
- name: 'Empty string as default',
- cursor: { lineNumber: 1, column: 2 },
- autoCompleteSet: [tt('query', '')],
- },
- ]
- );
-
- // NOTE: This test emits "error while getting completion terms Error: failed to resolve link
- // [GLOBAL.broken]: Error: failed to resolve global components for ['broken']". but that's
- // expected.
- contextTests(
- {
- a: {
- b: {},
- c: {},
- d: {
- t1a: {},
- },
- e: {},
- f: [{}],
- g: {},
- h: {},
- },
- },
- MAPPING,
- {
- globals: {
- gtarget: {
- t1: 2,
- t1a: {
- __scope_link: '.',
- },
- },
- },
- endpoints: {
- _current: {
- patterns: ['_current'],
- data_autocomplete_rules: {
- a: {
- b: {
- __scope_link: '.a',
- },
- c: {
- __scope_link: 'ext.target',
- },
- d: {
- __scope_link: 'GLOBAL.gtarget',
- },
- e: {
- __scope_link: 'ext',
- },
- f: [
- {
- __scope_link: 'ext.target',
- },
- ],
- g: {
- __scope_link: function () {
- return {
- a: 1,
- b: 2,
- };
- },
- },
- h: {
- __scope_link: 'GLOBAL.broken',
- },
- },
- },
- },
- ext: {
- patterns: ['ext'],
- data_autocomplete_rules: {
- target: {
- t2: 1,
- },
- },
- },
- },
- },
- 'GET _current',
- [
- {
- name: 'Relative scope link test',
- cursor: { lineNumber: 3, column: 13 },
- autoCompleteSet: [
- tt('b', {}),
- tt('c', {}),
- tt('d', {}),
- tt('e', {}),
- tt('f', [{}]),
- tt('g', {}),
- tt('h', {}),
- ],
- },
- {
- name: 'External scope link test',
- cursor: { lineNumber: 4, column: 13 },
- autoCompleteSet: [tt('t2', 1)],
- },
- {
- name: 'Global scope link test',
- cursor: { lineNumber: 5, column: 13 },
- autoCompleteSet: [tt('t1', 2), tt('t1a', {})],
- },
- {
- name: 'Global scope link with an internal scope link',
- cursor: { lineNumber: 6, column: 18 },
- autoCompleteSet: [tt('t1', 2), tt('t1a', {})],
- },
- {
- name: 'Entire endpoint scope link test',
- cursor: { lineNumber: 8, column: 13 },
- autoCompleteSet: [tt('target', {})],
- },
- {
- name: 'A scope link within an array',
- cursor: { lineNumber: 10, column: 11 },
- autoCompleteSet: [tt('t2', 1)],
- },
- {
- name: 'A function based scope link',
- cursor: { lineNumber: 12, column: 13 },
- autoCompleteSet: [tt('a', 1), tt('b', 2)],
- },
- {
- name: 'A global scope link with wrong link',
- cursor: { lineNumber: 13, column: 13 },
- assertThrows: /broken/,
- },
- ]
- );
-
- contextTests(
- {},
- MAPPING,
- {
- globals: {
- gtarget: {
- t1: 2,
- },
- },
- endpoints: {
- _current: {
- patterns: ['_current'],
- id: 'GET _current',
- data_autocomplete_rules: {
- __scope_link: 'GLOBAL.gtarget',
- },
- },
- },
- },
- 'GET _current',
- [
- {
- name: 'Top level scope link',
- cursor: { lineNumber: 1, column: 2 },
- autoCompleteSet: [tt('t1', 2)],
- },
- ]
- );
-
- contextTests(
- {
- a: {},
- },
- MAPPING,
- {
- endpoints: {
- _endpoint: {
- patterns: ['_endpoint'],
- data_autocomplete_rules: {
- a: {},
- b: {},
- },
- },
- },
- },
- 'GET _endpoint',
- [
- {
- name: 'Path after empty object',
- cursor: { lineNumber: 2, column: 11 },
- autoCompleteSet: ['a', 'b'],
- },
- ]
- );
-
- contextTests(
- {
- '': {},
- },
- MAPPING,
- SEARCH_KB,
- 'POST _search',
- [
- {
- name: 'Replace an empty string',
- cursor: { lineNumber: 2, column: 5 },
- rangeToReplace: {
- start: { lineNumber: 2, column: 4 },
- end: { lineNumber: 2, column: 10 },
- },
- },
- ]
- );
-
- contextTests(
- {
- a: [
- {
- c: {},
- },
- ],
- },
- MAPPING,
- {
- endpoints: {
- _endpoint: {
- patterns: ['_endpoint'],
- data_autocomplete_rules: {
- a: [{ b: 1 }],
- },
- },
- },
- },
- 'GET _endpoint',
- [
- {
- name: 'List of objects - internal autocomplete',
- cursor: { lineNumber: 4, column: 11 },
- autoCompleteSet: ['b'],
- },
- {
- name: 'List of objects - external template',
- cursor: { lineNumber: 1, column: 2 },
- autoCompleteSet: [tt('a', [{}])],
- },
- ]
- );
-
- contextTests(
- {
- query: {
- term: {
- field: 'something',
- },
- },
- facets: {
- test: {
- terms: {
- field: 'test',
- },
- },
- },
- size: 20,
- },
- MAPPING,
- SEARCH_KB,
- 'POST index1/_search',
- [
- {
- name: 'Field completion as scope',
- cursor: { lineNumber: 4, column: 11 },
- autoCompleteSet: [
- tt('field1.1.1', { f: 1 }, 'string'),
- tt('field1.1.2', { f: 1 }, 'string'),
- ],
- },
- {
- name: 'Field completion as value',
- cursor: { lineNumber: 10, column: 24 },
- autoCompleteSet: [
- { name: 'field1.1.1', meta: 'string' },
- { name: 'field1.1.2', meta: 'string' },
- ],
- },
- ]
- );
-
- // NOTE: This test emits "Can't extract a valid url token path", but that's expected.
- contextTests('POST _search\n', MAPPING, SEARCH_KB, null, [
- {
- name: 'initial doc start',
- cursor: { lineNumber: 2, column: 1 },
- autoCompleteSet: ['{'],
- prefixToAdd: '',
- suffixToAdd: '',
- },
- ]);
-
- contextTests(
- '{\n' + ' "query": {} \n' + '}\n' + '\n' + '\n',
- MAPPING,
- SEARCH_KB,
- 'POST _search',
- [
- {
- name: 'Cursor rows after request end',
- cursor: { lineNumber: 5, column: 1 },
- autoCompleteSet: ['GET', 'PUT', 'POST', 'DELETE', 'HEAD', 'PATCH'],
- prefixToAdd: '',
- suffixToAdd: ' ',
- },
- {
- name: 'Cursor just after request end',
- cursor: { lineNumber: 3, column: 2 },
- no_context: true,
- },
- ]
- );
-
- const CLUSTER_KB = {
- endpoints: {
- _search: {
- patterns: ['_search', '{index}/_search'],
- url_params: {
- search_type: ['count', 'query_then_fetch'],
- scroll: '10m',
- },
- methods: ['GET'],
- data_autocomplete_rules: {},
- },
- '_cluster/stats': {
- patterns: ['_cluster/stats'],
- indices_mode: 'none',
- data_autocomplete_rules: {},
- methods: ['GET'],
- },
- '_cluster/nodes/stats': {
- patterns: ['_cluster/nodes/stats'],
- data_autocomplete_rules: {},
- methods: ['GET'],
- },
- },
- };
-
- contextTests(null, MAPPING, CLUSTER_KB, 'GET _cluster', [
- {
- name: 'Endpoints with slashes - no slash',
- cursor: { lineNumber: 1, column: 9 },
- autoCompleteSet: ['_cluster/nodes/stats', '_cluster/stats', '_search', 'index1', 'index2'],
- prefixToAdd: '',
- suffixToAdd: '',
- },
- ]);
-
- contextTests(null, MAPPING, CLUSTER_KB, 'GET _cluster/', [
- {
- name: 'Endpoints with slashes - before slash',
- cursor: { lineNumber: 1, column: 8 },
- autoCompleteSet: ['_cluster/nodes/stats', '_cluster/stats', '_search', 'index1', 'index2'],
- prefixToAdd: '',
- suffixToAdd: '',
- },
- {
- name: 'Endpoints with slashes - on slash',
- cursor: { lineNumber: 1, column: 13 },
- autoCompleteSet: ['_cluster/nodes/stats', '_cluster/stats', '_search', 'index1', 'index2'],
- prefixToAdd: '',
- suffixToAdd: '',
- },
- {
- name: 'Endpoints with slashes - after slash',
- cursor: { lineNumber: 1, column: 14 },
- autoCompleteSet: ['nodes/stats', 'stats'],
- prefixToAdd: '',
- suffixToAdd: '',
- },
- ]);
-
- contextTests(null, MAPPING, CLUSTER_KB, 'GET _cluster/no', [
- {
- name: 'Endpoints with slashes - after slash',
- cursor: { lineNumber: 1, column: 15 },
- autoCompleteSet: [
- { name: 'nodes/stats', meta: 'endpoint' },
- { name: 'stats', meta: 'endpoint' },
- ],
- prefixToAdd: '',
- suffixToAdd: '',
- initialValue: 'no',
- },
- ]);
-
- contextTests(null, MAPPING, CLUSTER_KB, 'GET _cluster/nodes/st', [
- {
- name: 'Endpoints with two slashes',
- cursor: { lineNumber: 1, column: 21 },
- autoCompleteSet: ['stats'],
- prefixToAdd: '',
- suffixToAdd: '',
- initialValue: 'st',
- },
- ]);
-
- contextTests(null, MAPPING, CLUSTER_KB, 'GET ', [
- {
- name: 'Immediately after space + method',
- cursor: { lineNumber: 1, column: 5 },
- autoCompleteSet: [
- { name: '_cluster/nodes/stats', meta: 'endpoint' },
- { name: '_cluster/stats', meta: 'endpoint' },
- { name: '_search', meta: 'endpoint' },
- { name: 'index1', meta: 'index' },
- { name: 'index2', meta: 'index' },
- ],
- prefixToAdd: '',
- suffixToAdd: '',
- initialValue: '',
- },
- ]);
-
- contextTests(null, MAPPING, CLUSTER_KB, 'GET cl', [
- {
- name: 'Endpoints by subpart GET',
- cursor: { lineNumber: 1, column: 7 },
- autoCompleteSet: [
- { name: '_cluster/nodes/stats', meta: 'endpoint' },
- { name: '_cluster/stats', meta: 'endpoint' },
- { name: '_search', meta: 'endpoint' },
- { name: 'index1', meta: 'index' },
- { name: 'index2', meta: 'index' },
- ],
- prefixToAdd: '',
- suffixToAdd: '',
- initialValue: 'cl',
- method: 'GET',
- },
- ]);
-
- contextTests(null, MAPPING, CLUSTER_KB, 'POST cl', [
- {
- name: 'Endpoints by subpart POST',
- cursor: { lineNumber: 1, column: 8 },
- no_context: true,
- prefixToAdd: '',
- suffixToAdd: '',
- initialValue: 'cl',
- },
- ]);
-
- contextTests(null, MAPPING, CLUSTER_KB, 'GET _search?', [
- {
- name: 'Params just after ?',
- cursor: { lineNumber: 1, column: 13 },
- autoCompleteSet: [
- { name: 'filter_path', meta: 'param', insertValue: 'filter_path=' },
- { name: 'format', meta: 'param', insertValue: 'format=' },
- { name: 'pretty', meta: 'flag' },
- { name: 'scroll', meta: 'param', insertValue: 'scroll=' },
- { name: 'search_type', meta: 'param', insertValue: 'search_type=' },
- ],
- prefixToAdd: '',
- suffixToAdd: '',
- },
- ]);
-
- contextTests(null, MAPPING, CLUSTER_KB, 'GET _search?format=', [
- {
- name: 'Params values',
- cursor: { lineNumber: 1, column: 20 },
- autoCompleteSet: [
- { name: 'json', meta: 'format' },
- { name: 'yaml', meta: 'format' },
- ],
- prefixToAdd: '',
- suffixToAdd: '',
- },
- ]);
-
- contextTests(null, MAPPING, CLUSTER_KB, 'GET _search?format=yaml&', [
- {
- name: 'Params after amp',
- cursor: { lineNumber: 1, column: 25 },
- autoCompleteSet: [
- { name: 'filter_path', meta: 'param', insertValue: 'filter_path=' },
- { name: 'format', meta: 'param', insertValue: 'format=' },
- { name: 'pretty', meta: 'flag' },
- { name: 'scroll', meta: 'param', insertValue: 'scroll=' },
- { name: 'search_type', meta: 'param', insertValue: 'search_type=' },
- ],
- prefixToAdd: '',
- suffixToAdd: '',
- },
- ]);
-
- contextTests(null, MAPPING, CLUSTER_KB, 'GET _search?format=yaml&search', [
- {
- name: 'Params on existing param',
- cursor: { lineNumber: 1, column: 27 },
- rangeToReplace: {
- start: { lineNumber: 1, column: 25 },
- end: { lineNumber: 1, column: 31 },
- },
- autoCompleteSet: [
- { name: 'filter_path', meta: 'param', insertValue: 'filter_path=' },
- { name: 'format', meta: 'param', insertValue: 'format=' },
- { name: 'pretty', meta: 'flag' },
- { name: 'scroll', meta: 'param', insertValue: 'scroll=' },
- { name: 'search_type', meta: 'param', insertValue: 'search_type=' },
- ],
- prefixToAdd: '',
- suffixToAdd: '',
- },
- ]);
-
- contextTests(null, MAPPING, CLUSTER_KB, 'GET _search?format=yaml&search_type=cou', [
- {
- name: 'Params on existing value',
- cursor: { lineNumber: 1, column: 38 },
- rangeToReplace: {
- start: { lineNumber: 1, column: 37 },
- end: { lineNumber: 1, column: 40 },
- },
- autoCompleteSet: [
- { name: 'count', meta: 'search_type' },
- { name: 'query_then_fetch', meta: 'search_type' },
- ],
- prefixToAdd: '',
- suffixToAdd: '',
- },
- ]);
-
- contextTests(null, MAPPING, CLUSTER_KB, 'GET _search?format=yaml&search_type=cou', [
- {
- name: 'Params on just after = with existing value',
- cursor: { lineNumber: 1, column: 37 },
- rangeToReplace: {
- start: { lineNumber: 1, column: 37 },
- end: { lineNumber: 1, column: 37 },
- },
- autoCompleteSet: [
- { name: 'count', meta: 'search_type' },
- { name: 'query_then_fetch', meta: 'search_type' },
- ],
- prefixToAdd: '',
- suffixToAdd: '',
- },
- ]);
-
- contextTests(
- {
- query: {
- field: 'something',
- },
- facets: {},
- size: 20,
- },
- MAPPING,
- SEARCH_KB,
- 'POST http://somehost/_search',
- [
- {
- name: 'fullurl - existing dictionary key, no template',
- cursor: { lineNumber: 2, column: 7 },
- initialValue: 'query',
- addTemplate: false,
- prefixToAdd: '',
- suffixToAdd: '',
- rangeToReplace: {
- start: { lineNumber: 2, column: 4 },
- end: { lineNumber: 2, column: 11 },
- },
- autoCompleteSet: ['facets', 'query', 'size'],
- },
- {
- name: 'fullurl - existing inner dictionary key',
- cursor: { lineNumber: 3, column: 8 },
- initialValue: 'field',
- addTemplate: false,
- prefixToAdd: '',
- suffixToAdd: '',
- rangeToReplace: {
- start: { lineNumber: 3, column: 7 },
- end: { lineNumber: 3, column: 14 },
- },
- autoCompleteSet: ['match_all', 'term'],
- },
- {
- name: 'fullurl - existing dictionary key, yes template',
- cursor: { lineNumber: 5, column: 8 },
- initialValue: 'facets',
- addTemplate: true,
- prefixToAdd: '',
- suffixToAdd: '',
- rangeToReplace: {
- start: { lineNumber: 5, column: 4 },
- end: { lineNumber: 5, column: 16 },
- },
- autoCompleteSet: ['facets', 'query', 'size'],
- },
- ]
- );
-});
diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js
deleted file mode 100644
index 19d782f1b8e87..0000000000000
--- a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js
+++ /dev/null
@@ -1,641 +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", 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 './sense_editor.test.mocks';
-
-import $ from 'jquery';
-import _ from 'lodash';
-import { URL } from 'url';
-
-import { create } from './create';
-import { XJson } from '@kbn/es-ui-shared-plugin/public';
-import editorInput1 from './__fixtures__/editor_input1.txt';
-import { setStorage, createStorage } from '../../../services';
-
-const { collapseLiteralStrings } = XJson;
-
-describe('Editor', () => {
- let input;
- let oldUrl;
- let olldWindow;
- let storage;
-
- beforeEach(function () {
- // Set up our document body
- document.body.innerHTML = ``;
-
- input = create(document.querySelector('#ConAppEditor'));
- $(input.getCoreEditor().getContainer()).show();
- input.autocomplete._test.removeChangeListener();
- oldUrl = global.URL;
- olldWindow = { ...global.window };
- global.URL = URL;
- Object.defineProperty(global, 'window', {
- value: Object.create(window),
- writable: true,
- });
- Object.defineProperty(window, 'location', {
- value: {
- origin: 'http://localhost:5620',
- },
- });
- storage = createStorage({
- engine: global.window.localStorage,
- prefix: 'console_test',
- });
- setStorage(storage);
- });
- afterEach(function () {
- global.URL = oldUrl;
- global.window = olldWindow;
- $(input.getCoreEditor().getContainer()).hide();
- input.autocomplete._test.addChangeListener();
- setStorage(null);
- });
-
- let testCount = 0;
-
- const callWithEditorMethod = (editorMethod, fn) => async (done) => {
- const results = await input[editorMethod]();
- fn(results, done);
- };
-
- function utilsTest(name, prefix, data, testToRun) {
- const id = testCount++;
- if (typeof data === 'function') {
- testToRun = data;
- data = null;
- }
- if (data && typeof data !== 'string') {
- data = JSON.stringify(data, null, 3);
- }
- if (data) {
- if (prefix) {
- data = prefix + '\n' + data;
- }
- } else {
- data = prefix;
- }
-
- test('Utils test ' + id + ' : ' + name, function (done) {
- input.update(data, true).then(() => {
- testToRun(done);
- });
- });
- }
-
- function compareRequest(requests, expected) {
- if (!Array.isArray(requests)) {
- requests = [requests];
- expected = [expected];
- }
-
- _.each(requests, function (r) {
- delete r.range;
- });
- expect(requests).toEqual(expected);
- }
-
- const simpleRequest = {
- prefix: 'POST _search',
- data: ['{', ' "query": { "match_all": {} }', '}'].join('\n'),
- };
-
- const singleLineRequest = {
- prefix: 'POST _search',
- data: '{ "query": { "match_all": {} } }',
- };
-
- const getRequestNoData = {
- prefix: 'GET _stats',
- };
-
- const multiDocRequest = {
- prefix: 'POST _bulk',
- data_as_array: ['{ "index": { "_index": "index", "_type":"type" } }', '{ "field": 1 }'],
- };
- multiDocRequest.data = multiDocRequest.data_as_array.join('\n');
-
- utilsTest(
- 'simple request range',
- simpleRequest.prefix,
- simpleRequest.data,
- callWithEditorMethod('getRequestRange', (range, done) => {
- compareRequest(range, {
- start: { lineNumber: 1, column: 1 },
- end: { lineNumber: 4, column: 2 },
- });
- done();
- })
- );
-
- utilsTest(
- 'simple request data',
- simpleRequest.prefix,
- simpleRequest.data,
- callWithEditorMethod('getRequest', (request, done) => {
- const expected = {
- method: 'POST',
- url: '_search',
- data: [simpleRequest.data],
- };
- compareRequest(request, expected);
- done();
- })
- );
-
- utilsTest(
- 'simple request range, prefixed with spaces',
- ' ' + simpleRequest.prefix,
- simpleRequest.data,
- callWithEditorMethod('getRequestRange', (range, done) => {
- expect(range).toEqual({
- start: { lineNumber: 1, column: 1 },
- end: { lineNumber: 4, column: 2 },
- });
- done();
- })
- );
-
- utilsTest(
- 'simple request data, prefixed with spaces',
- ' ' + simpleRequest.prefix,
- simpleRequest.data,
- callWithEditorMethod('getRequest', (request, done) => {
- const expected = {
- method: 'POST',
- url: '_search',
- data: [simpleRequest.data],
- };
-
- compareRequest(request, expected);
- done();
- })
- );
-
- utilsTest(
- 'simple request range, suffixed with spaces',
- simpleRequest.prefix + ' ',
- simpleRequest.data + ' ',
- callWithEditorMethod('getRequestRange', (range, done) => {
- compareRequest(range, {
- start: { lineNumber: 1, column: 1 },
- end: { lineNumber: 4, column: 2 },
- });
- done();
- })
- );
-
- utilsTest(
- 'simple request data, suffixed with spaces',
- simpleRequest.prefix + ' ',
- simpleRequest.data + ' ',
- callWithEditorMethod('getRequest', (request, done) => {
- const expected = {
- method: 'POST',
- url: '_search',
- data: [simpleRequest.data],
- };
-
- compareRequest(request, expected);
- done();
- })
- );
-
- utilsTest(
- 'single line request range',
- singleLineRequest.prefix,
- singleLineRequest.data,
- callWithEditorMethod('getRequestRange', (range, done) => {
- compareRequest(range, {
- start: { lineNumber: 1, column: 1 },
- end: { lineNumber: 2, column: 33 },
- });
- done();
- })
- );
-
- utilsTest(
- 'full url: single line request data',
- 'POST https://somehost/_search',
- singleLineRequest.data,
- callWithEditorMethod('getRequest', (request, done) => {
- const expected = {
- method: 'POST',
- url: 'https://somehost/_search',
- data: [singleLineRequest.data],
- };
- compareRequest(request, expected);
- done();
- })
- );
-
- utilsTest(
- 'request with no data followed by a new line',
- getRequestNoData.prefix,
- '\n',
- callWithEditorMethod('getRequestRange', (range, done) => {
- compareRequest(range, {
- start: { lineNumber: 1, column: 1 },
- end: { lineNumber: 1, column: 11 },
- });
- done();
- })
- );
-
- utilsTest(
- 'request with no data followed by a new line (data)',
- getRequestNoData.prefix,
- '\n',
- callWithEditorMethod('getRequest', (request, done) => {
- const expected = {
- method: 'GET',
- url: '_stats',
- data: [],
- };
- compareRequest(request, expected);
- done();
- })
- );
-
- utilsTest(
- 'request with no data',
- getRequestNoData.prefix,
- getRequestNoData.data,
- callWithEditorMethod('getRequestRange', (range, done) => {
- expect(range).toEqual({
- start: { lineNumber: 1, column: 1 },
- end: { lineNumber: 1, column: 11 },
- });
- done();
- })
- );
-
- utilsTest(
- 'request with no data (data)',
- getRequestNoData.prefix,
- getRequestNoData.data,
- callWithEditorMethod('getRequest', (request, done) => {
- const expected = {
- method: 'GET',
- url: '_stats',
- data: [],
- };
- compareRequest(request, expected);
- done();
- })
- );
-
- utilsTest(
- 'multi doc request range',
- multiDocRequest.prefix,
- multiDocRequest.data,
- callWithEditorMethod('getRequestRange', (range, done) => {
- expect(range).toEqual({
- start: { lineNumber: 1, column: 1 },
- end: { lineNumber: 3, column: 15 },
- });
- done();
- })
- );
-
- utilsTest(
- 'multi doc request data',
- multiDocRequest.prefix,
- multiDocRequest.data,
- callWithEditorMethod('getRequest', (request, done) => {
- const expected = {
- method: 'POST',
- url: '_bulk',
- data: multiDocRequest.data_as_array,
- };
- compareRequest(request, expected);
- done();
- })
- );
-
- const scriptRequest = {
- prefix: 'POST _search',
- data: ['{', ' "query": { "script": """', ' some script ', ' """}', '}'].join('\n'),
- };
-
- utilsTest(
- 'script request range',
- scriptRequest.prefix,
- scriptRequest.data,
- callWithEditorMethod('getRequestRange', (range, done) => {
- compareRequest(range, {
- start: { lineNumber: 1, column: 1 },
- end: { lineNumber: 6, column: 2 },
- });
- done();
- })
- );
-
- utilsTest(
- 'simple request data',
- simpleRequest.prefix,
- simpleRequest.data,
- callWithEditorMethod('getRequest', (request, done) => {
- const expected = {
- method: 'POST',
- url: '_search',
- data: [collapseLiteralStrings(simpleRequest.data)],
- };
-
- compareRequest(request, expected);
- done();
- })
- );
-
- function multiReqTest(name, editorInput, range, expected) {
- utilsTest('multi request select - ' + name, editorInput, async function (done) {
- const requests = await input.getRequestsInRange(range, false);
- // convert to format returned by request.
- _.each(expected, function (req) {
- req.data = req.data == null ? [] : [JSON.stringify(req.data, null, 2)];
- });
-
- compareRequest(requests, expected);
- done();
- });
- }
-
- multiReqTest(
- 'mid body to mid body',
- editorInput1,
- { start: { lineNumber: 13 }, end: { lineNumber: 18 } },
- [
- {
- method: 'PUT',
- url: 'index_1/type1/1',
- data: {
- f: 1,
- },
- },
- {
- method: 'PUT',
- url: 'index_1/type1/2',
- data: {
- f: 2,
- },
- },
- ]
- );
-
- multiReqTest(
- 'single request start to end',
- editorInput1,
- { start: { lineNumber: 11 }, end: { lineNumber: 14 } },
- [
- {
- method: 'PUT',
- url: 'index_1/type1/1',
- data: {
- f: 1,
- },
- },
- ]
- );
-
- multiReqTest(
- 'start to end, with comment',
- editorInput1,
- { start: { lineNumber: 7 }, end: { lineNumber: 14 } },
- [
- {
- method: 'GET',
- url: '_stats?level=shards',
- data: null,
- },
- {
- method: 'PUT',
- url: 'index_1/type1/1',
- data: {
- f: 1,
- },
- },
- ]
- );
-
- multiReqTest(
- 'before start to after end, with comments',
- editorInput1,
- { start: { lineNumber: 5 }, end: { lineNumber: 15 } },
- [
- {
- method: 'GET',
- url: '_stats?level=shards',
- data: null,
- },
- {
- method: 'PUT',
- url: 'index_1/type1/1',
- data: {
- f: 1,
- },
- },
- ]
- );
-
- multiReqTest(
- 'between requests',
- editorInput1,
- { start: { lineNumber: 22 }, end: { lineNumber: 23 } },
- []
- );
-
- multiReqTest(
- 'between requests - with comment',
- editorInput1,
- { start: { lineNumber: 21 }, end: { lineNumber: 23 } },
- []
- );
-
- multiReqTest(
- 'between requests - before comment',
- editorInput1,
- { start: { lineNumber: 20 }, end: { lineNumber: 23 } },
- []
- );
-
- function multiReqCopyAsCurlTest(name, editorInput, range, expected) {
- utilsTest('multi request copy as curl - ' + name, editorInput, async function (done) {
- const curl = await input.getRequestsAsCURL('http://localhost:9200', range);
- expect(curl).toEqual(expected);
- done();
- });
- }
-
- multiReqCopyAsCurlTest(
- 'start to end, with comment',
- editorInput1,
- { start: { lineNumber: 7 }, end: { lineNumber: 14 } },
- `
-curl -XGET "http://localhost:9200/_stats?level=shards" -H "kbn-xsrf: reporting"
-
-#in between comment
-
-curl -XPUT "http://localhost:9200/index_1/type1/1" -H "kbn-xsrf: reporting" -H "Content-Type: application/json" -d'
-{
- "f": 1
-}'`.trim()
- );
-
- multiReqCopyAsCurlTest(
- 'with single quotes',
- editorInput1,
- { start: { lineNumber: 29 }, end: { lineNumber: 33 } },
- `
-curl -XPOST "http://localhost:9200/_sql?format=txt" -H "kbn-xsrf: reporting" -H "Content-Type: application/json" -d'
-{
- "query": "SELECT prenom FROM claude_index WHERE prenom = '\\''claude'\\'' ",
- "fetch_size": 1
-}'`.trim()
- );
-
- multiReqCopyAsCurlTest(
- 'with date math index',
- editorInput1,
- { start: { lineNumber: 35 }, end: { lineNumber: 35 } },
- `
- curl -XGET "http://localhost:9200/%3Cindex_1-%7Bnow%2Fd-2d%7D%3E%2C%3Cindex_1-%7Bnow%2Fd-1d%7D%3E%2C%3Cindex_1-%7Bnow%2Fd%7D%3E%2F_search?pretty" -H "kbn-xsrf: reporting"`.trim()
- );
-
- multiReqCopyAsCurlTest(
- 'with Kibana API request',
- editorInput1,
- { start: { lineNumber: 37 }, end: { lineNumber: 37 } },
- `
-curl -XGET "http://localhost:5620/api/spaces/space" -H \"kbn-xsrf: reporting\"`.trim()
- );
-
- describe('getRequestsAsCURL', () => {
- it('should return empty string if no requests', async () => {
- input?.getCoreEditor().setValue('', false);
- const curl = await input.getRequestsAsCURL('http://localhost:9200', {
- start: { lineNumber: 1 },
- end: { lineNumber: 1 },
- });
- expect(curl).toEqual('');
- });
-
- it('should replace variables in the URL', async () => {
- storage.set('variables', [{ name: 'exampleVariableA', value: 'valueA' }]);
- input?.getCoreEditor().setValue('GET ${exampleVariableA}', false);
- const curl = await input.getRequestsAsCURL('http://localhost:9200', {
- start: { lineNumber: 1 },
- end: { lineNumber: 1 },
- });
- expect(curl).toContain('valueA');
- });
-
- it('should replace variables in the body', async () => {
- storage.set('variables', [{ name: 'exampleVariableB', value: 'valueB' }]);
- console.log(storage.get('variables'));
- input
- ?.getCoreEditor()
- .setValue('GET _search\n{\t\t"query": {\n\t\t\t"${exampleVariableB}": ""\n\t}\n}', false);
- const curl = await input.getRequestsAsCURL('http://localhost:9200', {
- start: { lineNumber: 1 },
- end: { lineNumber: 6 },
- });
- expect(curl).toContain('valueB');
- });
-
- it('should strip comments in the URL', async () => {
- input?.getCoreEditor().setValue('GET _search // comment', false);
- const curl = await input.getRequestsAsCURL('http://localhost:9200', {
- start: { lineNumber: 1 },
- end: { lineNumber: 6 },
- });
- expect(curl).not.toContain('comment');
- });
-
- it('should strip comments in the body', async () => {
- input
- ?.getCoreEditor()
- .setValue('{\n\t"query": {\n\t\t"match_all": {} // comment \n\t}\n}', false);
- const curl = await input.getRequestsAsCURL('http://localhost:9200', {
- start: { lineNumber: 1 },
- end: { lineNumber: 8 },
- });
- console.log('curl', curl);
- expect(curl).not.toContain('comment');
- });
-
- it('should strip multi-line comments in the body', async () => {
- input
- ?.getCoreEditor()
- .setValue('{\n\t"query": {\n\t\t"match_all": {} /* comment */\n\t}\n}', false);
- const curl = await input.getRequestsAsCURL('http://localhost:9200', {
- start: { lineNumber: 1 },
- end: { lineNumber: 8 },
- });
- console.log('curl', curl);
- expect(curl).not.toContain('comment');
- });
-
- it('should replace multiple variables in the URL', async () => {
- storage.set('variables', [
- { name: 'exampleVariableA', value: 'valueA' },
- { name: 'exampleVariableB', value: 'valueB' },
- ]);
- input?.getCoreEditor().setValue('GET ${exampleVariableA}/${exampleVariableB}', false);
- const curl = await input.getRequestsAsCURL('http://localhost:9200', {
- start: { lineNumber: 1 },
- end: { lineNumber: 1 },
- });
- expect(curl).toContain('valueA');
- expect(curl).toContain('valueB');
- });
-
- it('should replace multiple variables in the body', async () => {
- storage.set('variables', [
- { name: 'exampleVariableA', value: 'valueA' },
- { name: 'exampleVariableB', value: 'valueB' },
- ]);
- input
- ?.getCoreEditor()
- .setValue(
- 'GET _search\n{\t\t"query": {\n\t\t\t"${exampleVariableA}": "${exampleVariableB}"\n\t}\n}',
- false
- );
- const curl = await input.getRequestsAsCURL('http://localhost:9200', {
- start: { lineNumber: 1 },
- end: { lineNumber: 6 },
- });
- expect(curl).toContain('valueA');
- expect(curl).toContain('valueB');
- });
-
- it('should replace variables in bulk request', async () => {
- storage.set('variables', [
- { name: 'exampleVariableA', value: 'valueA' },
- { name: 'exampleVariableB', value: 'valueB' },
- ]);
- input
- ?.getCoreEditor()
- .setValue(
- 'POST _bulk\n{"index": {"_id": "0"}}\n{"field" : "${exampleVariableA}"}\n{"index": {"_id": "1"}}\n{"field" : "${exampleVariableB}"}\n',
- false
- );
- const curl = await input.getRequestsAsCURL('http://localhost:9200', {
- start: { lineNumber: 1 },
- end: { lineNumber: 4 },
- });
- expect(curl).toContain('valueA');
- expect(curl).toContain('valueB');
- });
- });
-});
diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.mocks.ts b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.mocks.ts
deleted file mode 100644
index f0ec279fb4ffe..0000000000000
--- a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.mocks.ts
+++ /dev/null
@@ -1,20 +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", 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".
- */
-
-/* eslint no-undef: 0 */
-
-import '../legacy_core_editor/legacy_core_editor.test.mocks';
-
-import jQuery from 'jquery';
-jest.spyOn(jQuery, 'ajax').mockImplementation(
- () =>
- new Promise(() => {
- // never resolve
- }) as any
-);
diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts
deleted file mode 100644
index f6b0439cb283e..0000000000000
--- a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts
+++ /dev/null
@@ -1,534 +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", 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 _ from 'lodash';
-import { parse } from 'hjson';
-import { XJson } from '@kbn/es-ui-shared-plugin/public';
-
-import RowParser from '../../../lib/row_parser';
-import * as utils from '../../../lib/utils';
-import { constructUrl } from '../../../lib/es/es';
-
-import { CoreEditor, Position, Range } from '../../../types';
-import { createTokenIterator } from '../../factories';
-import createAutocompleter from '../../../lib/autocomplete/autocomplete';
-import { getStorage, StorageKeys } from '../../../services';
-import { DEFAULT_VARIABLES } from '../../../../common/constants';
-
-const { collapseLiteralStrings } = XJson;
-
-export class SenseEditor {
- currentReqRange: (Range & { markerRef: unknown }) | null;
- parser: RowParser;
-
- private readonly autocomplete: ReturnType;
-
- constructor(private readonly coreEditor: CoreEditor) {
- this.currentReqRange = null;
- this.parser = new RowParser(this.coreEditor);
- this.autocomplete = createAutocompleter({
- coreEditor,
- parser: this.parser,
- });
- this.coreEditor.registerAutocompleter(this.autocomplete.getCompletions);
- this.coreEditor.on(
- 'tokenizerUpdate',
- this.highlightCurrentRequestsAndUpdateActionBar.bind(this)
- );
- this.coreEditor.on('changeCursor', this.highlightCurrentRequestsAndUpdateActionBar.bind(this));
- this.coreEditor.on('changeScrollTop', this.updateActionsBar.bind(this));
- }
-
- prevRequestStart = (rowOrPos?: number | Position): Position => {
- let curRow: number;
-
- if (rowOrPos == null) {
- curRow = this.coreEditor.getCurrentPosition().lineNumber;
- } else if (_.isObject(rowOrPos)) {
- curRow = (rowOrPos as Position).lineNumber;
- } else {
- curRow = rowOrPos as number;
- }
-
- while (curRow > 0 && !this.parser.isStartRequestRow(curRow, this.coreEditor)) curRow--;
-
- return {
- lineNumber: curRow,
- column: 1,
- };
- };
-
- nextRequestStart = (rowOrPos?: number | Position) => {
- let curRow: number;
- if (rowOrPos == null) {
- curRow = this.coreEditor.getCurrentPosition().lineNumber;
- } else if (_.isObject(rowOrPos)) {
- curRow = (rowOrPos as Position).lineNumber;
- } else {
- curRow = rowOrPos as number;
- }
- const maxLines = this.coreEditor.getLineCount();
- for (; curRow < maxLines - 1; curRow++) {
- if (this.parser.isStartRequestRow(curRow, this.coreEditor)) {
- break;
- }
- }
- return {
- row: curRow,
- column: 0,
- };
- };
-
- autoIndent = _.debounce(async () => {
- await this.coreEditor.waitForLatestTokens();
- const reqRange = await this.getRequestRange();
- if (!reqRange) {
- return;
- }
- const parsedReq = await this.getRequest();
-
- if (!parsedReq) {
- return;
- }
-
- if (parsedReq.data.some((doc) => utils.hasComments(doc))) {
- /**
- * Comments require different approach for indentation and do not have condensed format
- * We need to delegate indentation logic to coreEditor since it has access to session and other methods used for formatting and indenting the comments
- */
- this.coreEditor.autoIndent(parsedReq.range);
- return;
- }
-
- if (parsedReq.data && parsedReq.data.length > 0) {
- let indent = parsedReq.data.length === 1; // unindent multi docs by default
- let formattedData = utils.formatRequestBodyDoc(parsedReq.data, indent);
- if (!formattedData.changed) {
- // toggle.
- indent = !indent;
- formattedData = utils.formatRequestBodyDoc(parsedReq.data, indent);
- }
- parsedReq.data = formattedData.data;
-
- this.replaceRequestRange(parsedReq, reqRange);
- }
- }, 25);
-
- update = async (data: string, reTokenizeAll = false) => {
- return this.coreEditor.setValue(data, reTokenizeAll);
- };
-
- replaceRequestRange = (
- newRequest: { method: string; url: string; data: string | string[] },
- requestRange: Range
- ) => {
- const text = utils.textFromRequest(newRequest);
- if (requestRange) {
- this.coreEditor.replaceRange(requestRange, text);
- } else {
- // just insert where we are
- this.coreEditor.insert(this.coreEditor.getCurrentPosition(), text);
- }
- };
-
- getRequestRange = async (lineNumber?: number): Promise => {
- await this.coreEditor.waitForLatestTokens();
-
- if (this.parser.isInBetweenRequestsRow(lineNumber)) {
- return null;
- }
-
- const reqStart = this.prevRequestStart(lineNumber);
- const reqEnd = this.nextRequestEnd(reqStart);
-
- return {
- start: {
- ...reqStart,
- },
- end: {
- ...reqEnd,
- },
- };
- };
-
- expandRangeToRequestEdges = async (
- range = this.coreEditor.getSelectionRange()
- ): Promise => {
- await this.coreEditor.waitForLatestTokens();
-
- let startLineNumber = range.start.lineNumber;
- let endLineNumber = range.end.lineNumber;
- const maxLine = Math.max(1, this.coreEditor.getLineCount());
-
- if (this.parser.isInBetweenRequestsRow(startLineNumber)) {
- /* Do nothing... */
- } else {
- for (; startLineNumber >= 1; startLineNumber--) {
- if (this.parser.isStartRequestRow(startLineNumber)) {
- break;
- }
- }
- }
-
- if (startLineNumber < 1 || startLineNumber > endLineNumber) {
- return null;
- }
- // move end row to the previous request end if between requests, otherwise walk forward
- if (this.parser.isInBetweenRequestsRow(endLineNumber)) {
- for (; endLineNumber >= startLineNumber; endLineNumber--) {
- if (this.parser.isEndRequestRow(endLineNumber)) {
- break;
- }
- }
- } else {
- for (; endLineNumber <= maxLine; endLineNumber++) {
- if (this.parser.isEndRequestRow(endLineNumber)) {
- break;
- }
- }
- }
-
- if (endLineNumber < startLineNumber || endLineNumber > maxLine) {
- return null;
- }
-
- const endColumn =
- (this.coreEditor.getLineValue(endLineNumber) || '').replace(/\s+$/, '').length + 1;
- return {
- start: {
- lineNumber: startLineNumber,
- column: 1,
- },
- end: {
- lineNumber: endLineNumber,
- column: endColumn,
- },
- };
- };
-
- getRequestInRange = async (range?: Range) => {
- await this.coreEditor.waitForLatestTokens();
- if (!range) {
- return null;
- }
- const request: {
- method: string;
- data: string[];
- url: string;
- range: Range;
- } = {
- method: '',
- data: [],
- url: '',
- range,
- };
-
- const pos = range.start;
- const tokenIter = createTokenIterator({ editor: this.coreEditor, position: pos });
- let t = tokenIter.getCurrentToken();
- if (this.parser.isEmptyToken(t)) {
- // if the row starts with some spaces, skip them.
- t = this.parser.nextNonEmptyToken(tokenIter);
- }
- if (t == null) {
- return null;
- }
-
- request.method = t.value;
- t = this.parser.nextNonEmptyToken(tokenIter);
-
- if (!t || t.type === 'method') {
- return null;
- }
-
- request.url = '';
-
- while (t && t.type && (t.type.indexOf('url') === 0 || t.type === 'variable.template')) {
- request.url += t.value;
- t = tokenIter.stepForward();
- }
- if (this.parser.isEmptyToken(t)) {
- // if the url row ends with some spaces, skip them.
- t = this.parser.nextNonEmptyToken(tokenIter);
- }
-
- // If the url row ends with a comment, skip it
- while (this.parser.isCommentToken(t)) {
- t = tokenIter.stepForward();
- }
-
- let bodyStartLineNumber = (t ? 0 : 1) + tokenIter.getCurrentPosition().lineNumber; // artificially increase end of docs.
- let dataEndPos: Position;
- while (
- bodyStartLineNumber < range.end.lineNumber ||
- (bodyStartLineNumber === range.end.lineNumber && 1 < range.end.column)
- ) {
- dataEndPos = this.nextDataDocEnd({
- lineNumber: bodyStartLineNumber,
- column: 1,
- });
- const bodyRange: Range = {
- start: {
- lineNumber: bodyStartLineNumber,
- column: 1,
- },
- end: dataEndPos,
- };
- const data = this.coreEditor.getValueInRange(bodyRange)!;
- request.data.push(data.trim());
- bodyStartLineNumber = dataEndPos.lineNumber + 1;
- }
-
- return request;
- };
-
- getRequestsInRange = async (
- range = this.coreEditor.getSelectionRange(),
- includeNonRequestBlocks = false
- ): Promise => {
- await this.coreEditor.waitForLatestTokens();
- if (!range) {
- return [];
- }
-
- const expandedRange = await this.expandRangeToRequestEdges(range);
- if (!expandedRange) {
- return [];
- }
-
- const requests: unknown[] = [];
-
- let rangeStartCursor = expandedRange.start.lineNumber;
- const endLineNumber = expandedRange.end.lineNumber;
-
- // move to the next request start (during the second iterations this may not be exactly on a request
- let currentLineNumber = expandedRange.start.lineNumber;
-
- const flushNonRequestBlock = () => {
- if (includeNonRequestBlocks) {
- const nonRequestPrefixBlock = this.coreEditor
- .getLines(rangeStartCursor, currentLineNumber - 1)
- .join('\n');
- if (nonRequestPrefixBlock) {
- requests.push(nonRequestPrefixBlock);
- }
- }
- };
-
- while (currentLineNumber <= endLineNumber) {
- if (this.parser.isStartRequestRow(currentLineNumber)) {
- flushNonRequestBlock();
- const request = await this.getRequest(currentLineNumber);
- if (!request) {
- // Something has probably gone wrong.
- return requests;
- } else {
- requests.push(request);
- rangeStartCursor = currentLineNumber = request.range.end.lineNumber + 1;
- }
- } else {
- ++currentLineNumber;
- }
- }
-
- flushNonRequestBlock();
-
- return requests;
- };
-
- getRequest = async (row?: number) => {
- await this.coreEditor.waitForLatestTokens();
- if (this.parser.isInBetweenRequestsRow(row)) {
- return null;
- }
-
- const range = await this.getRequestRange(row);
- return this.getRequestInRange(range!);
- };
-
- moveToPreviousRequestEdge = async () => {
- await this.coreEditor.waitForLatestTokens();
- const pos = this.coreEditor.getCurrentPosition();
- for (
- pos.lineNumber--;
- pos.lineNumber > 1 && !this.parser.isRequestEdge(pos.lineNumber);
- pos.lineNumber--
- ) {
- // loop for side effects
- }
- this.coreEditor.moveCursorToPosition({
- lineNumber: pos.lineNumber,
- column: 1,
- });
- };
-
- moveToNextRequestEdge = async (moveOnlyIfNotOnEdge: boolean) => {
- await this.coreEditor.waitForLatestTokens();
- const pos = this.coreEditor.getCurrentPosition();
- const maxRow = this.coreEditor.getLineCount();
- if (!moveOnlyIfNotOnEdge) {
- pos.lineNumber++;
- }
- for (
- ;
- pos.lineNumber < maxRow && !this.parser.isRequestEdge(pos.lineNumber);
- pos.lineNumber++
- ) {
- // loop for side effects
- }
- this.coreEditor.moveCursorToPosition({
- lineNumber: pos.lineNumber,
- column: 1,
- });
- };
-
- nextRequestEnd = (pos: Position): Position => {
- pos = pos || this.coreEditor.getCurrentPosition();
- const maxLines = this.coreEditor.getLineCount();
- let curLineNumber = pos.lineNumber;
- for (; curLineNumber <= maxLines; ++curLineNumber) {
- const curRowMode = this.parser.getRowParseMode(curLineNumber);
- // eslint-disable-next-line no-bitwise
- if ((curRowMode & this.parser.MODE.REQUEST_END) > 0) {
- break;
- }
- // eslint-disable-next-line no-bitwise
- if (curLineNumber !== pos.lineNumber && (curRowMode & this.parser.MODE.REQUEST_START) > 0) {
- break;
- }
- }
-
- const column =
- (this.coreEditor.getLineValue(curLineNumber) || '').replace(/\s+$/, '').length + 1;
-
- return {
- lineNumber: curLineNumber,
- column,
- };
- };
-
- nextDataDocEnd = (pos: Position): Position => {
- pos = pos || this.coreEditor.getCurrentPosition();
- let curLineNumber = pos.lineNumber;
- const maxLines = this.coreEditor.getLineCount();
- for (; curLineNumber < maxLines; curLineNumber++) {
- const curRowMode = this.parser.getRowParseMode(curLineNumber);
- // eslint-disable-next-line no-bitwise
- if ((curRowMode & this.parser.MODE.REQUEST_END) > 0) {
- break;
- }
- // eslint-disable-next-line no-bitwise
- if ((curRowMode & this.parser.MODE.MULTI_DOC_CUR_DOC_END) > 0) {
- break;
- }
- // eslint-disable-next-line no-bitwise
- if (curLineNumber !== pos.lineNumber && (curRowMode & this.parser.MODE.REQUEST_START) > 0) {
- break;
- }
- }
-
- const column =
- (this.coreEditor.getLineValue(curLineNumber) || '').length +
- 1; /* Range goes to 1 after last char */
-
- return {
- lineNumber: curLineNumber,
- column,
- };
- };
-
- highlightCurrentRequestsAndUpdateActionBar = _.debounce(async () => {
- await this.coreEditor.waitForLatestTokens();
- const expandedRange = await this.expandRangeToRequestEdges();
- if (expandedRange === null && this.currentReqRange === null) {
- return;
- }
- if (
- expandedRange !== null &&
- this.currentReqRange !== null &&
- expandedRange.start.lineNumber === this.currentReqRange.start.lineNumber &&
- expandedRange.end.lineNumber === this.currentReqRange.end.lineNumber
- ) {
- // same request, now see if we are on the first line and update the action bar
- const cursorLineNumber = this.coreEditor.getCurrentPosition().lineNumber;
- if (cursorLineNumber === this.currentReqRange.start.lineNumber) {
- this.updateActionsBar();
- }
- return; // nothing to do..
- }
-
- if (this.currentReqRange) {
- this.coreEditor.removeMarker(this.currentReqRange.markerRef);
- }
-
- this.currentReqRange = expandedRange as any;
- if (this.currentReqRange) {
- this.currentReqRange.markerRef = this.coreEditor.addMarker(this.currentReqRange);
- }
- this.updateActionsBar();
- }, 25);
-
- getRequestsAsCURL = async (elasticsearchBaseUrl: string, range?: Range): Promise => {
- const variables = getStorage().get(StorageKeys.VARIABLES, DEFAULT_VARIABLES);
- let requests = await this.getRequestsInRange(range, true);
- requests = utils.replaceVariables(requests, variables);
- const result = _.map(requests, (req) => {
- if (typeof req === 'string') {
- // no request block
- return req;
- }
-
- const path = req.url;
- const method = req.method;
- const data = req.data;
-
- // this is the first url defined in elasticsearch.hosts
- const url = constructUrl(elasticsearchBaseUrl, path);
-
- // Append 'kbn-xsrf' header to bypass (XSRF/CSRF) protections
- let ret = `curl -X${method.toUpperCase()} "${url}" -H "kbn-xsrf: reporting"`;
-
- if (data && data.length) {
- const joinedData = data.join('\n');
- let dataAsString: string;
-
- try {
- ret += ` -H "Content-Type: application/json" -d'\n`;
-
- if (utils.hasComments(joinedData)) {
- // if there are comments in the data, we need to strip them out
- const dataWithoutComments = parse(joinedData);
- dataAsString = collapseLiteralStrings(JSON.stringify(dataWithoutComments, null, 2));
- } else {
- dataAsString = collapseLiteralStrings(joinedData);
- }
- // We escape single quoted strings that are wrapped in single quoted strings
- ret += dataAsString.replace(/'/g, "'\\''");
- if (data.length > 1) {
- ret += '\n';
- } // end with a new line
- ret += "'";
- } catch (e) {
- throw new Error(`Error parsing data: ${e.message}`);
- }
- }
- return ret;
- });
-
- return result.join('\n');
- };
-
- updateActionsBar = () => {
- return this.coreEditor.legacyUpdateUI(this.currentReqRange);
- };
-
- getCoreEditor() {
- return this.coreEditor;
- }
-}
diff --git a/src/plugins/console/public/application/stores/editor.ts b/src/plugins/console/public/application/stores/editor.ts
index 556f4f64337e6..8ae24e5a422b7 100644
--- a/src/plugins/console/public/application/stores/editor.ts
+++ b/src/plugins/console/public/application/stores/editor.ts
@@ -12,7 +12,6 @@ import { produce } from 'immer';
import { identity } from 'fp-ts/lib/function';
import { DevToolsSettings, DEFAULT_SETTINGS } from '../../services';
import { TextObject } from '../../../common/text_object';
-import { SenseEditor } from '../models';
import { SHELL_TAB_ID } from '../containers/main/constants';
import { MonacoEditorActionsProvider } from '../containers/editor/monaco_editor_actions_provider';
import { RequestToRestore } from '../../types';
@@ -39,7 +38,7 @@ export const initialValue: Store = produce(
);
export type Action =
- | { type: 'setInputEditor'; payload: SenseEditor | MonacoEditorActionsProvider }
+ | { type: 'setInputEditor'; payload: MonacoEditorActionsProvider }
| { type: 'setCurrentTextObject'; payload: TextObject }
| { type: 'updateSettings'; payload: DevToolsSettings }
| { type: 'setCurrentView'; payload: string }
diff --git a/src/plugins/console/public/lib/ace_token_provider/index.ts b/src/plugins/console/public/lib/ace_token_provider/index.ts
deleted file mode 100644
index 8819ac19a1262..0000000000000
--- a/src/plugins/console/public/lib/ace_token_provider/index.ts
+++ /dev/null
@@ -1,10 +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", 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".
- */
-
-export * from './token_provider';
diff --git a/src/plugins/console/public/lib/ace_token_provider/token_provider.test.ts b/src/plugins/console/public/lib/ace_token_provider/token_provider.test.ts
deleted file mode 100644
index b36d9855414bd..0000000000000
--- a/src/plugins/console/public/lib/ace_token_provider/token_provider.test.ts
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", 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 '../../application/models/sense_editor/sense_editor.test.mocks';
-
-import $ from 'jquery';
-
-// TODO:
-// We import from application models as a convenient way to bootstrap loading up of an editor using
-// this lib. We also need to import application specific mocks which is not ideal.
-// In this situation, the token provider lib knows about app models in tests, which it really shouldn't. Should create
-// a better sandbox in future.
-import { create, SenseEditor } from '../../application/models/sense_editor';
-
-import { Position, Token, TokensProvider } from '../../types';
-
-interface RunTestArgs {
- input: string;
- done?: () => void;
-}
-
-describe('Ace (legacy) token provider', () => {
- let senseEditor: SenseEditor;
- let tokenProvider: TokensProvider;
- beforeEach(() => {
- // Set up our document body
- document.body.innerHTML = ``;
-
- senseEditor = create(document.querySelector('#ConAppEditor')!);
-
- $(senseEditor.getCoreEditor().getContainer())!.show();
-
- (senseEditor as any).autocomplete._test.removeChangeListener();
- tokenProvider = senseEditor.getCoreEditor().getTokenProvider();
- });
-
- afterEach(async () => {
- $(senseEditor.getCoreEditor().getContainer())!.hide();
- (senseEditor as any).autocomplete._test.addChangeListener();
- await senseEditor.update('', true);
- });
-
- describe('#getTokens', () => {
- const runTest = ({
- input,
- expectedTokens,
- done,
- lineNumber = 1,
- }: RunTestArgs & { expectedTokens: Token[] | null; lineNumber?: number }) => {
- senseEditor.update(input, true).then(() => {
- const tokens = tokenProvider.getTokens(lineNumber);
- expect(tokens).toEqual(expectedTokens);
- if (done) done();
- });
- };
-
- describe('base cases', () => {
- test('case 1 - only url', (done) => {
- runTest({
- input: `GET http://somehost/_search`,
- expectedTokens: [
- { type: 'method', value: 'GET', position: { lineNumber: 1, column: 1 } },
- { type: 'whitespace', value: ' ', position: { lineNumber: 1, column: 4 } },
- {
- type: 'url.protocol_host',
- value: 'http://somehost',
- position: { lineNumber: 1, column: 5 },
- },
- { type: 'url.slash', value: '/', position: { lineNumber: 1, column: 20 } },
- { type: 'url.part', value: '_search', position: { lineNumber: 1, column: 21 } },
- ],
- done,
- });
- });
-
- test('case 2 - basic auth in host name', (done) => {
- runTest({
- input: `GET http://test:user@somehost/`,
- expectedTokens: [
- { type: 'method', value: 'GET', position: { lineNumber: 1, column: 1 } },
- { type: 'whitespace', value: ' ', position: { lineNumber: 1, column: 4 } },
- {
- type: 'url.protocol_host',
- value: 'http://test:user@somehost',
- position: { lineNumber: 1, column: 5 },
- },
- { type: 'url.slash', value: '/', position: { lineNumber: 1, column: 30 } },
- ],
- done,
- });
- });
-
- test('case 3 - handles empty lines', (done) => {
- runTest({
- input: `POST abc
-
-
-{
-`,
- expectedTokens: [
- { type: 'method', value: 'POST', position: { lineNumber: 1, column: 1 } },
- { type: 'whitespace', value: ' ', position: { lineNumber: 1, column: 5 } },
- { type: 'url.part', value: 'abc', position: { lineNumber: 1, column: 6 } },
- ],
- done,
- lineNumber: 1,
- });
- });
- });
-
- describe('with newlines', () => {
- test('case 1 - newlines base case', (done) => {
- runTest({
- input: `GET http://test:user@somehost/
-{
- "wudup": "!"
-}`,
- expectedTokens: [
- { type: 'whitespace', value: ' ', position: { lineNumber: 3, column: 1 } },
- { type: 'variable', value: '"wudup"', position: { lineNumber: 3, column: 3 } },
- { type: 'punctuation.colon', value: ':', position: { lineNumber: 3, column: 10 } },
- { type: 'whitespace', value: ' ', position: { lineNumber: 3, column: 11 } },
- { type: 'string', value: '"!"', position: { lineNumber: 3, column: 12 } },
- ],
- done,
- lineNumber: 3,
- });
- });
- });
-
- describe('edge cases', () => {
- test('case 1 - getting token outside of document', (done) => {
- runTest({
- input: `GET http://test:user@somehost/
-{
- "wudup": "!"
-}`,
- expectedTokens: null,
- done,
- lineNumber: 100,
- });
- });
-
- test('case 2 - empty lines', (done) => {
- runTest({
- input: `GET http://test:user@somehost/
-
-
-
-
-{
- "wudup": "!"
-}`,
- expectedTokens: [],
- done,
- lineNumber: 5,
- });
- });
- });
- });
-
- describe('#getTokenAt', () => {
- const runTest = ({
- input,
- expectedToken,
- done,
- position,
- }: RunTestArgs & { expectedToken: Token | null; position: Position }) => {
- senseEditor.update(input, true).then(() => {
- const tokens = tokenProvider.getTokenAt(position);
- expect(tokens).toEqual(expectedToken);
- if (done) done();
- });
- };
-
- describe('base cases', () => {
- it('case 1 - gets a token from the url', (done) => {
- const input = `GET http://test:user@somehost/`;
- runTest({
- input,
- expectedToken: {
- position: { lineNumber: 1, column: 4 },
- type: 'whitespace',
- value: ' ',
- },
- position: { lineNumber: 1, column: 5 },
- });
-
- runTest({
- input,
- expectedToken: {
- position: { lineNumber: 1, column: 5 },
- type: 'url.protocol_host',
- value: 'http://test:user@somehost',
- },
- position: { lineNumber: 1, column: input.length },
- done,
- });
- });
- });
-
- describe('special cases', () => {
- it('case 1 - handles input outside of range', (done) => {
- runTest({
- input: `GET abc`,
- expectedToken: null,
- done,
- position: { lineNumber: 1, column: 99 },
- });
- });
- });
- });
-});
diff --git a/src/plugins/console/public/lib/ace_token_provider/token_provider.ts b/src/plugins/console/public/lib/ace_token_provider/token_provider.ts
deleted file mode 100644
index 9e61771946771..0000000000000
--- a/src/plugins/console/public/lib/ace_token_provider/token_provider.ts
+++ /dev/null
@@ -1,84 +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", 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 { IEditSession, TokenInfo as BraceTokenInfo } from 'brace';
-import { TokensProvider, Token, Position } from '../../types';
-
-// Brace's token information types are not accurate.
-interface TokenInfo extends BraceTokenInfo {
- type: string;
-}
-
-const toToken = (lineNumber: number, column: number, token: TokenInfo): Token => ({
- type: token.type,
- value: token.value,
- position: {
- lineNumber,
- column,
- },
-});
-
-const toTokens = (lineNumber: number, tokens: TokenInfo[]): Token[] => {
- let acc = '';
- return tokens.map((token) => {
- const column = acc.length + 1;
- acc += token.value;
- return toToken(lineNumber, column, token);
- });
-};
-
-const extractTokenFromAceTokenRow = (
- lineNumber: number,
- column: number,
- aceTokens: TokenInfo[]
-) => {
- let acc = '';
- for (const token of aceTokens) {
- const start = acc.length + 1;
- acc += token.value;
- const end = acc.length;
- if (column < start) continue;
- if (column > end + 1) continue;
- return toToken(lineNumber, start, token);
- }
- return null;
-};
-
-export class AceTokensProvider implements TokensProvider {
- constructor(private readonly session: IEditSession) {}
-
- getTokens(lineNumber: number): Token[] | null {
- if (lineNumber < 1) return null;
-
- // Important: must use a .session.getLength because this is a cached value.
- // Calculating line length here will lead to performance issues because this function
- // may be called inside of tight loops.
- const lineCount = this.session.getLength();
- if (lineNumber > lineCount) {
- return null;
- }
-
- const tokens = this.session.getTokens(lineNumber - 1) as unknown as TokenInfo[];
- if (!tokens || !tokens.length) {
- // We are inside of the document but have no tokens for this line. Return an empty
- // array to represent this empty line.
- return [];
- }
-
- return toTokens(lineNumber, tokens);
- }
-
- getTokenAt(pos: Position): Token | null {
- const tokens = this.session.getTokens(pos.lineNumber - 1) as unknown as TokenInfo[];
- if (tokens) {
- return extractTokenFromAceTokenRow(pos.lineNumber, pos.column, tokens);
- }
- return null;
- }
-}
diff --git a/src/plugins/console/public/lib/autocomplete/autocomplete.ts b/src/plugins/console/public/lib/autocomplete/autocomplete.ts
deleted file mode 100644
index 73ef1981cfc0b..0000000000000
--- a/src/plugins/console/public/lib/autocomplete/autocomplete.ts
+++ /dev/null
@@ -1,1316 +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", 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 _ from 'lodash';
-import { i18n } from '@kbn/i18n';
-
-// TODO: All of these imports need to be moved to the core editor so that it can inject components from there.
-import {
- getEndpointBodyCompleteComponents,
- getGlobalAutocompleteComponents,
- getTopLevelUrlCompleteComponents,
- getUnmatchedEndpointComponents,
-} from '../kb/kb';
-
-import { createTokenIterator } from '../../application/factories';
-import type { CoreEditor, Position, Range, Token } from '../../types';
-import type RowParser from '../row_parser';
-
-import * as utils from '../utils';
-
-import { populateContext } from './engine';
-import type { AutoCompleteContext, DataAutoCompleteRulesOneOf, ResultTerm } from './types';
-import { URL_PATH_END_MARKER, ConstantComponent } from './components';
-import { looksLikeTypingIn } from './looks_like_typing_in';
-
-let lastEvaluatedToken: Token | null = null;
-
-function isUrlParamsToken(token: { type: string } | null) {
- switch ((token || {}).type) {
- case 'url.param':
- case 'url.equal':
- case 'url.value':
- case 'url.questionmark':
- case 'url.amp':
- return true;
- default:
- return false;
- }
-}
-
-/* Logs the provided arguments to the console if the `window.autocomplete_trace` flag is set to true.
- * This function checks if the `autocomplete_trace` flag is enabled on the `window` object. This is
- * only used when executing functional tests.
- * If the flag is enabled, it logs each argument to the console.
- * If an argument is an object, it is stringified before logging.
- */
-const tracer = (...args: any[]) => {
- // @ts-ignore
- if (window.autocomplete_trace) {
- // eslint-disable-next-line no-console
- console.log.call(
- console,
- ..._.map(args, (arg) => {
- return typeof arg === 'object' ? JSON.stringify(arg) : arg;
- })
- );
- }
-};
-
-/**
- * Get the method and token paths for a specific position in the current editor buffer.
- *
- * This function can be used for getting autocomplete information or for getting more information
- * about the endpoint associated with autocomplete. In future, these concerns should be better
- * separated.
- *
- */
-export function getCurrentMethodAndTokenPaths(
- editor: CoreEditor,
- pos: Position,
- parser: RowParser,
- forceEndOfUrl?: boolean /* Flag for indicating whether we want to avoid early escape optimization. */
-) {
- const tokenIter = createTokenIterator({
- editor,
- position: pos,
- });
- const startPos = pos;
- let bodyTokenPath: string[] | null = [];
- const ret: AutoCompleteContext = {};
-
- const STATES = {
- looking_for_key: 0, // looking for a key but without jumping over anything but white space and colon.
- looking_for_scope_start: 1, // skip everything until scope start
- start: 3,
- };
- let state = STATES.start;
-
- // initialization problems -
- let t = tokenIter.getCurrentToken();
- if (t) {
- if (startPos.column === 1) {
- // if we are at the beginning of the line, the current token is the one after cursor, not before which
- // deviates from the standard.
- t = tokenIter.stepBackward();
- state = STATES.looking_for_scope_start;
- }
- } else {
- if (startPos.column === 1) {
- // empty lines do no have tokens, move one back
- t = tokenIter.stepBackward();
- state = STATES.start;
- }
- }
-
- let walkedSomeBody = false;
-
- // climb one scope at a time and get the scope key
- for (; t && t.type.indexOf('url') === -1 && t.type !== 'method'; t = tokenIter.stepBackward()) {
- if (t.type !== 'whitespace') {
- walkedSomeBody = true;
- } // marks we saw something
-
- switch (t.type) {
- case 'variable':
- if (state === STATES.looking_for_key) {
- bodyTokenPath.unshift(t.value.trim().replace(/"/g, ''));
- }
- state = STATES.looking_for_scope_start; // skip everything until the beginning of this scope
- break;
-
- case 'paren.lparen':
- bodyTokenPath.unshift(t.value);
- if (state === STATES.looking_for_scope_start) {
- // found it. go look for the relevant key
- state = STATES.looking_for_key;
- }
- break;
- case 'paren.rparen':
- // reset he search for key
- state = STATES.looking_for_scope_start;
- // and ignore this sub scope..
- let parenCount = 1;
- t = tokenIter.stepBackward();
- while (t && parenCount > 0) {
- switch (t.type) {
- case 'paren.lparen':
- parenCount--;
- break;
- case 'paren.rparen':
- parenCount++;
- break;
- }
- if (parenCount > 0) {
- t = tokenIter.stepBackward();
- }
- }
- if (!t) {
- tracer(`paren.rparen: oops we run out.. we don't know what's up return null`);
- return {};
- }
- continue;
- case 'punctuation.end_triple_quote':
- // reset the search for key
- state = STATES.looking_for_scope_start;
- for (t = tokenIter.stepBackward(); t; t = tokenIter.stepBackward()) {
- if (t.type === 'punctuation.start_triple_quote') {
- t = tokenIter.stepBackward();
- break;
- }
- }
- if (!t) {
- tracer(`paren.rparen: oops we run out.. we don't know what's up return null`);
- return {};
- }
- continue;
- case 'punctuation.start_triple_quote':
- if (state === STATES.start) {
- state = STATES.looking_for_key;
- } else if (state === STATES.looking_for_key) {
- state = STATES.looking_for_scope_start;
- }
- bodyTokenPath.unshift('"""');
- continue;
- case 'string':
- case 'constant.numeric':
- case 'constant.language.boolean':
- case 'text':
- if (state === STATES.start) {
- state = STATES.looking_for_key;
- } else if (state === STATES.looking_for_key) {
- state = STATES.looking_for_scope_start;
- }
-
- break;
- case 'punctuation.comma':
- if (state === STATES.start) {
- state = STATES.looking_for_scope_start;
- }
- break;
- case 'punctuation.colon':
- case 'whitespace':
- if (state === STATES.start) {
- state = STATES.looking_for_key;
- }
- break; // skip white space
- }
- }
-
- if (walkedSomeBody && (!bodyTokenPath || bodyTokenPath.length === 0) && !forceEndOfUrl) {
- tracer(
- 'we had some content and still no path',
- '-> the cursor is position after a closed body',
- '-> no auto complete'
- );
- return {};
- }
-
- ret.urlTokenPath = [];
- if (tokenIter.getCurrentPosition().lineNumber === startPos.lineNumber) {
- if (t && (t.type === 'url.part' || t.type === 'url.param' || t.type === 'url.value')) {
- // we are forcing the end of the url for the purposes of determining an endpoint
- if (forceEndOfUrl && t.type === 'url.part') {
- ret.urlTokenPath.push(t.value);
- ret.urlTokenPath.push(URL_PATH_END_MARKER);
- }
- // we are on the same line as cursor and dealing with a url. Current token is not part of the context
- t = tokenIter.stepBackward();
- // This will force method parsing
- while (t!.type === 'whitespace') {
- t = tokenIter.stepBackward();
- }
- }
- bodyTokenPath = null; // no not on a body line.
- }
-
- ret.bodyTokenPath = bodyTokenPath;
-
- ret.urlParamsTokenPath = null;
- ret.requestStartRow = tokenIter.getCurrentPosition().lineNumber;
- let curUrlPart:
- | null
- | string
- | Array>
- | undefined
- | Record;
-
- while (t && isUrlParamsToken(t)) {
- switch (t.type) {
- case 'url.value':
- if (Array.isArray(curUrlPart)) {
- curUrlPart.unshift(t.value);
- } else if (curUrlPart) {
- curUrlPart = [t.value, curUrlPart];
- } else {
- curUrlPart = t.value;
- }
- break;
- case 'url.comma':
- if (!curUrlPart) {
- curUrlPart = [];
- } else if (!Array.isArray(curUrlPart)) {
- curUrlPart = [curUrlPart];
- }
- break;
- case 'url.param':
- const v = curUrlPart;
- curUrlPart = {};
- curUrlPart[t.value] = v;
- break;
- case 'url.amp':
- case 'url.questionmark':
- if (!ret.urlParamsTokenPath) {
- ret.urlParamsTokenPath = [];
- }
- ret.urlParamsTokenPath.unshift((curUrlPart as Record) || {});
- curUrlPart = null;
- break;
- }
- t = tokenIter.stepBackward();
- }
-
- curUrlPart = null;
- while (t && t.type.indexOf('url') !== -1) {
- switch (t.type) {
- case 'url.part':
- if (Array.isArray(curUrlPart)) {
- curUrlPart.unshift(t.value);
- } else if (curUrlPart) {
- curUrlPart = [t.value, curUrlPart];
- } else {
- curUrlPart = t.value;
- }
- break;
- case 'url.comma':
- if (!curUrlPart) {
- curUrlPart = [];
- } else if (!Array.isArray(curUrlPart)) {
- curUrlPart = [curUrlPart];
- }
- break;
- case 'url.slash':
- if (curUrlPart) {
- ret.urlTokenPath.unshift(curUrlPart as string);
- curUrlPart = null;
- }
- break;
- }
- t = parser.prevNonEmptyToken(tokenIter);
- }
-
- if (curUrlPart) {
- ret.urlTokenPath.unshift(curUrlPart as string);
- }
-
- if (!ret.bodyTokenPath && !ret.urlParamsTokenPath) {
- if (ret.urlTokenPath.length > 0) {
- // // started on the url, first token is current token
- ret.otherTokenValues = ret.urlTokenPath[0];
- }
- } else {
- // mark the url as completed.
- ret.urlTokenPath.push(URL_PATH_END_MARKER);
- }
-
- if (t && t.type === 'method') {
- ret.method = t.value;
- }
- return ret;
-}
-
-// eslint-disable-next-line import/no-default-export
-export default function ({
- coreEditor: editor,
- parser,
-}: {
- coreEditor: CoreEditor;
- parser: RowParser;
-}) {
- function isUrlPathToken(token: Token | null) {
- switch ((token || ({} as Token)).type) {
- case 'url.slash':
- case 'url.comma':
- case 'url.part':
- return true;
- default:
- return false;
- }
- }
-
- function addMetaToTermsList(list: ResultTerm[], meta: string, template?: string): ResultTerm[] {
- return _.map(list, function (t) {
- if (typeof t !== 'object') {
- t = { name: t };
- }
- return _.defaults(t, { meta, template });
- });
- }
-
- function replaceLinesWithPrefixPieces(prefixPieces: string[], startLineNumber: number) {
- const middlePiecesCount = prefixPieces.length - 1;
- prefixPieces.forEach((piece, index) => {
- if (index >= middlePiecesCount) {
- return;
- }
- const line = startLineNumber + index + 1;
- const column = editor.getLineValue(line).length - 1;
- const start = { lineNumber: line, column: 0 };
- const end = { lineNumber: line, column };
- editor.replace({ start, end }, piece);
- });
- }
-
- /**
- * Get a different set of templates based on the value configured in the request.
- * For example, when creating a snapshot repository of different types (`fs`, `url` etc),
- * different properties are inserted in the textarea based on the type.
- * E.g. https://github.com/elastic/kibana/blob/main/src/plugins/console/server/lib/spec_definitions/json/overrides/snapshot.create_repository.json
- */
- function getConditionalTemplate(
- name: string,
- autocompleteRules: Record | null | undefined
- ) {
- const obj = autocompleteRules && autocompleteRules[name];
-
- if (obj) {
- const currentLineNumber = editor.getCurrentPosition().lineNumber;
-
- if (hasOneOfIn(obj)) {
- // Get the line number of value that should provide different templates based on that
- const startLine = getStartLineNumber(currentLineNumber, obj.__one_of);
- // Join line values from start to current line
- const lines = editor.getLines(startLine, currentLineNumber).join('\n');
- // Get the correct template by comparing the autocomplete rules against the lines
- const prop = getProperty(lines, obj.__one_of);
- if (prop && prop.__template) {
- return prop.__template;
- }
- }
- }
- }
-
- /**
- * Check if object has a property of '__one_of'
- */
- function hasOneOfIn(value: unknown): value is { __one_of: DataAutoCompleteRulesOneOf[] } {
- return typeof value === 'object' && value !== null && '__one_of' in value;
- }
-
- /**
- * Get the start line of value that matches the autocomplete rules condition
- */
- function getStartLineNumber(currentLine: number, rules: DataAutoCompleteRulesOneOf[]): number {
- if (currentLine === 1) {
- return currentLine;
- }
- const value = editor.getLineValue(currentLine);
- const prop = getProperty(value, rules);
- if (prop) {
- return currentLine;
- }
- return getStartLineNumber(currentLine - 1, rules);
- }
-
- /**
- * Get the matching property based on the given condition
- */
- function getProperty(condition: string, rules: DataAutoCompleteRulesOneOf[]) {
- return rules.find((rule) => {
- if (rule.__condition && rule.__condition.lines_regex) {
- return new RegExp(rule.__condition.lines_regex, 'm').test(condition);
- }
- return false;
- });
- }
-
- function applyTerm(term: ResultTerm) {
- const context = term.context!;
-
- if (context?.endpoint && term.value) {
- const { data_autocomplete_rules: autocompleteRules } = context.endpoint;
- const template = getConditionalTemplate(term.value, autocompleteRules);
- if (template) {
- term.template = template;
- }
- }
- // make sure we get up to date replacement info.
- addReplacementInfoToContext(context, editor.getCurrentPosition(), term.insertValue);
-
- let termAsString;
- if (context.autoCompleteType === 'body') {
- termAsString =
- typeof term.insertValue === 'string' ? '"' + term.insertValue + '"' : term.insertValue + '';
- if (term.insertValue === '[' || term.insertValue === '{') {
- termAsString = '';
- }
- } else {
- termAsString = term.insertValue + '';
- }
-
- let valueToInsert = termAsString;
- let templateInserted = false;
- if (context.addTemplate && !_.isUndefined(term.template) && !_.isNull(term.template)) {
- let indentedTemplateLines;
- // In order to allow triple quoted strings in template completion we check the `__raw_`
- // attribute to determine whether this template should go through JSON formatting.
- if (term.template.__raw && term.template.value) {
- indentedTemplateLines = term.template.value.split('\n');
- } else {
- indentedTemplateLines = utils.jsonToString(term.template, true).split('\n');
- }
- let currentIndentation = editor.getLineValue(context.rangeToReplace!.start.lineNumber);
- currentIndentation = currentIndentation.match(/^\s*/)![0];
- for (
- let i = 1;
- i < indentedTemplateLines.length;
- i++ // skip first line
- ) {
- indentedTemplateLines[i] = currentIndentation + indentedTemplateLines[i];
- }
-
- valueToInsert += ': ' + indentedTemplateLines.join('\n');
- templateInserted = true;
- } else {
- templateInserted = true;
- if (term.value === '[') {
- valueToInsert += '[]';
- } else if (term.value === '{') {
- valueToInsert += '{}';
- } else {
- templateInserted = false;
- }
- }
- const linesToMoveDown = (context.prefixToAdd ?? '').match(/\n|\r/g)?.length ?? 0;
-
- let prefix = context.prefixToAdd ?? '';
-
- // disable listening to the changes we are making.
- editor.off('changeSelection', editorChangeListener);
-
- // if should add chars on the previous not empty line
- if (linesToMoveDown) {
- const [firstPart = '', ...prefixPieces] = context.prefixToAdd?.split(/\n|\r/g) ?? [];
- const lastPart = _.last(prefixPieces) ?? '';
- const { start } = context.rangeToReplace!;
- const end = { ...start, column: start.column + firstPart.length };
-
- // adding only the content of prefix before newlines
- editor.replace({ start, end }, firstPart);
-
- // replacing prefix pieces without the last one, which is handled separately
- if (prefixPieces.length - 1 > 0) {
- replaceLinesWithPrefixPieces(prefixPieces, start.lineNumber);
- }
-
- // and the last prefix line, keeping the editor's own newlines.
- prefix = lastPart;
- context.rangeToReplace!.start.lineNumber = context.rangeToReplace!.end.lineNumber;
- context.rangeToReplace!.start.column = 0;
- }
-
- valueToInsert = prefix + valueToInsert + context.suffixToAdd;
-
- if (context.rangeToReplace!.start.column !== context.rangeToReplace!.end.column) {
- editor.replace(context.rangeToReplace!, valueToInsert);
- } else {
- editor.insert(valueToInsert);
- }
-
- editor.clearSelection(); // for some reason the above changes selection
-
- // go back to see whether we have one of ( : { & [ do not require a comma. All the rest do.
- let newPos = {
- lineNumber: context.rangeToReplace!.start.lineNumber,
- column:
- context.rangeToReplace!.start.column +
- termAsString.length +
- prefix.length +
- (templateInserted ? 0 : context.suffixToAdd!.length),
- };
-
- const tokenIter = createTokenIterator({
- editor,
- position: newPos,
- });
-
- if (context.autoCompleteType === 'body') {
- // look for the next place stand, just after a comma, {
- let nonEmptyToken = parser.nextNonEmptyToken(tokenIter);
- switch (nonEmptyToken ? nonEmptyToken.type : 'NOTOKEN') {
- case 'paren.rparen':
- newPos = tokenIter.getCurrentPosition();
- break;
- case 'punctuation.colon':
- nonEmptyToken = parser.nextNonEmptyToken(tokenIter);
- if ((nonEmptyToken || ({} as Token)).type === 'paren.lparen') {
- nonEmptyToken = parser.nextNonEmptyToken(tokenIter);
- newPos = tokenIter.getCurrentPosition();
- if (nonEmptyToken && nonEmptyToken.value.indexOf('"') === 0) {
- newPos.column++;
- } // don't stand on "
- }
- break;
- case 'paren.lparen':
- case 'punctuation.comma':
- tokenIter.stepForward();
- newPos = tokenIter.getCurrentPosition();
- break;
- }
- editor.moveCursorToPosition(newPos);
- }
-
- // re-enable listening to typing
- editor.on('changeSelection', editorChangeListener);
- }
-
- function getAutoCompleteContext(ctxEditor: CoreEditor, pos: Position) {
- // deduces all the parameters need to position and insert the auto complete
- const context: AutoCompleteContext = {
- autoCompleteSet: null, // instructions for what can be here
- endpoint: null,
- urlPath: null,
- method: null,
- activeScheme: null,
- editor: ctxEditor,
- };
-
- // context.updatedForToken = session.getTokenAt(pos.row, pos.column);
- //
- // if (!context.updatedForToken)
- // context.updatedForToken = { value: "", start: pos.column }; // empty line
- //
- // context.updatedForToken.row = pos.row; // extend
-
- context.autoCompleteType = getAutoCompleteType(pos);
- switch (context.autoCompleteType) {
- case 'path':
- addPathAutoCompleteSetToContext(context, pos);
- break;
- case 'url_params':
- addUrlParamsAutoCompleteSetToContext(context, pos);
- break;
- case 'method':
- addMethodAutoCompleteSetToContext(context);
- break;
- case 'body':
- addBodyAutoCompleteSetToContext(context, pos);
- break;
- default:
- return null;
- }
-
- const isMappingsFetchingInProgress =
- context.autoCompleteType === 'body' && !!context.asyncResultsState?.isLoading;
-
- if (!context.autoCompleteSet && !isMappingsFetchingInProgress) {
- tracer('nothing to do..', context);
- return null;
- }
-
- addReplacementInfoToContext(context, pos);
-
- context.createdWithToken = _.clone(context.updatedForToken);
-
- return context;
- }
-
- function getAutoCompleteType(pos: Position) {
- // return "method", "path" or "body" to determine auto complete type.
-
- let rowMode = parser.getRowParseMode();
-
- // eslint-disable-next-line no-bitwise
- if (rowMode & parser.MODE.IN_REQUEST) {
- return 'body';
- }
- // eslint-disable-next-line no-bitwise
- if (rowMode & parser.MODE.REQUEST_START) {
- // on url path, url params or method.
- const tokenIter = createTokenIterator({
- editor,
- position: pos,
- });
- let t = tokenIter.getCurrentToken();
-
- while (t!.type === 'url.comma') {
- t = tokenIter.stepBackward();
- }
- switch (t!.type) {
- case 'method':
- return 'method';
- case 'whitespace':
- t = parser.prevNonEmptyToken(tokenIter);
-
- switch ((t || ({} as Token)).type) {
- case 'method':
- // we moved one back
- return 'path';
- break;
- default:
- if (isUrlPathToken(t)) {
- return 'path';
- }
- if (isUrlParamsToken(t)) {
- return 'url_params';
- }
- return null;
- }
- break;
- default:
- if (isUrlPathToken(t)) {
- return 'path';
- }
- if (isUrlParamsToken(t)) {
- return 'url_params';
- }
- return null;
- }
- }
-
- // after start to avoid single line url only requests
- // eslint-disable-next-line no-bitwise
- if (rowMode & parser.MODE.REQUEST_END) {
- return 'body';
- }
-
- // in between request on an empty
- if (editor.getLineValue(pos.lineNumber).trim() === '') {
- // check if the previous line is a single line beginning of a new request
- rowMode = parser.getRowParseMode(pos.lineNumber - 1);
- if (
- // eslint-disable-next-line no-bitwise
- rowMode & parser.MODE.REQUEST_START &&
- // eslint-disable-next-line no-bitwise
- rowMode & parser.MODE.REQUEST_END
- ) {
- return 'body';
- }
- // o.w suggest a method
- return 'method';
- }
-
- return null;
- }
-
- function addReplacementInfoToContext(
- context: AutoCompleteContext,
- pos: Position,
- replacingTerm?: unknown
- ) {
- // extract the initial value, rangeToReplace & textBoxPosition
-
- // Scenarios for current token:
- // - Nice token { "bla|"
- // - Broken text token { bla|
- // - No token : { |
- // - Broken scenario { , bla|
- // - Nice token, broken before: {, "bla"
-
- context.updatedForToken = _.clone(
- editor.getTokenAt({ lineNumber: pos.lineNumber, column: pos.column })
- );
- if (!context.updatedForToken) {
- context.updatedForToken = {
- value: '',
- type: '',
- position: { column: pos.column, lineNumber: pos.lineNumber },
- };
- } // empty line
-
- let anchorToken = context.createdWithToken;
- if (!anchorToken) {
- anchorToken = context.updatedForToken;
- }
-
- switch (context.updatedForToken.type) {
- case 'variable':
- case 'string':
- case 'text':
- case 'constant.numeric':
- case 'constant.language.boolean':
- case 'method':
- case 'url.index':
- case 'url.type':
- case 'url.id':
- case 'url.method':
- case 'url.endpoint':
- case 'url.part':
- case 'url.param':
- case 'url.value':
- context.rangeToReplace = {
- start: { lineNumber: pos.lineNumber, column: anchorToken.position.column },
- end: {
- lineNumber: pos.lineNumber,
- column: context.updatedForToken.position.column + context.updatedForToken.value.length,
- },
- } as Range;
- context.replacingToken = true;
- break;
- default:
- if (replacingTerm && context.updatedForToken.value === replacingTerm) {
- context.rangeToReplace = {
- start: { lineNumber: pos.lineNumber, column: anchorToken.position.column },
- end: {
- lineNumber: pos.lineNumber,
- column:
- context.updatedForToken.position.column + context.updatedForToken.value.length,
- },
- } as Range;
- context.replacingToken = true;
- } else {
- // standing on white space, quotes or another punctuation - no replacing
- context.rangeToReplace = {
- start: { lineNumber: pos.lineNumber, column: pos.column },
- end: { lineNumber: pos.lineNumber, column: pos.column },
- } as Range;
- context.replacingToken = false;
- }
- break;
- }
-
- context.textBoxPosition = {
- lineNumber: context.rangeToReplace.start.lineNumber,
- column: context.rangeToReplace.start.column,
- };
-
- switch (context.autoCompleteType) {
- case 'path':
- addPathPrefixSuffixToContext(context);
- break;
- case 'url_params':
- addUrlParamsPrefixSuffixToContext(context);
- break;
- case 'method':
- addMethodPrefixSuffixToContext(context);
- break;
- case 'body':
- addBodyPrefixSuffixToContext(context);
- break;
- }
- }
-
- function addCommaToPrefixOnAutocomplete(
- nonEmptyToken: Token | null,
- context: AutoCompleteContext,
- charsToSkipOnSameLine: number = 1
- ) {
- if (nonEmptyToken && nonEmptyToken.type.indexOf('url') < 0) {
- const { position } = nonEmptyToken;
- // if not on the first line
- if (context.rangeToReplace && context.rangeToReplace.start?.lineNumber > 1) {
- const prevTokenLineNumber = position.lineNumber;
- const editorFromContext = context.editor as CoreEditor | undefined;
- const line = editorFromContext?.getLineValue(prevTokenLineNumber) ?? '';
- const prevLineLength = line.length;
- const linesToEnter = context.rangeToReplace.end.lineNumber - prevTokenLineNumber;
-
- const isTheSameLine = linesToEnter === 0;
- let startColumn = prevLineLength + 1;
- let spaces = context.rangeToReplace.start.column - 1;
-
- if (isTheSameLine) {
- // prevent last char line from replacing
- startColumn = position.column + charsToSkipOnSameLine;
- // one char for pasted " and one for ,
- spaces = context.rangeToReplace.end.column - startColumn - 2;
- }
-
- // go back to the end of the previous line
- context.rangeToReplace = {
- start: { lineNumber: prevTokenLineNumber, column: startColumn },
- end: { ...context.rangeToReplace.end },
- };
-
- spaces = spaces >= 0 ? spaces : 0;
- const spacesToEnter = isTheSameLine ? (spaces === 0 ? 1 : spaces) : spaces;
- const newLineChars = `\n`.repeat(linesToEnter >= 0 ? linesToEnter : 0);
- const whitespaceChars = ' '.repeat(spacesToEnter);
- // add a comma at the end of the previous line, a new line and indentation
- context.prefixToAdd = `,${newLineChars}${whitespaceChars}`;
- }
- }
- }
-
- function addBodyPrefixSuffixToContext(context: AutoCompleteContext) {
- // Figure out what happens next to the token to see whether it needs trailing commas etc.
-
- // Templates will be used if not destroying existing structure.
- // -> token : {} or token ]/} or token , but not token : SOMETHING ELSE
-
- context.prefixToAdd = '';
- context.suffixToAdd = '';
-
- let tokenIter = createTokenIterator({
- editor,
- position: editor.getCurrentPosition()!,
- });
- let nonEmptyToken = parser.nextNonEmptyToken(tokenIter);
- switch (nonEmptyToken ? nonEmptyToken.type : 'NOTOKEN') {
- case 'NOTOKEN':
- case 'paren.lparen':
- case 'paren.rparen':
- case 'punctuation.comma':
- context.addTemplate = true;
- break;
- case 'punctuation.colon':
- // test if there is an empty object - if so we replace it
- context.addTemplate = false;
-
- nonEmptyToken = parser.nextNonEmptyToken(tokenIter);
- if (!(nonEmptyToken && nonEmptyToken.value === '{')) {
- break;
- }
- nonEmptyToken = parser.nextNonEmptyToken(tokenIter);
- if (!(nonEmptyToken && nonEmptyToken.value === '}')) {
- break;
- }
- context.addTemplate = true;
- // extend range to replace to include all up to token
- context.rangeToReplace!.end.lineNumber = tokenIter.getCurrentTokenLineNumber() as number;
- context.rangeToReplace!.end.column =
- (tokenIter.getCurrentTokenColumn() as number) + nonEmptyToken.value.length;
-
- // move one more time to check if we need a trailing comma
- nonEmptyToken = parser.nextNonEmptyToken(tokenIter);
- switch (nonEmptyToken ? nonEmptyToken.type : 'NOTOKEN') {
- case 'NOTOKEN':
- case 'paren.rparen':
- case 'punctuation.comma':
- case 'punctuation.colon':
- break;
- default:
- context.suffixToAdd = ', ';
- }
-
- break;
- default:
- context.addTemplate = true;
- context.suffixToAdd = ', ';
- break; // for now play safe and do nothing. May be made smarter.
- }
-
- // go back to see whether we have one of ( : { & [ do not require a comma. All the rest do.
- tokenIter = createTokenIterator({ editor, position: editor.getCurrentPosition() });
- nonEmptyToken = tokenIter.getCurrentToken();
- let insertingRelativeToToken; // -1 is before token, 0 middle, +1 after token
- if (context.replacingToken) {
- insertingRelativeToToken = 0;
- } else {
- const pos = editor.getCurrentPosition();
- if (pos.column === context.updatedForToken!.position.column) {
- insertingRelativeToToken = -1;
- } else if (
- pos.column <
- context.updatedForToken!.position.column + context.updatedForToken!.value.length
- ) {
- insertingRelativeToToken = 0;
- } else {
- insertingRelativeToToken = 1;
- }
- }
- // we should actually look at what's happening before this token
- if (parser.isEmptyToken(nonEmptyToken) || insertingRelativeToToken <= 0) {
- nonEmptyToken = parser.prevNonEmptyToken(tokenIter);
- }
-
- switch (nonEmptyToken ? nonEmptyToken.type : 'NOTOKEN') {
- case 'NOTOKEN':
- case 'paren.lparen':
- case 'punctuation.comma':
- case 'punctuation.colon':
- case 'punctuation.start_triple_quote':
- case 'method':
- break;
- case 'text':
- case 'string':
- case 'constant.numeric':
- case 'constant.language.boolean':
- case 'punctuation.end_triple_quote':
- addCommaToPrefixOnAutocomplete(nonEmptyToken, context, nonEmptyToken?.value.length);
- break;
- default:
- addCommaToPrefixOnAutocomplete(nonEmptyToken, context);
- break;
- }
-
- return context;
- }
-
- function addUrlParamsPrefixSuffixToContext(context: AutoCompleteContext) {
- context.prefixToAdd = '';
- context.suffixToAdd = '';
- }
-
- function addMethodPrefixSuffixToContext(context: AutoCompleteContext) {
- context.prefixToAdd = '';
- context.suffixToAdd = '';
- const tokenIter = createTokenIterator({ editor, position: editor.getCurrentPosition() });
- const lineNumber = tokenIter.getCurrentPosition().lineNumber;
- const t = parser.nextNonEmptyToken(tokenIter);
-
- if (tokenIter.getCurrentPosition().lineNumber !== lineNumber || !t) {
- // we still have nothing next to the method, add a space..
- context.suffixToAdd = ' ';
- }
- }
-
- function addPathPrefixSuffixToContext(context: AutoCompleteContext) {
- context.prefixToAdd = '';
- context.suffixToAdd = '';
- }
-
- function addMethodAutoCompleteSetToContext(context: AutoCompleteContext) {
- context.autoCompleteSet = ['GET', 'PUT', 'POST', 'DELETE', 'HEAD', 'PATCH'].map((m, i) => ({
- name: m,
- score: -i,
- meta: i18n.translate('console.autocomplete.addMethodMetaText', { defaultMessage: 'method' }),
- }));
- }
-
- function addPathAutoCompleteSetToContext(context: AutoCompleteContext, pos: Position) {
- const ret = getCurrentMethodAndTokenPaths(editor, pos, parser);
- context.method = ret.method?.toUpperCase();
- context.token = ret.token;
- context.otherTokenValues = ret.otherTokenValues;
- context.urlTokenPath = ret.urlTokenPath;
-
- const components = getTopLevelUrlCompleteComponents(context.method);
- let urlTokenPath = context.urlTokenPath;
- let predicate: (term: ResultTerm) => boolean = () => true;
-
- const tokenIter = createTokenIterator({ editor, position: pos });
- const currentTokenType = tokenIter.getCurrentToken()?.type;
- const previousTokenType = tokenIter.stepBackward()?.type;
- if (!Array.isArray(urlTokenPath)) {
- // skip checks for url.comma
- } else if (previousTokenType === 'url.comma' && currentTokenType === 'url.comma') {
- predicate = () => false; // two consecutive commas empty the autocomplete
- } else if (
- (previousTokenType === 'url.part' && currentTokenType === 'url.comma') ||
- (previousTokenType === 'url.slash' && currentTokenType === 'url.comma') ||
- (previousTokenType === 'url.comma' && currentTokenType === 'url.part')
- ) {
- const lastUrlTokenPath = _.last(urlTokenPath) || []; // ['c', 'd'] from 'GET /a/b/c,d,'
- const constantComponents = _.filter(components, (c) => c instanceof ConstantComponent);
- const constantComponentNames = _.map(constantComponents, 'name');
-
- // check if neither 'c' nor 'd' is a constant component name such as '_search'
- if (_.every(lastUrlTokenPath, (token) => !_.includes(constantComponentNames, token))) {
- urlTokenPath = urlTokenPath.slice(0, -1); // drop the last 'c,d,' part from the url path
- predicate = (term) => term.meta === 'index'; // limit the autocomplete to indices only
- }
- }
-
- populateContext(urlTokenPath, context, editor, true, components);
- context.autoCompleteSet = _.filter(
- addMetaToTermsList(context.autoCompleteSet!, 'endpoint'),
- predicate
- );
- }
-
- function addUrlParamsAutoCompleteSetToContext(context: AutoCompleteContext, pos: Position) {
- const ret = getCurrentMethodAndTokenPaths(editor, pos, parser);
- context.method = ret.method;
- context.otherTokenValues = ret.otherTokenValues;
- context.urlTokenPath = ret.urlTokenPath;
- if (!ret.urlTokenPath) {
- // zero length tokenPath is true
-
- return context;
- }
-
- populateContext(
- ret.urlTokenPath,
- context,
- editor,
- false,
- getTopLevelUrlCompleteComponents(context.method)
- );
-
- if (!context.endpoint) {
- return context;
- }
-
- if (!ret.urlParamsTokenPath) {
- // zero length tokenPath is true
- return context;
- }
- let tokenPath: string[] = [];
- const currentParam = ret.urlParamsTokenPath.pop();
- if (currentParam) {
- tokenPath = Object.keys(currentParam); // single key object
- context.otherTokenValues = currentParam[tokenPath[0]];
- }
-
- populateContext(
- tokenPath,
- context,
- editor,
- true,
- context.endpoint.paramsAutocomplete.getTopLevelComponents(context.method)
- );
- return context;
- }
-
- function addBodyAutoCompleteSetToContext(context: AutoCompleteContext, pos: Position) {
- const ret = getCurrentMethodAndTokenPaths(editor, pos, parser);
- context.method = ret.method;
- context.otherTokenValues = ret.otherTokenValues;
- context.urlTokenPath = ret.urlTokenPath;
- context.requestStartRow = ret.requestStartRow;
- if (!ret.urlTokenPath) {
- // zero length tokenPath is true
- return context;
- }
-
- populateContext(
- ret.urlTokenPath,
- context,
- editor,
- false,
- getTopLevelUrlCompleteComponents(context.method)
- );
-
- context.bodyTokenPath = ret.bodyTokenPath;
- if (!ret.bodyTokenPath) {
- // zero length tokenPath is true
-
- return context;
- }
-
- const t = editor.getTokenAt(pos);
- if (t && t.type === 'punctuation.end_triple_quote' && pos.column !== t.position.column + 3) {
- // skip to populate context as the current position is not on the edge of end_triple_quote
- return context;
- }
-
- // needed for scope linking + global term resolving
- context.endpointComponentResolver = getEndpointBodyCompleteComponents;
- context.globalComponentResolver = getGlobalAutocompleteComponents;
- let components: unknown;
- if (context.endpoint) {
- components = context.endpoint.bodyAutocompleteRootComponents;
- } else {
- components = getUnmatchedEndpointComponents();
- }
- populateContext(ret.bodyTokenPath, context, editor, true, components);
-
- return context;
- }
-
- const evaluateCurrentTokenAfterAChange = _.debounce(function evaluateCurrentTokenAfterAChange(
- pos: Position
- ) {
- let currentToken = editor.getTokenAt(pos)!;
- tracer('has started evaluating current token', currentToken);
-
- if (!currentToken) {
- lastEvaluatedToken = null;
- currentToken = { position: { column: 0, lineNumber: 0 }, value: '', type: '' }; // empty row
- }
-
- currentToken.position.lineNumber = pos.lineNumber; // extend token with row. Ace doesn't supply it by default
- if (parser.isEmptyToken(currentToken)) {
- // empty token. check what's coming next
- const nextToken = editor.getTokenAt({ ...pos, column: pos.column + 1 })!;
- if (parser.isEmptyToken(nextToken)) {
- // Empty line, or we're not on the edge of current token. Save the current position as base
- currentToken.position.column = pos.column;
- lastEvaluatedToken = currentToken;
- } else {
- nextToken.position.lineNumber = pos.lineNumber;
- lastEvaluatedToken = nextToken;
- }
- tracer('not starting autocomplete due to empty current token');
- return;
- }
-
- if (!lastEvaluatedToken) {
- lastEvaluatedToken = currentToken;
- tracer('not starting autocomplete due to invalid last evaluated token');
- return; // wait for the next typing.
- }
-
- if (!looksLikeTypingIn(lastEvaluatedToken, currentToken, editor)) {
- tracer('not starting autocomplete', lastEvaluatedToken, '->', currentToken);
- // not on the same place or nothing changed, cache and wait for the next time
- lastEvaluatedToken = currentToken;
- return;
- }
-
- // don't automatically open the auto complete if some just hit enter (new line) or open a parentheses
- switch (currentToken.type || 'UNKNOWN') {
- case 'paren.lparen':
- case 'paren.rparen':
- case 'punctuation.colon':
- case 'punctuation.comma':
- case 'comment.line':
- case 'comment.punctuation':
- case 'comment.block':
- case 'UNKNOWN':
- tracer('not starting autocomplete for current token type', currentToken.type);
- return;
- }
-
- tracer('starting autocomplete', lastEvaluatedToken, '->', currentToken);
- lastEvaluatedToken = currentToken;
- editor.execCommand('startAutocomplete');
- },
- 100);
-
- function editorChangeListener() {
- const position = editor.getCurrentPosition();
- tracer('editor changed', position);
- if (position && !editor.isCompleterActive()) {
- tracer('will start evaluating current token');
- evaluateCurrentTokenAfterAChange(position);
- }
- }
-
- /**
- * Extracts terms from the autocomplete set.
- * @param context
- */
- function getTerms(context: AutoCompleteContext, autoCompleteSet: ResultTerm[]) {
- const terms = _.map(
- autoCompleteSet.filter((term) => Boolean(term) && term.name != null),
- function (term) {
- if (typeof term !== 'object') {
- term = {
- name: term,
- };
- } else {
- term = _.clone(term);
- }
- const defaults: {
- value?: string;
- meta: string;
- score: number;
- context: AutoCompleteContext;
- completer?: { insertMatch: (v: unknown) => void };
- } = {
- value: term.name + '',
- meta: 'API',
- score: 0,
- context,
- };
- // we only need our custom insertMatch behavior for the body
- if (context.autoCompleteType === 'body') {
- defaults.completer = {
- insertMatch() {
- return applyTerm(term);
- },
- };
- }
- return _.defaults(term, defaults);
- }
- );
-
- terms.sort(function (
- t1: { score: number; name?: string | boolean },
- t2: { score: number; name?: string | boolean }
- ) {
- /* score sorts from high to low */
- if (t1.score > t2.score) {
- return -1;
- }
- if (t1.score < t2.score) {
- return 1;
- }
- /* names sort from low to high */
- if (t1.name! < t2.name!) {
- return -1;
- }
- if (t1.name === t2.name) {
- return 0;
- }
- return 1;
- });
-
- return terms;
- }
-
- function getSuggestions(terms: ResultTerm[]) {
- return _.map(terms, function (t, i) {
- t.insertValue = t.insertValue || t.value;
- t.value = '' + t.value; // normalize to strings
- t.score = -i;
- return t;
- });
- }
-
- function getCompletions(
- position: Position,
- prefix: string,
- callback: (e: Error | null, result: ResultTerm[] | null) => void,
- annotationControls: {
- setAnnotation: (text: string) => void;
- removeAnnotation: () => void;
- }
- ) {
- try {
- const context = getAutoCompleteContext(editor, position);
-
- if (!context) {
- tracer('zero suggestions due to invalid autocomplete context');
- callback(null, []);
- } else {
- if (!context.asyncResultsState?.isLoading) {
- const terms = getTerms(context, context.autoCompleteSet!);
- const suggestions = getSuggestions(terms);
- tracer(suggestions?.length ?? 0, 'suggestions');
- callback(null, suggestions);
- }
-
- if (context.asyncResultsState) {
- annotationControls.setAnnotation(
- i18n.translate('console.autocomplete.fieldsFetchingAnnotation', {
- defaultMessage: 'Fields fetching is in progress',
- })
- );
-
- context.asyncResultsState.results.then((r) => {
- const asyncSuggestions = getSuggestions(getTerms(context, r));
- tracer(asyncSuggestions?.length ?? 0, 'async suggestions');
- callback(null, asyncSuggestions);
- annotationControls.removeAnnotation();
- });
- }
- }
- } catch (e) {
- // eslint-disable-next-line no-console
- console.error(e);
- callback(e, null);
- }
- }
-
- editor.on('changeSelection', editorChangeListener);
-
- return {
- getCompletions,
- // TODO: This needs to be cleaned up
- _test: {
- getCompletions: (
- _editor: unknown,
- _editSession: unknown,
- pos: Position,
- prefix: string,
- callback: (e: Error | null, result: ResultTerm[] | null) => void,
- annotationControls: {
- setAnnotation: (text: string) => void;
- removeAnnotation: () => void;
- }
- ) => getCompletions(pos, prefix, callback, annotationControls),
- addReplacementInfoToContext,
- addChangeListener: () => editor.on('changeSelection', editorChangeListener),
- removeChangeListener: () => editor.off('changeSelection', editorChangeListener),
- },
- };
-}
diff --git a/src/plugins/console/public/lib/autocomplete/get_endpoint_from_position.ts b/src/plugins/console/public/lib/autocomplete/get_endpoint_from_position.ts
deleted file mode 100644
index b65e277e41723..0000000000000
--- a/src/plugins/console/public/lib/autocomplete/get_endpoint_from_position.ts
+++ /dev/null
@@ -1,33 +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", 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 { CoreEditor, Position } from '../../types';
-import { getCurrentMethodAndTokenPaths } from './autocomplete';
-import type RowParser from '../row_parser';
-
-import { getTopLevelUrlCompleteComponents } from '../kb/kb';
-import { populateContext } from './engine';
-
-export function getEndpointFromPosition(editor: CoreEditor, pos: Position, parser: RowParser) {
- const lineValue = editor.getLineValue(pos.lineNumber);
- const context = {
- ...getCurrentMethodAndTokenPaths(
- editor,
- {
- column: lineValue.length + 1 /* Go to the very end of the line */,
- lineNumber: pos.lineNumber,
- },
- parser,
- true
- ),
- };
- const components = getTopLevelUrlCompleteComponents(context.method);
- populateContext(context.urlTokenPath, context, editor, true, components);
- return context.endpoint;
-}
diff --git a/src/plugins/console/public/lib/autocomplete/looks_like_typing_in.test.ts b/src/plugins/console/public/lib/autocomplete/looks_like_typing_in.test.ts
deleted file mode 100644
index 101fd96a79024..0000000000000
--- a/src/plugins/console/public/lib/autocomplete/looks_like_typing_in.test.ts
+++ /dev/null
@@ -1,224 +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", 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 '../../application/models/sense_editor/sense_editor.test.mocks';
-
-import { looksLikeTypingIn } from './looks_like_typing_in';
-import { create } from '../../application/models';
-import type { SenseEditor } from '../../application/models';
-import type { CoreEditor, Position, Token, TokensProvider } from '../../types';
-
-describe('looksLikeTypingIn', () => {
- let editor: SenseEditor;
- let coreEditor: CoreEditor;
- let tokenProvider: TokensProvider;
-
- beforeEach(() => {
- document.body.innerHTML = ``;
- editor = create(document.getElementById('ConAppEditor')!);
- coreEditor = editor.getCoreEditor();
- tokenProvider = coreEditor.getTokenProvider();
- });
-
- afterEach(async () => {
- await editor.update('', true);
- });
-
- describe('general typing in', () => {
- interface RunTestArgs {
- preamble: string;
- autocomplete?: string;
- input: string;
- }
-
- const runTest = async ({ preamble, autocomplete, input }: RunTestArgs) => {
- const pos: Position = { lineNumber: 1, column: 1 };
-
- await editor.update(preamble, true);
- pos.column += preamble.length;
- const lastEvaluatedToken = tokenProvider.getTokenAt(pos);
-
- if (autocomplete !== undefined) {
- await editor.update(coreEditor.getValue() + autocomplete, true);
- pos.column += autocomplete.length;
- }
-
- await editor.update(coreEditor.getValue() + input, true);
- pos.column += input.length;
- const currentToken = tokenProvider.getTokenAt(pos);
-
- expect(lastEvaluatedToken).not.toBeNull();
- expect(currentToken).not.toBeNull();
- expect(looksLikeTypingIn(lastEvaluatedToken!, currentToken!, coreEditor)).toBe(true);
- };
-
- const cases: RunTestArgs[] = [
- { preamble: 'G', input: 'E' },
- { preamble: 'GET .kibana', input: '/' },
- { preamble: 'GET .kibana', input: ',' },
- { preamble: 'GET .kibana', input: '?' },
- { preamble: 'GET .kibana/', input: '_' },
- { preamble: 'GET .kibana/', input: '?' },
- { preamble: 'GET .kibana,', input: '.' },
- { preamble: 'GET .kibana,', input: '?' },
- { preamble: 'GET .kibana?', input: 'k' },
- { preamble: 'GET .kibana?k', input: '=' },
- { preamble: 'GET .kibana?k=', input: 'v' },
- { preamble: 'GET .kibana?k=v', input: '&' },
- { preamble: 'GET .kibana?k', input: '&' },
- { preamble: 'GET .kibana?k&', input: 'k' },
- { preamble: 'GET ', autocomplete: '.kibana', input: '/' },
- { preamble: 'GET ', autocomplete: '.kibana', input: ',' },
- { preamble: 'GET ', autocomplete: '.kibana', input: '?' },
- { preamble: 'GET .ki', autocomplete: 'bana', input: '/' },
- { preamble: 'GET .ki', autocomplete: 'bana', input: ',' },
- { preamble: 'GET .ki', autocomplete: 'bana', input: '?' },
- { preamble: 'GET _nodes/', autocomplete: 'stats', input: '/' },
- { preamble: 'GET _nodes/sta', autocomplete: 'ts', input: '/' },
- { preamble: 'GET _nodes/', autocomplete: 'jvm', input: ',' },
- { preamble: 'GET _nodes/j', autocomplete: 'vm', input: ',' },
- { preamble: 'GET _nodes/jvm,', autocomplete: 'os', input: ',' },
- { preamble: 'GET .kibana,', autocomplete: '.security', input: ',' },
- { preamble: 'GET .kibana,.sec', autocomplete: 'urity', input: ',' },
- { preamble: 'GET .kibana,', autocomplete: '.security', input: '/' },
- { preamble: 'GET .kibana,.sec', autocomplete: 'urity', input: '/' },
- { preamble: 'GET .kibana,', autocomplete: '.security', input: '?' },
- { preamble: 'GET .kibana,.sec', autocomplete: 'urity', input: '?' },
- { preamble: 'GET .kibana/', autocomplete: '_search', input: '?' },
- { preamble: 'GET .kibana/_se', autocomplete: 'arch', input: '?' },
- { preamble: 'GET .kibana/_search?', autocomplete: 'expand_wildcards', input: '=' },
- { preamble: 'GET .kibana/_search?exp', autocomplete: 'and_wildcards', input: '=' },
- { preamble: 'GET .kibana/_search?expand_wildcards=', autocomplete: 'all', input: '&' },
- { preamble: 'GET .kibana/_search?expand_wildcards=a', autocomplete: 'll', input: '&' },
- { preamble: 'GET _cat/indices?s=index&', autocomplete: 'expand_wildcards', input: '=' },
- { preamble: 'GET _cat/indices?s=index&exp', autocomplete: 'and_wildcards', input: '=' },
- { preamble: 'GET _cat/indices?v&', autocomplete: 'expand_wildcards', input: '=' },
- { preamble: 'GET _cat/indices?v&exp', autocomplete: 'and_wildcards', input: '=' },
- // autocomplete skips one iteration of token evaluation if user types in every letter
- { preamble: 'GET .kibana', autocomplete: '/', input: '_' }, // token '/' may not be evaluated
- { preamble: 'GET .kibana', autocomplete: ',', input: '.' }, // token ',' may not be evaluated
- { preamble: 'GET .kibana', autocomplete: '?', input: 'k' }, // token '?' may not be evaluated
- ];
- for (const c of cases) {
- const name =
- c.autocomplete === undefined
- ? `'${c.preamble}' -> '${c.input}'`
- : `'${c.preamble}' -> '${c.autocomplete}' (autocomplte) -> '${c.input}'`;
- test(name, async () => runTest(c));
- }
- });
-
- describe('first typing in', () => {
- test(`'' -> 'G'`, () => {
- // this is based on an implementation within the evaluateCurrentTokenAfterAChange function
- const lastEvaluatedToken = { position: { column: 0, lineNumber: 0 }, value: '', type: '' };
- lastEvaluatedToken.position.lineNumber = coreEditor.getCurrentPosition().lineNumber;
-
- const currentToken = { position: { column: 1, lineNumber: 1 }, value: 'G', type: 'method' };
- expect(looksLikeTypingIn(lastEvaluatedToken, currentToken, coreEditor)).toBe(true);
- });
- });
-
- const matrices = [
- `
-GET .kibana/
-
-
-`
- .slice(1, -1)
- .split('\n'),
- `
-
- POST test/_doc
-{"message": "test"}
-
-GET /_cat/indices?v&s=
-
-DE
-`
- .slice(1, -1)
- .split('\n'),
- `
-
-PUT test/_doc/1
-{"field": "value"}
-`
- .slice(1, -1)
- .split('\n'),
- ];
-
- describe('navigating the editor via keyboard arrow keys', () => {
- const runHorizontalZigzagWalkTest = async (matrix: string[]) => {
- const width = matrix[0].length;
- const height = matrix.length;
-
- await editor.update(matrix.join('\n'), true);
- let lastEvaluatedToken = tokenProvider.getTokenAt(coreEditor.getCurrentPosition());
- let currentToken: Token | null;
-
- for (let i = 1; i < height * width * 2; i++) {
- const pos = {
- column: 1 + (i % width),
- lineNumber: 1 + Math.floor(i / width),
- };
- if (pos.lineNumber % 2 === 0) {
- pos.column = width - pos.column + 1;
- }
- if (pos.lineNumber > height) {
- pos.lineNumber = 2 * height - pos.lineNumber + 1;
- }
-
- currentToken = tokenProvider.getTokenAt(pos);
- expect(lastEvaluatedToken).not.toBeNull();
- expect(currentToken).not.toBeNull();
- expect(looksLikeTypingIn(lastEvaluatedToken!, currentToken!, coreEditor)).toBe(false);
- lastEvaluatedToken = currentToken;
- }
- };
-
- for (const matrix of matrices) {
- test(`horizontal zigzag walk ${matrix[0].length}x${matrix.length} map`, () =>
- runHorizontalZigzagWalkTest(matrix));
- }
- });
-
- describe('clicking around the editor', () => {
- const runRandomClickingTest = async (matrix: string[], attempts: number) => {
- const width = matrix[0].length;
- const height = matrix.length;
-
- await editor.update(matrix.join('\n'), true);
- let lastEvaluatedToken = tokenProvider.getTokenAt(coreEditor.getCurrentPosition());
- let currentToken: Token | null;
-
- for (let i = 1; i < attempts; i++) {
- const pos = {
- column: Math.ceil(Math.random() * width),
- lineNumber: Math.ceil(Math.random() * height),
- };
-
- currentToken = tokenProvider.getTokenAt(pos);
- expect(lastEvaluatedToken).not.toBeNull();
- expect(currentToken).not.toBeNull();
- expect(looksLikeTypingIn(lastEvaluatedToken!, currentToken!, coreEditor)).toBe(false);
- lastEvaluatedToken = currentToken;
- }
- };
-
- for (const matrix of matrices) {
- const attempts = 4 * matrix[0].length * matrix.length;
- test(`random clicking ${matrix[0].length}x${matrix.length} map ${attempts} times`, () =>
- runRandomClickingTest(matrix, attempts));
- }
- });
-});
diff --git a/src/plugins/console/public/lib/autocomplete/looks_like_typing_in.ts b/src/plugins/console/public/lib/autocomplete/looks_like_typing_in.ts
deleted file mode 100644
index a22c985a943f6..0000000000000
--- a/src/plugins/console/public/lib/autocomplete/looks_like_typing_in.ts
+++ /dev/null
@@ -1,109 +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", 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 type { CoreEditor, Position, Token } from '../../types';
-
-enum Move {
- ForwardOneCharacter = 1,
- ForwardOneToken, // the column position may jump to the next token by autocomplete
- ForwardTwoTokens, // the column position could jump two tokens due to autocomplete
-}
-
-const knownTypingInTokenTypes = new Map>>([
- [
- Move.ForwardOneCharacter,
- new Map>([
- // a pair of the last evaluated token type and a set of the current token types
- ['', new Set(['method'])],
- ['url.amp', new Set(['url.param'])],
- ['url.comma', new Set(['url.part', 'url.questionmark'])],
- ['url.equal', new Set(['url.value'])],
- ['url.param', new Set(['url.amp', 'url.equal'])],
- ['url.questionmark', new Set(['url.param'])],
- ['url.slash', new Set(['url.part', 'url.questionmark'])],
- ['url.value', new Set(['url.amp'])],
- ]),
- ],
- [
- Move.ForwardOneToken,
- new Map>([
- ['method', new Set(['url.part'])],
- ['url.amp', new Set(['url.amp', 'url.equal'])],
- ['url.comma', new Set(['url.comma', 'url.questionmark', 'url.slash'])],
- ['url.equal', new Set(['url.amp'])],
- ['url.param', new Set(['url.equal'])],
- ['url.part', new Set(['url.comma', 'url.questionmark', 'url.slash'])],
- ['url.questionmark', new Set(['url.equal'])],
- ['url.slash', new Set(['url.comma', 'url.questionmark', 'url.slash'])],
- ['url.value', new Set(['url.amp'])],
- ['whitespace', new Set(['url.comma', 'url.questionmark', 'url.slash'])],
- ]),
- ],
- [
- Move.ForwardTwoTokens,
- new Map>([['url.part', new Set(['url.param', 'url.part'])]]),
- ],
-]);
-
-const getOneCharacterNextOnTheRight = (pos: Position, coreEditor: CoreEditor): string => {
- const range = {
- start: { column: pos.column + 1, lineNumber: pos.lineNumber },
- end: { column: pos.column + 2, lineNumber: pos.lineNumber },
- };
- return coreEditor.getValueInRange(range);
-};
-
-/**
- * Examines a change from the last evaluated to the current token and one
- * character next to the current token position on the right. Returns true if
- * the change looks like typing in, false otherwise.
- *
- * This function is supposed to filter out situations where autocomplete is not
- * preferable, such as clicking around the editor, navigating the editor via
- * keyboard arrow keys, etc.
- */
-export const looksLikeTypingIn = (
- lastEvaluatedToken: Token,
- currentToken: Token,
- coreEditor: CoreEditor
-): boolean => {
- // if the column position moves to the right in the same line and the current
- // token length is 1, then user is possibly typing in a character.
- if (
- lastEvaluatedToken.position.column < currentToken.position.column &&
- lastEvaluatedToken.position.lineNumber === currentToken.position.lineNumber &&
- currentToken.value.length === 1 &&
- getOneCharacterNextOnTheRight(currentToken.position, coreEditor) === ''
- ) {
- const moves =
- lastEvaluatedToken.position.column + 1 === currentToken.position.column
- ? [Move.ForwardOneCharacter]
- : [Move.ForwardOneToken, Move.ForwardTwoTokens];
- for (const move of moves) {
- const tokenTypesPairs = knownTypingInTokenTypes.get(move) ?? new Map>();
- const currentTokenTypes = tokenTypesPairs.get(lastEvaluatedToken.type) ?? new Set();
- if (currentTokenTypes.has(currentToken.type)) {
- return true;
- }
- }
- }
-
- // if the column or the line number have changed for the last token or
- // user did not provided a new value, then we should not show autocomplete
- // this guards against triggering autocomplete when clicking around the editor
- if (
- lastEvaluatedToken.position.column !== currentToken.position.column ||
- lastEvaluatedToken.position.lineNumber !== currentToken.position.lineNumber ||
- lastEvaluatedToken.value === currentToken.value
- ) {
- return false;
- }
-
- return true;
-};
diff --git a/src/plugins/console/public/lib/autocomplete_entities/autocomplete_entities.test.js b/src/plugins/console/public/lib/autocomplete_entities/autocomplete_entities.test.js
index 5901c95b9a074..0cffb157abb4c 100644
--- a/src/plugins/console/public/lib/autocomplete_entities/autocomplete_entities.test.js
+++ b/src/plugins/console/public/lib/autocomplete_entities/autocomplete_entities.test.js
@@ -7,7 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import '../../application/models/sense_editor/sense_editor.test.mocks';
import { setAutocompleteInfo, AutocompleteInfo } from '../../services';
import { expandAliases } from './expand_aliases';
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
diff --git a/src/plugins/console/public/lib/curl_parsing/__fixtures__/curl_parsing.txt b/src/plugins/console/public/lib/curl_parsing/__fixtures__/curl_parsing.txt
deleted file mode 100644
index b6dd39479550d..0000000000000
--- a/src/plugins/console/public/lib/curl_parsing/__fixtures__/curl_parsing.txt
+++ /dev/null
@@ -1,146 +0,0 @@
-==========
-Curl 1
--------------------------------------
-curl -XPUT 'http://localhost:9200/twitter/tweet/1' -d '{
- "user" : "kimchy",
- "post_date" : "2009-11-15T14:12:12",
- "message" : "trying out Elastic Search"
-}'
--------------------------------------
-PUT /twitter/tweet/1
-{
- "user" : "kimchy",
- "post_date" : "2009-11-15T14:12:12",
- "message" : "trying out Elastic Search"
-}
-==========
-Curl 2
--------------------------------------
-curl -XGET "localhost/twitter/tweet/1?version=2" -d '{
- "message" : "elasticsearch now has versioning support, double cool!"
-}'
--------------------------------------
-GET /twitter/tweet/1?version=2
-{
- "message" : "elasticsearch now has versioning support, double cool!"
-}
-===========
-Curl 3
--------------------------------------
-curl -XPOST https://localhost/twitter/tweet/1?version=2 -d '{
- "message" : "elasticsearch now has versioning support, double cool!"
-}'
--------------------------------------
-POST /twitter/tweet/1?version=2
-{
- "message" : "elasticsearch now has versioning support, double cool!"
-}
-=========
-Curl 4
--------------------------------------
-curl -XPOST https://localhost/twitter
--------------------------------------
-POST /twitter
-==========
-Curl 5
--------------------------------------
-curl -X POST https://localhost/twitter/
--------------------------------------
-POST /twitter/
-=============
-Curl 6
--------------------------------------
-curl -s -XPOST localhost:9200/missing-test -d'
-{
- "mappings": {
- }
-}'
--------------------------------------
-POST /missing-test
-{
- "mappings": {
- }
-}
-=========================
-Curl 7
--------------------------------------
-curl 'localhost:9200/missing-test/doc/_search?pretty' -d'
-{
- "query": {
- },
-}'
--------------------------------------
-GET /missing-test/doc/_search?pretty
-{
- "query": {
- },
-}
-===========================
-Curl 8
--------------------------------------
-curl localhost:9200/ -d'
-{
- "query": {
- }
-}'
--------------------------------------
-GET /
-{
- "query": {
- }
-}
-====================================
-Curl Script
--------------------------------------
-#!bin/sh
-
-// test something
-curl 'localhost:9200/missing-test/doc/_search?pretty' -d'
-{
- "query": {
- },
-}'
-
-
-curl -XPOST https://localhost/twitter
-
-#someother comments
-curl localhost:9200/ -d'
-{
- "query": {
- }
-}'
-
-
--------------------
-# test something
-GET /missing-test/doc/_search?pretty
-{
- "query": {
- },
-}
-
-POST /twitter
-
-#someother comments
-GET /
-{
- "query": {
- }
-}
-====================================
-Curl with some text
--------------------------------------
-This is what I meant:
-
-curl 'localhost:9200/missing-test/doc/_search?'
-
-This, however, does work:
-curl 'localhost:9200/missing/doc/_search?'
--------------------
-### This is what I meant:
-
-GET /missing-test/doc/_search?
-
-### This, however, does work:
-GET /missing/doc/_search?
diff --git a/src/plugins/console/public/lib/curl_parsing/curl.js b/src/plugins/console/public/lib/curl_parsing/curl.js
deleted file mode 100644
index 4dd09d1b7d59b..0000000000000
--- a/src/plugins/console/public/lib/curl_parsing/curl.js
+++ /dev/null
@@ -1,194 +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", 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".
- */
-
-function detectCURLinLine(line) {
- // returns true if text matches a curl request
- return line.match(/^\s*?curl\s+(-X[A-Z]+)?\s*['"]?.*?['"]?(\s*$|\s+?-d\s*?['"])/);
-}
-
-export function detectCURL(text) {
- // returns true if text matches a curl request
- if (!text) return false;
- for (const line of text.split('\n')) {
- if (detectCURLinLine(line)) {
- return true;
- }
- }
- return false;
-}
-
-export function parseCURL(text) {
- let state = 'NONE';
- const out = [];
- let body = [];
- let line = '';
- const lines = text.trim().split('\n');
- let matches;
-
- const EmptyLine = /^\s*$/;
- const Comment = /^\s*(?:#|\/{2,})(.*)\n?$/;
- const ExecutionComment = /^\s*#!/;
- const ClosingSingleQuote = /^([^']*)'/;
- const ClosingDoubleQuote = /^((?:[^\\"]|\\.)*)"/;
- const EscapedQuotes = /^((?:[^\\"']|\\.)+)/;
-
- const LooksLikeCurl = /^\s*curl\s+/;
- const CurlVerb = /-X ?(GET|HEAD|POST|PUT|DELETE|PATCH)/;
-
- const HasProtocol = /[\s"']https?:\/\//;
- const CurlRequestWithProto = /[\s"']https?:\/\/[^\/ ]+\/+([^\s"']+)/;
- const CurlRequestWithoutProto = /[\s"'][^\/ ]+\/+([^\s"']+)/;
- const CurlData = /^.+\s(--data|-d)\s*/;
- const SenseLine = /^\s*(GET|HEAD|POST|PUT|DELETE|PATCH)\s+\/?(.+)/;
-
- if (lines.length > 0 && ExecutionComment.test(lines[0])) {
- lines.shift();
- }
-
- function nextLine() {
- if (line.length > 0) {
- return true;
- }
- if (lines.length === 0) {
- return false;
- }
- line = lines.shift().replace(/[\r\n]+/g, '\n') + '\n';
- return true;
- }
-
- function unescapeLastBodyEl() {
- const str = body.pop().replace(/\\([\\"'])/g, '$1');
- body.push(str);
- }
-
- // Is the next char a single or double quote?
- // If so remove it
- function detectQuote() {
- if (line.substr(0, 1) === "'") {
- line = line.substr(1);
- state = 'SINGLE_QUOTE';
- } else if (line.substr(0, 1) === '"') {
- line = line.substr(1);
- state = 'DOUBLE_QUOTE';
- } else {
- state = 'UNQUOTED';
- }
- }
-
- // Body is finished - append to output with final LF
- function addBodyToOut() {
- if (body.length > 0) {
- out.push(body.join(''));
- body = [];
- }
- state = 'LF';
- out.push('\n');
- }
-
- // If the pattern matches, then the state is about to change,
- // so add the capture to the body and detect the next state
- // Otherwise add the whole line
- function consumeMatching(pattern) {
- const matches = line.match(pattern);
- if (matches) {
- body.push(matches[1]);
- line = line.substr(matches[0].length);
- detectQuote();
- } else {
- body.push(line);
- line = '';
- }
- }
-
- function parseCurlLine() {
- let verb = 'GET';
- let request = '';
- let matches;
- if ((matches = line.match(CurlVerb))) {
- verb = matches[1];
- }
-
- // JS regexen don't support possessive quantifiers, so
- // we need two distinct patterns
- const pattern = HasProtocol.test(line) ? CurlRequestWithProto : CurlRequestWithoutProto;
-
- if ((matches = line.match(pattern))) {
- request = matches[1];
- }
-
- out.push(verb + ' /' + request + '\n');
-
- if ((matches = line.match(CurlData))) {
- line = line.substr(matches[0].length);
- detectQuote();
- if (EmptyLine.test(line)) {
- line = '';
- }
- } else {
- state = 'NONE';
- line = '';
- out.push('');
- }
- }
-
- while (nextLine()) {
- if (state === 'SINGLE_QUOTE') {
- consumeMatching(ClosingSingleQuote);
- } else if (state === 'DOUBLE_QUOTE') {
- consumeMatching(ClosingDoubleQuote);
- unescapeLastBodyEl();
- } else if (state === 'UNQUOTED') {
- consumeMatching(EscapedQuotes);
- if (body.length) {
- unescapeLastBodyEl();
- }
- if (state === 'UNQUOTED') {
- addBodyToOut();
- line = '';
- }
- }
-
- // the BODY state (used to match the body of a Sense request)
- // can be terminated early if it encounters
- // a comment or an empty line
- else if (state === 'BODY') {
- if (Comment.test(line) || EmptyLine.test(line)) {
- addBodyToOut();
- } else {
- body.push(line);
- line = '';
- }
- } else if (EmptyLine.test(line)) {
- if (state !== 'LF') {
- out.push('\n');
- state = 'LF';
- }
- line = '';
- } else if ((matches = line.match(Comment))) {
- out.push('#' + matches[1] + '\n');
- state = 'NONE';
- line = '';
- } else if (LooksLikeCurl.test(line)) {
- parseCurlLine();
- } else if ((matches = line.match(SenseLine))) {
- out.push(matches[1] + ' /' + matches[2] + '\n');
- line = '';
- state = 'BODY';
- }
-
- // Nothing else matches, so output with a prefix of !!! for debugging purposes
- else {
- out.push('### ' + line);
- line = '';
- }
- }
-
- addBodyToOut();
- return out.join('').trim();
-}
diff --git a/src/plugins/console/public/lib/curl_parsing/curl_parsing.test.js b/src/plugins/console/public/lib/curl_parsing/curl_parsing.test.js
deleted file mode 100644
index 80a60cd259717..0000000000000
--- a/src/plugins/console/public/lib/curl_parsing/curl_parsing.test.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", 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 _ from 'lodash';
-import { detectCURL, parseCURL } from './curl';
-import curlTests from './__fixtures__/curl_parsing.txt';
-
-describe('CURL', () => {
- const notCURLS = ['sldhfsljfhs', 's;kdjfsldkfj curl -XDELETE ""', '{ "hello": 1 }'];
- _.each(notCURLS, function (notCURL, i) {
- test('cURL Detection - broken strings ' + i, function () {
- expect(detectCURL(notCURL)).toEqual(false);
- });
- });
-
- curlTests.split(/^=+$/m).forEach(function (fixture) {
- if (fixture.trim() === '') {
- return;
- }
- fixture = fixture.split(/^-+$/m);
- const name = fixture[0].trim();
- const curlText = fixture[1];
- const response = fixture[2].trim();
-
- test('cURL Detection - ' + name, function () {
- expect(detectCURL(curlText)).toBe(true);
- const r = parseCURL(curlText);
- expect(r).toEqual(response);
- });
- });
-});
diff --git a/src/plugins/console/public/lib/kb/kb.test.js b/src/plugins/console/public/lib/kb/kb.test.js
index 70ea0ef33ae86..7560789718e58 100644
--- a/src/plugins/console/public/lib/kb/kb.test.js
+++ b/src/plugins/console/public/lib/kb/kb.test.js
@@ -10,7 +10,6 @@
import _ from 'lodash';
import { populateContext } from '../autocomplete/engine';
-import '../../application/models/sense_editor/sense_editor.test.mocks';
import * as kb from '.';
import { AutocompleteInfo, setAutocompleteInfo } from '../../services';
diff --git a/src/plugins/console/public/lib/row_parser.test.ts b/src/plugins/console/public/lib/row_parser.test.ts
deleted file mode 100644
index 869822b7bf055..0000000000000
--- a/src/plugins/console/public/lib/row_parser.test.ts
+++ /dev/null
@@ -1,107 +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", 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 '../application/models/legacy_core_editor/legacy_core_editor.test.mocks';
-
-import RowParser from './row_parser';
-import { create, MODE } from '../application/models';
-import type { SenseEditor } from '../application/models';
-import type { CoreEditor } from '../types';
-
-describe('RowParser', () => {
- let editor: SenseEditor | null;
- let parser: RowParser | null;
-
- beforeEach(function () {
- // Set up our document body
- document.body.innerHTML = ``;
- editor = create(document.getElementById('ConAppEditor')!);
- parser = new RowParser(editor.getCoreEditor() as CoreEditor);
- });
-
- afterEach(function () {
- editor?.getCoreEditor().destroy();
- editor = null;
- parser = null;
- });
-
- describe('getRowParseMode', () => {
- const forceRetokenize = false;
-
- it('should return MODE.BETWEEN_REQUESTS if line is empty', () => {
- editor?.getCoreEditor().setValue('', forceRetokenize);
- expect(parser?.getRowParseMode()).toBe(MODE.BETWEEN_REQUESTS);
- });
-
- it('should return MODE.BETWEEN_REQUESTS if line is a comment', () => {
- editor?.getCoreEditor().setValue('// comment', forceRetokenize);
- expect(parser?.getRowParseMode()).toBe(MODE.BETWEEN_REQUESTS);
- });
-
- it('should return MODE.REQUEST_START | MODE.REQUEST_END if line is a single line request', () => {
- editor?.getCoreEditor().setValue('GET _search', forceRetokenize);
- // eslint-disable-next-line no-bitwise
- expect(parser?.getRowParseMode()).toBe(MODE.REQUEST_START | MODE.REQUEST_END);
- });
-
- it('should return MODE.IN_REQUEST if line is a request with an opening curly brace', () => {
- editor?.getCoreEditor().setValue('{', forceRetokenize);
- expect(parser?.getRowParseMode()).toBe(MODE.IN_REQUEST);
- });
-
- it('should return MODE.MULTI_DOC_CUR_DOC_END | MODE.IN_REQUEST if line is a multi doc request with an opening curly brace', () => {
- editor?.getCoreEditor().setValue('GET _msearch\n{}\n{', forceRetokenize);
- const lineNumber = editor?.getCoreEditor().getLineCount()! - 1;
- expect(parser?.getRowParseMode(lineNumber)).toBe(
- // eslint-disable-next-line no-bitwise
- MODE.MULTI_DOC_CUR_DOC_END | MODE.IN_REQUEST
- );
- });
-
- it('should return MODE.MULTI_DOC_CUR_DOC_END | MODE.REQUEST_END if line is a multi doc request with a closing curly brace', () => {
- editor?.getCoreEditor().setValue('GET _msearch\n{}\n{"foo": 1}\n', forceRetokenize);
- const lineNumber = editor?.getCoreEditor().getLineCount()! - 1;
- expect(parser?.getRowParseMode(lineNumber)).toBe(
- // eslint-disable-next-line no-bitwise
- MODE.MULTI_DOC_CUR_DOC_END | MODE.REQUEST_END
- );
- });
-
- it('should return MODE.REQUEST_START | MODE.REQUEST_END if line is a request with variables', () => {
- editor?.getCoreEditor().setValue('GET /${exampleVariable}', forceRetokenize);
- // eslint-disable-next-line no-bitwise
- expect(parser?.getRowParseMode()).toBe(MODE.REQUEST_START | MODE.REQUEST_END);
- });
-
- it('should return MODE.REQUEST_START | MODE.REQUEST_END if a single request line ends with a closing curly brace', () => {
- editor?.getCoreEditor().setValue('DELETE /_bar/_baz%{test}', forceRetokenize);
- // eslint-disable-next-line no-bitwise
- expect(parser?.getRowParseMode()).toBe(MODE.REQUEST_START | MODE.REQUEST_END);
- });
-
- it('should return correct modes for multiple bulk requests', () => {
- editor
- ?.getCoreEditor()
- .setValue('POST _bulk\n{"index": {"_index": "test"}}\n{"foo": "bar"}\n', forceRetokenize);
- expect(parser?.getRowParseMode(0)).toBe(MODE.BETWEEN_REQUESTS);
- editor
- ?.getCoreEditor()
- .setValue('POST _bulk\n{"index": {"_index": "test"}}\n{"foo": "bar"}\n', forceRetokenize);
- const lineNumber = editor?.getCoreEditor().getLineCount()! - 1;
- expect(parser?.getRowParseMode(lineNumber)).toBe(
- // eslint-disable-next-line no-bitwise
- MODE.REQUEST_END | MODE.MULTI_DOC_CUR_DOC_END
- );
- });
- });
-});
diff --git a/src/plugins/console/public/lib/row_parser.ts b/src/plugins/console/public/lib/row_parser.ts
deleted file mode 100644
index 7078bb857d95b..0000000000000
--- a/src/plugins/console/public/lib/row_parser.ts
+++ /dev/null
@@ -1,161 +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", 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 { CoreEditor, Token } from '../types';
-import { TokenIterator } from './token_iterator';
-
-export const MODE = {
- REQUEST_START: 2,
- IN_REQUEST: 4,
- MULTI_DOC_CUR_DOC_END: 8,
- REQUEST_END: 16,
- BETWEEN_REQUESTS: 32,
-};
-
-// eslint-disable-next-line import/no-default-export
-export default class RowParser {
- constructor(private readonly editor: CoreEditor) {}
-
- MODE = MODE;
-
- getRowParseMode(lineNumber = this.editor.getCurrentPosition().lineNumber) {
- const linesCount = this.editor.getLineCount();
- if (lineNumber > linesCount || lineNumber < 1) {
- return MODE.BETWEEN_REQUESTS;
- }
- const mode = this.editor.getLineState(lineNumber);
-
- if (!mode) {
- return MODE.BETWEEN_REQUESTS;
- } // shouldn't really happen
- // If another "start" mode is added here because we want to allow for new language highlighting
- // please see https://github.com/elastic/kibana/pull/51446 for a discussion on why
- // should consider a different approach.
- if (mode !== 'start' && mode !== 'start-sql') {
- return MODE.IN_REQUEST;
- }
- let line = (this.editor.getLineValue(lineNumber) || '').trim();
-
- if (!line || line.startsWith('#') || line.startsWith('//') || line.startsWith('/*')) {
- return MODE.BETWEEN_REQUESTS;
- } // empty line or a comment waiting for a new req to start
-
- // Check for multi doc requests
- if (line.endsWith('}') && !this.isRequestLine(line)) {
- // check for a multi doc request must start a new json doc immediately after this one end.
- lineNumber++;
- if (lineNumber < linesCount + 1) {
- line = (this.editor.getLineValue(lineNumber) || '').trim();
- if (line.indexOf('{') === 0) {
- // next line is another doc in a multi doc
- // eslint-disable-next-line no-bitwise
- return MODE.MULTI_DOC_CUR_DOC_END | MODE.IN_REQUEST;
- }
- }
- // eslint-disable-next-line no-bitwise
- return MODE.REQUEST_END | MODE.MULTI_DOC_CUR_DOC_END; // end of request
- }
-
- // check for single line requests
- lineNumber++;
- if (lineNumber >= linesCount + 1) {
- // eslint-disable-next-line no-bitwise
- return MODE.REQUEST_START | MODE.REQUEST_END;
- }
- line = (this.editor.getLineValue(lineNumber) || '').trim();
- if (line.indexOf('{') !== 0) {
- // next line is another request
- // eslint-disable-next-line no-bitwise
- return MODE.REQUEST_START | MODE.REQUEST_END;
- }
-
- return MODE.REQUEST_START;
- }
-
- rowPredicate(lineNumber: number | undefined, editor: CoreEditor, value: number) {
- const mode = this.getRowParseMode(lineNumber);
- // eslint-disable-next-line no-bitwise
- return (mode & value) > 0;
- }
-
- isEndRequestRow(row?: number, _e?: CoreEditor) {
- const editor = _e || this.editor;
- return this.rowPredicate(row, editor, MODE.REQUEST_END);
- }
-
- isRequestEdge(row?: number, _e?: CoreEditor) {
- const editor = _e || this.editor;
- // eslint-disable-next-line no-bitwise
- return this.rowPredicate(row, editor, MODE.REQUEST_END | MODE.REQUEST_START);
- }
-
- isStartRequestRow(row?: number, _e?: CoreEditor) {
- const editor = _e || this.editor;
- return this.rowPredicate(row, editor, MODE.REQUEST_START);
- }
-
- isInBetweenRequestsRow(row?: number, _e?: CoreEditor) {
- const editor = _e || this.editor;
- return this.rowPredicate(row, editor, MODE.BETWEEN_REQUESTS);
- }
-
- isInRequestsRow(row?: number, _e?: CoreEditor) {
- const editor = _e || this.editor;
- return this.rowPredicate(row, editor, MODE.IN_REQUEST);
- }
-
- isMultiDocDocEndRow(row?: number, _e?: CoreEditor) {
- const editor = _e || this.editor;
- return this.rowPredicate(row, editor, MODE.MULTI_DOC_CUR_DOC_END);
- }
-
- isEmptyToken(tokenOrTokenIter: TokenIterator | Token | null) {
- const token =
- tokenOrTokenIter && (tokenOrTokenIter as TokenIterator).getCurrentToken
- ? (tokenOrTokenIter as TokenIterator).getCurrentToken()
- : tokenOrTokenIter;
- return !token || (token as Token).type === 'whitespace';
- }
-
- isUrlOrMethodToken(tokenOrTokenIter: TokenIterator | Token) {
- const t = (tokenOrTokenIter as TokenIterator)?.getCurrentToken() ?? (tokenOrTokenIter as Token);
- return t && t.type && (t.type === 'method' || t.type.indexOf('url') === 0);
- }
-
- nextNonEmptyToken(tokenIter: TokenIterator) {
- let t = tokenIter.stepForward();
- while (t && this.isEmptyToken(t)) {
- t = tokenIter.stepForward();
- }
- return t;
- }
-
- prevNonEmptyToken(tokenIter: TokenIterator) {
- let t = tokenIter.stepBackward();
- // empty rows return null token.
- while ((t || tokenIter.getCurrentPosition().lineNumber > 1) && this.isEmptyToken(t))
- t = tokenIter.stepBackward();
- return t;
- }
-
- isCommentToken(token: Token | null) {
- return (
- token &&
- token.type &&
- (token.type === 'comment.punctuation' ||
- token.type === 'comment.line' ||
- token.type === 'comment.block')
- );
- }
-
- isRequestLine(line: string) {
- const methods = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH', 'OPTIONS'];
- return methods.some((m) => line.startsWith(m));
- }
-}
diff --git a/src/plugins/console/public/styles/_app.scss b/src/plugins/console/public/styles/_app.scss
index 4c3ccb8b1cadc..0f0a671d920c3 100644
--- a/src/plugins/console/public/styles/_app.scss
+++ b/src/plugins/console/public/styles/_app.scss
@@ -36,8 +36,6 @@
width: 100%;
display: flex;
flex: 0 0 auto;
-
- // Required on IE11 to render ace editor correctly after first input.
position: relative;
&__spinner {
@@ -55,46 +53,6 @@
height: 100%;
display: flex;
flex: 1 1 1px;
-
- .ace_badge {
- font-family: $euiFontFamily;
- font-size: $euiFontSizeXS;
- font-weight: $euiFontWeightMedium;
- line-height: $euiLineHeight;
- padding: 0 $euiSizeS;
- display: inline-block;
- text-decoration: none;
- border-radius: calc($euiBorderRadius / 2);
- white-space: nowrap;
- vertical-align: middle;
- cursor: default;
- max-width: 100%;
-
- &--success {
- background-color: $euiColorVis0_behindText;
- color: chooseLightOrDarkText($euiColorVis0_behindText);
- }
-
- &--warning {
- background-color: $euiColorVis5_behindText;
- color: chooseLightOrDarkText($euiColorVis5_behindText);
- }
-
- &--primary {
- background-color: $euiColorVis1_behindText;
- color: chooseLightOrDarkText($euiColorVis1_behindText);
- }
-
- &--default {
- background-color: $euiColorLightShade;
- color: chooseLightOrDarkText($euiColorLightShade);
- }
-
- &--danger {
- background-color: $euiColorVis9_behindText;
- color: chooseLightOrDarkText($euiColorVis9_behindText);
- }
- }
}
.conApp__editorContent,
@@ -145,17 +103,6 @@
margin-inline: 0;
}
-// SASSTODO: This component seems to not be used anymore?
-// Possibly replaced by the Ace version
-.conApp__autoComplete {
- position: absolute;
- left: -1000px;
- visibility: hidden;
- /* by pass any other element in ace and resize bar, but not modal popups */
- z-index: $euiZLevel1 + 2;
- margin-top: 22px;
-}
-
.conApp__requestProgressBarContainer {
position: relative;
z-index: $euiZLevel2;
diff --git a/src/plugins/console/public/types/core_editor.ts b/src/plugins/console/public/types/core_editor.ts
index aa9bdf21c1c94..8d5ab2a582226 100644
--- a/src/plugins/console/public/types/core_editor.ts
+++ b/src/plugins/console/public/types/core_editor.ts
@@ -7,7 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import type { Editor } from 'brace';
import { ResultTerm } from '../lib/autocomplete/types';
import { TokensProvider } from './tokens_provider';
import { Token } from './token';
@@ -94,7 +93,7 @@ export enum LINE_MODE {
/**
* The CoreEditor is a component separate from the Editor implementation that provides Console
* app specific business logic. The CoreEditor is an interface to the lower-level editor implementation
- * being used which is usually vendor code such as Ace or Monaco.
+ * being used which is usually vendor code such as Monaco.
*/
export interface CoreEditor {
/**
@@ -260,7 +259,7 @@ export interface CoreEditor {
*/
registerKeyboardShortcut(opts: {
keys: string | { win?: string; mac?: string };
- fn: (editor: Editor) => void;
+ fn: (editor: any) => void;
name: string;
}): void;
diff --git a/src/plugins/console/tsconfig.json b/src/plugins/console/tsconfig.json
index 2b0f6127cd4af..02e4e7a9b7689 100644
--- a/src/plugins/console/tsconfig.json
+++ b/src/plugins/console/tsconfig.json
@@ -18,10 +18,8 @@
"@kbn/i18n-react",
"@kbn/shared-ux-utility",
"@kbn/core-http-browser",
- "@kbn/ace",
"@kbn/config-schema",
"@kbn/core-http-router-server-internal",
- "@kbn/web-worker-stub",
"@kbn/core-elasticsearch-server",
"@kbn/core-http-browser-mocks",
"@kbn/react-kibana-context-theme",
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index ea995a275449d..3b078d6bb8a90 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -320,8 +320,6 @@
"coloring.dynamicColoring.rangeType.label": "Type de valeur",
"coloring.dynamicColoring.rangeType.number": "Numéro",
"coloring.dynamicColoring.rangeType.percent": "Pourcent",
- "console.autocomplete.addMethodMetaText": "méthode",
- "console.autocomplete.fieldsFetchingAnnotation": "La récupération des champs est en cours",
"console.autocompleteSuggestions.apiLabel": "API",
"console.autocompleteSuggestions.endpointLabel": "point de terminaison",
"console.autocompleteSuggestions.methodLabel": "méthode",
@@ -362,10 +360,6 @@
"console.loadingError.title": "Impossible de charger la console",
"console.notification.clearHistory": "Effacer l'historique",
"console.notification.disableSavingToHistory": "Désactiver l'enregistrement",
- "console.notification.error.couldNotSaveRequestTitle": "Impossible d'enregistrer la requête dans l'historique de la console.",
- "console.notification.error.historyQuotaReachedMessage": "L'historique des requêtes est arrivé à saturation. Effacez l'historique de la console ou désactivez l'enregistrement de nouvelles requêtes.",
- "console.notification.error.noRequestSelectedTitle": "Aucune requête sélectionnée. Sélectionnez une requête en positionnant le curseur dessus.",
- "console.notification.error.unknownErrorTitle": "Erreur de requête inconnue",
"console.pageHeading": "Console",
"console.requestInProgressBadgeText": "Requête en cours",
"console.requestOptions.autoIndentButtonLabel": "Appliquer les indentations",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 2ec8bc11bc0c8..e579f87771b20 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -320,8 +320,6 @@
"coloring.dynamicColoring.rangeType.label": "値型",
"coloring.dynamicColoring.rangeType.number": "Number",
"coloring.dynamicColoring.rangeType.percent": "割合(%)",
- "console.autocomplete.addMethodMetaText": "メソド",
- "console.autocomplete.fieldsFetchingAnnotation": "フィールドの取得を実行しています",
"console.autocompleteSuggestions.apiLabel": "API",
"console.autocompleteSuggestions.endpointLabel": "エンドポイント",
"console.autocompleteSuggestions.methodLabel": "メソド",
@@ -362,10 +360,6 @@
"console.loadingError.title": "コンソールを読み込めません",
"console.notification.clearHistory": "履歴を消去",
"console.notification.disableSavingToHistory": "保存を無効にする",
- "console.notification.error.couldNotSaveRequestTitle": "リクエストをコンソール履歴に保存できませんでした。",
- "console.notification.error.historyQuotaReachedMessage": "リクエスト履歴が満杯です。コンソール履歴を消去するか、新しいリクエストの保存を無効にしてください。",
- "console.notification.error.noRequestSelectedTitle": "リクエストを選択していません。リクエストの中にカーソルを置いて選択します。",
- "console.notification.error.unknownErrorTitle": "不明なリクエストエラー",
"console.pageHeading": "コンソール",
"console.requestInProgressBadgeText": "リクエストが進行中",
"console.requestOptions.autoIndentButtonLabel": "インデントを適用",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 12ee59bb6fc9c..09662465c4833 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -319,8 +319,6 @@
"coloring.dynamicColoring.rangeType.label": "值类型",
"coloring.dynamicColoring.rangeType.number": "数字",
"coloring.dynamicColoring.rangeType.percent": "百分比",
- "console.autocomplete.addMethodMetaText": "方法",
- "console.autocomplete.fieldsFetchingAnnotation": "正在提取字段",
"console.autocompleteSuggestions.apiLabel": "API",
"console.autocompleteSuggestions.endpointLabel": "终端",
"console.autocompleteSuggestions.methodLabel": "方法",
@@ -361,10 +359,6 @@
"console.loadingError.title": "无法加载控制台",
"console.notification.clearHistory": "清除历史记录",
"console.notification.disableSavingToHistory": "禁止保存",
- "console.notification.error.couldNotSaveRequestTitle": "无法将请求保存到控制台历史记录。",
- "console.notification.error.historyQuotaReachedMessage": "请求历史记录已满。请清除控制台历史记录或禁止保存新的请求。",
- "console.notification.error.noRequestSelectedTitle": "未选择任何请求。将鼠标置于请求内即可选择。",
- "console.notification.error.unknownErrorTitle": "未知请求错误",
"console.pageHeading": "控制台",
"console.requestInProgressBadgeText": "进行中的请求",
"console.requestOptions.autoIndentButtonLabel": "应用行首缩进",
From 7df36721923159f45bc4fdbd26f76b20ad84249a Mon Sep 17 00:00:00 2001
From: Garrett Spong
Date: Wed, 9 Oct 2024 10:17:47 -0600
Subject: [PATCH 03/87] [Security Assistant] V2 Knowledge Base Settings
feedback and fixes (#194354)
## Summary
This PR is a follow up to #192665 and addresses a bunch of feedback and
fixes including:
- [X] Adds support for updating/editing entries
- [X] Fixes initial loading experience of the KB Settings Setup/Table
- [X] Fixes two bugs where `semantic_text` and `text` must be declared
for `IndexEntries` to work
- [X] Add new Settings Context Menu items for KB and Alerts
- [X] Add support for `required` entries in initial prompt
* See [this
trace](https://smith.langchain.com/public/84a17a31-8ce8-4bd9-911e-38a854484dd8/r)
for included knowledge. Note that the KnowledgeBaseRetrievalTool was not
selected.
* Note: All prompts were updated to include the `{knowledge_history}`
placeholder, and _not behind the feature flag_, as this will just be the
empty case until the feature flag is enabled.
TODO (in this or follow-up PR):
- [ ] Add suggestions to `index` and `fields` inputs
- [ ] Adds URL deeplinking to securityAssistantManagement
- [ ] Fix bug where updating entry does not re-create embeddings (see
[comment](https://github.com/elastic/kibana/pull/194354#discussion_r1786475496))
- [ ] Fix loading indicators when adding/editing entries
- [ ] API integration tests for update API (@e40pud)
### Checklist
Delete any items that are not applicable to this PR.
- [X] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
* Docs being tracked in
https://github.com/elastic/security-docs/issues/5337 for when feature
flag is enabled
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Patryk Kopycinski
---
.../entries/common_attributes.gen.ts | 6 +-
.../entries/common_attributes.schema.yaml | 6 +
.../impl/assistant/assistant_header/index.tsx | 79 +-------
.../assistant_header/translations.ts | 28 +++
.../alerts_settings}/alerts_settings.test.tsx | 4 +-
.../alerts_settings}/alerts_settings.tsx | 6 +-
.../alerts_settings_management.tsx | 15 +-
.../assistant_settings_management.test.tsx | 6 +
.../assistant_settings_management.tsx | 8 +-
.../settings_context_menu.tsx | 186 ++++++++++++++++++
.../impl/knowledge_base/alerts_range.tsx | 2 +-
.../knowledge_base_settings.tsx | 2 +-
.../document_entry_editor.tsx | 1 -
.../index.tsx | 106 ++++++++--
.../index_entry_editor.tsx | 47 ++++-
.../translations.ts | 41 +++-
.../use_knowledge_base_table.tsx | 8 +-
.../kbn-elastic-assistant/tsconfig.json | 1 +
.../create_knowledge_base_entry.ts | 108 +++++++++-
.../knowledge_base/helpers.ts | 5 +-
.../knowledge_base/index.ts | 53 ++++-
.../knowledge_base/types.ts | 33 ++++
.../server/ai_assistant_service/index.ts | 8 +-
.../graphs/default_assistant_graph/graph.ts | 2 +-
.../nodes/run_agent.ts | 14 ++
.../nodes/translations.ts | 6 +-
.../graphs/default_assistant_graph/prompts.ts | 2 +
.../entries/bulk_actions_route.ts | 23 ++-
.../knowledge_base/entries/create_route.ts | 2 +-
.../knowledge_base/entries/find_route.ts | 4 +-
.../management_settings.test.tsx | 5 +
.../stack_management/management_settings.tsx | 12 +-
32 files changed, 686 insertions(+), 143 deletions(-)
rename x-pack/packages/kbn-elastic-assistant/impl/{alerts/settings => assistant/settings/alerts_settings}/alerts_settings.test.tsx (89%)
rename x-pack/packages/kbn-elastic-assistant/impl/{alerts/settings => assistant/settings/alerts_settings}/alerts_settings.tsx (92%)
rename x-pack/packages/kbn-elastic-assistant/impl/{alerts/settings => assistant/settings/alerts_settings}/alerts_settings_management.tsx (68%)
create mode 100644 x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_menu.tsx
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.gen.ts
index 1af5c46b1c130..c32517fec0860 100644
--- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.gen.ts
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.gen.ts
@@ -106,7 +106,11 @@ export type BaseCreateProps = z.infer;
export const BaseCreateProps = BaseRequiredFields.merge(BaseDefaultableFields);
export type BaseUpdateProps = z.infer;
-export const BaseUpdateProps = BaseCreateProps.partial();
+export const BaseUpdateProps = BaseCreateProps.partial().merge(
+ z.object({
+ id: NonEmptyString,
+ })
+);
export type BaseResponseProps = z.infer;
export const BaseResponseProps = BaseRequiredFields.merge(BaseDefaultableFields.required());
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.schema.yaml
index c1c551059f04b..af7f4dd8e4221 100644
--- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.schema.yaml
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.schema.yaml
@@ -112,6 +112,12 @@ components:
allOf:
- $ref: "#/components/schemas/BaseCreateProps"
x-modify: partial
+ - type: object
+ properties:
+ id:
+ $ref: "../../common_attributes.schema.yaml#/components/schemas/NonEmptyString"
+ required:
+ - id
BaseResponseProps:
x-inline: true
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx
index d81a56fb97eef..ef37506f2af17 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx
@@ -5,16 +5,13 @@
* 2.0.
*/
-import React, { useState, useMemo, useCallback } from 'react';
+import React, { useMemo, useCallback } from 'react';
import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query';
import {
EuiFlexGroup,
EuiFlexItem,
- EuiPopover,
- EuiContextMenu,
EuiButtonIcon,
EuiPanel,
- EuiConfirmModal,
EuiToolTip,
EuiSkeletonTitle,
} from '@elastic/eui';
@@ -29,6 +26,7 @@ import { FlyoutNavigation } from '../assistant_overlay/flyout_navigation';
import { AssistantSettingsButton } from '../settings/assistant_settings_button';
import * as i18n from './translations';
import { AIConnector } from '../../connectorland/connector_selector';
+import { SettingsContextMenu } from '../settings/settings_context_menu/settings_context_menu';
interface OwnProps {
selectedConversation: Conversation | undefined;
@@ -94,21 +92,6 @@ export const AssistantHeader: React.FC = ({
[selectedConversation?.apiConfig?.connectorId]
);
- const [isPopoverOpen, setPopover] = useState(false);
-
- const onButtonClick = useCallback(() => {
- setPopover(!isPopoverOpen);
- }, [isPopoverOpen]);
-
- const closePopover = useCallback(() => {
- setPopover(false);
- }, []);
-
- const [isResetConversationModalVisible, setIsResetConversationModalVisible] = useState(false);
-
- const closeDestroyModal = useCallback(() => setIsResetConversationModalVisible(false), []);
- const showDestroyModal = useCallback(() => setIsResetConversationModalVisible(true), []);
-
const onConversationChange = useCallback(
(updatedConversation: Conversation) => {
onConversationSelected({
@@ -119,32 +102,6 @@ export const AssistantHeader: React.FC = ({
[onConversationSelected]
);
- const panels = useMemo(
- () => [
- {
- id: 0,
- items: [
- {
- name: i18n.RESET_CONVERSATION,
- css: css`
- color: ${euiThemeVars.euiColorDanger};
- `,
- onClick: showDestroyModal,
- icon: 'refresh',
- 'data-test-subj': 'clear-chat',
- },
- ],
- },
- ],
- [showDestroyModal]
- );
-
- const handleReset = useCallback(() => {
- onChatCleared();
- closeDestroyModal();
- closePopover();
- }, [onChatCleared, closeDestroyModal, closePopover]);
-
return (
<>
= ({
-
- }
- isOpen={isPopoverOpen}
- closePopover={closePopover}
- panelPaddingSize="none"
- anchorPosition="downLeft"
- >
-
-
+
- {isResetConversationModalVisible && (
-
- {i18n.CLEAR_CHAT_CONFIRMATION}
-
- )}
>
);
};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/translations.ts
index 68c926d2aa14c..e4f23e0970eb0 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/translations.ts
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/translations.ts
@@ -7,6 +7,34 @@
import { i18n } from '@kbn/i18n';
+export const AI_ASSISTANT_SETTINGS = i18n.translate(
+ 'xpack.elasticAssistant.assistant.settings.aiAssistantSettings',
+ {
+ defaultMessage: 'AI Assistant settings',
+ }
+);
+
+export const ANONYMIZATION = i18n.translate(
+ 'xpack.elasticAssistant.assistant.settings.anonymization',
+ {
+ defaultMessage: 'Anonymization',
+ }
+);
+
+export const KNOWLEDGE_BASE = i18n.translate(
+ 'xpack.elasticAssistant.assistant.settings.knowledgeBase',
+ {
+ defaultMessage: 'Knowledge Base',
+ }
+);
+
+export const ALERTS_TO_ANALYZE = i18n.translate(
+ 'xpack.elasticAssistant.assistant.settings.alertsToAnalyze',
+ {
+ defaultMessage: 'Alerts to analyze',
+ }
+);
+
export const RESET_CONVERSATION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.resetConversation',
{
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings.test.tsx
similarity index 89%
rename from x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings.test.tsx
rename to x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings.test.tsx
index 3e730451ba1d5..2a5cae76d5e77 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings.test.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings.test.tsx
@@ -9,8 +9,8 @@ import { render, screen, fireEvent } from '@testing-library/react';
import React from 'react';
import { AlertsSettings } from './alerts_settings';
-import { KnowledgeBaseConfig } from '../../assistant/types';
-import { DEFAULT_LATEST_ALERTS } from '../../assistant_context/constants';
+import { KnowledgeBaseConfig } from '../../types';
+import { DEFAULT_LATEST_ALERTS } from '../../../assistant_context/constants';
describe('AlertsSettings', () => {
beforeEach(() => {
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings.tsx
similarity index 92%
rename from x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings.tsx
rename to x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings.tsx
index e73bfa15e66be..60078178a1771 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings.tsx
@@ -9,9 +9,9 @@ import { EuiFlexGroup, EuiFormRow, EuiFlexItem, EuiSpacer, EuiText } from '@elas
import { css } from '@emotion/react';
import React from 'react';
-import { KnowledgeBaseConfig } from '../../assistant/types';
-import { AlertsRange } from '../../knowledge_base/alerts_range';
-import * as i18n from '../../knowledge_base/translations';
+import { KnowledgeBaseConfig } from '../../types';
+import { AlertsRange } from '../../../knowledge_base/alerts_range';
+import * as i18n from '../../../knowledge_base/translations';
export const MIN_LATEST_ALERTS = 10;
export const MAX_LATEST_ALERTS = 100;
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings_management.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings_management.tsx
similarity index 68%
rename from x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings_management.tsx
rename to x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings_management.tsx
index d103c1a8c03c2..1a6f826bd415f 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings_management.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings_management.tsx
@@ -7,19 +7,24 @@
import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import React from 'react';
-import { KnowledgeBaseConfig } from '../../assistant/types';
-import { AlertsRange } from '../../knowledge_base/alerts_range';
-import * as i18n from '../../knowledge_base/translations';
+import { KnowledgeBaseConfig } from '../../types';
+import { AlertsRange } from '../../../knowledge_base/alerts_range';
+import * as i18n from '../../../knowledge_base/translations';
interface Props {
knowledgeBase: KnowledgeBaseConfig;
setUpdatedKnowledgeBaseSettings: React.Dispatch>;
+ hasBorder?: boolean;
}
+/**
+ * Replaces the AlertsSettings component used in the existing settings modal. Once the modal is
+ * fully removed we can delete that component in favor of this one.
+ */
export const AlertsSettingsManagement: React.FC = React.memo(
- ({ knowledgeBase, setUpdatedKnowledgeBaseSettings }) => {
+ ({ knowledgeBase, setUpdatedKnowledgeBaseSettings, hasBorder = true }) => {
return (
-
+
{i18n.ALERTS_LABEL}
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx
index d8e207cbb23cd..dd472b3ee87ab 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx
@@ -25,6 +25,7 @@ import {
SYSTEM_PROMPTS_TAB,
} from './const';
import { mockSystemPrompts } from '../../mock/system_prompt';
+import { DataViewsContract } from '@kbn/data-views-plugin/public';
const mockConversations = {
[alertConvo.title]: alertConvo,
@@ -53,8 +54,13 @@ const mockContext = {
},
};
+const mockDataViews = {
+ getIndices: jest.fn(),
+} as unknown as DataViewsContract;
+
const testProps = {
selectedConversation: welcomeConvo,
+ dataViews: mockDataViews,
};
jest.mock('../../assistant_context');
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx
index 89c00fbf88773..4c50d14a5662e 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx
@@ -9,6 +9,7 @@ import React, { useEffect, useMemo } from 'react';
import { EuiAvatar, EuiPageTemplate, EuiTitle, useEuiShadow, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
+import { DataViewsContract } from '@kbn/data-views-plugin/public';
import { Conversation } from '../../..';
import * as i18n from './translations';
import { useAssistantContext } from '../../assistant_context';
@@ -33,6 +34,7 @@ import { KnowledgeBaseSettingsManagement } from '../../knowledge_base/knowledge_
import { EvaluationSettings } from '.';
interface Props {
+ dataViews: DataViewsContract;
selectedConversation: Conversation;
}
@@ -41,7 +43,7 @@ interface Props {
* anonymization, knowledge base, and evaluation via the `isModelEvaluationEnabled` feature flag.
*/
export const AssistantSettingsManagement: React.FC = React.memo(
- ({ selectedConversation: defaultSelectedConversation }) => {
+ ({ dataViews, selectedConversation: defaultSelectedConversation }) => {
const {
assistantFeatures: { assistantModelEvaluation: modelEvaluatorEnabled },
http,
@@ -158,7 +160,9 @@ export const AssistantSettingsManagement: React.FC = React.memo(
)}
{selectedSettingsTab === QUICK_PROMPTS_TAB && }
{selectedSettingsTab === ANONYMIZATION_TAB && }
- {selectedSettingsTab === KNOWLEDGE_BASE_TAB && }
+ {selectedSettingsTab === KNOWLEDGE_BASE_TAB && (
+
+ )}
{selectedSettingsTab === EVALUATION_TAB && }
>
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_menu.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_menu.tsx
new file mode 100644
index 0000000000000..b7f33b9a6af5a
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_menu.tsx
@@ -0,0 +1,186 @@
+/*
+ * 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, { ReactElement, useCallback, useMemo, useState } from 'react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiContextMenuPanel,
+ EuiContextMenuItem,
+ EuiConfirmModal,
+ EuiNotificationBadge,
+ EuiPopover,
+ EuiButtonIcon,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import { euiThemeVars } from '@kbn/ui-theme';
+import { useAssistantContext } from '../../../..';
+import * as i18n from '../../assistant_header/translations';
+
+interface Params {
+ isDisabled?: boolean;
+ onChatCleared?: () => void;
+}
+
+export const SettingsContextMenu: React.FC = React.memo(
+ ({ isDisabled = false, onChatCleared }: Params) => {
+ const {
+ navigateToApp,
+ knowledgeBase,
+ assistantFeatures: { assistantKnowledgeBaseByDefault: enableKnowledgeBaseByDefault },
+ } = useAssistantContext();
+
+ const [isPopoverOpen, setPopover] = useState(false);
+
+ const [isResetConversationModalVisible, setIsResetConversationModalVisible] = useState(false);
+ const closeDestroyModal = useCallback(() => setIsResetConversationModalVisible(false), []);
+
+ const onButtonClick = useCallback(() => {
+ setPopover(!isPopoverOpen);
+ }, [isPopoverOpen]);
+
+ const closePopover = useCallback(() => {
+ setPopover(false);
+ }, []);
+
+ const showDestroyModal = useCallback(() => {
+ closePopover?.();
+ setIsResetConversationModalVisible(true);
+ }, [closePopover]);
+
+ const handleNavigateToSettings = useCallback(
+ () =>
+ navigateToApp('management', {
+ path: 'kibana/securityAiAssistantManagement',
+ }),
+ [navigateToApp]
+ );
+
+ const handleNavigateToKnowledgeBase = useCallback(
+ () =>
+ navigateToApp('management', {
+ path: 'kibana/securityAiAssistantManagement',
+ }),
+ [navigateToApp]
+ );
+
+ // We are migrating away from the settings modal in favor of the new Stack Management UI
+ // Currently behind `assistantKnowledgeBaseByDefault` FF
+ const newItems: ReactElement[] = useMemo(
+ () => [
+
+ {i18n.AI_ASSISTANT_SETTINGS}
+ ,
+
+ {i18n.ANONYMIZATION}
+ ,
+
+ {i18n.KNOWLEDGE_BASE}
+ ,
+
+
+ {i18n.ALERTS_TO_ANALYZE}
+
+
+ {knowledgeBase.latestAlerts}
+
+
+
+ ,
+ ],
+ [handleNavigateToKnowledgeBase, handleNavigateToSettings, knowledgeBase]
+ );
+
+ const items = useMemo(
+ () => [
+ ...(enableKnowledgeBaseByDefault ? newItems : []),
+
+ {i18n.RESET_CONVERSATION}
+ ,
+ ],
+
+ [enableKnowledgeBaseByDefault, newItems, showDestroyModal]
+ );
+
+ const handleReset = useCallback(() => {
+ onChatCleared?.();
+ closeDestroyModal();
+ closePopover?.();
+ }, [onChatCleared, closeDestroyModal, closePopover]);
+
+ return (
+ <>
+
+ }
+ isOpen={isPopoverOpen}
+ closePopover={closePopover}
+ panelPaddingSize="none"
+ anchorPosition="leftUp"
+ >
+
+
+ {isResetConversationModalVisible && (
+
+ {i18n.CLEAR_CHAT_CONFIRMATION}
+
+ )}
+ >
+ );
+ }
+);
+
+SettingsContextMenu.displayName = 'SettingsContextMenu';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/alerts_range.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/alerts_range.tsx
index 152f0a91a7d04..63bd86121dcc1 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/alerts_range.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/alerts_range.tsx
@@ -12,7 +12,7 @@ import {
MAX_LATEST_ALERTS,
MIN_LATEST_ALERTS,
TICK_INTERVAL,
-} from '../alerts/settings/alerts_settings';
+} from '../assistant/settings/alerts_settings/alerts_settings';
import { KnowledgeBaseConfig } from '../assistant/types';
import { ALERTS_RANGE } from './translations';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings.tsx
index b56abafafd5db..aa873decdcd87 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings.tsx
@@ -23,7 +23,7 @@ import {
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
-import { AlertsSettings } from '../alerts/settings/alerts_settings';
+import { AlertsSettings } from '../assistant/settings/alerts_settings/alerts_settings';
import { useAssistantContext } from '../assistant_context';
import type { KnowledgeBaseConfig } from '../assistant/types';
import * as i18n from './translations';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/document_entry_editor.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/document_entry_editor.tsx
index 016da27d2c051..b33f221bfde3b 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/document_entry_editor.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/document_entry_editor.tsx
@@ -127,7 +127,6 @@ export const DocumentEntryEditor: React.FC = React.memo(({ entry, setEntr
id="requiredKnowledge"
onChange={onRequiredKnowledgeChanged}
checked={entry?.required ?? false}
- disabled={true}
/>
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx
index a2097177a2ca4..34e8601e37ce7 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx
@@ -6,8 +6,12 @@
*/
import {
+ EuiButton,
+ EuiFlexGroup,
+ EuiFlexItem,
EuiInMemoryTable,
EuiLink,
+ EuiLoadingSpinner,
EuiPanel,
EuiSearchBarProps,
EuiSpacer,
@@ -23,7 +27,9 @@ import {
KnowledgeBaseEntryCreateProps,
KnowledgeBaseEntryResponse,
} from '@kbn/elastic-assistant-common';
-import { AlertsSettingsManagement } from '../../alerts/settings/alerts_settings_management';
+import { css } from '@emotion/react';
+import { DataViewsContract } from '@kbn/data-views-plugin/public';
+import { AlertsSettingsManagement } from '../../assistant/settings/alerts_settings/alerts_settings_management';
import { useKnowledgeBaseEntries } from '../../assistant/api/knowledge_base/entries/use_knowledge_base_entries';
import { useAssistantContext } from '../../assistant_context';
import { useKnowledgeBaseTable } from './use_knowledge_base_table';
@@ -40,7 +46,7 @@ import { useFlyoutModalVisibility } from '../../assistant/common/components/assi
import { IndexEntryEditor } from './index_entry_editor';
import { DocumentEntryEditor } from './document_entry_editor';
import { KnowledgeBaseSettings } from '../knowledge_base_settings';
-import { SetupKnowledgeBaseButton } from '../setup_knowledge_base_button';
+import { ESQL_RESOURCE, SetupKnowledgeBaseButton } from '../setup_knowledge_base_button';
import { useDeleteKnowledgeBaseEntries } from '../../assistant/api/knowledge_base/entries/use_delete_knowledge_base_entries';
import {
isSystemEntry,
@@ -51,14 +57,24 @@ import { useCreateKnowledgeBaseEntry } from '../../assistant/api/knowledge_base/
import { useUpdateKnowledgeBaseEntries } from '../../assistant/api/knowledge_base/entries/use_update_knowledge_base_entries';
import { SETTINGS_UPDATED_TOAST_TITLE } from '../../assistant/settings/translations';
import { KnowledgeBaseConfig } from '../../assistant/types';
+import {
+ isKnowledgeBaseSetup,
+ useKnowledgeBaseStatus,
+} from '../../assistant/api/knowledge_base/use_knowledge_base_status';
+
+interface Params {
+ dataViews: DataViewsContract;
+}
-export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => {
+export const KnowledgeBaseSettingsManagement: React.FC = React.memo(({ dataViews }) => {
const {
assistantFeatures: { assistantKnowledgeBaseByDefault: enableKnowledgeBaseByDefault },
http,
toasts,
} = useAssistantContext();
const [hasPendingChanges, setHasPendingChanges] = useState(false);
+ const { data: kbStatus, isFetched } = useKnowledgeBaseStatus({ http, resource: ESQL_RESOURCE });
+ const isKbSetup = isKnowledgeBaseSetup(kbStatus);
// Only needed for legacy settings management
const { knowledgeBase, setUpdatedKnowledgeBaseSettings, resetSettings, saveSettings } =
@@ -123,12 +139,12 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => {
// Flyout Save/Cancel Actions
const onSaveConfirmed = useCallback(() => {
- if (isKnowledgeBaseEntryCreateProps(selectedEntry)) {
- createEntry(selectedEntry);
- closeFlyout();
- } else if (isKnowledgeBaseEntryResponse(selectedEntry)) {
+ if (isKnowledgeBaseEntryResponse(selectedEntry)) {
updateEntries([selectedEntry]);
closeFlyout();
+ } else if (isKnowledgeBaseEntryCreateProps(selectedEntry)) {
+ createEntry(selectedEntry);
+ closeFlyout();
}
}, [closeFlyout, selectedEntry, createEntry, updateEntries]);
@@ -137,7 +153,11 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => {
closeFlyout();
}, [closeFlyout]);
- const { data: entries } = useKnowledgeBaseEntries({
+ const {
+ data: entries,
+ isFetching: isFetchingEntries,
+ refetch: refetchEntries,
+ } = useKnowledgeBaseEntries({
http,
toasts,
enabled: enableKnowledgeBaseByDefault,
@@ -169,6 +189,9 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => {
[deleteEntry, entries.data, getColumns, openFlyout]
);
+ // Refresh button
+ const handleRefreshTable = useCallback(() => refetchEntries(), [refetchEntries]);
+
const onDocumentClicked = useCallback(() => {
setSelectedEntry({ type: DocumentEntryType.value, kbResource: 'user', source: 'user' });
openFlyout();
@@ -182,7 +205,30 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => {
const search: EuiSearchBarProps = useMemo(
() => ({
toolsRight: (
-
+
+
+
+
+
+
+
+
+
+
),
box: {
incremental: true,
@@ -190,7 +236,7 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => {
},
filters: [],
}),
- [onDocumentClicked, onIndexClicked]
+ [isFetchingEntries, handleRefreshTable, onDocumentClicked, onIndexClicked]
);
const flyoutTitle = useMemo(() => {
@@ -247,15 +293,40 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => {
),
}}
/>
-
-
+
+
+ {!isFetched ? (
+
+ ) : isKbSetup ? (
+
+ ) : (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
{
) : (
>>
}
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index_entry_editor.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index_entry_editor.tsx
index 19f8cfbbc52ba..f5dd2df3bcaac 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index_entry_editor.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index_entry_editor.tsx
@@ -17,14 +17,16 @@ import {
} from '@elastic/eui';
import React, { useCallback } from 'react';
import { IndexEntry } from '@kbn/elastic-assistant-common';
+import { DataViewsContract } from '@kbn/data-views-plugin/public';
import * as i18n from './translations';
interface Props {
+ dataViews: DataViewsContract;
entry?: IndexEntry;
setEntry: React.Dispatch>>;
}
-export const IndexEntryEditor: React.FC = React.memo(({ entry, setEntry }) => {
+export const IndexEntryEditor: React.FC = React.memo(({ dataViews, entry, setEntry }) => {
// Name
const setName = useCallback(
(e: React.ChangeEvent) =>
@@ -74,9 +76,17 @@ export const IndexEntryEditor: React.FC = React.memo(({ entry, setEntry }
entry?.users?.length === 0 ? sharingOptions[1].value : sharingOptions[0].value;
// Index
+ // TODO: For index field autocomplete
+ // const indexOptions = useMemo(() => {
+ // const indices = await dataViews.getIndices({
+ // pattern: e[0]?.value ?? '',
+ // isRollupIndex: () => false,
+ // });
+ // }, [dataViews]);
const setIndex = useCallback(
- (e: Array>) =>
- setEntry((prevEntry) => ({ ...prevEntry, index: e[0].value })),
+ async (e: Array>) => {
+ setEntry((prevEntry) => ({ ...prevEntry, index: e[0]?.value }));
+ },
[setEntry]
);
@@ -162,30 +172,51 @@ export const IndexEntryEditor: React.FC = React.memo(({ entry, setEntry }
-
+
-
+
+
+
+
);
});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/translations.ts
index ed4a3676975b8..0cc16089fdaae 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/translations.ts
+++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/translations.ts
@@ -251,14 +251,44 @@ export const ENTRY_FIELD_INPUT_LABEL = i18n.translate(
export const ENTRY_DESCRIPTION_INPUT_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryDescriptionInputLabel',
{
- defaultMessage: 'Description',
+ defaultMessage: 'Data Description',
+ }
+);
+
+export const ENTRY_DESCRIPTION_HELP_LABEL = i18n.translate(
+ 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryDescriptionHelpLabel',
+ {
+ defaultMessage:
+ 'A description of the type of data in this index and/or when the assistant should look for data here.',
}
);
export const ENTRY_QUERY_DESCRIPTION_INPUT_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryQueryDescriptionInputLabel',
{
- defaultMessage: 'Query Description',
+ defaultMessage: 'Query Instruction',
+ }
+);
+
+export const ENTRY_QUERY_DESCRIPTION_HELP_LABEL = i18n.translate(
+ 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryQueryDescriptionHelpLabel',
+ {
+ defaultMessage: 'Any instructions for extracting the search query from the user request.',
+ }
+);
+
+export const ENTRY_OUTPUT_FIELDS_INPUT_LABEL = i18n.translate(
+ 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryOutputFieldsInputLabel',
+ {
+ defaultMessage: 'Output Fields',
+ }
+);
+
+export const ENTRY_OUTPUT_FIELDS_HELP_LABEL = i18n.translate(
+ 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryOutputFieldsHelpLabel',
+ {
+ defaultMessage:
+ 'What fields should be sent to the LLM. Leave empty to send the entire document.',
}
);
@@ -269,6 +299,13 @@ export const ENTRY_INPUT_PLACEHOLDER = i18n.translate(
}
);
+export const ENTRY_FIELD_PLACEHOLDER = i18n.translate(
+ 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryFieldPlaceholder',
+ {
+ defaultMessage: 'semantic_text',
+ }
+);
+
export const KNOWLEDGE_BASE_DOCUMENTATION = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.knowledgeBaseDocumentation',
{
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/use_knowledge_base_table.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/use_knowledge_base_table.tsx
index 5af360a598205..d0038169cd597 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/use_knowledge_base_table.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/use_knowledge_base_table.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { EuiAvatar, EuiBadge, EuiBasicTableColumn, EuiIcon, EuiLink, EuiText } from '@elastic/eui';
+import { EuiAvatar, EuiBadge, EuiBasicTableColumn, EuiIcon, EuiText } from '@elastic/eui';
import { css } from '@emotion/react';
import React, { useCallback } from 'react';
import { FormattedDate } from '@kbn/i18n-react';
@@ -32,7 +32,7 @@ export const useKnowledgeBaseTable = () => {
if (['esql', 'security_labs'].includes(entry.kbResource)) {
return 'logoElastic';
}
- return 'visText';
+ return 'document';
} else if (entry.type === IndexEntryType.value) {
return 'index';
}
@@ -61,9 +61,7 @@ export const useKnowledgeBaseTable = () => {
},
{
name: i18n.COLUMN_NAME,
- render: (entry: KnowledgeBaseEntryResponse) => (
- onEntryNameClicked(entry)}>{entry.name}
- ),
+ render: ({ name }: KnowledgeBaseEntryResponse) => name,
sortable: ({ name }: KnowledgeBaseEntryResponse) => name,
width: '30%',
},
diff --git a/x-pack/packages/kbn-elastic-assistant/tsconfig.json b/x-pack/packages/kbn-elastic-assistant/tsconfig.json
index ed2631b597bd6..8d19fa86f4d11 100644
--- a/x-pack/packages/kbn-elastic-assistant/tsconfig.json
+++ b/x-pack/packages/kbn-elastic-assistant/tsconfig.json
@@ -30,5 +30,6 @@
"@kbn/core-doc-links-browser",
"@kbn/core",
"@kbn/zod",
+ "@kbn/data-views-plugin",
]
}
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.ts
index 7dac58ddecc9b..aef66d406bf74 100644
--- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.ts
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.ts
@@ -12,10 +12,11 @@ import {
DocumentEntryCreateFields,
KnowledgeBaseEntryCreateProps,
KnowledgeBaseEntryResponse,
+ KnowledgeBaseEntryUpdateProps,
Metadata,
} from '@kbn/elastic-assistant-common';
import { getKnowledgeBaseEntry } from './get_knowledge_base_entry';
-import { CreateKnowledgeBaseEntrySchema } from './types';
+import { CreateKnowledgeBaseEntrySchema, UpdateKnowledgeBaseEntrySchema } from './types';
export interface CreateKnowledgeBaseEntryParams {
esClient: ElasticsearchClient;
@@ -77,6 +78,111 @@ export const createKnowledgeBaseEntry = async ({
}
};
+interface TransformToUpdateSchemaProps {
+ user: AuthenticatedUser;
+ updatedAt: string;
+ entry: KnowledgeBaseEntryUpdateProps;
+ global?: boolean;
+}
+
+export const transformToUpdateSchema = ({
+ user,
+ updatedAt,
+ entry,
+ global = false,
+}: TransformToUpdateSchemaProps): UpdateKnowledgeBaseEntrySchema => {
+ const base = {
+ id: entry.id,
+ updated_at: updatedAt,
+ updated_by: user.profile_uid ?? 'unknown',
+ name: entry.name,
+ type: entry.type,
+ users: global
+ ? []
+ : [
+ {
+ id: user.profile_uid,
+ name: user.username,
+ },
+ ],
+ };
+
+ if (entry.type === 'index') {
+ const { inputSchema, outputFields, queryDescription, ...restEntry } = entry;
+ return {
+ ...base,
+ ...restEntry,
+ query_description: queryDescription,
+ input_schema:
+ entry.inputSchema?.map((schema) => ({
+ field_name: schema.fieldName,
+ field_type: schema.fieldType,
+ description: schema.description,
+ })) ?? undefined,
+ output_fields: outputFields ?? undefined,
+ };
+ }
+ return {
+ ...base,
+ kb_resource: entry.kbResource,
+ required: entry.required ?? false,
+ source: entry.source,
+ text: entry.text,
+ vector: undefined,
+ };
+};
+
+export const getUpdateScript = ({
+ entry,
+ isPatch,
+}: {
+ entry: UpdateKnowledgeBaseEntrySchema;
+ isPatch?: boolean;
+}) => {
+ return {
+ source: `
+ if (params.assignEmpty == true || params.containsKey('name')) {
+ ctx._source.name = params.name;
+ }
+ if (params.assignEmpty == true || params.containsKey('type')) {
+ ctx._source.type = params.type;
+ }
+ if (params.assignEmpty == true || params.containsKey('users')) {
+ ctx._source.users = params.users;
+ }
+ if (params.assignEmpty == true || params.containsKey('query_description')) {
+ ctx._source.query_description = params.query_description;
+ }
+ if (params.assignEmpty == true || params.containsKey('input_schema')) {
+ ctx._source.input_schema = params.input_schema;
+ }
+ if (params.assignEmpty == true || params.containsKey('output_fields')) {
+ ctx._source.output_fields = params.output_fields;
+ }
+ if (params.assignEmpty == true || params.containsKey('kb_resource')) {
+ ctx._source.kb_resource = params.kb_resource;
+ }
+ if (params.assignEmpty == true || params.containsKey('required')) {
+ ctx._source.required = params.required;
+ }
+ if (params.assignEmpty == true || params.containsKey('source')) {
+ ctx._source.source = params.source;
+ }
+ if (params.assignEmpty == true || params.containsKey('text')) {
+ ctx._source.text = params.text;
+ }
+ ctx._source.updated_at = params.updated_at;
+ ctx._source.updated_by = params.updated_by;
+ `,
+ lang: 'painless',
+ params: {
+ ...entry, // when assigning undefined in painless, it will remove property and wil set it to null
+ // for patch we don't want to remove unspecified value in payload
+ assignEmpty: !(isPatch ?? true),
+ },
+ };
+};
+
interface TransformToCreateSchemaProps {
createdAt: string;
spaceId: string;
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.ts
index 8ff8de6cfb408..de76a38135f0b 100644
--- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.ts
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.ts
@@ -6,6 +6,7 @@
*/
import { z } from '@kbn/zod';
+import { get } from 'lodash';
import { DynamicStructuredTool } from '@langchain/core/tools';
import { errors } from '@elastic/elasticsearch';
import { QueryDslQueryContainer, SearchRequest } from '@elastic/elasticsearch/lib/api/types';
@@ -189,7 +190,7 @@ export const getStructuredToolForIndexEntry = ({
standard: {
query: {
nested: {
- path: 'semantic_text.inference.chunks',
+ path: `${indexEntry.field}.inference.chunks`,
query: {
sparse_vector: {
inference_id: elserId,
@@ -220,7 +221,7 @@ export const getStructuredToolForIndexEntry = ({
}, {});
}
return {
- text: (hit._source as { text: string }).text,
+ text: get(hit._source, `${indexEntry.field}.inference.chunks[0].text`),
};
});
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts
index a81e18630138e..1906f59ab4b32 100644
--- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts
@@ -15,6 +15,7 @@ import { Document } from 'langchain/document';
import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import {
DocumentEntryType,
+ DocumentEntry,
IndexEntry,
KnowledgeBaseEntryCreateProps,
KnowledgeBaseEntryResponse,
@@ -431,7 +432,9 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
);
this.options.logger.debug(
() =>
- `getKnowledgeBaseDocuments() - Similarity Search Results:\n ${JSON.stringify(results)}`
+ `getKnowledgeBaseDocuments() - Similarity Search returned [${JSON.stringify(
+ results.length
+ )}] results`
);
return results;
@@ -441,6 +444,47 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
}
};
+ /**
+ * Returns all global and current user's private `required` document entries.
+ */
+ public getRequiredKnowledgeBaseDocumentEntries = async (): Promise => {
+ const user = this.options.currentUser;
+ if (user == null) {
+ throw new Error(
+ 'Authenticated user not found! Ensure kbDataClient was initialized from a request.'
+ );
+ }
+
+ try {
+ const userFilter = getKBUserFilter(user);
+ const results = await this.findDocuments({
+ // Note: This is a magic number to set some upward bound as to not blow the context with too
+ // many historical KB entries. Ideally we'd query for all and token trim.
+ perPage: 100,
+ page: 1,
+ sortField: 'created_at',
+ sortOrder: 'asc',
+ filter: `${userFilter} AND type:document AND kb_resource:user AND required:true`,
+ });
+ this.options.logger.debug(
+ `kbDataClient.getRequiredKnowledgeBaseDocumentEntries() - results:\n${JSON.stringify(
+ results
+ )}`
+ );
+
+ if (results) {
+ return transformESSearchToKnowledgeBaseEntry(results.data) as DocumentEntry[];
+ }
+ } catch (e) {
+ this.options.logger.error(
+ `kbDataClient.getRequiredKnowledgeBaseDocumentEntries() - Failed to fetch DocumentEntries`
+ );
+ return [];
+ }
+
+ return [];
+ };
+
/**
* Creates a new Knowledge Base Entry.
*
@@ -479,7 +523,10 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
};
/**
- * Returns AssistantTools for any 'relevant' KB IndexEntries that exist in the knowledge base
+ * Returns AssistantTools for any 'relevant' KB IndexEntries that exist in the knowledge base.
+ *
+ * Note: Accepts esClient so retrieval can be scoped to the current user as esClient on kbDataClient
+ * is scoped to system user.
*/
public getAssistantTools = async ({
assistantToolParams,
@@ -507,7 +554,7 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
page: 1,
sortField: 'created_at',
sortOrder: 'asc',
- filter: `${userFilter}${` AND type:index`}`, // TODO: Support global tools (no user filter), and filter by space as well
+ filter: `${userFilter} AND type:index`,
});
this.options.logger.debug(
`kbDataClient.getAssistantTools() - results:\n${JSON.stringify(results, null, 2)}`
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/types.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/types.ts
index ecf9260e999d2..3de1a15d79b2a 100644
--- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/types.ts
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/types.ts
@@ -82,6 +82,39 @@ export interface LegacyEsKnowledgeBaseEntrySchema {
model_id: string;
};
}
+export interface UpdateKnowledgeBaseEntrySchema {
+ id: string;
+ created_at?: string;
+ created_by?: string;
+ updated_at?: string;
+ updated_by?: string;
+ users?: Array<{
+ id?: string;
+ name?: string;
+ }>;
+ name?: string;
+ type?: string;
+ // Document Entry Fields
+ kb_resource?: string;
+ required?: boolean;
+ source?: string;
+ text?: string;
+ vector?: {
+ tokens: Record;
+ model_id: string;
+ };
+ // Index Entry Fields
+ index?: string;
+ field?: string;
+ description?: string;
+ query_description?: string;
+ input_schema?: Array<{
+ field_name: string;
+ field_type: string;
+ description: string;
+ }>;
+ output_fields?: string[];
+}
export interface CreateKnowledgeBaseEntrySchema {
'@timestamp'?: string;
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.ts
index 942f94c203873..08912f41a8bbc 100644
--- a/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.ts
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.ts
@@ -84,6 +84,7 @@ export class AIAssistantService {
private isKBSetupInProgress: boolean = false;
// Temporary 'feature flag' to determine if we should initialize the new kb mappings, toggled when accessing kbDataClient
private v2KnowledgeBaseEnabled: boolean = false;
+ private hasInitializedV2KnowledgeBase: boolean = false;
constructor(private readonly options: AIAssistantServiceOpts) {
this.initialized = false;
@@ -363,8 +364,13 @@ export class AIAssistantService {
// If either v2 KB or a modelIdOverride is provided, we need to reinitialize all persistence resources to make sure
// they're using the correct model/mappings. Technically all existing KB data is stale since it was created
// with a different model/mappings, but modelIdOverride is only intended for testing purposes at this time
- if (opts.v2KnowledgeBaseEnabled || opts.modelIdOverride != null) {
+ // Added hasInitializedV2KnowledgeBase to prevent the console noise from re-init on each KB request
+ if (
+ !this.hasInitializedV2KnowledgeBase &&
+ (opts.v2KnowledgeBaseEnabled || opts.modelIdOverride != null)
+ ) {
await this.initializeResources();
+ this.hasInitializedV2KnowledgeBase = true;
}
const res = await this.checkResourcesInstallation(opts);
diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/graph.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/graph.ts
index dba756b9f3c9e..4688caa176b56 100644
--- a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/graph.ts
+++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/graph.ts
@@ -137,7 +137,7 @@ export const getDefaultAssistantGraph = ({
})
)
.addNode(NodeType.AGENT, (state: AgentState) =>
- runAgent({ ...nodeParams, state, agentRunnable })
+ runAgent({ ...nodeParams, state, agentRunnable, kbDataClient: dataClients?.kbDataClient })
)
.addNode(NodeType.TOOLS, (state: AgentState) => executeTools({ ...nodeParams, state, tools }))
.addNode(NodeType.RESPOND, (state: AgentState) =>
diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/run_agent.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/run_agent.ts
index 2d076f6bd1472..053254a1d99b3 100644
--- a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/run_agent.ts
+++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/run_agent.ts
@@ -10,15 +10,20 @@ import { AgentRunnableSequence } from 'langchain/dist/agents/agent';
import { formatLatestUserMessage } from '../prompts';
import { AgentState, NodeParamsBase } from '../types';
import { NodeType } from '../constants';
+import { AIAssistantKnowledgeBaseDataClient } from '../../../../../ai_assistant_data_clients/knowledge_base';
export interface RunAgentParams extends NodeParamsBase {
state: AgentState;
config?: RunnableConfig;
agentRunnable: AgentRunnableSequence;
+ kbDataClient?: AIAssistantKnowledgeBaseDataClient;
}
export const AGENT_NODE_TAG = 'agent_run';
+const KNOWLEDGE_HISTORY_PREFIX = 'Knowledge History:';
+const NO_KNOWLEDGE_HISTORY = '[No existing knowledge history]';
+
/**
* Node to run the agent
*
@@ -26,18 +31,27 @@ export const AGENT_NODE_TAG = 'agent_run';
* @param state - The current state of the graph
* @param config - Any configuration that may've been supplied
* @param agentRunnable - The agent to run
+ * @param kbDataClient - Data client for accessing the Knowledge Base on behalf of the current user
*/
export async function runAgent({
logger,
state,
agentRunnable,
config,
+ kbDataClient,
}: RunAgentParams): Promise> {
logger.debug(() => `${NodeType.AGENT}: Node state:\n${JSON.stringify(state, null, 2)}`);
+ const knowledgeHistory = await kbDataClient?.getRequiredKnowledgeBaseDocumentEntries();
+
const agentOutcome = await agentRunnable.withConfig({ tags: [AGENT_NODE_TAG] }).invoke(
{
...state,
+ knowledge_history: `${KNOWLEDGE_HISTORY_PREFIX}\n${
+ knowledgeHistory?.length
+ ? JSON.stringify(knowledgeHistory.map((e) => e.text))
+ : NO_KNOWLEDGE_HISTORY
+ }`,
// prepend any user prompt (gemini)
input: formatLatestUserMessage(state.input, state.llmType),
chat_history: state.messages, // TODO: Message de-dupe with ...state spread
diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/translations.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/translations.ts
index e55e1081e6474..e5a1c14846e23 100644
--- a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/translations.ts
+++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/translations.ts
@@ -8,8 +8,10 @@
const YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT =
'You are a security analyst and expert in resolving security incidents. Your role is to assist by answering questions about Elastic Security.';
const IF_YOU_DONT_KNOW_THE_ANSWER = 'Do not answer questions unrelated to Elastic Security.';
+export const KNOWLEDGE_HISTORY =
+ 'If available, use the Knowledge History provided to try and answer the question. If not provided, you can try and query for additional knowledge via the KnowledgeBaseRetrievalTool.';
-export const DEFAULT_SYSTEM_PROMPT = `${YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT} ${IF_YOU_DONT_KNOW_THE_ANSWER}`;
+export const DEFAULT_SYSTEM_PROMPT = `${YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT} ${IF_YOU_DONT_KNOW_THE_ANSWER} ${KNOWLEDGE_HISTORY}`;
// system prompt from @afirstenberg
const BASE_GEMINI_PROMPT =
'You are an assistant that is an expert at using tools and Elastic Security, doing your best to use these tools to answer questions or follow instructions. It is very important to use tools to answer the question or follow the instructions rather than coming up with your own answer. Tool calls are good. Sometimes you may need to make several tool calls to accomplish the task or get an answer to the question that was asked. Use as many tool calls as necessary.';
@@ -19,7 +21,7 @@ export const GEMINI_SYSTEM_PROMPT = `${BASE_GEMINI_PROMPT} ${KB_CATCH}`;
export const BEDROCK_SYSTEM_PROMPT = `Use tools as often as possible, as they have access to the latest data and syntax. Always return value from ESQLKnowledgeBaseTool as is. Never return tags in the response, but make sure to include tags content in the response. Do not reflect on the quality of the returned search results in your response.`;
export const GEMINI_USER_PROMPT = `Now, always using the tools at your disposal, step by step, come up with a response to this request:\n\n`;
-export const STRUCTURED_SYSTEM_PROMPT = `Respond to the human as helpfully and accurately as possible. You have access to the following tools:
+export const STRUCTURED_SYSTEM_PROMPT = `Respond to the human as helpfully and accurately as possible. ${KNOWLEDGE_HISTORY} You have access to the following tools:
{tools}
diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/prompts.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/prompts.ts
index 883047ed7b9df..05cc8b50852f5 100644
--- a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/prompts.ts
+++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/prompts.ts
@@ -17,6 +17,7 @@ import {
export const formatPrompt = (prompt: string, additionalPrompt?: string) =>
ChatPromptTemplate.fromMessages([
['system', additionalPrompt ? `${prompt}\n\n${additionalPrompt}` : prompt],
+ ['placeholder', '{knowledge_history}'],
['placeholder', '{chat_history}'],
['human', '{input}'],
['placeholder', '{agent_scratchpad}'],
@@ -39,6 +40,7 @@ export const geminiToolCallingAgentPrompt = formatPrompt(systemPrompts.gemini);
export const formatPromptStructured = (prompt: string, additionalPrompt?: string) =>
ChatPromptTemplate.fromMessages([
['system', additionalPrompt ? `${prompt}\n\n${additionalPrompt}` : prompt],
+ ['placeholder', '{knowledge_history}'],
['placeholder', '{chat_history}'],
[
'human',
diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.ts
index 96045b17e6171..ce3f0c8c92693 100644
--- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.ts
+++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.ts
@@ -22,11 +22,18 @@ import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/
import { performChecks } from '../../helpers';
import { KNOWLEDGE_BASE_ENTRIES_TABLE_MAX_PAGE_SIZE } from '../../../../common/constants';
-import { EsKnowledgeBaseEntrySchema } from '../../../ai_assistant_data_clients/knowledge_base/types';
+import {
+ EsKnowledgeBaseEntrySchema,
+ UpdateKnowledgeBaseEntrySchema,
+} from '../../../ai_assistant_data_clients/knowledge_base/types';
import { ElasticAssistantPluginRouter } from '../../../types';
import { buildResponse } from '../../utils';
import { transformESSearchToKnowledgeBaseEntry } from '../../../ai_assistant_data_clients/knowledge_base/transforms';
-import { transformToCreateSchema } from '../../../ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry';
+import {
+ getUpdateScript,
+ transformToCreateSchema,
+ transformToUpdateSchema,
+} from '../../../ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry';
export interface BulkOperationError {
message: string;
@@ -210,7 +217,17 @@ export const bulkActionKnowledgeBaseEntriesRoute = (router: ElasticAssistantPlug
})
),
documentsToDelete: body.delete?.ids,
- documentsToUpdate: [], // TODO: Support bulk update
+ documentsToUpdate: body.update?.map((entry) =>
+ // TODO: KB-RBAC check, required when users != null as entry will either be created globally if empty
+ transformToUpdateSchema({
+ user: authenticatedUser,
+ updatedAt: changedAt,
+ entry,
+ global: entry.users != null && entry.users.length === 0,
+ })
+ ),
+ getUpdateScript: (entry: UpdateKnowledgeBaseEntrySchema) =>
+ getUpdateScript({ entry, isPatch: true }),
authenticatedUser,
});
const created =
diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/create_route.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/create_route.ts
index 3dbb5a9cf930e..51e3d48505ec2 100644
--- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/create_route.ts
+++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/create_route.ts
@@ -66,7 +66,7 @@ export const createKnowledgeBaseEntryRoute = (router: ElasticAssistantPluginRout
logger.debug(() => `Creating KB Entry:\n${JSON.stringify(request.body)}`);
const createResponse = await kbDataClient?.createKnowledgeBaseEntry({
knowledgeBaseEntry: request.body,
- // TODO: KB-RBAC check, required when users != null as entry will either be created globally if empty, or for specific users (only admin API feature)
+ // TODO: KB-RBAC check, required when users != null as entry will either be created globally if empty
global: request.body.users != null && request.body.users.length === 0,
});
diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/find_route.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/find_route.ts
index f10876c4be3ee..356d5d9150a67 100644
--- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/find_route.ts
+++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/find_route.ts
@@ -74,7 +74,7 @@ export const findKnowledgeBaseEntriesRoute = (router: ElasticAssistantPluginRout
});
const currentUser = ctx.elasticAssistant.getCurrentUser();
const userFilter = getKBUserFilter(currentUser);
- const systemFilter = ` AND kb_resource:"user"`;
+ const systemFilter = ` AND (kb_resource:"user" OR type:"index")`;
const additionalFilter = query.filter ? ` AND ${query.filter}` : '';
const result = await kbDataClient?.findDocuments({
@@ -160,7 +160,7 @@ export const findKnowledgeBaseEntriesRoute = (router: ElasticAssistantPluginRout
body: {
perPage: result.perPage,
page: result.page,
- total: result.total,
+ total: result.total + systemEntries.length,
data: [...transformESSearchToKnowledgeBaseEntry(result.data), ...systemEntries],
},
});
diff --git a/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.test.tsx b/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.test.tsx
index 1c988d14e845f..65a0ab84d3412 100644
--- a/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.test.tsx
+++ b/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.test.tsx
@@ -77,6 +77,11 @@ describe('ManagementSettings', () => {
securitySolutionAssistant: { 'ai-assistant': false },
},
},
+ data: {
+ dataViews: {
+ getIndices: jest.fn(),
+ },
+ },
security: {
userProfiles: {
getCurrent: jest.fn().mockResolvedValue({ data: { color: 'blue', initials: 'P' } }),
diff --git a/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.tsx b/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.tsx
index 90e39398474ec..48d89e02dfc71 100644
--- a/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.tsx
+++ b/x-pack/plugins/security_solution/public/assistant/stack_management/management_settings.tsx
@@ -37,6 +37,7 @@ export const ManagementSettings = React.memo(() => {
securitySolutionAssistant: { 'ai-assistant': securityAIAssistantEnabled },
},
},
+ data: { dataViews },
security,
} = useKibana().services;
@@ -46,8 +47,8 @@ export const ManagementSettings = React.memo(() => {
security?.userProfiles.getCurrent<{ avatar: UserAvatar }>({
dataPath: 'avatar',
}),
- select: (data) => {
- return data.data.avatar;
+ select: (d) => {
+ return d.data.avatar;
},
keepPreviousData: true,
refetchOnWindowFocus: false,
@@ -79,7 +80,12 @@ export const ManagementSettings = React.memo(() => {
}
if (conversations) {
- return ;
+ return (
+
+ );
}
return <>>;
From 2327681de7306c20bcca69fe77660c0a586c979d Mon Sep 17 00:00:00 2001
From: Jean-Louis Leysens
Date: Wed, 9 Oct 2024 18:31:42 +0200
Subject: [PATCH 04/87] [HTTP/OAS] Ability to exclude routes from introspection
(#192675)
---
.../src/http_resources_service.test.ts | 17 +++++
.../src/http_resources_service.ts | 1 +
.../src/router.ts | 3 +-
.../src/http_service.ts | 76 +++++++++++--------
.../http/core-http-server/src/router/route.ts | 11 ++-
packages/kbn-router-to-openapispec/index.ts | 5 +-
.../src/util.test.ts | 9 +++
.../kbn-router-to-openapispec/src/util.ts | 3 +-
.../server/integration_tests/http/oas.test.ts | 4 +-
9 files changed, 91 insertions(+), 38 deletions(-)
diff --git a/packages/core/http/core-http-resources-server-internal/src/http_resources_service.test.ts b/packages/core/http/core-http-resources-server-internal/src/http_resources_service.test.ts
index efce905e6564f..1a7757d4e1eaa 100644
--- a/packages/core/http/core-http-resources-server-internal/src/http_resources_service.test.ts
+++ b/packages/core/http/core-http-resources-server-internal/src/http_resources_service.test.ts
@@ -69,6 +69,23 @@ describe('HttpResources service', () => {
expect(registeredRouteConfig.options?.access).toBe('internal');
});
+ it('registration defaults to excluded from OAS', () => {
+ register({ ...routeConfig, options: { access: 'internal' } }, async (ctx, req, res) =>
+ res.ok()
+ );
+ const [[registeredRouteConfig]] = router.get.mock.calls;
+ expect(registeredRouteConfig.options?.excludeFromOAS).toBe(true);
+ });
+
+ it('registration allows being included in OAS', () => {
+ register(
+ { ...routeConfig, options: { access: 'internal', excludeFromOAS: false } },
+ async (ctx, req, res) => res.ok()
+ );
+ const [[registeredRouteConfig]] = router.get.mock.calls;
+ expect(registeredRouteConfig.options?.excludeFromOAS).toBe(false);
+ });
+
describe('renderCoreApp', () => {
it('formats successful response', async () => {
register(routeConfig, async (ctx, req, res) => {
diff --git a/packages/core/http/core-http-resources-server-internal/src/http_resources_service.ts b/packages/core/http/core-http-resources-server-internal/src/http_resources_service.ts
index d9e75d49e72cf..29114c0dffc07 100644
--- a/packages/core/http/core-http-resources-server-internal/src/http_resources_service.ts
+++ b/packages/core/http/core-http-resources-server-internal/src/http_resources_service.ts
@@ -89,6 +89,7 @@ export class HttpResourcesService implements CoreService !route.isVersioned);
}
- return [...this.routes];
+ return this.routes;
}
public handleLegacyErrors = wrapErrors;
diff --git a/packages/core/http/core-http-server-internal/src/http_service.ts b/packages/core/http/core-http-server-internal/src/http_service.ts
index e5a82f0abefb0..3f803b06f15fd 100644
--- a/packages/core/http/core-http-server-internal/src/http_service.ts
+++ b/packages/core/http/core-http-server-internal/src/http_service.ts
@@ -9,9 +9,13 @@
import { Observable, Subscription, combineLatest, firstValueFrom, of, mergeMap } from 'rxjs';
import { map } from 'rxjs';
+import { schema, TypeOf } from '@kbn/config-schema';
import { pick, Semaphore } from '@kbn/std';
-import { generateOpenApiDocument } from '@kbn/router-to-openapispec';
+import {
+ generateOpenApiDocument,
+ type GenerateOpenApiDocumentOptionsFilters,
+} from '@kbn/router-to-openapispec';
import { Logger } from '@kbn/logging';
import { Env } from '@kbn/config';
import type { CoreContext, CoreService } from '@kbn/core-base-server-internal';
@@ -254,49 +258,55 @@ export class HttpService
const baseUrl =
basePath.publicBaseUrl ?? `http://localhost:${config.port}${basePath.serverBasePath}`;
+ const stringOrStringArraySchema = schema.oneOf([
+ schema.string(),
+ schema.arrayOf(schema.string()),
+ ]);
+ const querySchema = schema.object({
+ access: schema.maybe(schema.oneOf([schema.literal('public'), schema.literal('internal')])),
+ excludePathsMatching: schema.maybe(stringOrStringArraySchema),
+ pathStartsWith: schema.maybe(stringOrStringArraySchema),
+ pluginId: schema.maybe(schema.string()),
+ version: schema.maybe(schema.string()),
+ });
+
server.route({
path: '/api/oas',
method: 'GET',
handler: async (req, h) => {
- const version = req.query?.version;
-
- let pathStartsWith: undefined | string[];
- if (typeof req.query?.pathStartsWith === 'string') {
- pathStartsWith = [req.query.pathStartsWith];
- } else {
- pathStartsWith = req.query?.pathStartsWith;
- }
-
- let excludePathsMatching: undefined | string[];
- if (typeof req.query?.excludePathsMatching === 'string') {
- excludePathsMatching = [req.query.excludePathsMatching];
- } else {
- excludePathsMatching = req.query?.excludePathsMatching;
+ let filters: GenerateOpenApiDocumentOptionsFilters;
+ let query: TypeOf;
+ try {
+ query = querySchema.validate(req.query);
+ filters = {
+ ...query,
+ excludePathsMatching:
+ typeof query.excludePathsMatching === 'string'
+ ? [query.excludePathsMatching]
+ : query.excludePathsMatching,
+ pathStartsWith:
+ typeof query.pathStartsWith === 'string'
+ ? [query.pathStartsWith]
+ : query.pathStartsWith,
+ };
+ } catch (e) {
+ return h.response({ message: e.message }).code(400);
}
-
- const pluginId = req.query?.pluginId;
-
- const access = req.query?.access as 'public' | 'internal' | undefined;
- if (access && !['public', 'internal'].some((a) => a === access)) {
- return h
- .response({
- message: 'Invalid access query parameter. Must be one of "public" or "internal".',
- })
- .code(400);
- }
-
return await firstValueFrom(
of(1).pipe(
HttpService.generateOasSemaphore.acquire(),
mergeMap(async () => {
try {
// Potentially quite expensive
- const result = generateOpenApiDocument(this.httpServer.getRouters({ pluginId }), {
- baseUrl,
- title: 'Kibana HTTP APIs',
- version: '0.0.0', // TODO get a better version here
- filters: { pathStartsWith, excludePathsMatching, access, version },
- });
+ const result = generateOpenApiDocument(
+ this.httpServer.getRouters({ pluginId: query.pluginId }),
+ {
+ baseUrl,
+ title: 'Kibana HTTP APIs',
+ version: '0.0.0', // TODO get a better version here
+ filters,
+ }
+ );
return h.response(result);
} catch (e) {
this.log.error(e);
diff --git a/packages/core/http/core-http-server/src/router/route.ts b/packages/core/http/core-http-server/src/router/route.ts
index bdf4f9f03c784..194191e6f423f 100644
--- a/packages/core/http/core-http-server/src/router/route.ts
+++ b/packages/core/http/core-http-server/src/router/route.ts
@@ -215,7 +215,7 @@ export interface RouteConfigOptions {
/**
* Defines intended request origin of the route:
* - public. The route is public, declared stable and intended for external access.
- * In the future, may require an incomming request to contain a specified header.
+ * In the future, may require an incoming request to contain a specified header.
* - internal. The route is internal and intended for internal access only.
*
* Defaults to 'internal' If not declared,
@@ -284,6 +284,14 @@ export interface RouteConfigOptions {
*/
deprecated?: boolean;
+ /**
+ * Whether this route should be treated as "invisible" and excluded from router
+ * OAS introspection.
+ *
+ * @default false
+ */
+ excludeFromOAS?: boolean;
+
/**
* Release version or date that this route will be removed
* Use with `deprecated: true`
@@ -292,6 +300,7 @@ export interface RouteConfigOptions {
* @example 9.0.0
*/
discontinued?: string;
+
/**
* Defines the security requirements for a route, including authorization and authentication.
*
diff --git a/packages/kbn-router-to-openapispec/index.ts b/packages/kbn-router-to-openapispec/index.ts
index 17f8253348ab3..1869167db0323 100644
--- a/packages/kbn-router-to-openapispec/index.ts
+++ b/packages/kbn-router-to-openapispec/index.ts
@@ -7,4 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-export { generateOpenApiDocument } from './src/generate_oas';
+export {
+ generateOpenApiDocument,
+ type GenerateOpenApiDocumentOptionsFilters,
+} from './src/generate_oas';
diff --git a/packages/kbn-router-to-openapispec/src/util.test.ts b/packages/kbn-router-to-openapispec/src/util.test.ts
index 79b4ddf8eba84..abbb605df79e5 100644
--- a/packages/kbn-router-to-openapispec/src/util.test.ts
+++ b/packages/kbn-router-to-openapispec/src/util.test.ts
@@ -163,6 +163,15 @@ describe('prepareRoutes', () => {
output: [{ path: '/api/foo', options: { access: pub } }],
filters: { excludePathsMatching: ['/api/b'], access: pub },
},
+ {
+ input: [
+ { path: '/api/foo', options: { access: pub, excludeFromOAS: true } },
+ { path: '/api/bar', options: { access: internal } },
+ { path: '/api/baz', options: { access: pub } },
+ ],
+ output: [{ path: '/api/baz', options: { access: pub } }],
+ filters: { excludePathsMatching: ['/api/bar'], access: pub },
+ },
])('returns the expected routes #%#', ({ input, output, filters }) => {
expect(prepareRoutes(input, filters)).toEqual(output);
});
diff --git a/packages/kbn-router-to-openapispec/src/util.ts b/packages/kbn-router-to-openapispec/src/util.ts
index 1aa2a080ccc18..55f7348dc199a 100644
--- a/packages/kbn-router-to-openapispec/src/util.ts
+++ b/packages/kbn-router-to-openapispec/src/util.ts
@@ -105,13 +105,14 @@ export const getVersionedHeaderParam = (
});
export const prepareRoutes = <
- R extends { path: string; options: { access?: 'public' | 'internal' } }
+ R extends { path: string; options: { access?: 'public' | 'internal'; excludeFromOAS?: boolean } }
>(
routes: R[],
filters: GenerateOpenApiDocumentOptionsFilters = {}
): R[] => {
if (Object.getOwnPropertyNames(filters).length === 0) return routes;
return routes.filter((route) => {
+ if (route.options.excludeFromOAS) return false;
if (
filters.excludePathsMatching &&
filters.excludePathsMatching.some((ex) => route.path.startsWith(ex))
diff --git a/src/core/server/integration_tests/http/oas.test.ts b/src/core/server/integration_tests/http/oas.test.ts
index c6a1d4e308356..413b8b01754b5 100644
--- a/src/core/server/integration_tests/http/oas.test.ts
+++ b/src/core/server/integration_tests/http/oas.test.ts
@@ -193,7 +193,9 @@ it('only accepts "public" or "internal" for "access" query param', async () => {
const server = await startService({ config: { server: { oas: { enabled: true } } } });
const result = await supertest(server.listener).get('/api/oas').query({ access: 'invalid' });
expect(result.body.message).toBe(
- 'Invalid access query parameter. Must be one of "public" or "internal".'
+ `[access]: types that failed validation:
+- [access.0]: expected value to equal [public]
+- [access.1]: expected value to equal [internal]`
);
expect(result.status).toBe(400);
});
From 61251bfdaffdb621558fff96d78cc8b2260c0abe Mon Sep 17 00:00:00 2001
From: Jean-Louis Leysens
Date: Wed, 9 Oct 2024 18:33:54 +0200
Subject: [PATCH 05/87] [Http] Added version header to unversioned, public
routes (#195464)
---
.../src/router.test.ts | 50 ++++++++++--
.../src/router.ts | 26 +++++--
.../src/util.test.ts | 17 ++++-
.../src/util.ts | 29 +++++++
.../versioned_router/core_versioned_route.ts | 13 +---
.../inject_response_headers.ts | 27 -------
.../integration_tests/http/router.test.ts | 76 +++++++++++++++++++
.../http/versioned_router.test.ts | 68 +++++++----------
8 files changed, 214 insertions(+), 92 deletions(-)
delete mode 100644 packages/core/http/core-http-router-server-internal/src/versioned_router/inject_response_headers.ts
diff --git a/packages/core/http/core-http-router-server-internal/src/router.test.ts b/packages/core/http/core-http-router-server-internal/src/router.test.ts
index 65f5b41f91fba..b506933574d4a 100644
--- a/packages/core/http/core-http-router-server-internal/src/router.test.ts
+++ b/packages/core/http/core-http-router-server-internal/src/router.test.ts
@@ -7,20 +7,22 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import { Router, type RouterOptions } from './router';
+import type { ResponseToolkit, ResponseObject } from '@hapi/hapi';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { isConfigSchema, schema } from '@kbn/config-schema';
-import { createFooValidation } from './router.test.util';
import { createRequestMock } from '@kbn/hapi-mocks/src/request';
+import { createFooValidation } from './router.test.util';
+import { Router, type RouterOptions } from './router';
import type { RouteValidatorRequestAndResponses } from '@kbn/core-http-server';
-const mockResponse: any = {
+const mockResponse = {
code: jest.fn().mockImplementation(() => mockResponse),
header: jest.fn().mockImplementation(() => mockResponse),
-};
-const mockResponseToolkit: any = {
+} as unknown as jest.Mocked;
+
+const mockResponseToolkit = {
response: jest.fn().mockReturnValue(mockResponse),
-};
+} as unknown as jest.Mocked;
const logger = loggingSystemMock.create().get();
const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {});
@@ -132,6 +134,42 @@ describe('Router', () => {
}
);
+ it('adds versioned header v2023-10-31 to public, unversioned routes', async () => {
+ const router = new Router('', logger, enhanceWithContext, routerOptions);
+ router.post(
+ {
+ path: '/public',
+ options: {
+ access: 'public',
+ },
+ validate: false,
+ },
+ (context, req, res) => res.ok({ headers: { AAAA: 'test' } }) // with some fake headers
+ );
+ router.post(
+ {
+ path: '/internal',
+ options: {
+ access: 'internal',
+ },
+ validate: false,
+ },
+ (context, req, res) => res.ok()
+ );
+ const [{ handler: publicHandler }, { handler: internalHandler }] = router.getRoutes();
+
+ await publicHandler(createRequestMock(), mockResponseToolkit);
+ expect(mockResponse.header).toHaveBeenCalledTimes(2);
+ const [first, second] = mockResponse.header.mock.calls
+ .concat()
+ .sort(([k1], [k2]) => k1.localeCompare(k2));
+ expect(first).toEqual(['AAAA', 'test']);
+ expect(second).toEqual(['elastic-api-version', '2023-10-31']);
+
+ await internalHandler(createRequestMock(), mockResponseToolkit);
+ expect(mockResponse.header).toHaveBeenCalledTimes(2); // no additional calls
+ });
+
it('constructs lazily provided validations once (idempotency)', async () => {
const router = new Router('', logger, enhanceWithContext, routerOptions);
const { fooValidation } = testValidation;
diff --git a/packages/core/http/core-http-router-server-internal/src/router.ts b/packages/core/http/core-http-router-server-internal/src/router.ts
index 13715dc166395..1a74e27910c1a 100644
--- a/packages/core/http/core-http-router-server-internal/src/router.ts
+++ b/packages/core/http/core-http-router-server-internal/src/router.ts
@@ -33,13 +33,13 @@ import { validBodyOutput, getRequestValidation } from '@kbn/core-http-server';
import type { RouteSecurityGetter } from '@kbn/core-http-server';
import type { DeepPartial } from '@kbn/utility-types';
import { RouteValidator } from './validator';
-import { CoreVersionedRouter } from './versioned_router';
+import { ALLOWED_PUBLIC_VERSION, CoreVersionedRouter } from './versioned_router';
import { CoreKibanaRequest } from './request';
import { kibanaResponseFactory } from './response';
import { HapiResponseAdapter } from './response_adapter';
import { wrapErrors } from './error_wrapper';
import { Method } from './versioned_router/types';
-import { prepareRouteConfigValidation } from './util';
+import { getVersionHeader, injectVersionHeader, prepareRouteConfigValidation } from './util';
import { stripIllegalHttp2Headers } from './strip_illegal_http2_headers';
import { validRouteSecurity } from './security_route_config_validator';
import { InternalRouteConfig } from './route';
@@ -201,10 +201,11 @@ export class Router(
route: InternalRouteConfig,
handler: RequestHandler
,
- internalOptions: { isVersioned: boolean } = { isVersioned: false }
+ { isVersioned }: { isVersioned: boolean } = { isVersioned: false }
) => {
route = prepareRouteConfigValidation(route);
const routeSchemas = routeSchemasFromRouteConfig(route, method);
+ const isPublicUnversionedRoute = route.options?.access === 'public' && !isVersioned;
this.routes.push({
handler: async (req, responseToolkit) =>
@@ -212,18 +213,19 @@ export class Router, route.options),
/** Below is added for introspection */
validationSchemas: route.validate,
- isVersioned: internalOptions.isVersioned,
+ isVersioned,
});
};
@@ -267,10 +269,12 @@ export class Router {
it('wraps only expected values in "once"', () => {
@@ -49,3 +50,17 @@ describe('prepareResponseValidation', () => {
expect(validation.response![500].body).toBeUndefined();
});
});
+
+describe('injectResponseHeaders', () => {
+ it('injects an empty value as expected', () => {
+ const result = injectResponseHeaders({}, kibanaResponseFactory.ok());
+ expect(result.options.headers).toEqual({});
+ });
+ it('merges values as expected', () => {
+ const result = injectResponseHeaders(
+ { foo: 'false', baz: 'true' },
+ kibanaResponseFactory.ok({ headers: { foo: 'true', bar: 'false' } })
+ );
+ expect(result.options.headers).toEqual({ foo: 'false', bar: 'false', baz: 'true' });
+ });
+});
diff --git a/packages/core/http/core-http-router-server-internal/src/util.ts b/packages/core/http/core-http-router-server-internal/src/util.ts
index 0d1c8abb0e103..176d33b589880 100644
--- a/packages/core/http/core-http-router-server-internal/src/util.ts
+++ b/packages/core/http/core-http-router-server-internal/src/util.ts
@@ -14,6 +14,9 @@ import {
type RouteMethod,
type RouteValidator,
} from '@kbn/core-http-server';
+import type { Mutable } from 'utility-types';
+import type { IKibanaResponse, ResponseHeaders } from '@kbn/core-http-server';
+import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import type { InternalRouteConfig } from './route';
function isStatusCode(key: string) {
@@ -63,3 +66,29 @@ export function prepareRouteConfigValidation(
}
return config;
}
+
+/**
+ * @note mutates the response object
+ * @internal
+ */
+export function injectResponseHeaders(
+ headers: ResponseHeaders,
+ response: IKibanaResponse
+): IKibanaResponse {
+ const mutableResponse = response as Mutable;
+ mutableResponse.options.headers = {
+ ...mutableResponse.options.headers,
+ ...headers,
+ };
+ return mutableResponse;
+}
+
+export function getVersionHeader(version: string): ResponseHeaders {
+ return {
+ [ELASTIC_HTTP_VERSION_HEADER]: version,
+ };
+}
+
+export function injectVersionHeader(version: string, response: IKibanaResponse): IKibanaResponse {
+ return injectResponseHeaders(getVersionHeader(version), response);
+}
diff --git a/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.ts
index 71ab30bbe8b80..e9a9e60de8193 100644
--- a/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.ts
+++ b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.ts
@@ -38,7 +38,7 @@ import {
readVersion,
removeQueryVersion,
} from './route_version_utils';
-import { injectResponseHeaders } from './inject_response_headers';
+import { getVersionHeader, injectVersionHeader } from '../util';
import { validRouteSecurity } from '../security_route_config_validator';
import { resolvers } from './handler_resolvers';
@@ -221,9 +221,7 @@ export class CoreVersionedRoute implements VersionedRoute {
req.params = params;
req.query = query;
} catch (e) {
- return res.badRequest({
- body: e.message,
- });
+ return res.badRequest({ body: e.message, headers: getVersionHeader(version) });
}
} else {
// Preserve behavior of not passing through unvalidated data
@@ -252,12 +250,7 @@ export class CoreVersionedRoute implements VersionedRoute {
}
}
- return injectResponseHeaders(
- {
- [ELASTIC_HTTP_VERSION_HEADER]: version,
- },
- response
- );
+ return injectVersionHeader(version, response);
};
private validateVersion(version: string) {
diff --git a/packages/core/http/core-http-router-server-internal/src/versioned_router/inject_response_headers.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/inject_response_headers.ts
deleted file mode 100644
index c27c92023f56e..0000000000000
--- a/packages/core/http/core-http-router-server-internal/src/versioned_router/inject_response_headers.ts
+++ /dev/null
@@ -1,27 +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", 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 type { Mutable } from 'utility-types';
-import type { IKibanaResponse } from '@kbn/core-http-server';
-
-/**
- * @note mutates the response object
- * @internal
- */
-export function injectResponseHeaders(headers: object, response: IKibanaResponse): IKibanaResponse {
- const mutableResponse = response as Mutable;
- mutableResponse.options = {
- ...mutableResponse.options,
- headers: {
- ...mutableResponse.options.headers,
- ...headers,
- },
- };
- return mutableResponse;
-}
diff --git a/src/core/server/integration_tests/http/router.test.ts b/src/core/server/integration_tests/http/router.test.ts
index 0b7bbb8ce55c3..c0a690e479e67 100644
--- a/src/core/server/integration_tests/http/router.test.ts
+++ b/src/core/server/integration_tests/http/router.test.ts
@@ -836,6 +836,82 @@ describe('Handler', () => {
expect(body).toEqual(12);
});
+
+ it('adds versioned header v2023-10-31 to public, unversioned routes', async () => {
+ const { server: innerServer, createRouter } = await server.setup(setupDeps);
+ const router = createRouter('/');
+
+ router.post(
+ {
+ path: '/public',
+ validate: { body: schema.object({ ok: schema.boolean() }) },
+ options: {
+ access: 'public',
+ },
+ },
+ (context, req, res) => {
+ if (req.body.ok) {
+ return res.ok({ body: 'ok', headers: { test: 'this' } });
+ }
+ return res.customError({ statusCode: 499, body: 'custom error' });
+ }
+ );
+ router.post(
+ {
+ path: '/internal',
+ validate: { body: schema.object({ ok: schema.boolean() }) },
+ },
+ (context, req, res) => {
+ return res.ok({ body: 'ok', headers: { test: 'this' } });
+ }
+ );
+ await server.start();
+
+ // Includes header if validation fails
+ {
+ const { headers } = await supertest(innerServer.listener)
+ .post('/public')
+ .send({ ok: null })
+ .expect(400);
+ expect(headers).toMatchObject({ 'elastic-api-version': '2023-10-31' });
+ }
+
+ // Includes header if custom error
+ {
+ const { headers } = await supertest(innerServer.listener)
+ .post('/public')
+ .send({ ok: false })
+ .expect(499);
+ expect(headers).toMatchObject({ 'elastic-api-version': '2023-10-31' });
+ }
+
+ // Includes header if OK
+ {
+ const { headers } = await supertest(innerServer.listener)
+ .post('/public')
+ .send({ ok: true })
+ .expect(200);
+ expect(headers).toMatchObject({ 'elastic-api-version': '2023-10-31' });
+ }
+
+ // Internal unversioned routes do not include the header for OK
+ {
+ const { headers } = await supertest(innerServer.listener)
+ .post('/internal')
+ .send({ ok: true })
+ .expect(200);
+ expect(headers).not.toMatchObject({ 'elastic-api-version': '2023-10-31' });
+ }
+
+ // Internal unversioned routes do not include the header for validation failures
+ {
+ const { headers } = await supertest(innerServer.listener)
+ .post('/internal')
+ .send({ ok: null })
+ .expect(400);
+ expect(headers).not.toMatchObject({ 'elastic-api-version': '2023-10-31' });
+ }
+ });
});
describe('handleLegacyErrors', () => {
diff --git a/src/core/server/integration_tests/http/versioned_router.test.ts b/src/core/server/integration_tests/http/versioned_router.test.ts
index 9f2b2625a6a7e..254337f82abcf 100644
--- a/src/core/server/integration_tests/http/versioned_router.test.ts
+++ b/src/core/server/integration_tests/http/versioned_router.test.ts
@@ -112,14 +112,12 @@ describe('Routing versioned requests', () => {
await server.start();
- await expect(supertest.get('/my-path').expect(200)).resolves.toEqual(
- expect.objectContaining({
- body: { v: '1' },
- header: expect.objectContaining({
- 'elastic-api-version': '2020-02-02',
- }),
- })
- );
+ await expect(supertest.get('/my-path').expect(200)).resolves.toMatchObject({
+ body: { v: '1' },
+ header: expect.objectContaining({
+ 'elastic-api-version': '2020-02-02',
+ }),
+ });
});
it('returns the expected output for badly formatted versions', async () => {
@@ -137,11 +135,9 @@ describe('Routing versioned requests', () => {
.set('Elastic-Api-Version', 'abc')
.expect(400)
.then(({ body }) => body)
- ).resolves.toEqual(
- expect.objectContaining({
- message: expect.stringMatching(/Invalid version/),
- })
- );
+ ).resolves.toMatchObject({
+ message: expect.stringMatching(/Invalid version/),
+ });
});
it('returns the expected responses for failed validation', async () => {
@@ -163,18 +159,14 @@ describe('Routing versioned requests', () => {
await server.start();
await expect(
- supertest
- .post('/my-path')
- .send({})
- .set('Elastic-Api-Version', '1')
- .expect(400)
- .then(({ body }) => body)
- ).resolves.toEqual(
- expect.objectContaining({
+ supertest.post('/my-path').send({}).set('Elastic-Api-Version', '1').expect(400)
+ ).resolves.toMatchObject({
+ body: {
error: 'Bad Request',
message: expect.stringMatching(/expected value of type/),
- })
- );
+ },
+ headers: { 'elastic-api-version': '1' }, // includes version if validation failed
+ });
expect(captureErrorMock).not.toHaveBeenCalled();
});
@@ -193,7 +185,7 @@ describe('Routing versioned requests', () => {
.set('Elastic-Api-Version', '2023-10-31')
.expect(200)
.then(({ header }) => header)
- ).resolves.toEqual(expect.objectContaining({ 'elastic-api-version': '2023-10-31' }));
+ ).resolves.toMatchObject({ 'elastic-api-version': '2023-10-31' });
});
it('runs response validation when in dev', async () => {
@@ -236,11 +228,9 @@ describe('Routing versioned requests', () => {
.set('Elastic-Api-Version', '1')
.expect(500)
.then(({ body }) => body)
- ).resolves.toEqual(
- expect.objectContaining({
- message: expect.stringMatching(/Failed output validation/),
- })
- );
+ ).resolves.toMatchObject({
+ message: expect.stringMatching(/Failed output validation/),
+ });
await expect(
supertest
@@ -248,11 +238,9 @@ describe('Routing versioned requests', () => {
.set('Elastic-Api-Version', '2')
.expect(500)
.then(({ body }) => body)
- ).resolves.toEqual(
- expect.objectContaining({
- message: expect.stringMatching(/Failed output validation/),
- })
- );
+ ).resolves.toMatchObject({
+ message: expect.stringMatching(/Failed output validation/),
+ });
// This should pass response validation
await expect(
@@ -261,11 +249,9 @@ describe('Routing versioned requests', () => {
.set('Elastic-Api-Version', '3')
.expect(200)
.then(({ body }) => body)
- ).resolves.toEqual(
- expect.objectContaining({
- v: '3',
- })
- );
+ ).resolves.toMatchObject({
+ v: '3',
+ });
expect(captureErrorMock).not.toHaveBeenCalled();
});
@@ -367,9 +353,7 @@ describe('Routing versioned requests', () => {
.set('Elastic-Api-Version', '2020-02-02')
.expect(500)
.then(({ body }) => body)
- ).resolves.toEqual(
- expect.objectContaining({ message: expect.stringMatching(/No handlers registered/) })
- );
+ ).resolves.toMatchObject({ message: expect.stringMatching(/No handlers registered/) });
expect(captureErrorMock).not.toHaveBeenCalled();
});
From a209fe8d7d7d1e27bb9b80475ea2821a9202e823 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Wed, 9 Oct 2024 18:46:31 +0200
Subject: [PATCH 06/87] [Lens][ES|QL] Do not refetch the attributes if the
query hasn't changed (#195196)
## Summary
When a user is creating a Lens ES|QL chart we run the suggestions api
even if the query hasn't changed. This PR adds a guard to avoid
refetching the attributes when the query hasn't changed at all.
---
.../shared/edit_on_the_fly/lens_configuration_flyout.tsx | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx
index ecc392a7e56b7..fd0407513f869 100644
--- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx
@@ -337,6 +337,7 @@ export function LensEditConfigurationFlyout({
setErrors([]);
updateSuggestion?.(attrs);
}
+ prevQuery.current = q;
setIsVisualizationLoading(false);
},
[
@@ -481,7 +482,6 @@ export function LensEditConfigurationFlyout({
query={query}
onTextLangQueryChange={(q) => {
setQuery(q);
- prevQuery.current = q;
}}
detectedTimestamp={adHocDataViews?.[0]?.timeFieldName}
hideTimeFilterInfo={hideTimeFilterInfo}
@@ -497,7 +497,8 @@ export function LensEditConfigurationFlyout({
editorIsInline
hideRunQueryText
onTextLangQuerySubmit={async (q, a) => {
- if (q) {
+ // do not run the suggestions if the query is the same as the previous one
+ if (q && !isEqual(q, prevQuery.current)) {
setIsVisualizationLoading(true);
await runQuery(q, a);
}
From 5ed13ee4a4b4325bae2f3e117a4fc400540fa542 Mon Sep 17 00:00:00 2001
From: "Christiane (Tina) Heiligers"
Date: Wed, 9 Oct 2024 10:12:52 -0700
Subject: [PATCH 07/87] Update deprecations carried over from 8 (#195491)
Fix https://github.com/elastic/kibana/issues/142915
### Risk Matrix
| Risk | Probability | Severity | Mitigation/Notes |
|---------------------------|-------------|----------|-------------------------|
| Third party plugin types throw type errors | Low | Low | type checks
will error when using a deprecated type. Plugin authors should extend
the supported types or define new ones inline |
### For maintainers
- [X] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
(no breaking changes)
---
.../core-application-browser/src/app_mount.ts | 2 +-
.../src/contracts.ts | 2 +-
.../src/plugin.ts | 4 +---
.../core/plugins/core-plugins-server/index.ts | 1 -
.../plugins/core-plugins-server/src/index.ts | 1 -
.../plugins/core-plugins-server/src/types.ts | 24 +------------------
src/core/server/index.ts | 1 -
7 files changed, 4 insertions(+), 31 deletions(-)
diff --git a/packages/core/application/core-application-browser/src/app_mount.ts b/packages/core/application/core-application-browser/src/app_mount.ts
index a34550bc98fcd..4fb38b10a3704 100644
--- a/packages/core/application/core-application-browser/src/app_mount.ts
+++ b/packages/core/application/core-application-browser/src/app_mount.ts
@@ -89,7 +89,7 @@ export interface AppMountParameters {
* This string should not include the base path from HTTP.
*
* @deprecated Use {@link AppMountParameters.history} instead.
- * @removeBy 8.8.0
+ * remove after https://github.com/elastic/kibana/issues/132600 is done
*
* @example
*
diff --git a/packages/core/elasticsearch/core-elasticsearch-server/src/contracts.ts b/packages/core/elasticsearch/core-elasticsearch-server/src/contracts.ts
index bc712a61a535e..4e0bd253eb8b4 100644
--- a/packages/core/elasticsearch/core-elasticsearch-server/src/contracts.ts
+++ b/packages/core/elasticsearch/core-elasticsearch-server/src/contracts.ts
@@ -81,7 +81,7 @@ export interface ElasticsearchServiceSetup {
setUnauthorizedErrorHandler: (handler: UnauthorizedErrorHandler) => void;
/**
- * @deprecated
+ * @deprecated Can be removed when https://github.com/elastic/kibana/issues/119862 is done.
*/
legacy: {
/**
diff --git a/packages/core/plugins/core-plugins-server-internal/src/plugin.ts b/packages/core/plugins/core-plugins-server-internal/src/plugin.ts
index 8837cb24083d6..cd330a647da66 100644
--- a/packages/core/plugins/core-plugins-server-internal/src/plugin.ts
+++ b/packages/core/plugins/core-plugins-server-internal/src/plugin.ts
@@ -15,7 +15,6 @@ import { isConfigSchema } from '@kbn/config-schema';
import type { Logger } from '@kbn/logging';
import { type PluginOpaqueId, PluginType } from '@kbn/core-base-common';
import type {
- AsyncPlugin,
Plugin,
PluginConfigDescriptor,
PluginInitializer,
@@ -58,8 +57,7 @@ export class PluginWrapper<
private instance?:
| Plugin
- | PrebootPlugin
- | AsyncPlugin;
+ | PrebootPlugin;
private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart, TStart]>();
public readonly startDependencies = firstValueFrom(this.startDependencies$);
diff --git a/packages/core/plugins/core-plugins-server/index.ts b/packages/core/plugins/core-plugins-server/index.ts
index b2c6057c4a1ac..a5fd0fd2e2ec3 100644
--- a/packages/core/plugins/core-plugins-server/index.ts
+++ b/packages/core/plugins/core-plugins-server/index.ts
@@ -10,7 +10,6 @@
export type {
PrebootPlugin,
Plugin,
- AsyncPlugin,
PluginConfigDescriptor,
PluginConfigSchema,
PluginInitializer,
diff --git a/packages/core/plugins/core-plugins-server/src/index.ts b/packages/core/plugins/core-plugins-server/src/index.ts
index 35b1b7c11d422..e48d077389ece 100644
--- a/packages/core/plugins/core-plugins-server/src/index.ts
+++ b/packages/core/plugins/core-plugins-server/src/index.ts
@@ -10,7 +10,6 @@
export type {
PrebootPlugin,
Plugin,
- AsyncPlugin,
PluginConfigDescriptor,
PluginConfigSchema,
PluginInitializer,
diff --git a/packages/core/plugins/core-plugins-server/src/types.ts b/packages/core/plugins/core-plugins-server/src/types.ts
index 6da8b2727733e..7be2647ba48d2 100644
--- a/packages/core/plugins/core-plugins-server/src/types.ts
+++ b/packages/core/plugins/core-plugins-server/src/types.ts
@@ -301,26 +301,6 @@ export interface Plugin<
stop?(): MaybePromise;
}
-/**
- * A plugin with asynchronous lifecycle methods.
- *
- * @deprecated Asynchronous lifecycles are deprecated, and should be migrated to sync {@link Plugin | plugin}
- * @removeBy 8.8.0
- * @public
- */
-export interface AsyncPlugin<
- TSetup = void,
- TStart = void,
- TPluginsSetup extends object = object,
- TPluginsStart extends object = object
-> {
- setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise;
-
- start(core: CoreStart, plugins: TPluginsStart): TStart | Promise;
-
- stop?(): MaybePromise;
-}
-
/**
* @public
*/
@@ -478,7 +458,5 @@ export type PluginInitializer<
> = (
core: PluginInitializerContext
) => Promise<
- | Plugin
- | PrebootPlugin
- | AsyncPlugin
+ Plugin | PrebootPlugin
>;
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index f4852bdc97fe3..1ac38b1d44157 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -267,7 +267,6 @@ export { PluginType } from '@kbn/core-base-common';
export type {
PrebootPlugin,
Plugin,
- AsyncPlugin,
PluginConfigDescriptor,
PluginConfigSchema,
PluginInitializer,
From 7448376119aa1aad0888eb68449c1b15fd0852ac Mon Sep 17 00:00:00 2001
From: Nathan Reese
Date: Wed, 9 Oct 2024 11:21:15 -0600
Subject: [PATCH 08/87] [dashboard] do not async import dashboard actions on
plugin load (#195616)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Part of https://github.com/elastic/kibana/issues/194171
Notice in the screen shot below, how opening Kibana home page loads
async dashboard chunks.
This PR replaces async import with a sync import. The page load bundle
size increases but this is no different than the current behavior, just
the import is now properly accounted for in page load stats. The next
step is to resolve https://github.com/elastic/kibana/issues/191642 and
reduce the page load impact of registering uiActions (but this is out of
scope for this PR).
---
src/plugins/dashboard/public/plugin.tsx | 15 +++++++--------
1 file changed, 7 insertions(+), 8 deletions(-)
diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx
index 0957bf9364524..b7a920eb08ce3 100644
--- a/src/plugins/dashboard/public/plugin.tsx
+++ b/src/plugins/dashboard/public/plugin.tsx
@@ -78,6 +78,7 @@ import {
} from './dashboard_container/panel_placement';
import type { FindDashboardsService } from './services/dashboard_content_management_service/types';
import { setKibanaServices, untilPluginStartServicesReady } from './services/kibana_services';
+import { buildAllDashboardActions } from './dashboard_actions';
export interface DashboardFeatureFlagConfig {
allowByValueEmbeddables: boolean;
@@ -322,14 +323,12 @@ export class DashboardPlugin
public start(core: CoreStart, plugins: DashboardStartDependencies): DashboardStart {
setKibanaServices(core, plugins);
- Promise.all([import('./dashboard_actions'), untilPluginStartServicesReady()]).then(
- ([{ buildAllDashboardActions }]) => {
- buildAllDashboardActions({
- plugins,
- allowByValueEmbeddables: this.dashboardFeatureFlagConfig?.allowByValueEmbeddables,
- });
- }
- );
+ untilPluginStartServicesReady().then(() => {
+ buildAllDashboardActions({
+ plugins,
+ allowByValueEmbeddables: this.dashboardFeatureFlagConfig?.allowByValueEmbeddables,
+ });
+ });
return {
locator: this.locator,
From 15bccdf233d847f34ee4cbcc30f8a8e775207c42 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Felix=20St=C3=BCrmer?=
Date: Wed, 9 Oct 2024 19:21:52 +0200
Subject: [PATCH 09/87] [Logs Overview] Overview component (iteration 1)
(#191899)
This introduces a "Logs Overview" component for use in solution UIs
behind a feature flag.
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Kerry Gallagher <471693+Kerry350@users.noreply.github.com>
Co-authored-by: Elastic Machine
---
.eslintrc.js | 1 +
.github/CODEOWNERS | 1 +
package.json | 5 +
.../src/lib/entity.ts | 12 +
.../src/lib/gaussian_events.ts | 74 +++++
.../src/lib/infra/host.ts | 10 +-
.../src/lib/infra/index.ts | 3 +-
.../src/lib/interval.ts | 18 +-
.../src/lib/logs/index.ts | 21 ++
.../src/lib/poisson_events.test.ts | 53 ++++
.../src/lib/poisson_events.ts | 77 +++++
.../src/lib/timerange.ts | 27 +-
.../distributed_unstructured_logs.ts | 197 ++++++++++++
.../scenarios/helpers/unstructured_logs.ts | 94 ++++++
packages/kbn-apm-synthtrace/tsconfig.json | 1 +
.../settings/setting_ids/index.ts | 1 +
.../src/worker/webpack.config.ts | 12 +
packages/kbn-xstate-utils/kibana.jsonc | 2 +-
.../kbn-xstate-utils/src/console_inspector.ts | 88 ++++++
packages/kbn-xstate-utils/src/index.ts | 1 +
.../server/collectors/management/schema.ts | 6 +
.../server/collectors/management/types.ts | 1 +
src/plugins/telemetry/schema/oss_plugins.json | 6 +
tsconfig.base.json | 2 +
x-pack/.i18nrc.json | 3 +
.../observability/logs_overview/README.md | 3 +
.../observability/logs_overview/index.ts | 21 ++
.../logs_overview/jest.config.js | 12 +
.../observability/logs_overview/kibana.jsonc | 5 +
.../observability/logs_overview/package.json | 7 +
.../discover_link/discover_link.tsx | 110 +++++++
.../src/components/discover_link/index.ts | 8 +
.../src/components/log_categories/index.ts | 8 +
.../log_categories/log_categories.tsx | 94 ++++++
.../log_categories_control_bar.tsx | 44 +++
.../log_categories_error_content.tsx | 44 +++
.../log_categories/log_categories_grid.tsx | 182 +++++++++++
.../log_categories_grid_cell.tsx | 99 ++++++
.../log_categories_grid_change_time_cell.tsx | 54 ++++
.../log_categories_grid_change_type_cell.tsx | 108 +++++++
.../log_categories_grid_count_cell.tsx | 32 ++
.../log_categories_grid_histogram_cell.tsx | 99 ++++++
.../log_categories_grid_pattern_cell.tsx | 60 ++++
.../log_categories_loading_content.tsx | 68 +++++
.../log_categories_result_content.tsx | 87 ++++++
.../src/components/logs_overview/index.ts | 10 +
.../logs_overview/logs_overview.tsx | 64 ++++
.../logs_overview_error_content.tsx | 41 +++
.../logs_overview_loading_content.tsx | 23 ++
.../categorize_documents.ts | 282 ++++++++++++++++++
.../categorize_logs_service.ts | 250 ++++++++++++++++
.../count_documents.ts | 60 ++++
.../services/categorize_logs_service/index.ts | 8 +
.../categorize_logs_service/queries.ts | 151 ++++++++++
.../services/categorize_logs_service/types.ts | 21 ++
.../observability/logs_overview/src/types.ts | 74 +++++
.../logs_overview/src/utils/logs_source.ts | 60 ++++
.../logs_overview/src/utils/xstate5_utils.ts | 13 +
.../observability/logs_overview/tsconfig.json | 39 +++
.../components/app/service_logs/index.tsx | 171 ++++++++++-
.../routing/service_detail/index.tsx | 2 +-
.../apm/public/plugin.ts | 2 +
.../components/tabs/logs/logs_tab_content.tsx | 94 ++++--
.../logs_shared/kibana.jsonc | 5 +-
.../public/components/logs_overview/index.tsx | 8 +
.../logs_overview/logs_overview.mock.tsx | 32 ++
.../logs_overview/logs_overview.tsx | 70 +++++
.../logs_shared/public/index.ts | 1 +
.../logs_shared/public/mocks.tsx | 2 +
.../logs_shared/public/plugin.ts | 23 +-
.../logs_shared/public/types.ts | 12 +-
.../logs_shared/server/feature_flags.ts | 33 ++
.../logs_shared/server/plugin.ts | 28 +-
.../logs_shared/tsconfig.json | 4 +
yarn.lock | 17 ++
75 files changed, 3405 insertions(+), 56 deletions(-)
create mode 100644 packages/kbn-apm-synthtrace-client/src/lib/gaussian_events.ts
create mode 100644 packages/kbn-apm-synthtrace-client/src/lib/poisson_events.test.ts
create mode 100644 packages/kbn-apm-synthtrace-client/src/lib/poisson_events.ts
create mode 100644 packages/kbn-apm-synthtrace/src/scenarios/distributed_unstructured_logs.ts
create mode 100644 packages/kbn-apm-synthtrace/src/scenarios/helpers/unstructured_logs.ts
create mode 100644 packages/kbn-xstate-utils/src/console_inspector.ts
create mode 100644 x-pack/packages/observability/logs_overview/README.md
create mode 100644 x-pack/packages/observability/logs_overview/index.ts
create mode 100644 x-pack/packages/observability/logs_overview/jest.config.js
create mode 100644 x-pack/packages/observability/logs_overview/kibana.jsonc
create mode 100644 x-pack/packages/observability/logs_overview/package.json
create mode 100644 x-pack/packages/observability/logs_overview/src/components/discover_link/discover_link.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/discover_link/index.ts
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/index.ts
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_control_bar.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_error_content.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_cell.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_time_cell.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_type_cell.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_count_cell.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_histogram_cell.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_pattern_cell.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_loading_content.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_result_content.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/logs_overview/index.ts
create mode 100644 x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_error_content.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_loading_content.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_documents.ts
create mode 100644 x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_logs_service.ts
create mode 100644 x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/count_documents.ts
create mode 100644 x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/index.ts
create mode 100644 x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/queries.ts
create mode 100644 x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/types.ts
create mode 100644 x-pack/packages/observability/logs_overview/src/types.ts
create mode 100644 x-pack/packages/observability/logs_overview/src/utils/logs_source.ts
create mode 100644 x-pack/packages/observability/logs_overview/src/utils/xstate5_utils.ts
create mode 100644 x-pack/packages/observability/logs_overview/tsconfig.json
create mode 100644 x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/index.tsx
create mode 100644 x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.mock.tsx
create mode 100644 x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.tsx
create mode 100644 x-pack/plugins/observability_solution/logs_shared/server/feature_flags.ts
diff --git a/.eslintrc.js b/.eslintrc.js
index 797b84522df3f..c604844089ef4 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -978,6 +978,7 @@ module.exports = {
files: [
'x-pack/plugins/observability_solution/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)',
'src/plugins/ai_assistant_management/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)',
+ 'x-pack/packages/observability/logs_overview/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)',
],
rules: {
'@kbn/i18n/strings_should_be_translated_with_i18n': 'warn',
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 9b3c46d065fe1..974a7d39f63b3 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -652,6 +652,7 @@ x-pack/packages/observability/alerting_test_data @elastic/obs-ux-management-team
x-pack/test/cases_api_integration/common/plugins/observability @elastic/response-ops
x-pack/packages/observability/get_padded_alert_time_range_util @elastic/obs-ux-management-team
x-pack/plugins/observability_solution/observability_logs_explorer @elastic/obs-ux-logs-team
+x-pack/packages/observability/logs_overview @elastic/obs-ux-logs-team
x-pack/plugins/observability_solution/observability_onboarding/e2e @elastic/obs-ux-logs-team @elastic/obs-ux-onboarding-team
x-pack/plugins/observability_solution/observability_onboarding @elastic/obs-ux-logs-team @elastic/obs-ux-onboarding-team
x-pack/plugins/observability_solution/observability @elastic/obs-ux-management-team
diff --git a/package.json b/package.json
index 57b84f1c46dcb..58cd08773696f 100644
--- a/package.json
+++ b/package.json
@@ -97,6 +97,7 @@
"@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.cd77847.0",
"@types/react": "~18.2.0",
"@types/react-dom": "~18.2.0",
+ "@xstate5/react/**/xstate": "^5.18.1",
"globby/fast-glob": "^3.2.11"
},
"dependencies": {
@@ -687,6 +688,7 @@
"@kbn/observability-fixtures-plugin": "link:x-pack/test/cases_api_integration/common/plugins/observability",
"@kbn/observability-get-padded-alert-time-range-util": "link:x-pack/packages/observability/get_padded_alert_time_range_util",
"@kbn/observability-logs-explorer-plugin": "link:x-pack/plugins/observability_solution/observability_logs_explorer",
+ "@kbn/observability-logs-overview": "link:x-pack/packages/observability/logs_overview",
"@kbn/observability-onboarding-plugin": "link:x-pack/plugins/observability_solution/observability_onboarding",
"@kbn/observability-plugin": "link:x-pack/plugins/observability_solution/observability",
"@kbn/observability-shared-plugin": "link:x-pack/plugins/observability_solution/observability_shared",
@@ -1050,6 +1052,7 @@
"@turf/helpers": "6.0.1",
"@turf/length": "^6.0.2",
"@xstate/react": "^3.2.2",
+ "@xstate5/react": "npm:@xstate/react@^4.1.2",
"adm-zip": "^0.5.9",
"ai": "^2.2.33",
"ajv": "^8.12.0",
@@ -1283,6 +1286,7 @@
"whatwg-fetch": "^3.0.0",
"xml2js": "^0.5.0",
"xstate": "^4.38.2",
+ "xstate5": "npm:xstate@^5.18.1",
"xterm": "^5.1.0",
"yauzl": "^2.10.0",
"yazl": "^2.5.1",
@@ -1304,6 +1308,7 @@
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-transform-class-properties": "^7.24.7",
+ "@babel/plugin-transform-logical-assignment-operators": "^7.24.7",
"@babel/plugin-transform-numeric-separator": "^7.24.7",
"@babel/plugin-transform-runtime": "^7.24.7",
"@babel/preset-env": "^7.24.7",
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/entity.ts b/packages/kbn-apm-synthtrace-client/src/lib/entity.ts
index 4d522ef07ff0e..b26dbfc7ffb46 100644
--- a/packages/kbn-apm-synthtrace-client/src/lib/entity.ts
+++ b/packages/kbn-apm-synthtrace-client/src/lib/entity.ts
@@ -7,6 +7,8 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
+export type ObjectEntry = [keyof T, T[keyof T]];
+
export type Fields | undefined = undefined> = {
'@timestamp'?: number;
} & (TMeta extends undefined ? {} : Partial<{ meta: TMeta }>);
@@ -27,4 +29,14 @@ export class Entity {
return this;
}
+
+ overrides(overrides: Partial) {
+ const overrideEntries = Object.entries(overrides) as Array>;
+
+ overrideEntries.forEach(([fieldName, value]) => {
+ this.fields[fieldName] = value;
+ });
+
+ return this;
+ }
}
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/gaussian_events.ts b/packages/kbn-apm-synthtrace-client/src/lib/gaussian_events.ts
new file mode 100644
index 0000000000000..4f1db28017d29
--- /dev/null
+++ b/packages/kbn-apm-synthtrace-client/src/lib/gaussian_events.ts
@@ -0,0 +1,74 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", 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 { castArray } from 'lodash';
+import { SynthtraceGenerator } from '../types';
+import { Fields } from './entity';
+import { Serializable } from './serializable';
+
+export class GaussianEvents {
+ constructor(
+ private readonly from: Date,
+ private readonly to: Date,
+ private readonly mean: Date,
+ private readonly width: number,
+ private readonly totalPoints: number
+ ) {}
+
+ *generator(
+ map: (
+ timestamp: number,
+ index: number
+ ) => Serializable | Array>
+ ): SynthtraceGenerator {
+ if (this.totalPoints <= 0) {
+ return;
+ }
+
+ const startTime = this.from.getTime();
+ const endTime = this.to.getTime();
+ const meanTime = this.mean.getTime();
+ const densityInterval = 1 / (this.totalPoints - 1);
+
+ for (let eventIndex = 0; eventIndex < this.totalPoints; eventIndex++) {
+ const quantile = eventIndex * densityInterval;
+
+ const standardScore = Math.sqrt(2) * inverseError(2 * quantile - 1);
+ const timestamp = Math.round(meanTime + standardScore * this.width);
+
+ if (timestamp >= startTime && timestamp <= endTime) {
+ yield* this.generateEvents(timestamp, eventIndex, map);
+ }
+ }
+ }
+
+ private *generateEvents(
+ timestamp: number,
+ eventIndex: number,
+ map: (
+ timestamp: number,
+ index: number
+ ) => Serializable | Array>
+ ): Generator> {
+ const events = castArray(map(timestamp, eventIndex));
+ for (const event of events) {
+ yield event;
+ }
+ }
+}
+
+function inverseError(x: number): number {
+ const a = 0.147;
+ const sign = x < 0 ? -1 : 1;
+
+ const part1 = 2 / (Math.PI * a) + Math.log(1 - x * x) / 2;
+ const part2 = Math.log(1 - x * x) / a;
+
+ return sign * Math.sqrt(Math.sqrt(part1 * part1 - part2) - part1);
+}
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/infra/host.ts b/packages/kbn-apm-synthtrace-client/src/lib/infra/host.ts
index 198949b482be3..30550d64c4df8 100644
--- a/packages/kbn-apm-synthtrace-client/src/lib/infra/host.ts
+++ b/packages/kbn-apm-synthtrace-client/src/lib/infra/host.ts
@@ -27,7 +27,7 @@ interface HostDocument extends Fields {
'cloud.provider'?: string;
}
-class Host extends Entity {
+export class Host extends Entity {
cpu({ cpuTotalValue }: { cpuTotalValue?: number } = {}) {
return new HostMetrics({
...this.fields,
@@ -175,3 +175,11 @@ export function host(name: string): Host {
'cloud.provider': 'gcp',
});
}
+
+export function minimalHost(name: string): Host {
+ return new Host({
+ 'agent.id': 'synthtrace',
+ 'host.hostname': name,
+ 'host.name': name,
+ });
+}
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/infra/index.ts b/packages/kbn-apm-synthtrace-client/src/lib/infra/index.ts
index 853a9549ce02c..2957605cffcd3 100644
--- a/packages/kbn-apm-synthtrace-client/src/lib/infra/index.ts
+++ b/packages/kbn-apm-synthtrace-client/src/lib/infra/index.ts
@@ -8,7 +8,7 @@
*/
import { dockerContainer, DockerContainerMetricsDocument } from './docker_container';
-import { host, HostMetricsDocument } from './host';
+import { host, HostMetricsDocument, minimalHost } from './host';
import { k8sContainer, K8sContainerMetricsDocument } from './k8s_container';
import { pod, PodMetricsDocument } from './pod';
import { awsRds, AWSRdsMetricsDocument } from './aws/rds';
@@ -24,6 +24,7 @@ export type InfraDocument =
export const infra = {
host,
+ minimalHost,
pod,
dockerContainer,
k8sContainer,
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/interval.ts b/packages/kbn-apm-synthtrace-client/src/lib/interval.ts
index 1d56c42e1fe12..5a5ed3ab5fdbe 100644
--- a/packages/kbn-apm-synthtrace-client/src/lib/interval.ts
+++ b/packages/kbn-apm-synthtrace-client/src/lib/interval.ts
@@ -34,6 +34,10 @@ interface IntervalOptions {
rate?: number;
}
+interface StepDetails {
+ stepMilliseconds: number;
+}
+
export class Interval {
private readonly intervalAmount: number;
private readonly intervalUnit: unitOfTime.DurationConstructor;
@@ -46,12 +50,16 @@ export class Interval {
this._rate = options.rate || 1;
}
+ private getIntervalMilliseconds(): number {
+ return moment.duration(this.intervalAmount, this.intervalUnit).asMilliseconds();
+ }
+
private getTimestamps() {
const from = this.options.from.getTime();
const to = this.options.to.getTime();
let time: number = from;
- const diff = moment.duration(this.intervalAmount, this.intervalUnit).asMilliseconds();
+ const diff = this.getIntervalMilliseconds();
const timestamps: number[] = [];
@@ -68,15 +76,19 @@ export class Interval {
*generator(
map: (
timestamp: number,
- index: number
+ index: number,
+ stepDetails: StepDetails
) => Serializable | Array>
): SynthtraceGenerator {
const timestamps = this.getTimestamps();
+ const stepDetails: StepDetails = {
+ stepMilliseconds: this.getIntervalMilliseconds(),
+ };
let index = 0;
for (const timestamp of timestamps) {
- const events = castArray(map(timestamp, index));
+ const events = castArray(map(timestamp, index, stepDetails));
index++;
for (const event of events) {
yield event;
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts b/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts
index e19f0f6fd6565..2bbc59eb37e70 100644
--- a/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts
+++ b/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts
@@ -68,6 +68,7 @@ export type LogDocument = Fields &
'event.duration': number;
'event.start': Date;
'event.end': Date;
+ labels?: Record;
test_field: string | string[];
date: Date;
severity: string;
@@ -156,6 +157,26 @@ function create(logsOptions: LogsOptions = defaultLogsOptions): Log {
).dataset('synth');
}
+function createMinimal({
+ dataset = 'synth',
+ namespace = 'default',
+}: {
+ dataset?: string;
+ namespace?: string;
+} = {}): Log {
+ return new Log(
+ {
+ 'input.type': 'logs',
+ 'data_stream.namespace': namespace,
+ 'data_stream.type': 'logs',
+ 'data_stream.dataset': dataset,
+ 'event.dataset': dataset,
+ },
+ { isLogsDb: false }
+ );
+}
+
export const log = {
create,
+ createMinimal,
};
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.test.ts b/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.test.ts
new file mode 100644
index 0000000000000..0741884550f32
--- /dev/null
+++ b/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.test.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 { PoissonEvents } from './poisson_events';
+import { Serializable } from './serializable';
+
+describe('poisson events', () => {
+ it('generates events within the given time range', () => {
+ const poissonEvents = new PoissonEvents(new Date(1000), new Date(2000), 10);
+
+ const events = Array.from(
+ poissonEvents.generator((timestamp) => new Serializable({ '@timestamp': timestamp }))
+ );
+
+ expect(events.length).toBeGreaterThanOrEqual(1);
+
+ for (const event of events) {
+ expect(event.fields['@timestamp']).toBeGreaterThanOrEqual(1000);
+ expect(event.fields['@timestamp']).toBeLessThanOrEqual(2000);
+ }
+ });
+
+ it('generates at least one event if the rate is greater than 0', () => {
+ const poissonEvents = new PoissonEvents(new Date(1000), new Date(2000), 1);
+
+ const events = Array.from(
+ poissonEvents.generator((timestamp) => new Serializable({ '@timestamp': timestamp }))
+ );
+
+ expect(events.length).toBeGreaterThanOrEqual(1);
+
+ for (const event of events) {
+ expect(event.fields['@timestamp']).toBeGreaterThanOrEqual(1000);
+ expect(event.fields['@timestamp']).toBeLessThanOrEqual(2000);
+ }
+ });
+
+ it('generates no event if the rate is 0', () => {
+ const poissonEvents = new PoissonEvents(new Date(1000), new Date(2000), 0);
+
+ const events = Array.from(
+ poissonEvents.generator((timestamp) => new Serializable({ '@timestamp': timestamp }))
+ );
+
+ expect(events.length).toBe(0);
+ });
+});
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.ts b/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.ts
new file mode 100644
index 0000000000000..e7fd24b8323e7
--- /dev/null
+++ b/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.ts
@@ -0,0 +1,77 @@
+/*
+ * 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 { castArray } from 'lodash';
+import { SynthtraceGenerator } from '../types';
+import { Fields } from './entity';
+import { Serializable } from './serializable';
+
+export class PoissonEvents {
+ constructor(
+ private readonly from: Date,
+ private readonly to: Date,
+ private readonly rate: number
+ ) {}
+
+ private getTotalTimePeriod(): number {
+ return this.to.getTime() - this.from.getTime();
+ }
+
+ private getInterarrivalTime(): number {
+ const distribution = -Math.log(1 - Math.random()) / this.rate;
+ const totalTimePeriod = this.getTotalTimePeriod();
+ return Math.floor(distribution * totalTimePeriod);
+ }
+
+ *generator(
+ map: (
+ timestamp: number,
+ index: number
+ ) => Serializable | Array>
+ ): SynthtraceGenerator {
+ if (this.rate <= 0) {
+ return;
+ }
+
+ let currentTime = this.from.getTime();
+ const endTime = this.to.getTime();
+ let eventIndex = 0;
+
+ while (currentTime < endTime) {
+ const interarrivalTime = this.getInterarrivalTime();
+ currentTime += interarrivalTime;
+
+ if (currentTime < endTime) {
+ yield* this.generateEvents(currentTime, eventIndex, map);
+ eventIndex++;
+ }
+ }
+
+ // ensure at least one event has been emitted
+ if (this.rate > 0 && eventIndex === 0) {
+ const forcedEventTime =
+ this.from.getTime() + Math.floor(Math.random() * this.getTotalTimePeriod());
+ yield* this.generateEvents(forcedEventTime, eventIndex, map);
+ }
+ }
+
+ private *generateEvents(
+ timestamp: number,
+ eventIndex: number,
+ map: (
+ timestamp: number,
+ index: number
+ ) => Serializable | Array>
+ ): Generator> {
+ const events = castArray(map(timestamp, eventIndex));
+ for (const event of events) {
+ yield event;
+ }
+ }
+}
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts b/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts
index ccdea4ee75197..1c6f12414a148 100644
--- a/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts
+++ b/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts
@@ -9,10 +9,12 @@
import datemath from '@kbn/datemath';
import type { Moment } from 'moment';
+import { GaussianEvents } from './gaussian_events';
import { Interval } from './interval';
+import { PoissonEvents } from './poisson_events';
export class Timerange {
- constructor(private from: Date, private to: Date) {}
+ constructor(public readonly from: Date, public readonly to: Date) {}
interval(interval: string) {
return new Interval({ from: this.from, to: this.to, interval });
@@ -21,6 +23,29 @@ export class Timerange {
ratePerMinute(rate: number) {
return this.interval(`1m`).rate(rate);
}
+
+ poissonEvents(rate: number) {
+ return new PoissonEvents(this.from, this.to, rate);
+ }
+
+ gaussianEvents(mean: Date, width: number, totalPoints: number) {
+ return new GaussianEvents(this.from, this.to, mean, width, totalPoints);
+ }
+
+ splitInto(segmentCount: number): Timerange[] {
+ const duration = this.to.getTime() - this.from.getTime();
+ const segmentDuration = duration / segmentCount;
+
+ return Array.from({ length: segmentCount }, (_, i) => {
+ const from = new Date(this.from.getTime() + i * segmentDuration);
+ const to = new Date(from.getTime() + segmentDuration);
+ return new Timerange(from, to);
+ });
+ }
+
+ toString() {
+ return `Timerange(from=${this.from.toISOString()}, to=${this.to.toISOString()})`;
+ }
}
type DateLike = Date | number | Moment | string;
diff --git a/packages/kbn-apm-synthtrace/src/scenarios/distributed_unstructured_logs.ts b/packages/kbn-apm-synthtrace/src/scenarios/distributed_unstructured_logs.ts
new file mode 100644
index 0000000000000..83860635ae64a
--- /dev/null
+++ b/packages/kbn-apm-synthtrace/src/scenarios/distributed_unstructured_logs.ts
@@ -0,0 +1,197 @@
+/*
+ * 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 { infra, LogDocument, log } from '@kbn/apm-synthtrace-client';
+import { fakerEN as faker } from '@faker-js/faker';
+import { z } from '@kbn/zod';
+import { Scenario } from '../cli/scenario';
+import { withClient } from '../lib/utils/with_client';
+import {
+ LogMessageGenerator,
+ generateUnstructuredLogMessage,
+ unstructuredLogMessageGenerators,
+} from './helpers/unstructured_logs';
+
+const scenarioOptsSchema = z.intersection(
+ z.object({
+ randomSeed: z.number().default(0),
+ messageGroup: z
+ .enum([
+ 'httpAccess',
+ 'userAuthentication',
+ 'networkEvent',
+ 'dbOperations',
+ 'taskOperations',
+ 'degradedOperations',
+ 'errorOperations',
+ ])
+ .default('dbOperations'),
+ }),
+ z
+ .discriminatedUnion('distribution', [
+ z.object({
+ distribution: z.literal('uniform'),
+ rate: z.number().default(1),
+ }),
+ z.object({
+ distribution: z.literal('poisson'),
+ rate: z.number().default(1),
+ }),
+ z.object({
+ distribution: z.literal('gaussian'),
+ mean: z.coerce.date().describe('Time of the peak of the gaussian distribution'),
+ width: z.number().default(5000).describe('Width of the gaussian distribution in ms'),
+ totalPoints: z
+ .number()
+ .default(100)
+ .describe('Total number of points in the gaussian distribution'),
+ }),
+ ])
+ .default({ distribution: 'uniform', rate: 1 })
+);
+
+type ScenarioOpts = z.output;
+
+const scenario: Scenario = async (runOptions) => {
+ return {
+ generate: ({ range, clients: { logsEsClient } }) => {
+ const { logger } = runOptions;
+ const scenarioOpts = scenarioOptsSchema.parse(runOptions.scenarioOpts ?? {});
+
+ faker.seed(scenarioOpts.randomSeed);
+ faker.setDefaultRefDate(range.from.toISOString());
+
+ logger.debug(`Generating ${scenarioOpts.distribution} logs...`);
+
+ // Logs Data logic
+ const LOG_LEVELS = ['info', 'debug', 'error', 'warn', 'trace', 'fatal'];
+
+ const clusterDefinions = [
+ {
+ 'orchestrator.cluster.id': faker.string.nanoid(),
+ 'orchestrator.cluster.name': 'synth-cluster-1',
+ 'orchestrator.namespace': 'default',
+ 'cloud.provider': 'gcp',
+ 'cloud.region': 'eu-central-1',
+ 'cloud.availability_zone': 'eu-central-1a',
+ 'cloud.project.id': faker.string.nanoid(),
+ },
+ {
+ 'orchestrator.cluster.id': faker.string.nanoid(),
+ 'orchestrator.cluster.name': 'synth-cluster-2',
+ 'orchestrator.namespace': 'production',
+ 'cloud.provider': 'aws',
+ 'cloud.region': 'us-east-1',
+ 'cloud.availability_zone': 'us-east-1a',
+ 'cloud.project.id': faker.string.nanoid(),
+ },
+ {
+ 'orchestrator.cluster.id': faker.string.nanoid(),
+ 'orchestrator.cluster.name': 'synth-cluster-3',
+ 'orchestrator.namespace': 'kube',
+ 'cloud.provider': 'azure',
+ 'cloud.region': 'area-51',
+ 'cloud.availability_zone': 'area-51a',
+ 'cloud.project.id': faker.string.nanoid(),
+ },
+ ];
+
+ const hostEntities = [
+ {
+ 'host.name': 'host-1',
+ 'agent.id': 'synth-agent-1',
+ 'agent.name': 'nodejs',
+ 'cloud.instance.id': faker.string.nanoid(),
+ 'orchestrator.resource.id': faker.string.nanoid(),
+ ...clusterDefinions[0],
+ },
+ {
+ 'host.name': 'host-2',
+ 'agent.id': 'synth-agent-2',
+ 'agent.name': 'custom',
+ 'cloud.instance.id': faker.string.nanoid(),
+ 'orchestrator.resource.id': faker.string.nanoid(),
+ ...clusterDefinions[1],
+ },
+ {
+ 'host.name': 'host-3',
+ 'agent.id': 'synth-agent-3',
+ 'agent.name': 'python',
+ 'cloud.instance.id': faker.string.nanoid(),
+ 'orchestrator.resource.id': faker.string.nanoid(),
+ ...clusterDefinions[2],
+ },
+ ].map((hostDefinition) =>
+ infra.minimalHost(hostDefinition['host.name']).overrides(hostDefinition)
+ );
+
+ const serviceNames = Array(3)
+ .fill(null)
+ .map((_, idx) => `synth-service-${idx}`);
+
+ const generatorFactory =
+ scenarioOpts.distribution === 'uniform'
+ ? range.interval('1s').rate(scenarioOpts.rate)
+ : scenarioOpts.distribution === 'poisson'
+ ? range.poissonEvents(scenarioOpts.rate)
+ : range.gaussianEvents(scenarioOpts.mean, scenarioOpts.width, scenarioOpts.totalPoints);
+
+ const logs = generatorFactory.generator((timestamp) => {
+ const entity = faker.helpers.arrayElement(hostEntities);
+ const serviceName = faker.helpers.arrayElement(serviceNames);
+ const level = faker.helpers.arrayElement(LOG_LEVELS);
+ const messages = logMessageGenerators[scenarioOpts.messageGroup](faker);
+
+ return messages.map((message) =>
+ log
+ .createMinimal()
+ .message(message)
+ .logLevel(level)
+ .service(serviceName)
+ .overrides({
+ ...entity.fields,
+ labels: {
+ scenario: 'rare',
+ population: scenarioOpts.distribution,
+ },
+ })
+ .timestamp(timestamp)
+ );
+ });
+
+ return [
+ withClient(
+ logsEsClient,
+ logger.perf('generating_logs', () => [logs])
+ ),
+ ];
+ },
+ };
+};
+
+export default scenario;
+
+const logMessageGenerators = {
+ httpAccess: generateUnstructuredLogMessage([unstructuredLogMessageGenerators.httpAccess]),
+ userAuthentication: generateUnstructuredLogMessage([
+ unstructuredLogMessageGenerators.userAuthentication,
+ ]),
+ networkEvent: generateUnstructuredLogMessage([unstructuredLogMessageGenerators.networkEvent]),
+ dbOperations: generateUnstructuredLogMessage([unstructuredLogMessageGenerators.dbOperation]),
+ taskOperations: generateUnstructuredLogMessage([
+ unstructuredLogMessageGenerators.taskStatusSuccess,
+ ]),
+ degradedOperations: generateUnstructuredLogMessage([
+ unstructuredLogMessageGenerators.taskStatusFailure,
+ ]),
+ errorOperations: generateUnstructuredLogMessage([
+ unstructuredLogMessageGenerators.error,
+ unstructuredLogMessageGenerators.restart,
+ ]),
+} satisfies Record;
diff --git a/packages/kbn-apm-synthtrace/src/scenarios/helpers/unstructured_logs.ts b/packages/kbn-apm-synthtrace/src/scenarios/helpers/unstructured_logs.ts
new file mode 100644
index 0000000000000..490bd449e2b60
--- /dev/null
+++ b/packages/kbn-apm-synthtrace/src/scenarios/helpers/unstructured_logs.ts
@@ -0,0 +1,94 @@
+/*
+ * 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 { Faker, faker } from '@faker-js/faker';
+
+export type LogMessageGenerator = (f: Faker) => string[];
+
+export const unstructuredLogMessageGenerators = {
+ httpAccess: (f: Faker) => [
+ `${f.internet.ip()} - - [${f.date
+ .past()
+ .toISOString()
+ .replace('T', ' ')
+ .replace(
+ /\..+/,
+ ''
+ )}] "${f.internet.httpMethod()} ${f.internet.url()} HTTP/1.1" ${f.helpers.arrayElement([
+ 200, 301, 404, 500,
+ ])} ${f.number.int({ min: 100, max: 5000 })}`,
+ ],
+ dbOperation: (f: Faker) => [
+ `${f.database.engine()}: ${f.database.column()} ${f.helpers.arrayElement([
+ 'created',
+ 'updated',
+ 'deleted',
+ 'inserted',
+ ])} successfully ${f.number.int({ max: 100000 })} times`,
+ ],
+ taskStatusSuccess: (f: Faker) => [
+ `${f.hacker.noun()}: ${f.word.words()} ${f.helpers.arrayElement([
+ 'triggered',
+ 'executed',
+ 'processed',
+ 'handled',
+ ])} successfully at ${f.date.recent().toISOString()}`,
+ ],
+ taskStatusFailure: (f: Faker) => [
+ `${f.hacker.noun()}: ${f.helpers.arrayElement([
+ 'triggering',
+ 'execution',
+ 'processing',
+ 'handling',
+ ])} of ${f.word.words()} failed at ${f.date.recent().toISOString()}`,
+ ],
+ error: (f: Faker) => [
+ `${f.helpers.arrayElement([
+ 'Error',
+ 'Exception',
+ 'Failure',
+ 'Crash',
+ 'Bug',
+ 'Issue',
+ ])}: ${f.hacker.phrase()}`,
+ `Stopping ${f.number.int(42)} background tasks...`,
+ 'Shutting down process...',
+ ],
+ restart: (f: Faker) => {
+ const service = f.database.engine();
+ return [
+ `Restarting ${service}...`,
+ `Waiting for queue to drain...`,
+ `Service ${service} restarted ${f.helpers.arrayElement([
+ 'successfully',
+ 'with errors',
+ 'with warnings',
+ ])}`,
+ ];
+ },
+ userAuthentication: (f: Faker) => [
+ `User ${f.internet.userName()} ${f.helpers.arrayElement([
+ 'logged in',
+ 'logged out',
+ 'failed to login',
+ ])}`,
+ ],
+ networkEvent: (f: Faker) => [
+ `Network ${f.helpers.arrayElement([
+ 'connection',
+ 'disconnection',
+ 'data transfer',
+ ])} ${f.helpers.arrayElement(['from', 'to'])} ${f.internet.ip()}`,
+ ],
+} satisfies Record;
+
+export const generateUnstructuredLogMessage =
+ (generators: LogMessageGenerator[] = Object.values(unstructuredLogMessageGenerators)) =>
+ (f: Faker = faker) =>
+ f.helpers.arrayElement(generators)(f);
diff --git a/packages/kbn-apm-synthtrace/tsconfig.json b/packages/kbn-apm-synthtrace/tsconfig.json
index d0f5c5801597a..db93e36421b83 100644
--- a/packages/kbn-apm-synthtrace/tsconfig.json
+++ b/packages/kbn-apm-synthtrace/tsconfig.json
@@ -10,6 +10,7 @@
"@kbn/apm-synthtrace-client",
"@kbn/dev-utils",
"@kbn/elastic-agent-utils",
+ "@kbn/zod",
],
"exclude": [
"target/**/*",
diff --git a/packages/kbn-management/settings/setting_ids/index.ts b/packages/kbn-management/settings/setting_ids/index.ts
index 2b8c5de0b71df..e926007f77f25 100644
--- a/packages/kbn-management/settings/setting_ids/index.ts
+++ b/packages/kbn-management/settings/setting_ids/index.ts
@@ -142,6 +142,7 @@ export const OBSERVABILITY_APM_ENABLE_SERVICE_INVENTORY_TABLE_SEARCH_BAR =
'observability:apmEnableServiceInventoryTableSearchBar';
export const OBSERVABILITY_LOGS_EXPLORER_ALLOWED_DATA_VIEWS_ID =
'observability:logsExplorer:allowedDataViews';
+export const OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID = 'observability:newLogsOverview';
export const OBSERVABILITY_ENTITY_CENTRIC_EXPERIENCE = 'observability:entityCentricExperience';
export const OBSERVABILITY_LOGS_DATA_ACCESS_LOG_SOURCES_ID = 'observability:logSources';
export const OBSERVABILITY_ENABLE_LOGS_STREAM = 'observability:enableLogsStream';
diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts
index 539d3098030e0..52a837724480d 100644
--- a/packages/kbn-optimizer/src/worker/webpack.config.ts
+++ b/packages/kbn-optimizer/src/worker/webpack.config.ts
@@ -247,6 +247,18 @@ export function getWebpackConfig(
},
},
},
+ {
+ test: /node_modules\/@?xstate5\/.*\.js$/,
+ use: {
+ loader: 'babel-loader',
+ options: {
+ babelrc: false,
+ envName: worker.dist ? 'production' : 'development',
+ presets: [BABEL_PRESET],
+ plugins: ['@babel/plugin-transform-logical-assignment-operators'],
+ },
+ },
+ },
{
test: /\.(html|md|txt|tmpl)$/,
use: {
diff --git a/packages/kbn-xstate-utils/kibana.jsonc b/packages/kbn-xstate-utils/kibana.jsonc
index cd1151a3f2103..1fb3507854b98 100644
--- a/packages/kbn-xstate-utils/kibana.jsonc
+++ b/packages/kbn-xstate-utils/kibana.jsonc
@@ -1,5 +1,5 @@
{
- "type": "shared-common",
+ "type": "shared-browser",
"id": "@kbn/xstate-utils",
"owner": "@elastic/obs-ux-logs-team"
}
diff --git a/packages/kbn-xstate-utils/src/console_inspector.ts b/packages/kbn-xstate-utils/src/console_inspector.ts
new file mode 100644
index 0000000000000..8792ab44f3c28
--- /dev/null
+++ b/packages/kbn-xstate-utils/src/console_inspector.ts
@@ -0,0 +1,88 @@
+/*
+ * 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 {
+ ActorRefLike,
+ AnyActorRef,
+ InspectedActorEvent,
+ InspectedEventEvent,
+ InspectedSnapshotEvent,
+ InspectionEvent,
+} from 'xstate5';
+import { isDevMode } from './dev_tools';
+
+export const createConsoleInspector = () => {
+ if (!isDevMode()) {
+ return () => {};
+ }
+
+ // eslint-disable-next-line no-console
+ const log = console.info.bind(console);
+
+ const logActorEvent = (actorEvent: InspectedActorEvent) => {
+ if (isActorRef(actorEvent.actorRef)) {
+ log(
+ '✨ %c%s%c is a new actor of type %c%s%c:',
+ ...styleAsActor(actorEvent.actorRef.id),
+ ...styleAsKeyword(actorEvent.type),
+ actorEvent.actorRef
+ );
+ } else {
+ log('✨ New %c%s%c actor without id:', ...styleAsKeyword(actorEvent.type), actorEvent);
+ }
+ };
+
+ const logEventEvent = (eventEvent: InspectedEventEvent) => {
+ if (isActorRef(eventEvent.actorRef)) {
+ log(
+ '🔔 %c%s%c received event %c%s%c from %c%s%c:',
+ ...styleAsActor(eventEvent.actorRef.id),
+ ...styleAsKeyword(eventEvent.event.type),
+ ...styleAsKeyword(eventEvent.sourceRef?.id),
+ eventEvent
+ );
+ } else {
+ log('🔔 Event', ...styleAsKeyword(eventEvent.event.type), ':', eventEvent);
+ }
+ };
+
+ const logSnapshotEvent = (snapshotEvent: InspectedSnapshotEvent) => {
+ if (isActorRef(snapshotEvent.actorRef)) {
+ log(
+ '📸 %c%s%c updated due to %c%s%c:',
+ ...styleAsActor(snapshotEvent.actorRef.id),
+ ...styleAsKeyword(snapshotEvent.event.type),
+ snapshotEvent.snapshot
+ );
+ } else {
+ log('📸 Snapshot due to %c%s%c:', ...styleAsKeyword(snapshotEvent.event.type), snapshotEvent);
+ }
+ };
+
+ return (inspectionEvent: InspectionEvent) => {
+ if (inspectionEvent.type === '@xstate.actor') {
+ logActorEvent(inspectionEvent);
+ } else if (inspectionEvent.type === '@xstate.event') {
+ logEventEvent(inspectionEvent);
+ } else if (inspectionEvent.type === '@xstate.snapshot') {
+ logSnapshotEvent(inspectionEvent);
+ } else {
+ log(`❓ Received inspection event:`, inspectionEvent);
+ }
+ };
+};
+
+const isActorRef = (actorRefLike: ActorRefLike): actorRefLike is AnyActorRef =>
+ 'id' in actorRefLike;
+
+const keywordStyle = 'font-weight: bold';
+const styleAsKeyword = (value: any) => [keywordStyle, value, ''] as const;
+
+const actorStyle = 'font-weight: bold; text-decoration: underline';
+const styleAsActor = (value: any) => [actorStyle, value, ''] as const;
diff --git a/packages/kbn-xstate-utils/src/index.ts b/packages/kbn-xstate-utils/src/index.ts
index 107585ba2096f..3edf83e8a32c2 100644
--- a/packages/kbn-xstate-utils/src/index.ts
+++ b/packages/kbn-xstate-utils/src/index.ts
@@ -9,5 +9,6 @@
export * from './actions';
export * from './dev_tools';
+export * from './console_inspector';
export * from './notification_channel';
export * from './types';
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 dc2d2ad2c5de2..e5ddfbe4dd037 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
@@ -705,4 +705,10 @@ export const stackManagementSchema: MakeSchemaFrom = {
_meta: { description: 'Non-default value of setting.' },
},
},
+ 'observability:newLogsOverview': {
+ type: 'boolean',
+ _meta: {
+ description: 'Enable the new logs overview component.',
+ },
+ },
};
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 ef20ab223dfb6..2acb487e7ed08 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
@@ -56,6 +56,7 @@ export interface UsageStats {
'observability:logsExplorer:allowedDataViews': string[];
'observability:logSources': string[];
'observability:enableLogsStream': boolean;
+ 'observability:newLogsOverview': boolean;
'observability:aiAssistantSimulatedFunctionCalling': boolean;
'observability:aiAssistantSearchConnectorIndexPattern': string;
'visualization:heatmap:maxBuckets': number;
diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json
index 958280d9eba00..830cffc17cf1c 100644
--- a/src/plugins/telemetry/schema/oss_plugins.json
+++ b/src/plugins/telemetry/schema/oss_plugins.json
@@ -10768,6 +10768,12 @@
"description": "Non-default value of setting."
}
},
+ "observability:newLogsOverview": {
+ "type": "boolean",
+ "_meta": {
+ "description": "Enable the new logs overview component."
+ }
+ },
"observability:searchExcludedDataTiers": {
"type": "array",
"items": {
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 3df30d9cf8c30..4bc68d806f043 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -1298,6 +1298,8 @@
"@kbn/observability-get-padded-alert-time-range-util/*": ["x-pack/packages/observability/get_padded_alert_time_range_util/*"],
"@kbn/observability-logs-explorer-plugin": ["x-pack/plugins/observability_solution/observability_logs_explorer"],
"@kbn/observability-logs-explorer-plugin/*": ["x-pack/plugins/observability_solution/observability_logs_explorer/*"],
+ "@kbn/observability-logs-overview": ["x-pack/packages/observability/logs_overview"],
+ "@kbn/observability-logs-overview/*": ["x-pack/packages/observability/logs_overview/*"],
"@kbn/observability-onboarding-e2e": ["x-pack/plugins/observability_solution/observability_onboarding/e2e"],
"@kbn/observability-onboarding-e2e/*": ["x-pack/plugins/observability_solution/observability_onboarding/e2e/*"],
"@kbn/observability-onboarding-plugin": ["x-pack/plugins/observability_solution/observability_onboarding"],
diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json
index a46e291093411..50f2b77b84ad7 100644
--- a/x-pack/.i18nrc.json
+++ b/x-pack/.i18nrc.json
@@ -95,6 +95,9 @@
"xpack.observabilityLogsExplorer": "plugins/observability_solution/observability_logs_explorer",
"xpack.observability_onboarding": "plugins/observability_solution/observability_onboarding",
"xpack.observabilityShared": "plugins/observability_solution/observability_shared",
+ "xpack.observabilityLogsOverview": [
+ "packages/observability/logs_overview/src/components"
+ ],
"xpack.osquery": ["plugins/osquery"],
"xpack.painlessLab": "plugins/painless_lab",
"xpack.profiling": ["plugins/observability_solution/profiling"],
diff --git a/x-pack/packages/observability/logs_overview/README.md b/x-pack/packages/observability/logs_overview/README.md
new file mode 100644
index 0000000000000..20d3f0f02b7df
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/README.md
@@ -0,0 +1,3 @@
+# @kbn/observability-logs-overview
+
+Empty package generated by @kbn/generate
diff --git a/x-pack/packages/observability/logs_overview/index.ts b/x-pack/packages/observability/logs_overview/index.ts
new file mode 100644
index 0000000000000..057d1d3acd152
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/index.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.
+ */
+
+export {
+ LogsOverview,
+ LogsOverviewErrorContent,
+ LogsOverviewLoadingContent,
+ type LogsOverviewDependencies,
+ type LogsOverviewErrorContentProps,
+ type LogsOverviewProps,
+} from './src/components/logs_overview';
+export type {
+ DataViewLogsSourceConfiguration,
+ IndexNameLogsSourceConfiguration,
+ LogsSourceConfiguration,
+ SharedSettingLogsSourceConfiguration,
+} from './src/utils/logs_source';
diff --git a/x-pack/packages/observability/logs_overview/jest.config.js b/x-pack/packages/observability/logs_overview/jest.config.js
new file mode 100644
index 0000000000000..2ee88ee990253
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/jest.config.js
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../../../..',
+ roots: ['/x-pack/packages/observability/logs_overview'],
+};
diff --git a/x-pack/packages/observability/logs_overview/kibana.jsonc b/x-pack/packages/observability/logs_overview/kibana.jsonc
new file mode 100644
index 0000000000000..90b3375086720
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/kibana.jsonc
@@ -0,0 +1,5 @@
+{
+ "type": "shared-browser",
+ "id": "@kbn/observability-logs-overview",
+ "owner": "@elastic/obs-ux-logs-team"
+}
diff --git a/x-pack/packages/observability/logs_overview/package.json b/x-pack/packages/observability/logs_overview/package.json
new file mode 100644
index 0000000000000..77a529e7e59f7
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@kbn/observability-logs-overview",
+ "private": true,
+ "version": "1.0.0",
+ "license": "Elastic License 2.0",
+ "sideEffects": false
+}
diff --git a/x-pack/packages/observability/logs_overview/src/components/discover_link/discover_link.tsx b/x-pack/packages/observability/logs_overview/src/components/discover_link/discover_link.tsx
new file mode 100644
index 0000000000000..fe108289985a9
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/discover_link/discover_link.tsx
@@ -0,0 +1,110 @@
+/*
+ * 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import { EuiButton } from '@elastic/eui';
+import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
+import { FilterStateStore, buildCustomFilter } from '@kbn/es-query';
+import { i18n } from '@kbn/i18n';
+import { getRouterLinkProps } from '@kbn/router-utils';
+import type { SharePluginStart } from '@kbn/share-plugin/public';
+import React, { useCallback, useMemo } from 'react';
+import type { IndexNameLogsSourceConfiguration } from '../../utils/logs_source';
+
+export interface DiscoverLinkProps {
+ documentFilters?: QueryDslQueryContainer[];
+ logsSource: IndexNameLogsSourceConfiguration;
+ timeRange: {
+ start: string;
+ end: string;
+ };
+ dependencies: DiscoverLinkDependencies;
+}
+
+export interface DiscoverLinkDependencies {
+ share: SharePluginStart;
+}
+
+export const DiscoverLink = React.memo(
+ ({ dependencies: { share }, documentFilters, logsSource, timeRange }: DiscoverLinkProps) => {
+ const discoverLocatorParams = useMemo(
+ () => ({
+ dataViewSpec: {
+ id: logsSource.indexName,
+ name: logsSource.indexName,
+ title: logsSource.indexName,
+ timeFieldName: logsSource.timestampField,
+ },
+ timeRange: {
+ from: timeRange.start,
+ to: timeRange.end,
+ },
+ filters: documentFilters?.map((filter) =>
+ buildCustomFilter(
+ logsSource.indexName,
+ filter,
+ false,
+ false,
+ categorizedLogsFilterLabel,
+ FilterStateStore.APP_STATE
+ )
+ ),
+ }),
+ [
+ documentFilters,
+ logsSource.indexName,
+ logsSource.timestampField,
+ timeRange.end,
+ timeRange.start,
+ ]
+ );
+
+ const discoverLocator = useMemo(
+ () => share.url.locators.get('DISCOVER_APP_LOCATOR'),
+ [share.url.locators]
+ );
+
+ const discoverUrl = useMemo(
+ () => discoverLocator?.getRedirectUrl(discoverLocatorParams),
+ [discoverLocatorParams, discoverLocator]
+ );
+
+ const navigateToDiscover = useCallback(() => {
+ discoverLocator?.navigate(discoverLocatorParams);
+ }, [discoverLocatorParams, discoverLocator]);
+
+ const discoverLinkProps = getRouterLinkProps({
+ href: discoverUrl,
+ onClick: navigateToDiscover,
+ });
+
+ return (
+
+ {discoverLinkTitle}
+
+ );
+ }
+);
+
+export const discoverLinkTitle = i18n.translate(
+ 'xpack.observabilityLogsOverview.discoverLinkTitle',
+ {
+ defaultMessage: 'Open in Discover',
+ }
+);
+
+export const categorizedLogsFilterLabel = i18n.translate(
+ 'xpack.observabilityLogsOverview.categorizedLogsFilterLabel',
+ {
+ defaultMessage: 'Categorized log entries',
+ }
+);
diff --git a/x-pack/packages/observability/logs_overview/src/components/discover_link/index.ts b/x-pack/packages/observability/logs_overview/src/components/discover_link/index.ts
new file mode 100644
index 0000000000000..738bf51d4529d
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/discover_link/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 './discover_link';
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/index.ts b/x-pack/packages/observability/logs_overview/src/components/log_categories/index.ts
new file mode 100644
index 0000000000000..786475396237c
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/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 './log_categories';
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories.tsx
new file mode 100644
index 0000000000000..6204667827281
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories.tsx
@@ -0,0 +1,94 @@
+/*
+ * 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import { ISearchGeneric } from '@kbn/search-types';
+import { createConsoleInspector } from '@kbn/xstate-utils';
+import { useMachine } from '@xstate5/react';
+import React, { useCallback } from 'react';
+import {
+ categorizeLogsService,
+ createCategorizeLogsServiceImplementations,
+} from '../../services/categorize_logs_service';
+import { IndexNameLogsSourceConfiguration } from '../../utils/logs_source';
+import { LogCategoriesErrorContent } from './log_categories_error_content';
+import { LogCategoriesLoadingContent } from './log_categories_loading_content';
+import {
+ LogCategoriesResultContent,
+ LogCategoriesResultContentDependencies,
+} from './log_categories_result_content';
+
+export interface LogCategoriesProps {
+ dependencies: LogCategoriesDependencies;
+ documentFilters?: QueryDslQueryContainer[];
+ logsSource: IndexNameLogsSourceConfiguration;
+ // The time range could be made optional if we want to support an internal
+ // time range picker
+ timeRange: {
+ start: string;
+ end: string;
+ };
+}
+
+export type LogCategoriesDependencies = LogCategoriesResultContentDependencies & {
+ search: ISearchGeneric;
+};
+
+export const LogCategories: React.FC = ({
+ dependencies,
+ documentFilters = [],
+ logsSource,
+ timeRange,
+}) => {
+ const [categorizeLogsServiceState, sendToCategorizeLogsService] = useMachine(
+ categorizeLogsService.provide(
+ createCategorizeLogsServiceImplementations({ search: dependencies.search })
+ ),
+ {
+ inspect: consoleInspector,
+ input: {
+ index: logsSource.indexName,
+ startTimestamp: timeRange.start,
+ endTimestamp: timeRange.end,
+ timeField: logsSource.timestampField,
+ messageField: logsSource.messageField,
+ documentFilters,
+ },
+ }
+ );
+
+ const cancelOperation = useCallback(() => {
+ sendToCategorizeLogsService({
+ type: 'cancel',
+ });
+ }, [sendToCategorizeLogsService]);
+
+ if (categorizeLogsServiceState.matches('done')) {
+ return (
+
+ );
+ } else if (categorizeLogsServiceState.matches('failed')) {
+ return ;
+ } else if (categorizeLogsServiceState.matches('countingDocuments')) {
+ return ;
+ } else if (
+ categorizeLogsServiceState.matches('fetchingSampledCategories') ||
+ categorizeLogsServiceState.matches('fetchingRemainingCategories')
+ ) {
+ return ;
+ } else {
+ return null;
+ }
+};
+
+const consoleInspector = createConsoleInspector();
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_control_bar.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_control_bar.tsx
new file mode 100644
index 0000000000000..4538b0ec2fd5d
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_control_bar.tsx
@@ -0,0 +1,44 @@
+/*
+ * 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import type { SharePluginStart } from '@kbn/share-plugin/public';
+import React from 'react';
+import type { IndexNameLogsSourceConfiguration } from '../../utils/logs_source';
+import { DiscoverLink } from '../discover_link';
+
+export interface LogCategoriesControlBarProps {
+ documentFilters?: QueryDslQueryContainer[];
+ logsSource: IndexNameLogsSourceConfiguration;
+ timeRange: {
+ start: string;
+ end: string;
+ };
+ dependencies: LogCategoriesControlBarDependencies;
+}
+
+export interface LogCategoriesControlBarDependencies {
+ share: SharePluginStart;
+}
+
+export const LogCategoriesControlBar: React.FC = React.memo(
+ ({ dependencies, documentFilters, logsSource, timeRange }) => {
+ return (
+
+
+
+
+
+ );
+ }
+);
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_error_content.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_error_content.tsx
new file mode 100644
index 0000000000000..1a335e3265294
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_error_content.tsx
@@ -0,0 +1,44 @@
+/*
+ * 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 { EuiCodeBlock, EuiEmptyPrompt } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+
+export interface LogCategoriesErrorContentProps {
+ error?: Error;
+}
+
+export const LogCategoriesErrorContent: React.FC = ({ error }) => {
+ return (
+ {logsOverviewErrorTitle}}
+ body={
+
+ {error?.stack ?? error?.toString() ?? unknownErrorDescription}
+
+ }
+ layout="vertical"
+ />
+ );
+};
+
+const logsOverviewErrorTitle = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.errorTitle',
+ {
+ defaultMessage: 'Failed to categorize logs',
+ }
+);
+
+const unknownErrorDescription = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.unknownErrorDescription',
+ {
+ defaultMessage: 'An unspecified error occurred.',
+ }
+);
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid.tsx
new file mode 100644
index 0000000000000..d9e960685de99
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid.tsx
@@ -0,0 +1,182 @@
+/*
+ * 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 {
+ EuiDataGrid,
+ EuiDataGridColumnSortingConfig,
+ EuiDataGridPaginationProps,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { createConsoleInspector } from '@kbn/xstate-utils';
+import { useMachine } from '@xstate5/react';
+import _ from 'lodash';
+import React, { useMemo } from 'react';
+import { assign, setup } from 'xstate5';
+import { LogCategory } from '../../types';
+import {
+ LogCategoriesGridCellDependencies,
+ LogCategoriesGridColumnId,
+ createCellContext,
+ logCategoriesGridColumnIds,
+ logCategoriesGridColumns,
+ renderLogCategoriesGridCell,
+} from './log_categories_grid_cell';
+
+export interface LogCategoriesGridProps {
+ dependencies: LogCategoriesGridDependencies;
+ logCategories: LogCategory[];
+}
+
+export type LogCategoriesGridDependencies = LogCategoriesGridCellDependencies;
+
+export const LogCategoriesGrid: React.FC = ({
+ dependencies,
+ logCategories,
+}) => {
+ const [gridState, dispatchGridEvent] = useMachine(gridStateService, {
+ input: {
+ visibleColumns: logCategoriesGridColumns.map(({ id }) => id),
+ },
+ inspect: consoleInspector,
+ });
+
+ const sortedLogCategories = useMemo(() => {
+ const sortingCriteria = gridState.context.sortingColumns.map(
+ ({ id, direction }): [(logCategory: LogCategory) => any, 'asc' | 'desc'] => {
+ switch (id) {
+ case 'count':
+ return [(logCategory: LogCategory) => logCategory.documentCount, direction];
+ case 'change_type':
+ // TODO: use better sorting weight for change types
+ return [(logCategory: LogCategory) => logCategory.change.type, direction];
+ case 'change_time':
+ return [
+ (logCategory: LogCategory) =>
+ 'timestamp' in logCategory.change ? logCategory.change.timestamp ?? '' : '',
+ direction,
+ ];
+ default:
+ return [_.identity, direction];
+ }
+ }
+ );
+ return _.orderBy(
+ logCategories,
+ sortingCriteria.map(([accessor]) => accessor),
+ sortingCriteria.map(([, direction]) => direction)
+ );
+ }, [gridState.context.sortingColumns, logCategories]);
+
+ return (
+
+ dispatchGridEvent({ type: 'changeVisibleColumns', visibleColumns }),
+ }}
+ cellContext={createCellContext(sortedLogCategories, dependencies)}
+ pagination={{
+ ...gridState.context.pagination,
+ onChangeItemsPerPage: (pageSize) => dispatchGridEvent({ type: 'changePageSize', pageSize }),
+ onChangePage: (pageIndex) => dispatchGridEvent({ type: 'changePageIndex', pageIndex }),
+ }}
+ renderCellValue={renderLogCategoriesGridCell}
+ rowCount={sortedLogCategories.length}
+ sorting={{
+ columns: gridState.context.sortingColumns,
+ onSort: (sortingColumns) =>
+ dispatchGridEvent({ type: 'changeSortingColumns', sortingColumns }),
+ }}
+ />
+ );
+};
+
+const gridStateService = setup({
+ types: {
+ context: {} as {
+ visibleColumns: string[];
+ pagination: Pick;
+ sortingColumns: LogCategoriesGridSortingConfig[];
+ },
+ events: {} as
+ | {
+ type: 'changePageSize';
+ pageSize: number;
+ }
+ | {
+ type: 'changePageIndex';
+ pageIndex: number;
+ }
+ | {
+ type: 'changeSortingColumns';
+ sortingColumns: EuiDataGridColumnSortingConfig[];
+ }
+ | {
+ type: 'changeVisibleColumns';
+ visibleColumns: string[];
+ },
+ input: {} as {
+ visibleColumns: string[];
+ },
+ },
+}).createMachine({
+ id: 'logCategoriesGridState',
+ context: ({ input }) => ({
+ visibleColumns: input.visibleColumns,
+ pagination: { pageIndex: 0, pageSize: 20, pageSizeOptions: [10, 20, 50] },
+ sortingColumns: [{ id: 'change_time', direction: 'desc' }],
+ }),
+ on: {
+ changePageSize: {
+ actions: assign(({ context, event }) => ({
+ pagination: {
+ ...context.pagination,
+ pageIndex: 0,
+ pageSize: event.pageSize,
+ },
+ })),
+ },
+ changePageIndex: {
+ actions: assign(({ context, event }) => ({
+ pagination: {
+ ...context.pagination,
+ pageIndex: event.pageIndex,
+ },
+ })),
+ },
+ changeSortingColumns: {
+ actions: assign(({ event }) => ({
+ sortingColumns: event.sortingColumns.filter(
+ (sortingConfig): sortingConfig is LogCategoriesGridSortingConfig =>
+ (logCategoriesGridColumnIds as string[]).includes(sortingConfig.id)
+ ),
+ })),
+ },
+ changeVisibleColumns: {
+ actions: assign(({ event }) => ({
+ visibleColumns: event.visibleColumns,
+ })),
+ },
+ },
+});
+
+const consoleInspector = createConsoleInspector();
+
+const logCategoriesGridLabel = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategoriesGrid.euiDataGrid.logCategoriesLabel',
+ { defaultMessage: 'Log categories' }
+);
+
+interface TypedEuiDataGridColumnSortingConfig
+ extends EuiDataGridColumnSortingConfig {
+ id: ColumnId;
+}
+
+type LogCategoriesGridSortingConfig =
+ TypedEuiDataGridColumnSortingConfig;
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_cell.tsx
new file mode 100644
index 0000000000000..d6ab4969eaf7b
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_cell.tsx
@@ -0,0 +1,99 @@
+/*
+ * 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 { EuiDataGridColumn, RenderCellValue } from '@elastic/eui';
+import React from 'react';
+import { LogCategory } from '../../types';
+import {
+ LogCategoriesGridChangeTimeCell,
+ LogCategoriesGridChangeTimeCellDependencies,
+ logCategoriesGridChangeTimeColumn,
+} from './log_categories_grid_change_time_cell';
+import {
+ LogCategoriesGridChangeTypeCell,
+ logCategoriesGridChangeTypeColumn,
+} from './log_categories_grid_change_type_cell';
+import {
+ LogCategoriesGridCountCell,
+ logCategoriesGridCountColumn,
+} from './log_categories_grid_count_cell';
+import {
+ LogCategoriesGridHistogramCell,
+ LogCategoriesGridHistogramCellDependencies,
+ logCategoriesGridHistoryColumn,
+} from './log_categories_grid_histogram_cell';
+import {
+ LogCategoriesGridPatternCell,
+ logCategoriesGridPatternColumn,
+} from './log_categories_grid_pattern_cell';
+
+export interface LogCategoriesGridCellContext {
+ dependencies: LogCategoriesGridCellDependencies;
+ logCategories: LogCategory[];
+}
+
+export type LogCategoriesGridCellDependencies = LogCategoriesGridHistogramCellDependencies &
+ LogCategoriesGridChangeTimeCellDependencies;
+
+export const renderLogCategoriesGridCell: RenderCellValue = ({
+ rowIndex,
+ columnId,
+ isExpanded,
+ ...rest
+}) => {
+ const { dependencies, logCategories } = getCellContext(rest);
+
+ const logCategory = logCategories[rowIndex];
+
+ switch (columnId as LogCategoriesGridColumnId) {
+ case 'pattern':
+ return ;
+ case 'count':
+ return ;
+ case 'history':
+ return (
+
+ );
+ case 'change_type':
+ return ;
+ case 'change_time':
+ return (
+
+ );
+ default:
+ return <>->;
+ }
+};
+
+export const logCategoriesGridColumns = [
+ logCategoriesGridPatternColumn,
+ logCategoriesGridCountColumn,
+ logCategoriesGridChangeTypeColumn,
+ logCategoriesGridChangeTimeColumn,
+ logCategoriesGridHistoryColumn,
+] satisfies EuiDataGridColumn[];
+
+export const logCategoriesGridColumnIds = logCategoriesGridColumns.map(({ id }) => id);
+
+export type LogCategoriesGridColumnId = (typeof logCategoriesGridColumns)[number]['id'];
+
+const cellContextKey = 'cellContext';
+
+const getCellContext = (cellContext: object): LogCategoriesGridCellContext =>
+ (cellContextKey in cellContext
+ ? cellContext[cellContextKey]
+ : {}) as LogCategoriesGridCellContext;
+
+export const createCellContext = (
+ logCategories: LogCategory[],
+ dependencies: LogCategoriesGridCellDependencies
+): { [cellContextKey]: LogCategoriesGridCellContext } => ({
+ [cellContextKey]: {
+ dependencies,
+ logCategories,
+ },
+});
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_time_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_time_cell.tsx
new file mode 100644
index 0000000000000..5ad8cbdd49346
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_time_cell.tsx
@@ -0,0 +1,54 @@
+/*
+ * 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 { EuiDataGridColumn } from '@elastic/eui';
+import { SettingsStart } from '@kbn/core-ui-settings-browser';
+import { i18n } from '@kbn/i18n';
+import moment from 'moment';
+import React, { useMemo } from 'react';
+import { LogCategory } from '../../types';
+
+export const logCategoriesGridChangeTimeColumn = {
+ id: 'change_time' as const,
+ display: i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategoriesGrid.changeTimeColumnLabel',
+ {
+ defaultMessage: 'Change at',
+ }
+ ),
+ isSortable: true,
+ initialWidth: 220,
+ schema: 'datetime',
+} satisfies EuiDataGridColumn;
+
+export interface LogCategoriesGridChangeTimeCellProps {
+ dependencies: LogCategoriesGridChangeTimeCellDependencies;
+ logCategory: LogCategory;
+}
+
+export interface LogCategoriesGridChangeTimeCellDependencies {
+ uiSettings: SettingsStart;
+}
+
+export const LogCategoriesGridChangeTimeCell: React.FC = ({
+ dependencies,
+ logCategory,
+}) => {
+ const dateFormat = useMemo(
+ () => dependencies.uiSettings.client.get('dateFormat'),
+ [dependencies.uiSettings.client]
+ );
+ if (!('timestamp' in logCategory.change && logCategory.change.timestamp != null)) {
+ return null;
+ }
+
+ if (dateFormat) {
+ return <>{moment(logCategory.change.timestamp).format(dateFormat)}>;
+ } else {
+ return <>{logCategory.change.timestamp}>;
+ }
+};
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_type_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_type_cell.tsx
new file mode 100644
index 0000000000000..af6349bd0e18c
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_type_cell.tsx
@@ -0,0 +1,108 @@
+/*
+ * 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 { EuiBadge, EuiDataGridColumn } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { LogCategory } from '../../types';
+
+export const logCategoriesGridChangeTypeColumn = {
+ id: 'change_type' as const,
+ display: i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategoriesGrid.changeTypeColumnLabel',
+ {
+ defaultMessage: 'Change type',
+ }
+ ),
+ isSortable: true,
+ initialWidth: 110,
+} satisfies EuiDataGridColumn;
+
+export interface LogCategoriesGridChangeTypeCellProps {
+ logCategory: LogCategory;
+}
+
+export const LogCategoriesGridChangeTypeCell: React.FC = ({
+ logCategory,
+}) => {
+ switch (logCategory.change.type) {
+ case 'dip':
+ return {dipBadgeLabel} ;
+ case 'spike':
+ return {spikeBadgeLabel} ;
+ case 'step':
+ return {stepBadgeLabel} ;
+ case 'distribution':
+ return {distributionBadgeLabel} ;
+ case 'rare':
+ return {rareBadgeLabel} ;
+ case 'trend':
+ return {trendBadgeLabel} ;
+ case 'other':
+ return {otherBadgeLabel} ;
+ case 'none':
+ return <>->;
+ default:
+ return {unknownBadgeLabel} ;
+ }
+};
+
+const dipBadgeLabel = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.dipChangeTypeBadgeLabel',
+ {
+ defaultMessage: 'Dip',
+ }
+);
+
+const spikeBadgeLabel = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.spikeChangeTypeBadgeLabel',
+ {
+ defaultMessage: 'Spike',
+ }
+);
+
+const stepBadgeLabel = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.spikeChangeTypeBadgeLabel',
+ {
+ defaultMessage: 'Step',
+ }
+);
+
+const distributionBadgeLabel = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.distributionChangeTypeBadgeLabel',
+ {
+ defaultMessage: 'Distribution',
+ }
+);
+
+const trendBadgeLabel = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.spikeChangeTypeBadgeLabel',
+ {
+ defaultMessage: 'Trend',
+ }
+);
+
+const otherBadgeLabel = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.otherChangeTypeBadgeLabel',
+ {
+ defaultMessage: 'Other',
+ }
+);
+
+const unknownBadgeLabel = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.unknownChangeTypeBadgeLabel',
+ {
+ defaultMessage: 'Unknown',
+ }
+);
+
+const rareBadgeLabel = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.rareChangeTypeBadgeLabel',
+ {
+ defaultMessage: 'Rare',
+ }
+);
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_count_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_count_cell.tsx
new file mode 100644
index 0000000000000..f2247aab5212e
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_count_cell.tsx
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiDataGridColumn } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedNumber } from '@kbn/i18n-react';
+import React from 'react';
+import { LogCategory } from '../../types';
+
+export const logCategoriesGridCountColumn = {
+ id: 'count' as const,
+ display: i18n.translate('xpack.observabilityLogsOverview.logCategoriesGrid.countColumnLabel', {
+ defaultMessage: 'Events',
+ }),
+ isSortable: true,
+ schema: 'numeric',
+ initialWidth: 100,
+} satisfies EuiDataGridColumn;
+
+export interface LogCategoriesGridCountCellProps {
+ logCategory: LogCategory;
+}
+
+export const LogCategoriesGridCountCell: React.FC = ({
+ logCategory,
+}) => {
+ return ;
+};
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_histogram_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_histogram_cell.tsx
new file mode 100644
index 0000000000000..2fb50b0f2f3b4
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_histogram_cell.tsx
@@ -0,0 +1,99 @@
+/*
+ * 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 {
+ BarSeries,
+ Chart,
+ LineAnnotation,
+ LineAnnotationStyle,
+ PartialTheme,
+ Settings,
+ Tooltip,
+ TooltipType,
+} from '@elastic/charts';
+import { EuiDataGridColumn } from '@elastic/eui';
+import { ChartsPluginStart } from '@kbn/charts-plugin/public';
+import { i18n } from '@kbn/i18n';
+import { RecursivePartial } from '@kbn/utility-types';
+import React from 'react';
+import { LogCategory, LogCategoryHistogramBucket } from '../../types';
+
+export const logCategoriesGridHistoryColumn = {
+ id: 'history' as const,
+ display: i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategoriesGrid.histogramColumnLabel',
+ {
+ defaultMessage: 'Timeline',
+ }
+ ),
+ isSortable: false,
+ initialWidth: 250,
+ isExpandable: false,
+} satisfies EuiDataGridColumn;
+
+export interface LogCategoriesGridHistogramCellProps {
+ dependencies: LogCategoriesGridHistogramCellDependencies;
+ logCategory: LogCategory;
+}
+
+export interface LogCategoriesGridHistogramCellDependencies {
+ charts: ChartsPluginStart;
+}
+
+export const LogCategoriesGridHistogramCell: React.FC = ({
+ dependencies: { charts },
+ logCategory,
+}) => {
+ const baseTheme = charts.theme.useChartsBaseTheme();
+ const sparklineTheme = charts.theme.useSparklineOverrides();
+
+ return (
+
+
+
+
+ {'timestamp' in logCategory.change && logCategory.change.timestamp != null ? (
+
+ ) : null}
+
+ );
+};
+
+const localThemeOverrides: PartialTheme = {
+ scales: {
+ histogramPadding: 0.1,
+ },
+ background: {
+ color: 'transparent',
+ },
+};
+
+const annotationStyle: RecursivePartial = {
+ line: {
+ strokeWidth: 2,
+ },
+};
+
+const timestampAccessor = (histogram: LogCategoryHistogramBucket) =>
+ new Date(histogram.timestamp).getTime();
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_pattern_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_pattern_cell.tsx
new file mode 100644
index 0000000000000..d507487a99e3c
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_pattern_cell.tsx
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiDataGridColumn, useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/react';
+import { i18n } from '@kbn/i18n';
+import React, { useMemo } from 'react';
+import { LogCategory } from '../../types';
+
+export const logCategoriesGridPatternColumn = {
+ id: 'pattern' as const,
+ display: i18n.translate('xpack.observabilityLogsOverview.logCategoriesGrid.patternColumnLabel', {
+ defaultMessage: 'Pattern',
+ }),
+ isSortable: false,
+ schema: 'string',
+} satisfies EuiDataGridColumn;
+
+export interface LogCategoriesGridPatternCellProps {
+ logCategory: LogCategory;
+}
+
+export const LogCategoriesGridPatternCell: React.FC = ({
+ logCategory,
+}) => {
+ const theme = useEuiTheme();
+ const { euiTheme } = theme;
+ const termsList = useMemo(() => logCategory.terms.split(' '), [logCategory.terms]);
+
+ const commonStyle = css`
+ display: inline-block;
+ font-family: ${euiTheme.font.familyCode};
+ margin-right: ${euiTheme.size.xs};
+ `;
+
+ const termStyle = css`
+ ${commonStyle};
+ `;
+
+ const separatorStyle = css`
+ ${commonStyle};
+ color: ${euiTheme.colors.successText};
+ `;
+
+ return (
+
+ *
+ {termsList.map((term, index) => (
+
+ {term}
+ *
+
+ ))}
+
+ );
+};
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_loading_content.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_loading_content.tsx
new file mode 100644
index 0000000000000..0fde469fe717d
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_loading_content.tsx
@@ -0,0 +1,68 @@
+/*
+ * 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 { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui';
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+
+export interface LogCategoriesLoadingContentProps {
+ onCancel?: () => void;
+ stage: 'counting' | 'categorizing';
+}
+
+export const LogCategoriesLoadingContent: React.FC = ({
+ onCancel,
+ stage,
+}) => {
+ return (
+ }
+ title={
+
+ {stage === 'counting'
+ ? logCategoriesLoadingStateCountingTitle
+ : logCategoriesLoadingStateCategorizingTitle}
+
+ }
+ actions={
+ onCancel != null
+ ? [
+ {
+ onCancel();
+ }}
+ >
+ {logCategoriesLoadingStateCancelButtonLabel}
+ ,
+ ]
+ : []
+ }
+ />
+ );
+};
+
+const logCategoriesLoadingStateCountingTitle = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategoriesGrid.loadingStageCountingTitle',
+ {
+ defaultMessage: 'Estimating log volume',
+ }
+);
+
+const logCategoriesLoadingStateCategorizingTitle = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategoriesGrid.loadingStageCategorizingTitle',
+ {
+ defaultMessage: 'Categorizing logs',
+ }
+);
+
+const logCategoriesLoadingStateCancelButtonLabel = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategoriesGrid.loadingStateCancelButtonLabel',
+ {
+ defaultMessage: 'Cancel',
+ }
+);
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_result_content.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_result_content.tsx
new file mode 100644
index 0000000000000..e16bdda7cb44a
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_result_content.tsx
@@ -0,0 +1,87 @@
+/*
+ * 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { LogCategory } from '../../types';
+import { IndexNameLogsSourceConfiguration } from '../../utils/logs_source';
+import {
+ LogCategoriesControlBar,
+ LogCategoriesControlBarDependencies,
+} from './log_categories_control_bar';
+import { LogCategoriesGrid, LogCategoriesGridDependencies } from './log_categories_grid';
+
+export interface LogCategoriesResultContentProps {
+ dependencies: LogCategoriesResultContentDependencies;
+ documentFilters?: QueryDslQueryContainer[];
+ logCategories: LogCategory[];
+ logsSource: IndexNameLogsSourceConfiguration;
+ timeRange: {
+ start: string;
+ end: string;
+ };
+}
+
+export type LogCategoriesResultContentDependencies = LogCategoriesControlBarDependencies &
+ LogCategoriesGridDependencies;
+
+export const LogCategoriesResultContent: React.FC = ({
+ dependencies,
+ documentFilters,
+ logCategories,
+ logsSource,
+ timeRange,
+}) => {
+ if (logCategories.length === 0) {
+ return ;
+ } else {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+};
+
+export const LogCategoriesEmptyResultContent: React.FC = () => {
+ return (
+ {emptyResultContentDescription}
}
+ color="subdued"
+ layout="horizontal"
+ title={{emptyResultContentTitle} }
+ titleSize="m"
+ />
+ );
+};
+
+const emptyResultContentTitle = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.emptyResultContentTitle',
+ {
+ defaultMessage: 'No log categories found',
+ }
+);
+
+const emptyResultContentDescription = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.emptyResultContentDescription',
+ {
+ defaultMessage:
+ 'No suitable documents within the time range. Try searching for a longer time period.',
+ }
+);
diff --git a/x-pack/packages/observability/logs_overview/src/components/logs_overview/index.ts b/x-pack/packages/observability/logs_overview/src/components/logs_overview/index.ts
new file mode 100644
index 0000000000000..878f634f078ad
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/logs_overview/index.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export * from './logs_overview';
+export * from './logs_overview_error_content';
+export * from './logs_overview_loading_content';
diff --git a/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview.tsx b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview.tsx
new file mode 100644
index 0000000000000..988656eb1571e
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview.tsx
@@ -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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import { type LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public';
+import React from 'react';
+import useAsync from 'react-use/lib/useAsync';
+import { LogsSourceConfiguration, normalizeLogsSource } from '../../utils/logs_source';
+import { LogCategories, LogCategoriesDependencies } from '../log_categories';
+import { LogsOverviewErrorContent } from './logs_overview_error_content';
+import { LogsOverviewLoadingContent } from './logs_overview_loading_content';
+
+export interface LogsOverviewProps {
+ dependencies: LogsOverviewDependencies;
+ documentFilters?: QueryDslQueryContainer[];
+ logsSource?: LogsSourceConfiguration;
+ timeRange: {
+ start: string;
+ end: string;
+ };
+}
+
+export type LogsOverviewDependencies = LogCategoriesDependencies & {
+ logsDataAccess: LogsDataAccessPluginStart;
+};
+
+export const LogsOverview: React.FC = React.memo(
+ ({
+ dependencies,
+ documentFilters = defaultDocumentFilters,
+ logsSource = defaultLogsSource,
+ timeRange,
+ }) => {
+ const normalizedLogsSource = useAsync(
+ () => normalizeLogsSource({ logsDataAccess: dependencies.logsDataAccess })(logsSource),
+ [dependencies.logsDataAccess, logsSource]
+ );
+
+ if (normalizedLogsSource.loading) {
+ return ;
+ }
+
+ if (normalizedLogsSource.error != null || normalizedLogsSource.value == null) {
+ return ;
+ }
+
+ return (
+
+ );
+ }
+);
+
+const defaultDocumentFilters: QueryDslQueryContainer[] = [];
+
+const defaultLogsSource: LogsSourceConfiguration = { type: 'shared_setting' };
diff --git a/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_error_content.tsx b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_error_content.tsx
new file mode 100644
index 0000000000000..73586756bb908
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_error_content.tsx
@@ -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 { EuiCodeBlock, EuiEmptyPrompt } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+
+export interface LogsOverviewErrorContentProps {
+ error?: Error;
+}
+
+export const LogsOverviewErrorContent: React.FC = ({ error }) => {
+ return (
+ {logsOverviewErrorTitle}}
+ body={
+
+ {error?.stack ?? error?.toString() ?? unknownErrorDescription}
+
+ }
+ layout="vertical"
+ />
+ );
+};
+
+const logsOverviewErrorTitle = i18n.translate('xpack.observabilityLogsOverview.errorTitle', {
+ defaultMessage: 'Error',
+});
+
+const unknownErrorDescription = i18n.translate(
+ 'xpack.observabilityLogsOverview.unknownErrorDescription',
+ {
+ defaultMessage: 'An unspecified error occurred.',
+ }
+);
diff --git a/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_loading_content.tsx b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_loading_content.tsx
new file mode 100644
index 0000000000000..7645fdb90f0ac
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_loading_content.tsx
@@ -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 { EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+
+export const LogsOverviewLoadingContent: React.FC = ({}) => {
+ return (
+ }
+ title={{logsOverviewLoadingTitle} }
+ />
+ );
+};
+
+const logsOverviewLoadingTitle = i18n.translate('xpack.observabilityLogsOverview.loadingTitle', {
+ defaultMessage: 'Loading',
+});
diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_documents.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_documents.ts
new file mode 100644
index 0000000000000..7260efe63d435
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_documents.ts
@@ -0,0 +1,282 @@
+/*
+ * 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 { ISearchGeneric } from '@kbn/search-types';
+import { lastValueFrom } from 'rxjs';
+import { fromPromise } from 'xstate5';
+import { createRandomSamplerWrapper } from '@kbn/ml-random-sampler-utils';
+import { z } from '@kbn/zod';
+import { LogCategorizationParams } from './types';
+import { createCategorizationRequestParams } from './queries';
+import { LogCategory, LogCategoryChange } from '../../types';
+
+// the fraction of a category's histogram below which the category is considered rare
+const rarityThreshold = 0.2;
+const maxCategoriesCount = 1000;
+
+export const categorizeDocuments = ({ search }: { search: ISearchGeneric }) =>
+ fromPromise<
+ {
+ categories: LogCategory[];
+ hasReachedLimit: boolean;
+ },
+ LogCategorizationParams & {
+ samplingProbability: number;
+ ignoredCategoryTerms: string[];
+ minDocsPerCategory: number;
+ }
+ >(
+ async ({
+ input: {
+ index,
+ endTimestamp,
+ startTimestamp,
+ timeField,
+ messageField,
+ samplingProbability,
+ ignoredCategoryTerms,
+ documentFilters = [],
+ minDocsPerCategory,
+ },
+ signal,
+ }) => {
+ const randomSampler = createRandomSamplerWrapper({
+ probability: samplingProbability,
+ seed: 1,
+ });
+
+ const requestParams = createCategorizationRequestParams({
+ index,
+ timeField,
+ messageField,
+ startTimestamp,
+ endTimestamp,
+ randomSampler,
+ additionalFilters: documentFilters,
+ ignoredCategoryTerms,
+ minDocsPerCategory,
+ maxCategoriesCount,
+ });
+
+ const { rawResponse } = await lastValueFrom(
+ search({ params: requestParams }, { abortSignal: signal })
+ );
+
+ if (rawResponse.aggregations == null) {
+ throw new Error('No aggregations found in large categories response');
+ }
+
+ const logCategoriesAggResult = randomSampler.unwrap(rawResponse.aggregations);
+
+ if (!('categories' in logCategoriesAggResult)) {
+ throw new Error('No categorization aggregation found in large categories response');
+ }
+
+ const logCategories =
+ (logCategoriesAggResult.categories.buckets as unknown[]).map(mapCategoryBucket) ?? [];
+
+ return {
+ categories: logCategories,
+ hasReachedLimit: logCategories.length >= maxCategoriesCount,
+ };
+ }
+ );
+
+const mapCategoryBucket = (bucket: any): LogCategory =>
+ esCategoryBucketSchema
+ .transform((parsedBucket) => ({
+ change: mapChangePoint(parsedBucket),
+ documentCount: parsedBucket.doc_count,
+ histogram: parsedBucket.histogram,
+ terms: parsedBucket.key,
+ }))
+ .parse(bucket);
+
+const mapChangePoint = ({ change, histogram }: EsCategoryBucket): LogCategoryChange => {
+ switch (change.type) {
+ case 'stationary':
+ if (isRareInHistogram(histogram)) {
+ return {
+ type: 'rare',
+ timestamp: findFirstNonZeroBucket(histogram)?.timestamp ?? histogram[0].timestamp,
+ };
+ } else {
+ return {
+ type: 'none',
+ };
+ }
+ case 'dip':
+ case 'spike':
+ return {
+ type: change.type,
+ timestamp: change.bucket.key,
+ };
+ case 'step_change':
+ return {
+ type: 'step',
+ timestamp: change.bucket.key,
+ };
+ case 'distribution_change':
+ return {
+ type: 'distribution',
+ timestamp: change.bucket.key,
+ };
+ case 'trend_change':
+ return {
+ type: 'trend',
+ timestamp: change.bucket.key,
+ correlationCoefficient: change.details.r_value,
+ };
+ case 'unknown':
+ return {
+ type: 'unknown',
+ rawChange: change.rawChange,
+ };
+ case 'non_stationary':
+ default:
+ return {
+ type: 'other',
+ };
+ }
+};
+
+/**
+ * The official types are lacking the change_point aggregation
+ */
+const esChangePointBucketSchema = z.object({
+ key: z.string().datetime(),
+ doc_count: z.number(),
+});
+
+const esChangePointDetailsSchema = z.object({
+ p_value: z.number(),
+});
+
+const esChangePointCorrelationSchema = esChangePointDetailsSchema.extend({
+ r_value: z.number(),
+});
+
+const esChangePointSchema = z.union([
+ z
+ .object({
+ bucket: esChangePointBucketSchema,
+ type: z.object({
+ dip: esChangePointDetailsSchema,
+ }),
+ })
+ .transform(({ bucket, type: { dip: details } }) => ({
+ type: 'dip' as const,
+ bucket,
+ details,
+ })),
+ z
+ .object({
+ bucket: esChangePointBucketSchema,
+ type: z.object({
+ spike: esChangePointDetailsSchema,
+ }),
+ })
+ .transform(({ bucket, type: { spike: details } }) => ({
+ type: 'spike' as const,
+ bucket,
+ details,
+ })),
+ z
+ .object({
+ bucket: esChangePointBucketSchema,
+ type: z.object({
+ step_change: esChangePointDetailsSchema,
+ }),
+ })
+ .transform(({ bucket, type: { step_change: details } }) => ({
+ type: 'step_change' as const,
+ bucket,
+ details,
+ })),
+ z
+ .object({
+ bucket: esChangePointBucketSchema,
+ type: z.object({
+ trend_change: esChangePointCorrelationSchema,
+ }),
+ })
+ .transform(({ bucket, type: { trend_change: details } }) => ({
+ type: 'trend_change' as const,
+ bucket,
+ details,
+ })),
+ z
+ .object({
+ bucket: esChangePointBucketSchema,
+ type: z.object({
+ distribution_change: esChangePointDetailsSchema,
+ }),
+ })
+ .transform(({ bucket, type: { distribution_change: details } }) => ({
+ type: 'distribution_change' as const,
+ bucket,
+ details,
+ })),
+ z
+ .object({
+ type: z.object({
+ non_stationary: esChangePointCorrelationSchema.extend({
+ trend: z.enum(['increasing', 'decreasing']),
+ }),
+ }),
+ })
+ .transform(({ type: { non_stationary: details } }) => ({
+ type: 'non_stationary' as const,
+ details,
+ })),
+ z
+ .object({
+ type: z.object({
+ stationary: z.object({}),
+ }),
+ })
+ .transform(() => ({ type: 'stationary' as const })),
+ z
+ .object({
+ type: z.object({}),
+ })
+ .transform((value) => ({ type: 'unknown' as const, rawChange: JSON.stringify(value) })),
+]);
+
+const esHistogramSchema = z
+ .object({
+ buckets: z.array(
+ z
+ .object({
+ key_as_string: z.string(),
+ doc_count: z.number(),
+ })
+ .transform((bucket) => ({
+ timestamp: bucket.key_as_string,
+ documentCount: bucket.doc_count,
+ }))
+ ),
+ })
+ .transform(({ buckets }) => buckets);
+
+type EsHistogram = z.output;
+
+const esCategoryBucketSchema = z.object({
+ key: z.string(),
+ doc_count: z.number(),
+ change: esChangePointSchema,
+ histogram: esHistogramSchema,
+});
+
+type EsCategoryBucket = z.output;
+
+const isRareInHistogram = (histogram: EsHistogram): boolean =>
+ histogram.filter((bucket) => bucket.documentCount > 0).length <
+ histogram.length * rarityThreshold;
+
+const findFirstNonZeroBucket = (histogram: EsHistogram) =>
+ histogram.find((bucket) => bucket.documentCount > 0);
diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_logs_service.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_logs_service.ts
new file mode 100644
index 0000000000000..deeb758d2d737
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_logs_service.ts
@@ -0,0 +1,250 @@
+/*
+ * 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 { MachineImplementationsFrom, assign, setup } from 'xstate5';
+import { LogCategory } from '../../types';
+import { getPlaceholderFor } from '../../utils/xstate5_utils';
+import { categorizeDocuments } from './categorize_documents';
+import { countDocuments } from './count_documents';
+import { CategorizeLogsServiceDependencies, LogCategorizationParams } from './types';
+
+export const categorizeLogsService = setup({
+ types: {
+ input: {} as LogCategorizationParams,
+ output: {} as {
+ categories: LogCategory[];
+ documentCount: number;
+ hasReachedLimit: boolean;
+ samplingProbability: number;
+ },
+ context: {} as {
+ categories: LogCategory[];
+ documentCount: number;
+ error?: Error;
+ hasReachedLimit: boolean;
+ parameters: LogCategorizationParams;
+ samplingProbability: number;
+ },
+ events: {} as {
+ type: 'cancel';
+ },
+ },
+ actors: {
+ countDocuments: getPlaceholderFor(countDocuments),
+ categorizeDocuments: getPlaceholderFor(categorizeDocuments),
+ },
+ actions: {
+ storeError: assign((_, params: { error: unknown }) => ({
+ error: params.error instanceof Error ? params.error : new Error(String(params.error)),
+ })),
+ storeCategories: assign(
+ ({ context }, params: { categories: LogCategory[]; hasReachedLimit: boolean }) => ({
+ categories: [...context.categories, ...params.categories],
+ hasReachedLimit: params.hasReachedLimit,
+ })
+ ),
+ storeDocumentCount: assign(
+ (_, params: { documentCount: number; samplingProbability: number }) => ({
+ documentCount: params.documentCount,
+ samplingProbability: params.samplingProbability,
+ })
+ ),
+ },
+ guards: {
+ hasTooFewDocuments: (_guardArgs, params: { documentCount: number }) => params.documentCount < 1,
+ requiresSampling: (_guardArgs, params: { samplingProbability: number }) =>
+ params.samplingProbability < 1,
+ },
+}).createMachine({
+ /** @xstate-layout N4IgpgJg5mDOIC5QGMCGAXMUD2AnAlgF5gAy2UsAdMtgK4B26+9UAItsrQLZiOwDEEbPTCVmAN2wBrUWkw4CxMhWp1GzNh2690sBBI4Z8wgNoAGALrmLiUAAdssfE2G2QAD0QBmMwA5KACy+AQFmob4AjABMwQBsADQgAJ6IkYEAnJkA7FmxZlERmQGxAL4liXJYeESk5FQ0DEws7Jw8fILCogYy1BhVirUqDerNWm26+vSScsb01iYRNkggDk4u9G6eCD7+QSFhftFxiSkIvgCsWZSxEVlRsbFZ52Zm515lFX0KNcr1ak2aVo6ARCERiKbSWRfapKOqqRoaFraPiTaZGUyWExRJb2RzOWabbx+QLBULhI7FE7eWL+F45GnRPIRZkfECVb6wob-RFjYH8MC4XB4Sh2AA2GAAZnguL15DDBn8EaMgSiDDMMVZLG5VvjXMstjsSftyTFKclEOdzgFKF5zukvA8zBFnl50udWez5b94SNAcjdPw0PRkGBRdZtXj1oTtsS9mTDqaEuaEBF8udKFkIr5fK6olkzOksgEPdCBt6JWB0MgABYaADKqC4YsgAGFS-g4B0wd0oXKBg2m6LW+24OHljqo-rEMzbpQos8-K7fC9CknTrF0rEbbb0oVMoWIgF3eU2e3OVQK1XaywB82IG2+x2BAKhbgReL0FLcDLPf3G3eH36J8x1xNYCSnFNmSuecXhzdJlydTcqQQLJfHSOc0PyLJN3SMxYiPEtH3PShLxret-yHe8RwEIMQzDLVx0jcDQC2GdoIXOCENXZDsyiOcAiiKJ0iiPDLi8V1CKA4jSOvKAACUwC4VBmA0QDvk7UEughHpfxqBSlJUlg1OqUcGNA3UNggrMs347IjzdaIvGQwSvECXI8k3Z43gEiJJI5BUSMrMiWH05T6FU6j+UFYUxUlaVZSksBQsMqBjIIUycRWJi9RY6dIn8KIAjsu1zkc5CAmiG1fBiaIzB8B0QmPT4iICmSNGS8KjMi2jQxArKwJyjw8pswriocqInOTLwIi3ASD1yQpswCd5WXobAIDgNxdPPCMBss3KEAAWjXRBDvTfcLsu9Jlr8r04WGAEkXGeBGL26MBOQzIt2ut4cwmirCt8W6yzhNqbwo4dH0216LOjTMIjnBdYhK1DYgdHjihtZbUIdWIXJuYGflBoLZI6iKoZe8zJwOw9KtGt1kbuTcsmQrwi0oeCQjzZ5blwt1Cek5TKN22GIIKZbAgKC45pyLyeLwtz4Kyabs1QgWAs0kXqaGhBxdcnzpaE2XXmch0MORmaBJeLwjbKMogA */
+ id: 'categorizeLogs',
+ context: ({ input }) => ({
+ categories: [],
+ documentCount: 0,
+ hasReachedLimit: false,
+ parameters: input,
+ samplingProbability: 1,
+ }),
+ initial: 'countingDocuments',
+ states: {
+ countingDocuments: {
+ invoke: {
+ src: 'countDocuments',
+ input: ({ context }) => context.parameters,
+ onDone: [
+ {
+ target: 'done',
+ guard: {
+ type: 'hasTooFewDocuments',
+ params: ({ event }) => event.output,
+ },
+ actions: [
+ {
+ type: 'storeDocumentCount',
+ params: ({ event }) => event.output,
+ },
+ ],
+ },
+ {
+ target: 'fetchingSampledCategories',
+ guard: {
+ type: 'requiresSampling',
+ params: ({ event }) => event.output,
+ },
+ actions: [
+ {
+ type: 'storeDocumentCount',
+ params: ({ event }) => event.output,
+ },
+ ],
+ },
+ {
+ target: 'fetchingRemainingCategories',
+ actions: [
+ {
+ type: 'storeDocumentCount',
+ params: ({ event }) => event.output,
+ },
+ ],
+ },
+ ],
+ onError: {
+ target: 'failed',
+ actions: [
+ {
+ type: 'storeError',
+ params: ({ event }) => ({ error: event.error }),
+ },
+ ],
+ },
+ },
+
+ on: {
+ cancel: {
+ target: 'failed',
+ actions: [
+ {
+ type: 'storeError',
+ params: () => ({ error: new Error('Counting cancelled') }),
+ },
+ ],
+ },
+ },
+ },
+
+ fetchingSampledCategories: {
+ invoke: {
+ src: 'categorizeDocuments',
+ id: 'categorizeSampledCategories',
+ input: ({ context }) => ({
+ ...context.parameters,
+ samplingProbability: context.samplingProbability,
+ ignoredCategoryTerms: [],
+ minDocsPerCategory: 10,
+ }),
+ onDone: {
+ target: 'fetchingRemainingCategories',
+ actions: [
+ {
+ type: 'storeCategories',
+ params: ({ event }) => event.output,
+ },
+ ],
+ },
+ onError: {
+ target: 'failed',
+ actions: [
+ {
+ type: 'storeError',
+ params: ({ event }) => ({ error: event.error }),
+ },
+ ],
+ },
+ },
+
+ on: {
+ cancel: {
+ target: 'failed',
+ actions: [
+ {
+ type: 'storeError',
+ params: () => ({ error: new Error('Categorization cancelled') }),
+ },
+ ],
+ },
+ },
+ },
+
+ fetchingRemainingCategories: {
+ invoke: {
+ src: 'categorizeDocuments',
+ id: 'categorizeRemainingCategories',
+ input: ({ context }) => ({
+ ...context.parameters,
+ samplingProbability: 1,
+ ignoredCategoryTerms: context.categories.map((category) => category.terms),
+ minDocsPerCategory: 0,
+ }),
+ onDone: {
+ target: 'done',
+ actions: [
+ {
+ type: 'storeCategories',
+ params: ({ event }) => event.output,
+ },
+ ],
+ },
+ onError: {
+ target: 'failed',
+ actions: [
+ {
+ type: 'storeError',
+ params: ({ event }) => ({ error: event.error }),
+ },
+ ],
+ },
+ },
+
+ on: {
+ cancel: {
+ target: 'failed',
+ actions: [
+ {
+ type: 'storeError',
+ params: () => ({ error: new Error('Categorization cancelled') }),
+ },
+ ],
+ },
+ },
+ },
+
+ failed: {
+ type: 'final',
+ },
+
+ done: {
+ type: 'final',
+ },
+ },
+ output: ({ context }) => ({
+ categories: context.categories,
+ documentCount: context.documentCount,
+ hasReachedLimit: context.hasReachedLimit,
+ samplingProbability: context.samplingProbability,
+ }),
+});
+
+export const createCategorizeLogsServiceImplementations = ({
+ search,
+}: CategorizeLogsServiceDependencies): MachineImplementationsFrom<
+ typeof categorizeLogsService
+> => ({
+ actors: {
+ categorizeDocuments: categorizeDocuments({ search }),
+ countDocuments: countDocuments({ search }),
+ },
+});
diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/count_documents.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/count_documents.ts
new file mode 100644
index 0000000000000..359f9ddac2bd8
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/count_documents.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { getSampleProbability } from '@kbn/ml-random-sampler-utils';
+import { ISearchGeneric } from '@kbn/search-types';
+import { lastValueFrom } from 'rxjs';
+import { fromPromise } from 'xstate5';
+import { LogCategorizationParams } from './types';
+import { createCategorizationQuery } from './queries';
+
+export const countDocuments = ({ search }: { search: ISearchGeneric }) =>
+ fromPromise<
+ {
+ documentCount: number;
+ samplingProbability: number;
+ },
+ LogCategorizationParams
+ >(
+ async ({
+ input: { index, endTimestamp, startTimestamp, timeField, messageField, documentFilters },
+ signal,
+ }) => {
+ const { rawResponse: totalHitsResponse } = await lastValueFrom(
+ search(
+ {
+ params: {
+ index,
+ size: 0,
+ track_total_hits: true,
+ query: createCategorizationQuery({
+ messageField,
+ timeField,
+ startTimestamp,
+ endTimestamp,
+ additionalFilters: documentFilters,
+ }),
+ },
+ },
+ { abortSignal: signal }
+ )
+ );
+
+ const documentCount =
+ totalHitsResponse.hits.total == null
+ ? 0
+ : typeof totalHitsResponse.hits.total === 'number'
+ ? totalHitsResponse.hits.total
+ : totalHitsResponse.hits.total.value;
+ const samplingProbability = getSampleProbability(documentCount);
+
+ return {
+ documentCount,
+ samplingProbability,
+ };
+ }
+ );
diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/index.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/index.ts
new file mode 100644
index 0000000000000..149359b7d2015
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/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 './categorize_logs_service';
diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/queries.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/queries.ts
new file mode 100644
index 0000000000000..aef12da303bcc
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/queries.ts
@@ -0,0 +1,151 @@
+/*
+ * 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import { calculateAuto } from '@kbn/calculate-auto';
+import { RandomSamplerWrapper } from '@kbn/ml-random-sampler-utils';
+import moment from 'moment';
+
+const isoTimestampFormat = "YYYY-MM-DD'T'HH:mm:ss.SSS'Z'";
+
+export const createCategorizationQuery = ({
+ messageField,
+ timeField,
+ startTimestamp,
+ endTimestamp,
+ additionalFilters = [],
+ ignoredCategoryTerms = [],
+}: {
+ messageField: string;
+ timeField: string;
+ startTimestamp: string;
+ endTimestamp: string;
+ additionalFilters?: QueryDslQueryContainer[];
+ ignoredCategoryTerms?: string[];
+}): QueryDslQueryContainer => {
+ return {
+ bool: {
+ filter: [
+ {
+ exists: {
+ field: messageField,
+ },
+ },
+ {
+ range: {
+ [timeField]: {
+ gte: startTimestamp,
+ lte: endTimestamp,
+ format: 'strict_date_time',
+ },
+ },
+ },
+ ...additionalFilters,
+ ],
+ must_not: ignoredCategoryTerms.map(createCategoryQuery(messageField)),
+ },
+ };
+};
+
+export const createCategorizationRequestParams = ({
+ index,
+ timeField,
+ messageField,
+ startTimestamp,
+ endTimestamp,
+ randomSampler,
+ minDocsPerCategory = 0,
+ additionalFilters = [],
+ ignoredCategoryTerms = [],
+ maxCategoriesCount = 1000,
+}: {
+ startTimestamp: string;
+ endTimestamp: string;
+ index: string;
+ timeField: string;
+ messageField: string;
+ randomSampler: RandomSamplerWrapper;
+ minDocsPerCategory?: number;
+ additionalFilters?: QueryDslQueryContainer[];
+ ignoredCategoryTerms?: string[];
+ maxCategoriesCount?: number;
+}) => {
+ const startMoment = moment(startTimestamp, isoTimestampFormat);
+ const endMoment = moment(endTimestamp, isoTimestampFormat);
+ const fixedIntervalDuration = calculateAuto.atLeast(
+ 24,
+ moment.duration(endMoment.diff(startMoment))
+ );
+ const fixedIntervalSize = `${Math.ceil(fixedIntervalDuration?.asMinutes() ?? 1)}m`;
+
+ return {
+ index,
+ size: 0,
+ track_total_hits: false,
+ query: createCategorizationQuery({
+ messageField,
+ timeField,
+ startTimestamp,
+ endTimestamp,
+ additionalFilters,
+ ignoredCategoryTerms,
+ }),
+ aggs: randomSampler.wrap({
+ histogram: {
+ date_histogram: {
+ field: timeField,
+ fixed_interval: fixedIntervalSize,
+ extended_bounds: {
+ min: startTimestamp,
+ max: endTimestamp,
+ },
+ },
+ },
+ categories: {
+ categorize_text: {
+ field: messageField,
+ size: maxCategoriesCount,
+ categorization_analyzer: {
+ tokenizer: 'standard',
+ },
+ ...(minDocsPerCategory > 0 ? { min_doc_count: minDocsPerCategory } : {}),
+ },
+ aggs: {
+ histogram: {
+ date_histogram: {
+ field: timeField,
+ fixed_interval: fixedIntervalSize,
+ extended_bounds: {
+ min: startTimestamp,
+ max: endTimestamp,
+ },
+ },
+ },
+ change: {
+ // @ts-expect-error the official types don't support the change_point aggregation
+ change_point: {
+ buckets_path: 'histogram>_count',
+ },
+ },
+ },
+ },
+ }),
+ };
+};
+
+export const createCategoryQuery =
+ (messageField: string) =>
+ (categoryTerms: string): QueryDslQueryContainer => ({
+ match: {
+ [messageField]: {
+ query: categoryTerms,
+ operator: 'AND' as const,
+ fuzziness: 0,
+ auto_generate_synonyms_phrase_query: false,
+ },
+ },
+ });
diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/types.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/types.ts
new file mode 100644
index 0000000000000..e094317a98d62
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/types.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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import { ISearchGeneric } from '@kbn/search-types';
+
+export interface CategorizeLogsServiceDependencies {
+ search: ISearchGeneric;
+}
+
+export interface LogCategorizationParams {
+ documentFilters: QueryDslQueryContainer[];
+ endTimestamp: string;
+ index: string;
+ messageField: string;
+ startTimestamp: string;
+ timeField: string;
+}
diff --git a/x-pack/packages/observability/logs_overview/src/types.ts b/x-pack/packages/observability/logs_overview/src/types.ts
new file mode 100644
index 0000000000000..4c3d27eca7e7c
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/types.ts
@@ -0,0 +1,74 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface LogCategory {
+ change: LogCategoryChange;
+ documentCount: number;
+ histogram: LogCategoryHistogramBucket[];
+ terms: string;
+}
+
+export type LogCategoryChange =
+ | LogCategoryNoChange
+ | LogCategoryRareChange
+ | LogCategorySpikeChange
+ | LogCategoryDipChange
+ | LogCategoryStepChange
+ | LogCategoryDistributionChange
+ | LogCategoryTrendChange
+ | LogCategoryOtherChange
+ | LogCategoryUnknownChange;
+
+export interface LogCategoryNoChange {
+ type: 'none';
+}
+
+export interface LogCategoryRareChange {
+ type: 'rare';
+ timestamp: string;
+}
+
+export interface LogCategorySpikeChange {
+ type: 'spike';
+ timestamp: string;
+}
+
+export interface LogCategoryDipChange {
+ type: 'dip';
+ timestamp: string;
+}
+
+export interface LogCategoryStepChange {
+ type: 'step';
+ timestamp: string;
+}
+
+export interface LogCategoryTrendChange {
+ type: 'trend';
+ timestamp: string;
+ correlationCoefficient: number;
+}
+
+export interface LogCategoryDistributionChange {
+ type: 'distribution';
+ timestamp: string;
+}
+
+export interface LogCategoryOtherChange {
+ type: 'other';
+ timestamp?: string;
+}
+
+export interface LogCategoryUnknownChange {
+ type: 'unknown';
+ rawChange: string;
+}
+
+export interface LogCategoryHistogramBucket {
+ documentCount: number;
+ timestamp: string;
+}
diff --git a/x-pack/packages/observability/logs_overview/src/utils/logs_source.ts b/x-pack/packages/observability/logs_overview/src/utils/logs_source.ts
new file mode 100644
index 0000000000000..0c8767c8702d4
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/utils/logs_source.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { type AbstractDataView } from '@kbn/data-views-plugin/common';
+import { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public';
+
+export type LogsSourceConfiguration =
+ | SharedSettingLogsSourceConfiguration
+ | IndexNameLogsSourceConfiguration
+ | DataViewLogsSourceConfiguration;
+
+export interface SharedSettingLogsSourceConfiguration {
+ type: 'shared_setting';
+ timestampField?: string;
+ messageField?: string;
+}
+
+export interface IndexNameLogsSourceConfiguration {
+ type: 'index_name';
+ indexName: string;
+ timestampField: string;
+ messageField: string;
+}
+
+export interface DataViewLogsSourceConfiguration {
+ type: 'data_view';
+ dataView: AbstractDataView;
+ messageField?: string;
+}
+
+export const normalizeLogsSource =
+ ({ logsDataAccess }: { logsDataAccess: LogsDataAccessPluginStart }) =>
+ async (logsSource: LogsSourceConfiguration): Promise => {
+ switch (logsSource.type) {
+ case 'index_name':
+ return logsSource;
+ case 'shared_setting':
+ const logSourcesFromSharedSettings =
+ await logsDataAccess.services.logSourcesService.getLogSources();
+ return {
+ type: 'index_name',
+ indexName: logSourcesFromSharedSettings
+ .map((logSource) => logSource.indexPattern)
+ .join(','),
+ timestampField: logsSource.timestampField ?? '@timestamp',
+ messageField: logsSource.messageField ?? 'message',
+ };
+ case 'data_view':
+ return {
+ type: 'index_name',
+ indexName: logsSource.dataView.getIndexPattern(),
+ timestampField: logsSource.dataView.timeFieldName ?? '@timestamp',
+ messageField: logsSource.messageField ?? 'message',
+ };
+ }
+ };
diff --git a/x-pack/packages/observability/logs_overview/src/utils/xstate5_utils.ts b/x-pack/packages/observability/logs_overview/src/utils/xstate5_utils.ts
new file mode 100644
index 0000000000000..3df0bf4ea3988
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/utils/xstate5_utils.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const getPlaceholderFor = any>(
+ implementationFactory: ImplementationFactory
+): ReturnType =>
+ (() => {
+ throw new Error('Not implemented');
+ }) as ReturnType;
diff --git a/x-pack/packages/observability/logs_overview/tsconfig.json b/x-pack/packages/observability/logs_overview/tsconfig.json
new file mode 100644
index 0000000000000..886062ae8855f
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/tsconfig.json
@@ -0,0 +1,39 @@
+{
+ "extends": "../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "target/types",
+ "types": [
+ "jest",
+ "node",
+ "react",
+ "@kbn/ambient-ui-types",
+ "@kbn/ambient-storybook-types",
+ "@emotion/react/types/css-prop"
+ ]
+ },
+ "include": [
+ "**/*.ts",
+ "**/*.tsx",
+ ],
+ "exclude": [
+ "target/**/*"
+ ],
+ "kbn_references": [
+ "@kbn/data-views-plugin",
+ "@kbn/i18n",
+ "@kbn/search-types",
+ "@kbn/xstate-utils",
+ "@kbn/core-ui-settings-browser",
+ "@kbn/i18n-react",
+ "@kbn/charts-plugin",
+ "@kbn/utility-types",
+ "@kbn/logs-data-access-plugin",
+ "@kbn/ml-random-sampler-utils",
+ "@kbn/zod",
+ "@kbn/calculate-auto",
+ "@kbn/discover-plugin",
+ "@kbn/es-query",
+ "@kbn/router-utils",
+ "@kbn/share-plugin",
+ ]
+}
diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_logs/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/service_logs/index.tsx
index 4df52758ceda3..a1dadbf186b91 100644
--- a/x-pack/plugins/observability_solution/apm/public/components/app/service_logs/index.tsx
+++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_logs/index.tsx
@@ -5,19 +5,36 @@
* 2.0.
*/
-import React from 'react';
+import React, { useMemo } from 'react';
import moment from 'moment';
+import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { LogStream } from '@kbn/logs-shared-plugin/public';
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
-import { useFetcher } from '../../../hooks/use_fetcher';
-import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
-import { APIReturnType } from '../../../services/rest/create_call_apm_api';
-
import { CONTAINER_ID, SERVICE_ENVIRONMENT, SERVICE_NAME } from '../../../../common/es_fields/apm';
+import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
+import { useKibana } from '../../../context/kibana_context/use_kibana';
import { useAnyOfApmParams } from '../../../hooks/use_apm_params';
+import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import { useTimeRange } from '../../../hooks/use_time_range';
+import { APIReturnType } from '../../../services/rest/create_call_apm_api';
export function ServiceLogs() {
+ const {
+ services: {
+ logsShared: { LogsOverview },
+ },
+ } = useKibana();
+
+ const isLogsOverviewEnabled = LogsOverview.useIsEnabled();
+
+ if (isLogsOverviewEnabled) {
+ return ;
+ } else {
+ return ;
+ }
+}
+
+export function ClassicServiceLogsStream() {
const { serviceName } = useApmServiceContext();
const {
@@ -58,6 +75,54 @@ export function ServiceLogs() {
);
}
+export function ServiceLogsOverview() {
+ const {
+ services: { logsShared },
+ } = useKibana();
+ const { serviceName } = useApmServiceContext();
+ const {
+ query: { environment, kuery, rangeFrom, rangeTo },
+ } = useAnyOfApmParams('/services/{serviceName}/logs');
+ const { start, end } = useTimeRange({ rangeFrom, rangeTo });
+ const timeRange = useMemo(() => ({ start, end }), [start, end]);
+
+ const { data: logFilters, status } = useFetcher(
+ async (callApmApi) => {
+ if (start == null || end == null) {
+ return;
+ }
+
+ const { containerIds } = await callApmApi(
+ 'GET /internal/apm/services/{serviceName}/infrastructure_attributes',
+ {
+ params: {
+ path: { serviceName },
+ query: {
+ environment,
+ kuery,
+ start,
+ end,
+ },
+ },
+ }
+ );
+
+ return [getInfrastructureFilter({ containerIds, environment, serviceName })];
+ },
+ [environment, kuery, serviceName, start, end]
+ );
+
+ if (status === FETCH_STATUS.SUCCESS) {
+ return ;
+ } else if (status === FETCH_STATUS.FAILURE) {
+ return (
+
+ );
+ } else {
+ return ;
+ }
+}
+
export function getInfrastructureKQLFilter({
data,
serviceName,
@@ -84,3 +149,99 @@ export function getInfrastructureKQLFilter({
return [serviceNameAndEnvironmentCorrelation, ...containerIdCorrelation].join(' or ');
}
+
+export function getInfrastructureFilter({
+ containerIds,
+ environment,
+ serviceName,
+}: {
+ containerIds: string[];
+ environment: string;
+ serviceName: string;
+}): QueryDslQueryContainer {
+ return {
+ bool: {
+ should: [
+ ...getServiceShouldClauses({ environment, serviceName }),
+ ...getContainerShouldClauses({ containerIds }),
+ ],
+ minimum_should_match: 1,
+ },
+ };
+}
+
+export function getServiceShouldClauses({
+ environment,
+ serviceName,
+}: {
+ environment: string;
+ serviceName: string;
+}): QueryDslQueryContainer[] {
+ const serviceNameFilter: QueryDslQueryContainer = {
+ term: {
+ [SERVICE_NAME]: serviceName,
+ },
+ };
+
+ if (environment === ENVIRONMENT_ALL.value) {
+ return [serviceNameFilter];
+ } else {
+ return [
+ {
+ bool: {
+ filter: [
+ serviceNameFilter,
+ {
+ term: {
+ [SERVICE_ENVIRONMENT]: environment,
+ },
+ },
+ ],
+ },
+ },
+ {
+ bool: {
+ filter: [serviceNameFilter],
+ must_not: [
+ {
+ exists: {
+ field: SERVICE_ENVIRONMENT,
+ },
+ },
+ ],
+ },
+ },
+ ];
+ }
+}
+
+export function getContainerShouldClauses({
+ containerIds = [],
+}: {
+ containerIds: string[];
+}): QueryDslQueryContainer[] {
+ if (containerIds.length === 0) {
+ return [];
+ }
+
+ return [
+ {
+ bool: {
+ filter: [
+ {
+ terms: {
+ [CONTAINER_ID]: containerIds,
+ },
+ },
+ ],
+ must_not: [
+ {
+ term: {
+ [SERVICE_NAME]: '*',
+ },
+ },
+ ],
+ },
+ },
+ ];
+}
diff --git a/x-pack/plugins/observability_solution/apm/public/components/routing/service_detail/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/routing/service_detail/index.tsx
index d746e0464fd40..8a4a1c32877c5 100644
--- a/x-pack/plugins/observability_solution/apm/public/components/routing/service_detail/index.tsx
+++ b/x-pack/plugins/observability_solution/apm/public/components/routing/service_detail/index.tsx
@@ -330,7 +330,7 @@ export const serviceDetailRoute = {
}),
element: ,
searchBarOptions: {
- showUnifiedSearchBar: false,
+ showQueryInput: false,
},
}),
'/services/{serviceName}/infrastructure': {
diff --git a/x-pack/plugins/observability_solution/apm/public/plugin.ts b/x-pack/plugins/observability_solution/apm/public/plugin.ts
index 9a9f45f42a39e..b21bdedac9ef8 100644
--- a/x-pack/plugins/observability_solution/apm/public/plugin.ts
+++ b/x-pack/plugins/observability_solution/apm/public/plugin.ts
@@ -69,6 +69,7 @@ import { from } from 'rxjs';
import { map } from 'rxjs';
import type { CloudSetup } from '@kbn/cloud-plugin/public';
import type { ServerlessPluginStart } from '@kbn/serverless/public';
+import { LogsSharedClientStartExports } from '@kbn/logs-shared-plugin/public';
import type { ConfigSchema } from '.';
import { registerApmRuleTypes } from './components/alerting/rule_types/register_apm_rule_types';
import { registerEmbeddables } from './embeddable/register_embeddables';
@@ -142,6 +143,7 @@ export interface ApmPluginStartDeps {
dashboard: DashboardStart;
metricsDataAccess: MetricsDataPluginStart;
uiSettings: IUiSettingsClient;
+ logsShared: LogsSharedClientStartExports;
}
const applicationsTitle = i18n.translate('xpack.apm.navigation.rootTitle', {
diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx b/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx
index 27344ccd1f108..78443c9a6ec81 100644
--- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx
+++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx
@@ -5,21 +5,37 @@
* 2.0.
*/
-import React, { useMemo } from 'react';
+import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { LogStream } from '@kbn/logs-shared-plugin/public';
-import { i18n } from '@kbn/i18n';
+import React, { useMemo } from 'react';
import { InfraLoadingPanel } from '../../../../../../components/loading';
+import { useKibanaContextForPlugin } from '../../../../../../hooks/use_kibana';
+import { useLogViewReference } from '../../../../../../hooks/use_log_view_reference';
+import { buildCombinedAssetFilter } from '../../../../../../utils/filters/build';
import { useHostsViewContext } from '../../../hooks/use_hosts_view';
-import { useUnifiedSearchContext } from '../../../hooks/use_unified_search';
import { useLogsSearchUrlState } from '../../../hooks/use_logs_search_url_state';
+import { useUnifiedSearchContext } from '../../../hooks/use_unified_search';
import { LogsLinkToStream } from './logs_link_to_stream';
import { LogsSearchBar } from './logs_search_bar';
-import { buildCombinedAssetFilter } from '../../../../../../utils/filters/build';
-import { useLogViewReference } from '../../../../../../hooks/use_log_view_reference';
export const LogsTabContent = () => {
+ const {
+ services: {
+ logsShared: { LogsOverview },
+ },
+ } = useKibanaContextForPlugin();
+ const isLogsOverviewEnabled = LogsOverview.useIsEnabled();
+ if (isLogsOverviewEnabled) {
+ return ;
+ } else {
+ return ;
+ }
+};
+
+export const LogsTabLogStreamContent = () => {
const [filterQuery] = useLogsSearchUrlState();
const { getDateRangeAsTimestamp } = useUnifiedSearchContext();
const { from, to } = useMemo(() => getDateRangeAsTimestamp(), [getDateRangeAsTimestamp]);
@@ -53,22 +69,7 @@ export const LogsTabContent = () => {
}, [filterQuery.query, hostNodes]);
if (loading || logViewLoading || !logView) {
- return (
-
-
-
- }
- />
-
-
- );
+ return ;
}
return (
@@ -84,6 +85,7 @@ export const LogsTabContent = () => {
query={logsLinkToStreamQuery}
logView={logView}
/>
+ ]
@@ -112,3 +114,53 @@ const createHostsFilterQueryParam = (hostNodes: string[]): string => {
return hostsQueryParam;
};
+
+const LogsTabLogsOverviewContent = () => {
+ const {
+ services: {
+ logsShared: { LogsOverview },
+ },
+ } = useKibanaContextForPlugin();
+
+ const { parsedDateRange } = useUnifiedSearchContext();
+ const timeRange = useMemo(
+ () => ({ start: parsedDateRange.from, end: parsedDateRange.to }),
+ [parsedDateRange.from, parsedDateRange.to]
+ );
+
+ const { hostNodes, loading, error } = useHostsViewContext();
+ const logFilters = useMemo(
+ () => [
+ buildCombinedAssetFilter({
+ field: 'host.name',
+ values: hostNodes.map((p) => p.name),
+ }).query as QueryDslQueryContainer,
+ ],
+ [hostNodes]
+ );
+
+ if (loading) {
+ return ;
+ } else if (error != null) {
+ return ;
+ } else {
+ return ;
+ }
+};
+
+const LogsTabLoadingContent = () => (
+
+
+
+ }
+ />
+
+
+);
diff --git a/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc b/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc
index ea93fd326dac7..10c8fe32cfe9c 100644
--- a/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc
+++ b/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc
@@ -9,13 +9,14 @@
"browser": true,
"configPath": ["xpack", "logs_shared"],
"requiredPlugins": [
+ "charts",
"data",
"dataViews",
"discoverShared",
- "usageCollection",
+ "logsDataAccess",
"observabilityShared",
"share",
- "logsDataAccess"
+ "usageCollection",
],
"optionalPlugins": [
"observabilityAIAssistant",
diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/index.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/index.tsx
new file mode 100644
index 0000000000000..627cdc8447eea
--- /dev/null
+++ b/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/index.tsx
@@ -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 './logs_overview';
diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.mock.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.mock.tsx
new file mode 100644
index 0000000000000..435766bff793d
--- /dev/null
+++ b/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.mock.tsx
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import type {
+ LogsOverviewProps,
+ SelfContainedLogsOverviewComponent,
+ SelfContainedLogsOverviewHelpers,
+} from './logs_overview';
+
+export const createLogsOverviewMock = () => {
+ const LogsOverviewMock = jest.fn(LogsOverviewMockImpl) as unknown as ILogsOverviewMock;
+
+ LogsOverviewMock.useIsEnabled = jest.fn(() => true);
+
+ LogsOverviewMock.ErrorContent = jest.fn(() =>
);
+
+ LogsOverviewMock.LoadingContent = jest.fn(() =>
);
+
+ return LogsOverviewMock;
+};
+
+const LogsOverviewMockImpl = (_props: LogsOverviewProps) => {
+ return
;
+};
+
+type ILogsOverviewMock = jest.Mocked &
+ jest.Mocked;
diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.tsx
new file mode 100644
index 0000000000000..7b60aee5be57c
--- /dev/null
+++ b/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.tsx
@@ -0,0 +1,70 @@
+/*
+ * 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 { OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID } from '@kbn/management-settings-ids';
+import type {
+ LogsOverviewProps as FullLogsOverviewProps,
+ LogsOverviewDependencies,
+ LogsOverviewErrorContentProps,
+} from '@kbn/observability-logs-overview';
+import { dynamic } from '@kbn/shared-ux-utility';
+import React from 'react';
+import useObservable from 'react-use/lib/useObservable';
+
+const LazyLogsOverview = dynamic(() =>
+ import('@kbn/observability-logs-overview').then((mod) => ({ default: mod.LogsOverview }))
+);
+
+const LazyLogsOverviewErrorContent = dynamic(() =>
+ import('@kbn/observability-logs-overview').then((mod) => ({
+ default: mod.LogsOverviewErrorContent,
+ }))
+);
+
+const LazyLogsOverviewLoadingContent = dynamic(() =>
+ import('@kbn/observability-logs-overview').then((mod) => ({
+ default: mod.LogsOverviewLoadingContent,
+ }))
+);
+
+export type LogsOverviewProps = Omit;
+
+export interface SelfContainedLogsOverviewHelpers {
+ useIsEnabled: () => boolean;
+ ErrorContent: React.ComponentType;
+ LoadingContent: React.ComponentType;
+}
+
+export type SelfContainedLogsOverviewComponent = React.ComponentType;
+
+export type SelfContainedLogsOverview = SelfContainedLogsOverviewComponent &
+ SelfContainedLogsOverviewHelpers;
+
+export const createLogsOverview = (
+ dependencies: LogsOverviewDependencies
+): SelfContainedLogsOverview => {
+ const SelfContainedLogsOverview = (props: LogsOverviewProps) => {
+ return ;
+ };
+
+ const isEnabled$ = dependencies.uiSettings.client.get$(
+ OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID,
+ defaultIsEnabled
+ );
+
+ SelfContainedLogsOverview.useIsEnabled = (): boolean => {
+ return useObservable(isEnabled$, defaultIsEnabled);
+ };
+
+ SelfContainedLogsOverview.ErrorContent = LazyLogsOverviewErrorContent;
+
+ SelfContainedLogsOverview.LoadingContent = LazyLogsOverviewLoadingContent;
+
+ return SelfContainedLogsOverview;
+};
+
+const defaultIsEnabled = false;
diff --git a/x-pack/plugins/observability_solution/logs_shared/public/index.ts b/x-pack/plugins/observability_solution/logs_shared/public/index.ts
index a602b25786116..3d601c9936f2d 100644
--- a/x-pack/plugins/observability_solution/logs_shared/public/index.ts
+++ b/x-pack/plugins/observability_solution/logs_shared/public/index.ts
@@ -50,6 +50,7 @@ export type {
UpdatedDateRange,
VisibleInterval,
} from './components/logging/log_text_stream/scrollable_log_text_stream_view';
+export type { LogsOverviewProps } from './components/logs_overview';
export const WithSummary = dynamic(() => import('./containers/logs/log_summary/with_summary'));
export const LogEntryFlyout = dynamic(
diff --git a/x-pack/plugins/observability_solution/logs_shared/public/mocks.tsx b/x-pack/plugins/observability_solution/logs_shared/public/mocks.tsx
index a9b0ebd6a6aa3..ffb867abbcc17 100644
--- a/x-pack/plugins/observability_solution/logs_shared/public/mocks.tsx
+++ b/x-pack/plugins/observability_solution/logs_shared/public/mocks.tsx
@@ -6,12 +6,14 @@
*/
import { createLogAIAssistantMock } from './components/log_ai_assistant/log_ai_assistant.mock';
+import { createLogsOverviewMock } from './components/logs_overview/logs_overview.mock';
import { createLogViewsServiceStartMock } from './services/log_views/log_views_service.mock';
import { LogsSharedClientStartExports } from './types';
export const createLogsSharedPluginStartMock = (): jest.Mocked => ({
logViews: createLogViewsServiceStartMock(),
LogAIAssistant: createLogAIAssistantMock(),
+ LogsOverview: createLogsOverviewMock(),
});
export const _ensureTypeCompatibility = (): LogsSharedClientStartExports =>
diff --git a/x-pack/plugins/observability_solution/logs_shared/public/plugin.ts b/x-pack/plugins/observability_solution/logs_shared/public/plugin.ts
index d6f4ac81fe266..fc17e9b17cc82 100644
--- a/x-pack/plugins/observability_solution/logs_shared/public/plugin.ts
+++ b/x-pack/plugins/observability_solution/logs_shared/public/plugin.ts
@@ -12,6 +12,7 @@ import {
TraceLogsLocatorDefinition,
} from '../common/locators';
import { createLogAIAssistant, createLogsAIAssistantRenderer } from './components/log_ai_assistant';
+import { createLogsOverview } from './components/logs_overview';
import { LogViewsService } from './services/log_views';
import {
LogsSharedClientCoreSetup,
@@ -51,8 +52,16 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass {
}
public start(core: CoreStart, plugins: LogsSharedClientStartDeps) {
- const { http } = core;
- const { data, dataViews, discoverShared, observabilityAIAssistant, logsDataAccess } = plugins;
+ const { http, settings } = core;
+ const {
+ charts,
+ data,
+ dataViews,
+ discoverShared,
+ logsDataAccess,
+ observabilityAIAssistant,
+ share,
+ } = plugins;
const logViews = this.logViews.start({
http,
@@ -61,9 +70,18 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass {
search: data.search,
});
+ const LogsOverview = createLogsOverview({
+ charts,
+ logsDataAccess,
+ search: data.search.search,
+ uiSettings: settings,
+ share,
+ });
+
if (!observabilityAIAssistant) {
return {
logViews,
+ LogsOverview,
};
}
@@ -77,6 +95,7 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass {
return {
logViews,
LogAIAssistant,
+ LogsOverview,
};
}
diff --git a/x-pack/plugins/observability_solution/logs_shared/public/types.ts b/x-pack/plugins/observability_solution/logs_shared/public/types.ts
index 58b180ee8b6ef..4237c28c621b8 100644
--- a/x-pack/plugins/observability_solution/logs_shared/public/types.ts
+++ b/x-pack/plugins/observability_solution/logs_shared/public/types.ts
@@ -5,19 +5,19 @@
* 2.0.
*/
+import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
import type { CoreSetup, CoreStart, Plugin as PluginClass } from '@kbn/core/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { DiscoverSharedPublicStart } from '@kbn/discover-shared-plugin/public';
-import { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public';
+import type { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public';
import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public';
import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
-
-import { LogsSharedLocators } from '../common/locators';
+import type { LogsSharedLocators } from '../common/locators';
import type { LogAIAssistantProps } from './components/log_ai_assistant/log_ai_assistant';
-// import type { OsqueryPluginStart } from '../../osquery/public';
-import { LogViewsServiceSetup, LogViewsServiceStart } from './services/log_views';
+import type { SelfContainedLogsOverview } from './components/logs_overview';
+import type { LogViewsServiceSetup, LogViewsServiceStart } from './services/log_views';
// Our own setup and start contract values
export interface LogsSharedClientSetupExports {
@@ -28,6 +28,7 @@ export interface LogsSharedClientSetupExports {
export interface LogsSharedClientStartExports {
logViews: LogViewsServiceStart;
LogAIAssistant?: (props: Omit) => JSX.Element;
+ LogsOverview: SelfContainedLogsOverview;
}
export interface LogsSharedClientSetupDeps {
@@ -35,6 +36,7 @@ export interface LogsSharedClientSetupDeps {
}
export interface LogsSharedClientStartDeps {
+ charts: ChartsPluginStart;
data: DataPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
discoverShared: DiscoverSharedPublicStart;
diff --git a/x-pack/plugins/observability_solution/logs_shared/server/feature_flags.ts b/x-pack/plugins/observability_solution/logs_shared/server/feature_flags.ts
new file mode 100644
index 0000000000000..0298416bd3f26
--- /dev/null
+++ b/x-pack/plugins/observability_solution/logs_shared/server/feature_flags.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 { schema } from '@kbn/config-schema';
+import { UiSettingsParams } from '@kbn/core-ui-settings-common';
+import { i18n } from '@kbn/i18n';
+import { OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID } from '@kbn/management-settings-ids';
+
+const technicalPreviewLabel = i18n.translate('xpack.logsShared.technicalPreviewSettingLabel', {
+ defaultMessage: 'Technical Preview',
+});
+
+export const featureFlagUiSettings: Record = {
+ [OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID]: {
+ category: ['observability'],
+ name: i18n.translate('xpack.logsShared.newLogsOverviewSettingName', {
+ defaultMessage: 'New logs overview',
+ }),
+ value: false,
+ description: i18n.translate('xpack.logsShared.newLogsOverviewSettingDescription', {
+ defaultMessage: '{technicalPreviewLabel} Enable the new logs overview experience.',
+
+ values: { technicalPreviewLabel: `[${technicalPreviewLabel}] ` },
+ }),
+ type: 'boolean',
+ schema: schema.boolean(),
+ requiresPageReload: true,
+ },
+};
diff --git a/x-pack/plugins/observability_solution/logs_shared/server/plugin.ts b/x-pack/plugins/observability_solution/logs_shared/server/plugin.ts
index 7c97e175ed64f..d1f6399104fc2 100644
--- a/x-pack/plugins/observability_solution/logs_shared/server/plugin.ts
+++ b/x-pack/plugins/observability_solution/logs_shared/server/plugin.ts
@@ -5,8 +5,19 @@
* 2.0.
*/
-import { PluginInitializerContext, CoreStart, Plugin, Logger } from '@kbn/core/server';
-
+import { CoreStart, Logger, Plugin, PluginInitializerContext } from '@kbn/core/server';
+import { defaultLogViewId } from '../common/log_views';
+import { LogsSharedConfig } from '../common/plugin_config';
+import { registerDeprecations } from './deprecations';
+import { featureFlagUiSettings } from './feature_flags';
+import { KibanaFramework } from './lib/adapters/framework/kibana_framework_adapter';
+import { LogsSharedKibanaLogEntriesAdapter } from './lib/adapters/log_entries/kibana_log_entries_adapter';
+import { LogsSharedLogEntriesDomain } from './lib/domains/log_entries_domain';
+import { LogsSharedBackendLibs, LogsSharedDomainLibs } from './lib/logs_shared_types';
+import { initLogsSharedServer } from './logs_shared_server';
+import { logViewSavedObjectType } from './saved_objects';
+import { LogEntriesService } from './services/log_entries';
+import { LogViewsService } from './services/log_views';
import {
LogsSharedPluginCoreSetup,
LogsSharedPluginSetup,
@@ -15,17 +26,6 @@ import {
LogsSharedServerPluginStartDeps,
UsageCollector,
} from './types';
-import { logViewSavedObjectType } from './saved_objects';
-import { initLogsSharedServer } from './logs_shared_server';
-import { LogViewsService } from './services/log_views';
-import { KibanaFramework } from './lib/adapters/framework/kibana_framework_adapter';
-import { LogsSharedBackendLibs, LogsSharedDomainLibs } from './lib/logs_shared_types';
-import { LogsSharedLogEntriesDomain } from './lib/domains/log_entries_domain';
-import { LogsSharedKibanaLogEntriesAdapter } from './lib/adapters/log_entries/kibana_log_entries_adapter';
-import { LogEntriesService } from './services/log_entries';
-import { LogsSharedConfig } from '../common/plugin_config';
-import { registerDeprecations } from './deprecations';
-import { defaultLogViewId } from '../common/log_views';
export class LogsSharedPlugin
implements
@@ -88,6 +88,8 @@ export class LogsSharedPlugin
registerDeprecations({ core });
+ core.uiSettings.register(featureFlagUiSettings);
+
return {
...domainLibs,
logViews,
diff --git a/x-pack/plugins/observability_solution/logs_shared/tsconfig.json b/x-pack/plugins/observability_solution/logs_shared/tsconfig.json
index 38cbba7c252c0..788f55c9b6fc5 100644
--- a/x-pack/plugins/observability_solution/logs_shared/tsconfig.json
+++ b/x-pack/plugins/observability_solution/logs_shared/tsconfig.json
@@ -44,5 +44,9 @@
"@kbn/logs-data-access-plugin",
"@kbn/core-deprecations-common",
"@kbn/core-deprecations-server",
+ "@kbn/management-settings-ids",
+ "@kbn/observability-logs-overview",
+ "@kbn/charts-plugin",
+ "@kbn/core-ui-settings-common",
]
}
diff --git a/yarn.lock b/yarn.lock
index 54a38b2c0e5d3..019de6121540e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5879,6 +5879,10 @@
version "0.0.0"
uid ""
+"@kbn/observability-logs-overview@link:x-pack/packages/observability/logs_overview":
+ version "0.0.0"
+ uid ""
+
"@kbn/observability-onboarding-e2e@link:x-pack/plugins/observability_solution/observability_onboarding/e2e":
version "0.0.0"
uid ""
@@ -12105,6 +12109,14 @@
use-isomorphic-layout-effect "^1.1.2"
use-sync-external-store "^1.0.0"
+"@xstate5/react@npm:@xstate/react@^4.1.2":
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/@xstate/react/-/react-4.1.2.tgz#4bfcdf2d9e9ef1eaea7388d1896649345e6679cd"
+ integrity sha512-orAidFrKCrU0ZwN5l/ABPlBfW2ziRDT2RrYoktRlZ0WRoLvA2E/uAC1JpZt43mCLtc8jrdwYCgJiqx1V8NvGTw==
+ dependencies:
+ use-isomorphic-layout-effect "^1.1.2"
+ use-sync-external-store "^1.2.0"
+
"@xtuc/ieee754@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
@@ -32800,6 +32812,11 @@ xpath@^0.0.33:
resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.33.tgz#5136b6094227c5df92002e7c3a13516a5074eb07"
integrity sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA==
+"xstate5@npm:xstate@^5.18.1", xstate@^5.18.1:
+ version "5.18.1"
+ resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.18.1.tgz#c4d43ceaba6e6c31705d36bd96e285de4be4f7f4"
+ integrity sha512-m02IqcCQbaE/kBQLunwub/5i8epvkD2mFutnL17Oeg1eXTShe1sRF4D5mhv1dlaFO4vbW5gRGRhraeAD5c938g==
+
xstate@^4.38.2:
version "4.38.2"
resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.2.tgz#1b74544fc9c8c6c713ba77f81c6017e65aa89804"
From 3f75a1d3d56e1d2c84ed0d4c5b18b3beb8357d3b Mon Sep 17 00:00:00 2001
From: Khristinin Nikita
Date: Wed, 9 Oct 2024 19:23:46 +0200
Subject: [PATCH 10/87] Remove feature flag for manual rule run (#193833)
## Summary
Remove feature flag for manual rule run
---------
Co-authored-by: Elastic Machine
---
.../common/experimental_features.ts | 5 --
.../execution_log_search_bar.test.tsx.snap | 14 +++++
.../execution_log_search_bar.tsx | 19 +++---
.../execution_log_table.tsx | 27 +++-----
.../components/manual_rule_run/index.tsx | 4 +-
.../components/rule_backfills_info/index.tsx | 25 +++-----
.../bulk_actions/use_bulk_actions.tsx | 23 +++----
.../rules_table/use_rules_table_actions.tsx | 52 +++++++--------
.../use_execution_results.tsx | 11 +---
.../rule_actions_overflow/index.test.tsx | 19 ------
.../rules/rule_actions_overflow/index.tsx | 63 ++++++++-----------
.../logic/bulk_actions/validations.ts | 5 --
.../config/ess/config.base.ts | 1 -
.../configs/serverless.config.ts | 5 +-
.../configs/serverless.config.ts | 1 -
.../manual_rule_run.ts | 4 +-
.../configs/serverless.config.ts | 3 -
.../test/security_solution_cypress/config.ts | 5 +-
.../detection_engine/rule_edit/preview.cy.ts | 4 +-
.../rule_gaps/bulk_manual_rule_run.cy.ts | 4 +-
.../rule_gaps/manual_rule_run.cy.ts | 4 +-
.../rule_details/backfill_group.cy.ts | 9 +--
.../rule_details/execution_log.cy.ts | 7 ---
.../serverless_config.ts | 5 +-
24 files changed, 104 insertions(+), 215 deletions(-)
diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts
index 67b9a57af1628..1ae20af759611 100644
--- a/x-pack/plugins/security_solution/common/experimental_features.ts
+++ b/x-pack/plugins/security_solution/common/experimental_features.ts
@@ -230,11 +230,6 @@ export const allowedExperimentalValues = Object.freeze({
*/
valueListItemsModalEnabled: true,
- /**
- * Enables the manual rule run
- */
- manualRuleRunEnabled: false,
-
/**
* Adds a new option to filter descendants of a process for Management / Event Filters
*/
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/__snapshots__/execution_log_search_bar.test.tsx.snap b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/__snapshots__/execution_log_search_bar.test.tsx.snap
index 009e6dcc58ace..4f5e9954cced0 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/__snapshots__/execution_log_search_bar.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/__snapshots__/execution_log_search_bar.test.tsx.snap
@@ -13,6 +13,20 @@ exports[`ExecutionLogSearchBar snapshots renders correctly against snapshot 1`]
+
+
+
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/execution_log_search_bar.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/execution_log_search_bar.tsx
index db43b104ec713..3c70fa7c33c9c 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/execution_log_search_bar.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/execution_log_search_bar.tsx
@@ -20,7 +20,6 @@ import {
} from '../../../../../../common/detection_engine/rule_management/execution_log';
import { ExecutionStatusFilter, ExecutionRunTypeFilter } from '../../../../rule_monitoring';
-import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import * as i18n from './translations';
export const EXECUTION_LOG_SCHEMA_MAPPING = {
@@ -75,7 +74,6 @@ export const ExecutionLogSearchBar = React.memo(
},
[onSearch]
);
- const isManualRuleRunEnabled = useIsExperimentalFeatureEnabled('manualRuleRunEnabled');
return (
@@ -93,15 +91,14 @@ export const ExecutionLogSearchBar = React.memo(
- {isManualRuleRunEnabled && (
-
-
-
- )}
+
+
+
+
= ({
timelines,
telemetry,
} = useKibana().services;
- const isManualRuleRunEnabled = useIsExperimentalFeatureEnabled('manualRuleRunEnabled');
const {
[RuleDetailTabs.executionResults]: {
@@ -473,15 +470,10 @@ const ExecutionLogTableComponent: React.FC = ({
);
const executionLogColumns = useMemo(() => {
- const columns = [...EXECUTION_LOG_COLUMNS].filter((item) => {
- if ('field' in item) {
- return item.field === 'type' ? isManualRuleRunEnabled : true;
- }
- return true;
- });
+ const columns = [...EXECUTION_LOG_COLUMNS];
let messageColumnWidth = 50;
- if (showSourceEventTimeRange && isManualRuleRunEnabled) {
+ if (showSourceEventTimeRange) {
columns.push(...getSourceEventTimeRangeColumns());
messageColumnWidth = 30;
}
@@ -506,7 +498,6 @@ const ExecutionLogTableComponent: React.FC = ({
return columns;
}, [
- isManualRuleRunEnabled,
actions,
docLinks,
showMetricColumns,
@@ -583,14 +574,12 @@ const ExecutionLogTableComponent: React.FC = ({
updatedAt: dataUpdatedAt,
})}
- {isManualRuleRunEnabled && (
-
- )}
+
{i18n.MANUAL_RULE_RUN_MODAL_TITLE}
-
+
}
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/components/rule_backfills_info/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/components/rule_backfills_info/index.tsx
index 2bacc44b15a76..2a0981e2f5259 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/components/rule_backfills_info/index.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_gaps/components/rule_backfills_info/index.tsx
@@ -25,9 +25,8 @@ import { hasUserCRUDPermission } from '../../../../common/utils/privileges';
import { useUserData } from '../../../../detections/components/user_info';
import { getBackfillRowsFromResponse } from './utils';
import { HeaderSection } from '../../../../common/components/header_section';
-import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { TableHeaderTooltipCell } from '../../../rule_management_ui/components/rules_table/table_header_tooltip_cell';
-import { TECHNICAL_PREVIEW, TECHNICAL_PREVIEW_TOOLTIP } from '../../../../common/translations';
+import { BETA, BETA_TOOLTIP } from '../../../../common/translations';
import { useKibana } from '../../../../common/lib/kibana';
const DEFAULT_PAGE_SIZE = 10;
@@ -143,26 +142,16 @@ const getBackfillsTableColumns = (hasCRUDPermissions: boolean) => {
};
export const RuleBackfillsInfo = React.memo<{ ruleId: string }>(({ ruleId }) => {
- const isManualRuleRunEnabled = useIsExperimentalFeatureEnabled('manualRuleRunEnabled');
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
const [{ canUserCRUD }] = useUserData();
const hasCRUDPermissions = hasUserCRUDPermission(canUserCRUD);
const { timelines } = useKibana().services;
- const { data, isLoading, isError, refetch, dataUpdatedAt } = useFindBackfillsForRules(
- {
- ruleIds: [ruleId],
- page: pageIndex + 1,
- perPage: pageSize,
- },
- {
- enabled: isManualRuleRunEnabled,
- }
- );
-
- if (!isManualRuleRunEnabled) {
- return null;
- }
+ const { data, isLoading, isError, refetch, dataUpdatedAt } = useFindBackfillsForRules({
+ ruleIds: [ruleId],
+ page: pageIndex + 1,
+ perPage: pageSize,
+ });
const backfills: BackfillRow[] = getBackfillRowsFromResponse(data?.data ?? []);
@@ -197,7 +186,7 @@ export const RuleBackfillsInfo = React.memo<{ ruleId: string }>(({ ruleId }) =>
title={i18n.BACKFILL_TABLE_TITLE}
subtitle={i18n.BACKFILL_TABLE_SUBTITLE}
/>
-
+
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx
index c2c176563ca48..68e58b4db073f 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx
@@ -16,7 +16,6 @@ import { MAX_MANUAL_RULE_RUN_BULK_SIZE } from '../../../../../../common/constant
import type { TimeRange } from '../../../../rule_gaps/types';
import { useKibana } from '../../../../../common/lib/kibana';
import { convertRulesFilterToKQL } from '../../../../../../common/detection_engine/rule_management/rule_filtering';
-import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { DuplicateOptions } from '../../../../../../common/detection_engine/rule_management/constants';
import type {
BulkActionEditPayload,
@@ -89,7 +88,6 @@ export const useBulkActions = ({
actions: { clearRulesSelection, setIsPreflightInProgress },
} = rulesTableContext;
- const isManualRuleRunEnabled = useIsExperimentalFeatureEnabled('manualRuleRunEnabled');
const getBulkItemsPopoverContent = useCallback(
(closePopover: () => void): EuiContextMenuPanelDescriptor[] => {
const selectedRules = rules.filter(({ id }) => selectedRuleIds.includes(id));
@@ -448,18 +446,14 @@ export const useBulkActions = ({
onClick: handleExportAction,
icon: undefined,
},
- ...(isManualRuleRunEnabled
- ? [
- {
- key: i18n.BULK_ACTION_MANUAL_RULE_RUN,
- name: i18n.BULK_ACTION_MANUAL_RULE_RUN,
- 'data-test-subj': 'scheduleRuleRunBulk',
- disabled: containsLoading || (!containsEnabled && !isAllSelected),
- onClick: handleScheduleRuleRunAction,
- icon: undefined,
- },
- ]
- : []),
+ {
+ key: i18n.BULK_ACTION_MANUAL_RULE_RUN,
+ name: i18n.BULK_ACTION_MANUAL_RULE_RUN,
+ 'data-test-subj': 'scheduleRuleRunBulk',
+ disabled: containsLoading || (!containsEnabled && !isAllSelected),
+ onClick: handleScheduleRuleRunAction,
+ icon: undefined,
+ },
{
key: i18n.BULK_ACTION_DISABLE,
name: i18n.BULK_ACTION_DISABLE,
@@ -600,7 +594,6 @@ export const useBulkActions = ({
filterOptions,
completeBulkEditForm,
startServices,
- isManualRuleRunEnabled,
]
);
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rules_table_actions.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rules_table_actions.tsx
index 984df06342a1a..4cc7a03426657 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rules_table_actions.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rules_table_actions.tsx
@@ -8,7 +8,6 @@
import type { DefaultItemAction } from '@elastic/eui';
import { EuiToolTip } from '@elastic/eui';
import React from 'react';
-import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { DuplicateOptions } from '../../../../../common/detection_engine/rule_management/constants';
import { BulkActionTypeEnum } from '../../../../../common/api/detection_engine/rule_management';
import { SINGLE_RULE_ACTIONS } from '../../../../common/lib/apm/user_actions';
@@ -47,8 +46,6 @@ export const useRulesTableActions = ({
const downloadExportedRules = useDownloadExportedRules();
const { scheduleRuleRun } = useScheduleRuleRun();
- const isManualRuleRunEnabled = useIsExperimentalFeatureEnabled('manualRuleRunEnabled');
-
return [
{
type: 'icon',
@@ -120,33 +117,28 @@ export const useRulesTableActions = ({
},
enabled: (rule: Rule) => !rule.immutable,
},
- ...(isManualRuleRunEnabled
- ? [
- {
- type: 'icon',
- 'data-test-subj': 'manualRuleRunAction',
- description: (rule) =>
- !rule.enabled ? i18n.MANUAL_RULE_RUN_TOOLTIP : i18n.MANUAL_RULE_RUN,
- icon: 'play',
- name: i18n.MANUAL_RULE_RUN,
- onClick: async (rule: Rule) => {
- startTransaction({ name: SINGLE_RULE_ACTIONS.MANUAL_RULE_RUN });
- const modalManualRuleRunConfirmationResult = await showManualRuleRunConfirmation();
- telemetry.reportManualRuleRunOpenModal({
- type: 'single',
- });
- if (modalManualRuleRunConfirmationResult === null) {
- return;
- }
- await scheduleRuleRun({
- ruleIds: [rule.id],
- timeRange: modalManualRuleRunConfirmationResult,
- });
- },
- enabled: (rule: Rule) => rule.enabled,
- } as DefaultItemAction,
- ]
- : []),
+ {
+ type: 'icon',
+ 'data-test-subj': 'manualRuleRunAction',
+ description: (rule) => (!rule.enabled ? i18n.MANUAL_RULE_RUN_TOOLTIP : i18n.MANUAL_RULE_RUN),
+ icon: 'play',
+ name: i18n.MANUAL_RULE_RUN,
+ onClick: async (rule: Rule) => {
+ startTransaction({ name: SINGLE_RULE_ACTIONS.MANUAL_RULE_RUN });
+ const modalManualRuleRunConfirmationResult = await showManualRuleRunConfirmation();
+ telemetry.reportManualRuleRunOpenModal({
+ type: 'single',
+ });
+ if (modalManualRuleRunConfirmationResult === null) {
+ return;
+ }
+ await scheduleRuleRun({
+ ruleIds: [rule.id],
+ timeRange: modalManualRuleRunConfirmationResult,
+ });
+ },
+ enabled: (rule: Rule) => rule.enabled,
+ },
{
type: 'icon',
'data-test-subj': 'deleteRuleAction',
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_results_table/use_execution_results.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_results_table/use_execution_results.tsx
index 8660139676351..e6ee5769ee822 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_results_table/use_execution_results.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_results_table/use_execution_results.tsx
@@ -7,29 +7,20 @@
import { useQuery } from '@tanstack/react-query';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
-
-import { RuleRunTypeEnum } from '../../../../../common/api/detection_engine/rule_monitoring';
import type { GetRuleExecutionResultsResponse } from '../../../../../common/api/detection_engine/rule_monitoring';
import type { FetchRuleExecutionResultsArgs } from '../../api';
import { api } from '../../api';
-import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import * as i18n from './translations';
export type UseExecutionResultsArgs = Omit;
export const useExecutionResults = (args: UseExecutionResultsArgs) => {
const { addError } = useAppToasts();
- const isManualRuleRunEnabled = useIsExperimentalFeatureEnabled('manualRuleRunEnabled');
return useQuery(
['detectionEngine', 'ruleMonitoring', 'executionResults', args],
({ signal }) => {
- let runTypeFilters = args.runTypeFilters;
-
- // if manual rule run is disabled, only show standard runs
- if (!isManualRuleRunEnabled) {
- runTypeFilters = [RuleRunTypeEnum.standard];
- }
+ const runTypeFilters = args.runTypeFilters;
return api.fetchRuleExecutionResults({ ...args, runTypeFilters, signal });
},
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx
index 298ae1c503533..e1ff950bc5e32 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx
@@ -274,25 +274,6 @@ describe('RuleActionsOverflow', () => {
expect(getByTestId('rules-details-popover')).not.toHaveTextContent(/.+/);
});
- test('it does not show "Manual run" action item when feature flag "manualRuleRunEnabled" is set to false', () => {
- useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
-
- const { getByTestId } = render(
- Promise.resolve(true)}
- />,
- { wrapper: TestProviders }
- );
- fireEvent.click(getByTestId('rules-details-popover-button-icon'));
-
- expect(getByTestId('rules-details-menu-panel')).not.toHaveTextContent('Manual run');
- });
-
test('it calls telemetry.reportManualRuleRunOpenModal when rules-details-manual-rule-run is clicked', async () => {
const { getByTestId } = render(
{
navigateToApp(APP_UI_ID, {
deepLinkId: SecurityPageName.rules,
@@ -152,39 +149,32 @@ const RuleActionsOverflowComponent = ({
>
{i18nActions.EXPORT_RULE}
,
- ...(isManualRuleRunEnabled
- ? [
- {
- startTransaction({ name: SINGLE_RULE_ACTIONS.MANUAL_RULE_RUN });
- closePopover();
- const modalManualRuleRunConfirmationResult =
- await showManualRuleRunConfirmation();
- telemetry.reportManualRuleRunOpenModal({
- type: 'single',
- });
- if (modalManualRuleRunConfirmationResult === null) {
- return;
- }
- await scheduleRuleRun({
- ruleIds: [rule.id],
- timeRange: modalManualRuleRunConfirmationResult,
- });
- }}
- >
- {i18nActions.MANUAL_RULE_RUN}
- ,
- ]
- : []),
+ {
+ startTransaction({ name: SINGLE_RULE_ACTIONS.MANUAL_RULE_RUN });
+ closePopover();
+ const modalManualRuleRunConfirmationResult = await showManualRuleRunConfirmation();
+ telemetry.reportManualRuleRunOpenModal({
+ type: 'single',
+ });
+ if (modalManualRuleRunConfirmationResult === null) {
+ return;
+ }
+ await scheduleRuleRun({
+ ruleIds: [rule.id],
+ timeRange: modalManualRuleRunConfirmationResult,
+ });
+ }}
+ >
+ {i18nActions.MANUAL_RULE_RUN}
+ ,
{
// check whether "manual rule run" feature is enabled
- await throwDryRunError(
- () =>
- invariant(experimentalFeatures?.manualRuleRunEnabled, 'Manual rule run feature is disabled.'),
- BulkActionsDryRunErrCode.MANUAL_RULE_RUN_FEATURE
- );
await throwDryRunError(
() => invariant(rule.enabled, 'Cannot schedule manual rule run for a disabled rule'),
diff --git a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts
index 705c0b8686dd0..3ab6d5059fd07 100644
--- a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts
+++ b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts
@@ -85,7 +85,6 @@ export function createTestConfig(options: CreateTestConfigOptions, testFiles?: s
'loggingRequestsEnabled',
'riskScoringPersistence',
'riskScoringRoutesEnabled',
- 'manualRuleRunEnabled',
])}`,
'--xpack.task_manager.poll_interval=1000',
`--xpack.actions.preconfigured=${JSON.stringify(PRECONFIGURED_ACTION_CONNECTORS)}`,
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts
index 8f64a859b7002..ce949d5cc23fc 100644
--- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts
@@ -17,9 +17,6 @@ export default createTestConfig({
'testing_ignored.constant',
'/testing_regex*/',
])}`, // See tests within the file "ignore_fields.ts" which use these values in "alertIgnoreFields"
- `--xpack.securitySolution.enableExperimental=${JSON.stringify([
- 'manualRuleRunEnabled',
- 'loggingRequestsEnabled',
- ])}`,
+ `--xpack.securitySolution.enableExperimental=${JSON.stringify(['loggingRequestsEnabled'])}`,
],
});
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_gaps/trial_license_complete_tier/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_gaps/trial_license_complete_tier/configs/serverless.config.ts
index 783adb64f6c2e..43904f7c217f3 100644
--- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_gaps/trial_license_complete_tier/configs/serverless.config.ts
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_gaps/trial_license_complete_tier/configs/serverless.config.ts
@@ -16,6 +16,5 @@ export default createTestConfig({
'testing_ignored.constant',
'/testing_regex*/',
])}`, // See tests within the file "ignore_fields.ts" which use these values in "alertIgnoreFields"
- `--xpack.securitySolution.enableExperimental=${JSON.stringify(['manualRuleRunEnabled'])}`,
],
});
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_gaps/trial_license_complete_tier/manual_rule_run.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_gaps/trial_license_complete_tier/manual_rule_run.ts
index 8a6167fc69301..153185456544d 100644
--- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_gaps/trial_license_complete_tier/manual_rule_run.ts
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_gaps/trial_license_complete_tier/manual_rule_run.ts
@@ -42,9 +42,7 @@ export default ({ getService }: FtrProviderContext) => {
const log = getService('log');
const es = getService('es');
- // Currently FF are not supported on MKI environments, so this test should be skipped from MKI environments.
- // Once `manualRuleRunEnabled` FF is removed, we can remove `@skipInServerlessMKI` as well
- describe('@ess @serverless @skipInServerlessMKI manual_rule_run', () => {
+ describe('@ess @serverless manual_rule_run', () => {
beforeEach(async () => {
await createAlertsIndex(supertest, log);
});
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_management/trial_license_complete_tier/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_management/trial_license_complete_tier/configs/serverless.config.ts
index 52a1074c87904..ca9396db04661 100644
--- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_management/trial_license_complete_tier/configs/serverless.config.ts
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_management/trial_license_complete_tier/configs/serverless.config.ts
@@ -12,7 +12,4 @@ export default createTestConfig({
reportName:
'Rules Management - Rule Management Integration Tests - Serverless Env - Complete Tier',
},
- kbnTestServerArgs: [
- `--xpack.securitySolution.enableExperimental=${JSON.stringify(['manualRuleRunEnabled'])}`,
- ],
});
diff --git a/x-pack/test/security_solution_cypress/config.ts b/x-pack/test/security_solution_cypress/config.ts
index 88752eb1b5f93..05bc2e381527a 100644
--- a/x-pack/test/security_solution_cypress/config.ts
+++ b/x-pack/test/security_solution_cypress/config.ts
@@ -44,10 +44,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
// See https://github.com/elastic/kibana/pull/125396 for details
'--xpack.alerting.rules.minimumScheduleInterval.value=1s',
'--xpack.ruleRegistry.unsafe.legacyMultiTenancy.enabled=true',
- `--xpack.securitySolution.enableExperimental=${JSON.stringify([
- 'manualRuleRunEnabled',
- 'loggingRequestsEnabled',
- ])}`,
+ `--xpack.securitySolution.enableExperimental=${JSON.stringify(['loggingRequestsEnabled'])}`,
// mock cloud to enable the guided onboarding tour in e2e tests
'--xpack.cloud.id=test',
`--home.disableWelcomeScreen=true`,
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/preview.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/preview.cy.ts
index ce298bafbfea0..c2e41c9d4680c 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/preview.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/preview.cy.ts
@@ -32,9 +32,7 @@ const expectedValidEsqlQuery = 'from auditbeat* METADATA _id';
describe(
'Detection rules, preview',
{
- // Currently FF are not supported on MKI environments, so this test should be skipped from MKI environments.
- // Once `manualRuleRunEnabled` FF is removed, we can remove `@skipInServerlessMKI` as well
- tags: ['@ess', '@serverless', '@skipInServerlessMKI'],
+ tags: ['@ess', '@serverless'],
env: {
kbnServerArgs: [
`--xpack.securitySolution.enableExperimental=${JSON.stringify(['loggingRequestsEnabled'])}`,
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_gaps/bulk_manual_rule_run.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_gaps/bulk_manual_rule_run.cy.ts
index 17cde9485a13c..5a66dcdc0de84 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_gaps/bulk_manual_rule_run.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_gaps/bulk_manual_rule_run.cy.ts
@@ -19,9 +19,7 @@ import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
import { createRule } from '../../../../tasks/api_calls/rules';
import { login } from '../../../../tasks/login';
-// Currently FF are not supported on MKI environments, so this test should be skipped from MKI environments.
-// Once `manualRuleRunEnabled` FF is removed, we can remove `@skipInServerlessMKI` as well
-describe('Manual rule run', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => {
+describe('Manual rule run', { tags: ['@ess', '@serverless'] }, () => {
beforeEach(() => {
login();
deleteAlertsAndRules();
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_gaps/manual_rule_run.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_gaps/manual_rule_run.cy.ts
index 29e2379367c0b..f40f4284b84b5 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_gaps/manual_rule_run.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_gaps/manual_rule_run.cy.ts
@@ -18,9 +18,7 @@ import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
import { createRule } from '../../../../tasks/api_calls/rules';
import { login } from '../../../../tasks/login';
-// Currently FF are not supported on MKI environments, so this test should be skipped from MKI environments.
-// Once `manualRuleRunEnabled` FF is removed, we can remove `@skipInServerlessMKI` as well
-describe('Manual rule run', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => {
+describe('Manual rule run', { tags: ['@ess', '@serverless'] }, () => {
beforeEach(() => {
login();
deleteAlertsAndRules();
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/backfill_group.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/backfill_group.cy.ts
index 2f97e2f3c0721..f747e6be43e5a 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/backfill_group.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/backfill_group.cy.ts
@@ -34,14 +34,7 @@ import {
describe(
'Backfill groups',
{
- tags: ['@ess', '@serverless', '@skipInServerlessMKI'],
- env: {
- ftrConfig: {
- kbnServerArgs: [
- `--xpack.securitySolution.enableExperimental=${JSON.stringify(['manualRuleRunEnabled'])}`,
- ],
- },
- },
+ tags: ['@ess', '@serverless'],
},
function () {
before(() => {
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/execution_log.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/execution_log.cy.ts
index a34826d2c8cb4..dc9e3e5719d27 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/execution_log.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/execution_log.cy.ts
@@ -27,13 +27,6 @@ describe.skip(
'Event log',
{
tags: ['@ess', '@serverless'],
- env: {
- ftrConfig: {
- kbnServerArgs: [
- `--xpack.securitySolution.enableExperimental=${JSON.stringify(['manualRuleRunEnabled'])}`,
- ],
- },
- },
},
function () {
before(() => {
diff --git a/x-pack/test/security_solution_cypress/serverless_config.ts b/x-pack/test/security_solution_cypress/serverless_config.ts
index 13877fcbf5af4..71a63b697187f 100644
--- a/x-pack/test/security_solution_cypress/serverless_config.ts
+++ b/x-pack/test/security_solution_cypress/serverless_config.ts
@@ -34,10 +34,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
{ product_line: 'endpoint', product_tier: 'complete' },
{ product_line: 'cloud', product_tier: 'complete' },
])}`,
- `--xpack.securitySolution.enableExperimental=${JSON.stringify([
- 'manualRuleRunEnabled',
- 'loggingRequestsEnabled',
- ])}`,
+ `--xpack.securitySolution.enableExperimental=${JSON.stringify(['loggingRequestsEnabled'])}`,
'--csp.strict=false',
'--csp.warnLegacyBrowsers=false',
],
From 8e986a6dd945160d80d9b400f4acb7f9181d962a Mon Sep 17 00:00:00 2001
From: Justin Kambic
Date: Wed, 9 Oct 2024 13:27:49 -0400
Subject: [PATCH 11/87] [Synthetics] Add warning if TLS config not set for
Synthetics (#195395)
## Summary
Recently while debugging a production issue where the Synthetics plugin
was receiving 401 errors while trying to reach the Synthetics Service
health endpoint, we isolated that there was an issue with the mTLS
handshake between Kibana and the service.
Unfortunately, we were unsure if there was some missing custom config
(especially relevant in Serverless Kibana), or if the certificate values
were not present in the first place.
Adding this warning will help us make this determination better in the
future when reviewing Kibana logs, as we will be assured if the config
is not defined via this warning.
---
.../server/synthetics_service/service_api_client.ts | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/service_api_client.ts b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/service_api_client.ts
index 3d334f32e9407..73f286e40d310 100644
--- a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/service_api_client.ts
+++ b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/service_api_client.ts
@@ -143,6 +143,10 @@ export class ServiceAPIClient {
cert: tlsConfig.certificate,
key: tlsConfig.key,
});
+ } else if (!this.server.isDev) {
+ this.logger.warn(
+ 'TLS certificate and key are not provided. Falling back to default HTTPS agent.'
+ );
}
return baseHttpsAgent;
From 1bf3f2a0b0484d601f797a20ec7c81ae05c522bb Mon Sep 17 00:00:00 2001
From: Steph Milovic
Date: Wed, 9 Oct 2024 11:35:45 -0600
Subject: [PATCH 12/87] [Security Assistant] Knowledge base conflict fix
(#195659)
## Summary
Fixes on merge fail
https://buildkite.com/elastic/kibana-on-merge/builds/51840
---
.../use_knowledge_base_status.tsx | 17 +++++++++++++++++
.../index.tsx | 4 ++--
2 files changed, 19 insertions(+), 2 deletions(-)
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_knowledge_base_status.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_knowledge_base_status.tsx
index ba6317329d350..75e78f2a06948 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_knowledge_base_status.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_knowledge_base_status.tsx
@@ -78,3 +78,20 @@ export const useInvalidateKnowledgeBaseStatus = () => {
});
}, [queryClient]);
};
+
+/**
+ * Helper for determining if Knowledge Base setup is complete.
+ *
+ * Note: Consider moving to API
+ *
+ * @param kbStatus ReadKnowledgeBaseResponse
+ */
+export const isKnowledgeBaseSetup = (kbStatus: ReadKnowledgeBaseResponse | undefined): boolean => {
+ return (
+ (kbStatus?.elser_exists &&
+ kbStatus?.security_labs_exists &&
+ kbStatus?.index_exists &&
+ kbStatus?.pipeline_exists) ??
+ false
+ );
+};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx
index 34e8601e37ce7..5cf887ae3375d 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx
@@ -46,7 +46,7 @@ import { useFlyoutModalVisibility } from '../../assistant/common/components/assi
import { IndexEntryEditor } from './index_entry_editor';
import { DocumentEntryEditor } from './document_entry_editor';
import { KnowledgeBaseSettings } from '../knowledge_base_settings';
-import { ESQL_RESOURCE, SetupKnowledgeBaseButton } from '../setup_knowledge_base_button';
+import { SetupKnowledgeBaseButton } from '../setup_knowledge_base_button';
import { useDeleteKnowledgeBaseEntries } from '../../assistant/api/knowledge_base/entries/use_delete_knowledge_base_entries';
import {
isSystemEntry,
@@ -73,7 +73,7 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(({ d
toasts,
} = useAssistantContext();
const [hasPendingChanges, setHasPendingChanges] = useState(false);
- const { data: kbStatus, isFetched } = useKnowledgeBaseStatus({ http, resource: ESQL_RESOURCE });
+ const { data: kbStatus, isFetched } = useKnowledgeBaseStatus({ http });
const isKbSetup = isKnowledgeBaseSetup(kbStatus);
// Only needed for legacy settings management
From ac2e0c81cf4b1b75a0fa05e6dc15022a4c3ba0fa Mon Sep 17 00:00:00 2001
From: Nick Partridge
Date: Wed, 9 Oct 2024 13:01:21 -0500
Subject: [PATCH 13/87] Update `@elastic/charts` renovate config labels
(#195642)
---
renovate.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/renovate.json b/renovate.json
index dccc37ef702a4..ff7ee4b0aaafa 100644
--- a/renovate.json
+++ b/renovate.json
@@ -58,7 +58,7 @@
"matchDepNames": ["@elastic/charts"],
"reviewers": ["team:visualizations", "markov00", "nickofthyme"],
"matchBaseBranches": ["main"],
- "labels": ["release_note:skip", "backport:skip", "Team:Visualizations"],
+ "labels": ["release_note:skip", "backport:prev-minor", "Team:Visualizations"],
"enabled": true
},
{
From d2644ffe49ea732e5f048957a51350efbc321687 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cau=C3=AA=20Marcondes?=
<55978943+cauemarcondes@users.noreply.github.com>
Date: Wed, 9 Oct 2024 19:19:49 +0100
Subject: [PATCH 14/87] [Inventory] Fixing entity links (#195625)
Regression from https://github.com/elastic/kibana/pull/195204
---
.../inventory/common/entities.ts | 9 +-
.../entity_name/entity_name.test.tsx | 152 ++++++++++++++++++
.../entities_grid/entity_name/index.tsx | 16 +-
3 files changed, 167 insertions(+), 10 deletions(-)
create mode 100644 x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx
diff --git a/x-pack/plugins/observability_solution/inventory/common/entities.ts b/x-pack/plugins/observability_solution/inventory/common/entities.ts
index 218e3d50905a9..40fae48cb9dc3 100644
--- a/x-pack/plugins/observability_solution/inventory/common/entities.ts
+++ b/x-pack/plugins/observability_solution/inventory/common/entities.ts
@@ -78,26 +78,27 @@ interface BaseEntity {
[ENTITY_TYPE]: EntityType;
[ENTITY_DISPLAY_NAME]: string;
[ENTITY_DEFINITION_ID]: string;
- [ENTITY_IDENTITY_FIELDS]: string[];
+ [ENTITY_IDENTITY_FIELDS]: string | string[];
+ [key: string]: any;
}
/**
* These types are based on service, host and container from the built in definition.
*/
-interface ServiceEntity extends BaseEntity {
+export interface ServiceEntity extends BaseEntity {
[ENTITY_TYPE]: 'service';
[SERVICE_NAME]: string;
[SERVICE_ENVIRONMENT]?: string | string[] | null;
[AGENT_NAME]: string | string[] | null;
}
-interface HostEntity extends BaseEntity {
+export interface HostEntity extends BaseEntity {
[ENTITY_TYPE]: 'host';
[HOST_NAME]: string;
[CLOUD_PROVIDER]: string | string[] | null;
}
-interface ContainerEntity extends BaseEntity {
+export interface ContainerEntity extends BaseEntity {
[ENTITY_TYPE]: 'container';
[CONTAINER_ID]: string;
[CLOUD_PROVIDER]: string | string[] | null;
diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx
new file mode 100644
index 0000000000000..36aad3d8e3a97
--- /dev/null
+++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx
@@ -0,0 +1,152 @@
+/*
+ * 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 KibanaReactContextValue } from '@kbn/kibana-react-plugin/public';
+import * as useKibana from '../../../hooks/use_kibana';
+import { EntityName } from '.';
+import { ContainerEntity, HostEntity, ServiceEntity } from '../../../../common/entities';
+import { render, screen } from '@testing-library/react';
+import React from 'react';
+import { ASSET_DETAILS_LOCATOR_ID } from '@kbn/observability-shared-plugin/common/locators/infra/asset_details_locator';
+
+describe('EntityName', () => {
+ jest.spyOn(useKibana, 'useKibana').mockReturnValue({
+ services: {
+ share: {
+ url: {
+ locators: {
+ get: (locatorId: string) => {
+ return {
+ getRedirectUrl: (params: { [key: string]: any }) => {
+ if (locatorId === ASSET_DETAILS_LOCATOR_ID) {
+ return `assets_url/${params.assetType}/${params.assetId}`;
+ }
+ return `services_url/${params.serviceName}?environment=${params.environment}`;
+ },
+ };
+ },
+ },
+ },
+ },
+ },
+ } as unknown as KibanaReactContextValue);
+
+ afterAll(() => {
+ jest.clearAllMocks();
+ });
+
+ it('returns host link', () => {
+ const entity: HostEntity = {
+ 'entity.lastSeenTimestamp': 'foo',
+ 'entity.id': '1',
+ 'entity.type': 'host',
+ 'entity.displayName': 'foo',
+ 'entity.identityFields': 'host.name',
+ 'host.name': 'foo',
+ 'entity.definitionId': 'host',
+ 'cloud.provider': null,
+ };
+ render( );
+ expect(screen.queryByTestId('entityNameLink')?.getAttribute('href')).toEqual(
+ 'assets_url/host/foo'
+ );
+ expect(screen.queryByTestId('entityNameDisplayName')?.textContent).toEqual('foo');
+ });
+
+ it('returns container link', () => {
+ const entity: ContainerEntity = {
+ 'entity.lastSeenTimestamp': 'foo',
+ 'entity.id': '1',
+ 'entity.type': 'container',
+ 'entity.displayName': 'foo',
+ 'entity.identityFields': 'container.id',
+ 'container.id': 'foo',
+ 'entity.definitionId': 'container',
+ 'cloud.provider': null,
+ };
+ render( );
+ expect(screen.queryByTestId('entityNameLink')?.getAttribute('href')).toEqual(
+ 'assets_url/container/foo'
+ );
+ expect(screen.queryByTestId('entityNameDisplayName')?.textContent).toEqual('foo');
+ });
+
+ it('returns service link without environment', () => {
+ const entity: ServiceEntity = {
+ 'entity.lastSeenTimestamp': 'foo',
+ 'entity.id': '1',
+ 'entity.type': 'service',
+ 'entity.displayName': 'foo',
+ 'entity.identityFields': 'service.name',
+ 'service.name': 'foo',
+ 'entity.definitionId': 'service',
+ 'agent.name': 'bar',
+ };
+ render( );
+ expect(screen.queryByTestId('entityNameLink')?.getAttribute('href')).toEqual(
+ 'services_url/foo?environment=undefined'
+ );
+ expect(screen.queryByTestId('entityNameDisplayName')?.textContent).toEqual('foo');
+ });
+
+ it('returns service link with environment', () => {
+ const entity: ServiceEntity = {
+ 'entity.lastSeenTimestamp': 'foo',
+ 'entity.id': '1',
+ 'entity.type': 'service',
+ 'entity.displayName': 'foo',
+ 'entity.identityFields': 'service.name',
+ 'service.name': 'foo',
+ 'entity.definitionId': 'service',
+ 'agent.name': 'bar',
+ 'service.environment': 'baz',
+ };
+ render( );
+ expect(screen.queryByTestId('entityNameLink')?.getAttribute('href')).toEqual(
+ 'services_url/foo?environment=baz'
+ );
+ expect(screen.queryByTestId('entityNameDisplayName')?.textContent).toEqual('foo');
+ });
+
+ it('returns service link with first environment when it is an array', () => {
+ const entity: ServiceEntity = {
+ 'entity.lastSeenTimestamp': 'foo',
+ 'entity.id': '1',
+ 'entity.type': 'service',
+ 'entity.displayName': 'foo',
+ 'entity.identityFields': 'service.name',
+ 'service.name': 'foo',
+ 'entity.definitionId': 'service',
+ 'agent.name': 'bar',
+ 'service.environment': ['baz', 'bar', 'foo'],
+ };
+ render( );
+ expect(screen.queryByTestId('entityNameLink')?.getAttribute('href')).toEqual(
+ 'services_url/foo?environment=baz'
+ );
+ expect(screen.queryByTestId('entityNameDisplayName')?.textContent).toEqual('foo');
+ });
+
+ it('returns service link identity fields is an array', () => {
+ const entity: ServiceEntity = {
+ 'entity.lastSeenTimestamp': 'foo',
+ 'entity.id': '1',
+ 'entity.type': 'service',
+ 'entity.displayName': 'foo',
+ 'entity.identityFields': ['service.name', 'service.environment'],
+ 'service.name': 'foo',
+ 'entity.definitionId': 'service',
+ 'agent.name': 'bar',
+ 'service.environment': 'baz',
+ };
+ render( );
+ expect(screen.queryByTestId('entityNameLink')?.getAttribute('href')).toEqual(
+ 'services_url/foo?environment=baz'
+ );
+ expect(screen.queryByTestId('entityNameDisplayName')?.textContent).toEqual('foo');
+ });
+});
diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx
index debe91d52dec1..f3488dfddbc4e 100644
--- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx
+++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx
@@ -36,33 +36,37 @@ export function EntityName({ entity }: EntityNameProps) {
const getEntityRedirectUrl = useCallback(() => {
const type = entity[ENTITY_TYPE];
// For service, host and container type there is only one identity field
- const identityField = entity[ENTITY_IDENTITY_FIELDS][0];
+ const identityField = Array.isArray(entity[ENTITY_IDENTITY_FIELDS])
+ ? entity[ENTITY_IDENTITY_FIELDS][0]
+ : entity[ENTITY_IDENTITY_FIELDS];
+ const identityValue = entity[identityField];
- // Any unrecognised types will always return undefined
switch (type) {
case 'host':
case 'container':
return assetDetailsLocator?.getRedirectUrl({
- assetId: identityField,
+ assetId: identityValue,
assetType: type,
});
case 'service':
return serviceOverviewLocator?.getRedirectUrl({
- serviceName: identityField,
+ serviceName: identityValue,
environment: [entity[SERVICE_ENVIRONMENT] || undefined].flat()[0],
});
}
}, [entity, assetDetailsLocator, serviceOverviewLocator]);
return (
-
+
- {entity[ENTITY_DISPLAY_NAME]}
+
+ {entity[ENTITY_DISPLAY_NAME]}
+
From a31b16e411974d0ecd29e5eb7b546bef6ae4502e Mon Sep 17 00:00:00 2001
From: Brad White
Date: Wed, 9 Oct 2024 12:40:00 -0600
Subject: [PATCH 15/87] Revert "[Logs Overview] Overview component (iteration
1) (#191899)"
This reverts commit 15bccdf233d847f34ee4cbcc30f8a8e775207c42.
---
.eslintrc.js | 1 -
.github/CODEOWNERS | 1 -
package.json | 5 -
.../src/lib/entity.ts | 12 -
.../src/lib/gaussian_events.ts | 74 -----
.../src/lib/infra/host.ts | 10 +-
.../src/lib/infra/index.ts | 3 +-
.../src/lib/interval.ts | 18 +-
.../src/lib/logs/index.ts | 21 --
.../src/lib/poisson_events.test.ts | 53 ----
.../src/lib/poisson_events.ts | 77 -----
.../src/lib/timerange.ts | 27 +-
.../distributed_unstructured_logs.ts | 197 ------------
.../scenarios/helpers/unstructured_logs.ts | 94 ------
packages/kbn-apm-synthtrace/tsconfig.json | 1 -
.../settings/setting_ids/index.ts | 1 -
.../src/worker/webpack.config.ts | 12 -
packages/kbn-xstate-utils/kibana.jsonc | 2 +-
.../kbn-xstate-utils/src/console_inspector.ts | 88 ------
packages/kbn-xstate-utils/src/index.ts | 1 -
.../server/collectors/management/schema.ts | 6 -
.../server/collectors/management/types.ts | 1 -
src/plugins/telemetry/schema/oss_plugins.json | 6 -
tsconfig.base.json | 2 -
x-pack/.i18nrc.json | 3 -
.../observability/logs_overview/README.md | 3 -
.../observability/logs_overview/index.ts | 21 --
.../logs_overview/jest.config.js | 12 -
.../observability/logs_overview/kibana.jsonc | 5 -
.../observability/logs_overview/package.json | 7 -
.../discover_link/discover_link.tsx | 110 -------
.../src/components/discover_link/index.ts | 8 -
.../src/components/log_categories/index.ts | 8 -
.../log_categories/log_categories.tsx | 94 ------
.../log_categories_control_bar.tsx | 44 ---
.../log_categories_error_content.tsx | 44 ---
.../log_categories/log_categories_grid.tsx | 182 -----------
.../log_categories_grid_cell.tsx | 99 ------
.../log_categories_grid_change_time_cell.tsx | 54 ----
.../log_categories_grid_change_type_cell.tsx | 108 -------
.../log_categories_grid_count_cell.tsx | 32 --
.../log_categories_grid_histogram_cell.tsx | 99 ------
.../log_categories_grid_pattern_cell.tsx | 60 ----
.../log_categories_loading_content.tsx | 68 -----
.../log_categories_result_content.tsx | 87 ------
.../src/components/logs_overview/index.ts | 10 -
.../logs_overview/logs_overview.tsx | 64 ----
.../logs_overview_error_content.tsx | 41 ---
.../logs_overview_loading_content.tsx | 23 --
.../categorize_documents.ts | 282 ------------------
.../categorize_logs_service.ts | 250 ----------------
.../count_documents.ts | 60 ----
.../services/categorize_logs_service/index.ts | 8 -
.../categorize_logs_service/queries.ts | 151 ----------
.../services/categorize_logs_service/types.ts | 21 --
.../observability/logs_overview/src/types.ts | 74 -----
.../logs_overview/src/utils/logs_source.ts | 60 ----
.../logs_overview/src/utils/xstate5_utils.ts | 13 -
.../observability/logs_overview/tsconfig.json | 39 ---
.../components/app/service_logs/index.tsx | 171 +----------
.../routing/service_detail/index.tsx | 2 +-
.../apm/public/plugin.ts | 2 -
.../components/tabs/logs/logs_tab_content.tsx | 94 ++----
.../logs_shared/kibana.jsonc | 5 +-
.../public/components/logs_overview/index.tsx | 8 -
.../logs_overview/logs_overview.mock.tsx | 32 --
.../logs_overview/logs_overview.tsx | 70 -----
.../logs_shared/public/index.ts | 1 -
.../logs_shared/public/mocks.tsx | 2 -
.../logs_shared/public/plugin.ts | 23 +-
.../logs_shared/public/types.ts | 12 +-
.../logs_shared/server/feature_flags.ts | 33 --
.../logs_shared/server/plugin.ts | 28 +-
.../logs_shared/tsconfig.json | 4 -
yarn.lock | 17 --
75 files changed, 56 insertions(+), 3405 deletions(-)
delete mode 100644 packages/kbn-apm-synthtrace-client/src/lib/gaussian_events.ts
delete mode 100644 packages/kbn-apm-synthtrace-client/src/lib/poisson_events.test.ts
delete mode 100644 packages/kbn-apm-synthtrace-client/src/lib/poisson_events.ts
delete mode 100644 packages/kbn-apm-synthtrace/src/scenarios/distributed_unstructured_logs.ts
delete mode 100644 packages/kbn-apm-synthtrace/src/scenarios/helpers/unstructured_logs.ts
delete mode 100644 packages/kbn-xstate-utils/src/console_inspector.ts
delete mode 100644 x-pack/packages/observability/logs_overview/README.md
delete mode 100644 x-pack/packages/observability/logs_overview/index.ts
delete mode 100644 x-pack/packages/observability/logs_overview/jest.config.js
delete mode 100644 x-pack/packages/observability/logs_overview/kibana.jsonc
delete mode 100644 x-pack/packages/observability/logs_overview/package.json
delete mode 100644 x-pack/packages/observability/logs_overview/src/components/discover_link/discover_link.tsx
delete mode 100644 x-pack/packages/observability/logs_overview/src/components/discover_link/index.ts
delete mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/index.ts
delete mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories.tsx
delete mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_control_bar.tsx
delete mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_error_content.tsx
delete mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid.tsx
delete mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_cell.tsx
delete mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_time_cell.tsx
delete mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_type_cell.tsx
delete mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_count_cell.tsx
delete mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_histogram_cell.tsx
delete mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_pattern_cell.tsx
delete mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_loading_content.tsx
delete mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_result_content.tsx
delete mode 100644 x-pack/packages/observability/logs_overview/src/components/logs_overview/index.ts
delete mode 100644 x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview.tsx
delete mode 100644 x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_error_content.tsx
delete mode 100644 x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_loading_content.tsx
delete mode 100644 x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_documents.ts
delete mode 100644 x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_logs_service.ts
delete mode 100644 x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/count_documents.ts
delete mode 100644 x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/index.ts
delete mode 100644 x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/queries.ts
delete mode 100644 x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/types.ts
delete mode 100644 x-pack/packages/observability/logs_overview/src/types.ts
delete mode 100644 x-pack/packages/observability/logs_overview/src/utils/logs_source.ts
delete mode 100644 x-pack/packages/observability/logs_overview/src/utils/xstate5_utils.ts
delete mode 100644 x-pack/packages/observability/logs_overview/tsconfig.json
delete mode 100644 x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/index.tsx
delete mode 100644 x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.mock.tsx
delete mode 100644 x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.tsx
delete mode 100644 x-pack/plugins/observability_solution/logs_shared/server/feature_flags.ts
diff --git a/.eslintrc.js b/.eslintrc.js
index c604844089ef4..797b84522df3f 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -978,7 +978,6 @@ module.exports = {
files: [
'x-pack/plugins/observability_solution/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)',
'src/plugins/ai_assistant_management/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)',
- 'x-pack/packages/observability/logs_overview/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)',
],
rules: {
'@kbn/i18n/strings_should_be_translated_with_i18n': 'warn',
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 974a7d39f63b3..9b3c46d065fe1 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -652,7 +652,6 @@ x-pack/packages/observability/alerting_test_data @elastic/obs-ux-management-team
x-pack/test/cases_api_integration/common/plugins/observability @elastic/response-ops
x-pack/packages/observability/get_padded_alert_time_range_util @elastic/obs-ux-management-team
x-pack/plugins/observability_solution/observability_logs_explorer @elastic/obs-ux-logs-team
-x-pack/packages/observability/logs_overview @elastic/obs-ux-logs-team
x-pack/plugins/observability_solution/observability_onboarding/e2e @elastic/obs-ux-logs-team @elastic/obs-ux-onboarding-team
x-pack/plugins/observability_solution/observability_onboarding @elastic/obs-ux-logs-team @elastic/obs-ux-onboarding-team
x-pack/plugins/observability_solution/observability @elastic/obs-ux-management-team
diff --git a/package.json b/package.json
index 58cd08773696f..57b84f1c46dcb 100644
--- a/package.json
+++ b/package.json
@@ -97,7 +97,6 @@
"@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.cd77847.0",
"@types/react": "~18.2.0",
"@types/react-dom": "~18.2.0",
- "@xstate5/react/**/xstate": "^5.18.1",
"globby/fast-glob": "^3.2.11"
},
"dependencies": {
@@ -688,7 +687,6 @@
"@kbn/observability-fixtures-plugin": "link:x-pack/test/cases_api_integration/common/plugins/observability",
"@kbn/observability-get-padded-alert-time-range-util": "link:x-pack/packages/observability/get_padded_alert_time_range_util",
"@kbn/observability-logs-explorer-plugin": "link:x-pack/plugins/observability_solution/observability_logs_explorer",
- "@kbn/observability-logs-overview": "link:x-pack/packages/observability/logs_overview",
"@kbn/observability-onboarding-plugin": "link:x-pack/plugins/observability_solution/observability_onboarding",
"@kbn/observability-plugin": "link:x-pack/plugins/observability_solution/observability",
"@kbn/observability-shared-plugin": "link:x-pack/plugins/observability_solution/observability_shared",
@@ -1052,7 +1050,6 @@
"@turf/helpers": "6.0.1",
"@turf/length": "^6.0.2",
"@xstate/react": "^3.2.2",
- "@xstate5/react": "npm:@xstate/react@^4.1.2",
"adm-zip": "^0.5.9",
"ai": "^2.2.33",
"ajv": "^8.12.0",
@@ -1286,7 +1283,6 @@
"whatwg-fetch": "^3.0.0",
"xml2js": "^0.5.0",
"xstate": "^4.38.2",
- "xstate5": "npm:xstate@^5.18.1",
"xterm": "^5.1.0",
"yauzl": "^2.10.0",
"yazl": "^2.5.1",
@@ -1308,7 +1304,6 @@
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-transform-class-properties": "^7.24.7",
- "@babel/plugin-transform-logical-assignment-operators": "^7.24.7",
"@babel/plugin-transform-numeric-separator": "^7.24.7",
"@babel/plugin-transform-runtime": "^7.24.7",
"@babel/preset-env": "^7.24.7",
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/entity.ts b/packages/kbn-apm-synthtrace-client/src/lib/entity.ts
index b26dbfc7ffb46..4d522ef07ff0e 100644
--- a/packages/kbn-apm-synthtrace-client/src/lib/entity.ts
+++ b/packages/kbn-apm-synthtrace-client/src/lib/entity.ts
@@ -7,8 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-export type ObjectEntry = [keyof T, T[keyof T]];
-
export type Fields | undefined = undefined> = {
'@timestamp'?: number;
} & (TMeta extends undefined ? {} : Partial<{ meta: TMeta }>);
@@ -29,14 +27,4 @@ export class Entity {
return this;
}
-
- overrides(overrides: Partial) {
- const overrideEntries = Object.entries(overrides) as Array>;
-
- overrideEntries.forEach(([fieldName, value]) => {
- this.fields[fieldName] = value;
- });
-
- return this;
- }
}
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/gaussian_events.ts b/packages/kbn-apm-synthtrace-client/src/lib/gaussian_events.ts
deleted file mode 100644
index 4f1db28017d29..0000000000000
--- a/packages/kbn-apm-synthtrace-client/src/lib/gaussian_events.ts
+++ /dev/null
@@ -1,74 +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", 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 { castArray } from 'lodash';
-import { SynthtraceGenerator } from '../types';
-import { Fields } from './entity';
-import { Serializable } from './serializable';
-
-export class GaussianEvents {
- constructor(
- private readonly from: Date,
- private readonly to: Date,
- private readonly mean: Date,
- private readonly width: number,
- private readonly totalPoints: number
- ) {}
-
- *generator(
- map: (
- timestamp: number,
- index: number
- ) => Serializable | Array>
- ): SynthtraceGenerator {
- if (this.totalPoints <= 0) {
- return;
- }
-
- const startTime = this.from.getTime();
- const endTime = this.to.getTime();
- const meanTime = this.mean.getTime();
- const densityInterval = 1 / (this.totalPoints - 1);
-
- for (let eventIndex = 0; eventIndex < this.totalPoints; eventIndex++) {
- const quantile = eventIndex * densityInterval;
-
- const standardScore = Math.sqrt(2) * inverseError(2 * quantile - 1);
- const timestamp = Math.round(meanTime + standardScore * this.width);
-
- if (timestamp >= startTime && timestamp <= endTime) {
- yield* this.generateEvents(timestamp, eventIndex, map);
- }
- }
- }
-
- private *generateEvents(
- timestamp: number,
- eventIndex: number,
- map: (
- timestamp: number,
- index: number
- ) => Serializable | Array>
- ): Generator> {
- const events = castArray(map(timestamp, eventIndex));
- for (const event of events) {
- yield event;
- }
- }
-}
-
-function inverseError(x: number): number {
- const a = 0.147;
- const sign = x < 0 ? -1 : 1;
-
- const part1 = 2 / (Math.PI * a) + Math.log(1 - x * x) / 2;
- const part2 = Math.log(1 - x * x) / a;
-
- return sign * Math.sqrt(Math.sqrt(part1 * part1 - part2) - part1);
-}
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/infra/host.ts b/packages/kbn-apm-synthtrace-client/src/lib/infra/host.ts
index 30550d64c4df8..198949b482be3 100644
--- a/packages/kbn-apm-synthtrace-client/src/lib/infra/host.ts
+++ b/packages/kbn-apm-synthtrace-client/src/lib/infra/host.ts
@@ -27,7 +27,7 @@ interface HostDocument extends Fields {
'cloud.provider'?: string;
}
-export class Host extends Entity {
+class Host extends Entity {
cpu({ cpuTotalValue }: { cpuTotalValue?: number } = {}) {
return new HostMetrics({
...this.fields,
@@ -175,11 +175,3 @@ export function host(name: string): Host {
'cloud.provider': 'gcp',
});
}
-
-export function minimalHost(name: string): Host {
- return new Host({
- 'agent.id': 'synthtrace',
- 'host.hostname': name,
- 'host.name': name,
- });
-}
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/infra/index.ts b/packages/kbn-apm-synthtrace-client/src/lib/infra/index.ts
index 2957605cffcd3..853a9549ce02c 100644
--- a/packages/kbn-apm-synthtrace-client/src/lib/infra/index.ts
+++ b/packages/kbn-apm-synthtrace-client/src/lib/infra/index.ts
@@ -8,7 +8,7 @@
*/
import { dockerContainer, DockerContainerMetricsDocument } from './docker_container';
-import { host, HostMetricsDocument, minimalHost } from './host';
+import { host, HostMetricsDocument } from './host';
import { k8sContainer, K8sContainerMetricsDocument } from './k8s_container';
import { pod, PodMetricsDocument } from './pod';
import { awsRds, AWSRdsMetricsDocument } from './aws/rds';
@@ -24,7 +24,6 @@ export type InfraDocument =
export const infra = {
host,
- minimalHost,
pod,
dockerContainer,
k8sContainer,
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/interval.ts b/packages/kbn-apm-synthtrace-client/src/lib/interval.ts
index 5a5ed3ab5fdbe..1d56c42e1fe12 100644
--- a/packages/kbn-apm-synthtrace-client/src/lib/interval.ts
+++ b/packages/kbn-apm-synthtrace-client/src/lib/interval.ts
@@ -34,10 +34,6 @@ interface IntervalOptions {
rate?: number;
}
-interface StepDetails {
- stepMilliseconds: number;
-}
-
export class Interval {
private readonly intervalAmount: number;
private readonly intervalUnit: unitOfTime.DurationConstructor;
@@ -50,16 +46,12 @@ export class Interval {
this._rate = options.rate || 1;
}
- private getIntervalMilliseconds(): number {
- return moment.duration(this.intervalAmount, this.intervalUnit).asMilliseconds();
- }
-
private getTimestamps() {
const from = this.options.from.getTime();
const to = this.options.to.getTime();
let time: number = from;
- const diff = this.getIntervalMilliseconds();
+ const diff = moment.duration(this.intervalAmount, this.intervalUnit).asMilliseconds();
const timestamps: number[] = [];
@@ -76,19 +68,15 @@ export class Interval {
*generator(
map: (
timestamp: number,
- index: number,
- stepDetails: StepDetails
+ index: number
) => Serializable | Array>
): SynthtraceGenerator {
const timestamps = this.getTimestamps();
- const stepDetails: StepDetails = {
- stepMilliseconds: this.getIntervalMilliseconds(),
- };
let index = 0;
for (const timestamp of timestamps) {
- const events = castArray(map(timestamp, index, stepDetails));
+ const events = castArray(map(timestamp, index));
index++;
for (const event of events) {
yield event;
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts b/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts
index 2bbc59eb37e70..e19f0f6fd6565 100644
--- a/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts
+++ b/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts
@@ -68,7 +68,6 @@ export type LogDocument = Fields &
'event.duration': number;
'event.start': Date;
'event.end': Date;
- labels?: Record;
test_field: string | string[];
date: Date;
severity: string;
@@ -157,26 +156,6 @@ function create(logsOptions: LogsOptions = defaultLogsOptions): Log {
).dataset('synth');
}
-function createMinimal({
- dataset = 'synth',
- namespace = 'default',
-}: {
- dataset?: string;
- namespace?: string;
-} = {}): Log {
- return new Log(
- {
- 'input.type': 'logs',
- 'data_stream.namespace': namespace,
- 'data_stream.type': 'logs',
- 'data_stream.dataset': dataset,
- 'event.dataset': dataset,
- },
- { isLogsDb: false }
- );
-}
-
export const log = {
create,
- createMinimal,
};
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.test.ts b/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.test.ts
deleted file mode 100644
index 0741884550f32..0000000000000
--- a/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.test.ts
+++ /dev/null
@@ -1,53 +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", 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 { PoissonEvents } from './poisson_events';
-import { Serializable } from './serializable';
-
-describe('poisson events', () => {
- it('generates events within the given time range', () => {
- const poissonEvents = new PoissonEvents(new Date(1000), new Date(2000), 10);
-
- const events = Array.from(
- poissonEvents.generator((timestamp) => new Serializable({ '@timestamp': timestamp }))
- );
-
- expect(events.length).toBeGreaterThanOrEqual(1);
-
- for (const event of events) {
- expect(event.fields['@timestamp']).toBeGreaterThanOrEqual(1000);
- expect(event.fields['@timestamp']).toBeLessThanOrEqual(2000);
- }
- });
-
- it('generates at least one event if the rate is greater than 0', () => {
- const poissonEvents = new PoissonEvents(new Date(1000), new Date(2000), 1);
-
- const events = Array.from(
- poissonEvents.generator((timestamp) => new Serializable({ '@timestamp': timestamp }))
- );
-
- expect(events.length).toBeGreaterThanOrEqual(1);
-
- for (const event of events) {
- expect(event.fields['@timestamp']).toBeGreaterThanOrEqual(1000);
- expect(event.fields['@timestamp']).toBeLessThanOrEqual(2000);
- }
- });
-
- it('generates no event if the rate is 0', () => {
- const poissonEvents = new PoissonEvents(new Date(1000), new Date(2000), 0);
-
- const events = Array.from(
- poissonEvents.generator((timestamp) => new Serializable({ '@timestamp': timestamp }))
- );
-
- expect(events.length).toBe(0);
- });
-});
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.ts b/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.ts
deleted file mode 100644
index e7fd24b8323e7..0000000000000
--- a/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", 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 { castArray } from 'lodash';
-import { SynthtraceGenerator } from '../types';
-import { Fields } from './entity';
-import { Serializable } from './serializable';
-
-export class PoissonEvents {
- constructor(
- private readonly from: Date,
- private readonly to: Date,
- private readonly rate: number
- ) {}
-
- private getTotalTimePeriod(): number {
- return this.to.getTime() - this.from.getTime();
- }
-
- private getInterarrivalTime(): number {
- const distribution = -Math.log(1 - Math.random()) / this.rate;
- const totalTimePeriod = this.getTotalTimePeriod();
- return Math.floor(distribution * totalTimePeriod);
- }
-
- *generator(
- map: (
- timestamp: number,
- index: number
- ) => Serializable | Array>
- ): SynthtraceGenerator {
- if (this.rate <= 0) {
- return;
- }
-
- let currentTime = this.from.getTime();
- const endTime = this.to.getTime();
- let eventIndex = 0;
-
- while (currentTime < endTime) {
- const interarrivalTime = this.getInterarrivalTime();
- currentTime += interarrivalTime;
-
- if (currentTime < endTime) {
- yield* this.generateEvents(currentTime, eventIndex, map);
- eventIndex++;
- }
- }
-
- // ensure at least one event has been emitted
- if (this.rate > 0 && eventIndex === 0) {
- const forcedEventTime =
- this.from.getTime() + Math.floor(Math.random() * this.getTotalTimePeriod());
- yield* this.generateEvents(forcedEventTime, eventIndex, map);
- }
- }
-
- private *generateEvents(
- timestamp: number,
- eventIndex: number,
- map: (
- timestamp: number,
- index: number
- ) => Serializable | Array>
- ): Generator> {
- const events = castArray(map(timestamp, eventIndex));
- for (const event of events) {
- yield event;
- }
- }
-}
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts b/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts
index 1c6f12414a148..ccdea4ee75197 100644
--- a/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts
+++ b/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts
@@ -9,12 +9,10 @@
import datemath from '@kbn/datemath';
import type { Moment } from 'moment';
-import { GaussianEvents } from './gaussian_events';
import { Interval } from './interval';
-import { PoissonEvents } from './poisson_events';
export class Timerange {
- constructor(public readonly from: Date, public readonly to: Date) {}
+ constructor(private from: Date, private to: Date) {}
interval(interval: string) {
return new Interval({ from: this.from, to: this.to, interval });
@@ -23,29 +21,6 @@ export class Timerange {
ratePerMinute(rate: number) {
return this.interval(`1m`).rate(rate);
}
-
- poissonEvents(rate: number) {
- return new PoissonEvents(this.from, this.to, rate);
- }
-
- gaussianEvents(mean: Date, width: number, totalPoints: number) {
- return new GaussianEvents(this.from, this.to, mean, width, totalPoints);
- }
-
- splitInto(segmentCount: number): Timerange[] {
- const duration = this.to.getTime() - this.from.getTime();
- const segmentDuration = duration / segmentCount;
-
- return Array.from({ length: segmentCount }, (_, i) => {
- const from = new Date(this.from.getTime() + i * segmentDuration);
- const to = new Date(from.getTime() + segmentDuration);
- return new Timerange(from, to);
- });
- }
-
- toString() {
- return `Timerange(from=${this.from.toISOString()}, to=${this.to.toISOString()})`;
- }
}
type DateLike = Date | number | Moment | string;
diff --git a/packages/kbn-apm-synthtrace/src/scenarios/distributed_unstructured_logs.ts b/packages/kbn-apm-synthtrace/src/scenarios/distributed_unstructured_logs.ts
deleted file mode 100644
index 83860635ae64a..0000000000000
--- a/packages/kbn-apm-synthtrace/src/scenarios/distributed_unstructured_logs.ts
+++ /dev/null
@@ -1,197 +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", 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 { infra, LogDocument, log } from '@kbn/apm-synthtrace-client';
-import { fakerEN as faker } from '@faker-js/faker';
-import { z } from '@kbn/zod';
-import { Scenario } from '../cli/scenario';
-import { withClient } from '../lib/utils/with_client';
-import {
- LogMessageGenerator,
- generateUnstructuredLogMessage,
- unstructuredLogMessageGenerators,
-} from './helpers/unstructured_logs';
-
-const scenarioOptsSchema = z.intersection(
- z.object({
- randomSeed: z.number().default(0),
- messageGroup: z
- .enum([
- 'httpAccess',
- 'userAuthentication',
- 'networkEvent',
- 'dbOperations',
- 'taskOperations',
- 'degradedOperations',
- 'errorOperations',
- ])
- .default('dbOperations'),
- }),
- z
- .discriminatedUnion('distribution', [
- z.object({
- distribution: z.literal('uniform'),
- rate: z.number().default(1),
- }),
- z.object({
- distribution: z.literal('poisson'),
- rate: z.number().default(1),
- }),
- z.object({
- distribution: z.literal('gaussian'),
- mean: z.coerce.date().describe('Time of the peak of the gaussian distribution'),
- width: z.number().default(5000).describe('Width of the gaussian distribution in ms'),
- totalPoints: z
- .number()
- .default(100)
- .describe('Total number of points in the gaussian distribution'),
- }),
- ])
- .default({ distribution: 'uniform', rate: 1 })
-);
-
-type ScenarioOpts = z.output;
-
-const scenario: Scenario = async (runOptions) => {
- return {
- generate: ({ range, clients: { logsEsClient } }) => {
- const { logger } = runOptions;
- const scenarioOpts = scenarioOptsSchema.parse(runOptions.scenarioOpts ?? {});
-
- faker.seed(scenarioOpts.randomSeed);
- faker.setDefaultRefDate(range.from.toISOString());
-
- logger.debug(`Generating ${scenarioOpts.distribution} logs...`);
-
- // Logs Data logic
- const LOG_LEVELS = ['info', 'debug', 'error', 'warn', 'trace', 'fatal'];
-
- const clusterDefinions = [
- {
- 'orchestrator.cluster.id': faker.string.nanoid(),
- 'orchestrator.cluster.name': 'synth-cluster-1',
- 'orchestrator.namespace': 'default',
- 'cloud.provider': 'gcp',
- 'cloud.region': 'eu-central-1',
- 'cloud.availability_zone': 'eu-central-1a',
- 'cloud.project.id': faker.string.nanoid(),
- },
- {
- 'orchestrator.cluster.id': faker.string.nanoid(),
- 'orchestrator.cluster.name': 'synth-cluster-2',
- 'orchestrator.namespace': 'production',
- 'cloud.provider': 'aws',
- 'cloud.region': 'us-east-1',
- 'cloud.availability_zone': 'us-east-1a',
- 'cloud.project.id': faker.string.nanoid(),
- },
- {
- 'orchestrator.cluster.id': faker.string.nanoid(),
- 'orchestrator.cluster.name': 'synth-cluster-3',
- 'orchestrator.namespace': 'kube',
- 'cloud.provider': 'azure',
- 'cloud.region': 'area-51',
- 'cloud.availability_zone': 'area-51a',
- 'cloud.project.id': faker.string.nanoid(),
- },
- ];
-
- const hostEntities = [
- {
- 'host.name': 'host-1',
- 'agent.id': 'synth-agent-1',
- 'agent.name': 'nodejs',
- 'cloud.instance.id': faker.string.nanoid(),
- 'orchestrator.resource.id': faker.string.nanoid(),
- ...clusterDefinions[0],
- },
- {
- 'host.name': 'host-2',
- 'agent.id': 'synth-agent-2',
- 'agent.name': 'custom',
- 'cloud.instance.id': faker.string.nanoid(),
- 'orchestrator.resource.id': faker.string.nanoid(),
- ...clusterDefinions[1],
- },
- {
- 'host.name': 'host-3',
- 'agent.id': 'synth-agent-3',
- 'agent.name': 'python',
- 'cloud.instance.id': faker.string.nanoid(),
- 'orchestrator.resource.id': faker.string.nanoid(),
- ...clusterDefinions[2],
- },
- ].map((hostDefinition) =>
- infra.minimalHost(hostDefinition['host.name']).overrides(hostDefinition)
- );
-
- const serviceNames = Array(3)
- .fill(null)
- .map((_, idx) => `synth-service-${idx}`);
-
- const generatorFactory =
- scenarioOpts.distribution === 'uniform'
- ? range.interval('1s').rate(scenarioOpts.rate)
- : scenarioOpts.distribution === 'poisson'
- ? range.poissonEvents(scenarioOpts.rate)
- : range.gaussianEvents(scenarioOpts.mean, scenarioOpts.width, scenarioOpts.totalPoints);
-
- const logs = generatorFactory.generator((timestamp) => {
- const entity = faker.helpers.arrayElement(hostEntities);
- const serviceName = faker.helpers.arrayElement(serviceNames);
- const level = faker.helpers.arrayElement(LOG_LEVELS);
- const messages = logMessageGenerators[scenarioOpts.messageGroup](faker);
-
- return messages.map((message) =>
- log
- .createMinimal()
- .message(message)
- .logLevel(level)
- .service(serviceName)
- .overrides({
- ...entity.fields,
- labels: {
- scenario: 'rare',
- population: scenarioOpts.distribution,
- },
- })
- .timestamp(timestamp)
- );
- });
-
- return [
- withClient(
- logsEsClient,
- logger.perf('generating_logs', () => [logs])
- ),
- ];
- },
- };
-};
-
-export default scenario;
-
-const logMessageGenerators = {
- httpAccess: generateUnstructuredLogMessage([unstructuredLogMessageGenerators.httpAccess]),
- userAuthentication: generateUnstructuredLogMessage([
- unstructuredLogMessageGenerators.userAuthentication,
- ]),
- networkEvent: generateUnstructuredLogMessage([unstructuredLogMessageGenerators.networkEvent]),
- dbOperations: generateUnstructuredLogMessage([unstructuredLogMessageGenerators.dbOperation]),
- taskOperations: generateUnstructuredLogMessage([
- unstructuredLogMessageGenerators.taskStatusSuccess,
- ]),
- degradedOperations: generateUnstructuredLogMessage([
- unstructuredLogMessageGenerators.taskStatusFailure,
- ]),
- errorOperations: generateUnstructuredLogMessage([
- unstructuredLogMessageGenerators.error,
- unstructuredLogMessageGenerators.restart,
- ]),
-} satisfies Record;
diff --git a/packages/kbn-apm-synthtrace/src/scenarios/helpers/unstructured_logs.ts b/packages/kbn-apm-synthtrace/src/scenarios/helpers/unstructured_logs.ts
deleted file mode 100644
index 490bd449e2b60..0000000000000
--- a/packages/kbn-apm-synthtrace/src/scenarios/helpers/unstructured_logs.ts
+++ /dev/null
@@ -1,94 +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", 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 { Faker, faker } from '@faker-js/faker';
-
-export type LogMessageGenerator = (f: Faker) => string[];
-
-export const unstructuredLogMessageGenerators = {
- httpAccess: (f: Faker) => [
- `${f.internet.ip()} - - [${f.date
- .past()
- .toISOString()
- .replace('T', ' ')
- .replace(
- /\..+/,
- ''
- )}] "${f.internet.httpMethod()} ${f.internet.url()} HTTP/1.1" ${f.helpers.arrayElement([
- 200, 301, 404, 500,
- ])} ${f.number.int({ min: 100, max: 5000 })}`,
- ],
- dbOperation: (f: Faker) => [
- `${f.database.engine()}: ${f.database.column()} ${f.helpers.arrayElement([
- 'created',
- 'updated',
- 'deleted',
- 'inserted',
- ])} successfully ${f.number.int({ max: 100000 })} times`,
- ],
- taskStatusSuccess: (f: Faker) => [
- `${f.hacker.noun()}: ${f.word.words()} ${f.helpers.arrayElement([
- 'triggered',
- 'executed',
- 'processed',
- 'handled',
- ])} successfully at ${f.date.recent().toISOString()}`,
- ],
- taskStatusFailure: (f: Faker) => [
- `${f.hacker.noun()}: ${f.helpers.arrayElement([
- 'triggering',
- 'execution',
- 'processing',
- 'handling',
- ])} of ${f.word.words()} failed at ${f.date.recent().toISOString()}`,
- ],
- error: (f: Faker) => [
- `${f.helpers.arrayElement([
- 'Error',
- 'Exception',
- 'Failure',
- 'Crash',
- 'Bug',
- 'Issue',
- ])}: ${f.hacker.phrase()}`,
- `Stopping ${f.number.int(42)} background tasks...`,
- 'Shutting down process...',
- ],
- restart: (f: Faker) => {
- const service = f.database.engine();
- return [
- `Restarting ${service}...`,
- `Waiting for queue to drain...`,
- `Service ${service} restarted ${f.helpers.arrayElement([
- 'successfully',
- 'with errors',
- 'with warnings',
- ])}`,
- ];
- },
- userAuthentication: (f: Faker) => [
- `User ${f.internet.userName()} ${f.helpers.arrayElement([
- 'logged in',
- 'logged out',
- 'failed to login',
- ])}`,
- ],
- networkEvent: (f: Faker) => [
- `Network ${f.helpers.arrayElement([
- 'connection',
- 'disconnection',
- 'data transfer',
- ])} ${f.helpers.arrayElement(['from', 'to'])} ${f.internet.ip()}`,
- ],
-} satisfies Record;
-
-export const generateUnstructuredLogMessage =
- (generators: LogMessageGenerator[] = Object.values(unstructuredLogMessageGenerators)) =>
- (f: Faker = faker) =>
- f.helpers.arrayElement(generators)(f);
diff --git a/packages/kbn-apm-synthtrace/tsconfig.json b/packages/kbn-apm-synthtrace/tsconfig.json
index db93e36421b83..d0f5c5801597a 100644
--- a/packages/kbn-apm-synthtrace/tsconfig.json
+++ b/packages/kbn-apm-synthtrace/tsconfig.json
@@ -10,7 +10,6 @@
"@kbn/apm-synthtrace-client",
"@kbn/dev-utils",
"@kbn/elastic-agent-utils",
- "@kbn/zod",
],
"exclude": [
"target/**/*",
diff --git a/packages/kbn-management/settings/setting_ids/index.ts b/packages/kbn-management/settings/setting_ids/index.ts
index e926007f77f25..2b8c5de0b71df 100644
--- a/packages/kbn-management/settings/setting_ids/index.ts
+++ b/packages/kbn-management/settings/setting_ids/index.ts
@@ -142,7 +142,6 @@ export const OBSERVABILITY_APM_ENABLE_SERVICE_INVENTORY_TABLE_SEARCH_BAR =
'observability:apmEnableServiceInventoryTableSearchBar';
export const OBSERVABILITY_LOGS_EXPLORER_ALLOWED_DATA_VIEWS_ID =
'observability:logsExplorer:allowedDataViews';
-export const OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID = 'observability:newLogsOverview';
export const OBSERVABILITY_ENTITY_CENTRIC_EXPERIENCE = 'observability:entityCentricExperience';
export const OBSERVABILITY_LOGS_DATA_ACCESS_LOG_SOURCES_ID = 'observability:logSources';
export const OBSERVABILITY_ENABLE_LOGS_STREAM = 'observability:enableLogsStream';
diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts
index 52a837724480d..539d3098030e0 100644
--- a/packages/kbn-optimizer/src/worker/webpack.config.ts
+++ b/packages/kbn-optimizer/src/worker/webpack.config.ts
@@ -247,18 +247,6 @@ export function getWebpackConfig(
},
},
},
- {
- test: /node_modules\/@?xstate5\/.*\.js$/,
- use: {
- loader: 'babel-loader',
- options: {
- babelrc: false,
- envName: worker.dist ? 'production' : 'development',
- presets: [BABEL_PRESET],
- plugins: ['@babel/plugin-transform-logical-assignment-operators'],
- },
- },
- },
{
test: /\.(html|md|txt|tmpl)$/,
use: {
diff --git a/packages/kbn-xstate-utils/kibana.jsonc b/packages/kbn-xstate-utils/kibana.jsonc
index 1fb3507854b98..cd1151a3f2103 100644
--- a/packages/kbn-xstate-utils/kibana.jsonc
+++ b/packages/kbn-xstate-utils/kibana.jsonc
@@ -1,5 +1,5 @@
{
- "type": "shared-browser",
+ "type": "shared-common",
"id": "@kbn/xstate-utils",
"owner": "@elastic/obs-ux-logs-team"
}
diff --git a/packages/kbn-xstate-utils/src/console_inspector.ts b/packages/kbn-xstate-utils/src/console_inspector.ts
deleted file mode 100644
index 8792ab44f3c28..0000000000000
--- a/packages/kbn-xstate-utils/src/console_inspector.ts
+++ /dev/null
@@ -1,88 +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", 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 {
- ActorRefLike,
- AnyActorRef,
- InspectedActorEvent,
- InspectedEventEvent,
- InspectedSnapshotEvent,
- InspectionEvent,
-} from 'xstate5';
-import { isDevMode } from './dev_tools';
-
-export const createConsoleInspector = () => {
- if (!isDevMode()) {
- return () => {};
- }
-
- // eslint-disable-next-line no-console
- const log = console.info.bind(console);
-
- const logActorEvent = (actorEvent: InspectedActorEvent) => {
- if (isActorRef(actorEvent.actorRef)) {
- log(
- '✨ %c%s%c is a new actor of type %c%s%c:',
- ...styleAsActor(actorEvent.actorRef.id),
- ...styleAsKeyword(actorEvent.type),
- actorEvent.actorRef
- );
- } else {
- log('✨ New %c%s%c actor without id:', ...styleAsKeyword(actorEvent.type), actorEvent);
- }
- };
-
- const logEventEvent = (eventEvent: InspectedEventEvent) => {
- if (isActorRef(eventEvent.actorRef)) {
- log(
- '🔔 %c%s%c received event %c%s%c from %c%s%c:',
- ...styleAsActor(eventEvent.actorRef.id),
- ...styleAsKeyword(eventEvent.event.type),
- ...styleAsKeyword(eventEvent.sourceRef?.id),
- eventEvent
- );
- } else {
- log('🔔 Event', ...styleAsKeyword(eventEvent.event.type), ':', eventEvent);
- }
- };
-
- const logSnapshotEvent = (snapshotEvent: InspectedSnapshotEvent) => {
- if (isActorRef(snapshotEvent.actorRef)) {
- log(
- '📸 %c%s%c updated due to %c%s%c:',
- ...styleAsActor(snapshotEvent.actorRef.id),
- ...styleAsKeyword(snapshotEvent.event.type),
- snapshotEvent.snapshot
- );
- } else {
- log('📸 Snapshot due to %c%s%c:', ...styleAsKeyword(snapshotEvent.event.type), snapshotEvent);
- }
- };
-
- return (inspectionEvent: InspectionEvent) => {
- if (inspectionEvent.type === '@xstate.actor') {
- logActorEvent(inspectionEvent);
- } else if (inspectionEvent.type === '@xstate.event') {
- logEventEvent(inspectionEvent);
- } else if (inspectionEvent.type === '@xstate.snapshot') {
- logSnapshotEvent(inspectionEvent);
- } else {
- log(`❓ Received inspection event:`, inspectionEvent);
- }
- };
-};
-
-const isActorRef = (actorRefLike: ActorRefLike): actorRefLike is AnyActorRef =>
- 'id' in actorRefLike;
-
-const keywordStyle = 'font-weight: bold';
-const styleAsKeyword = (value: any) => [keywordStyle, value, ''] as const;
-
-const actorStyle = 'font-weight: bold; text-decoration: underline';
-const styleAsActor = (value: any) => [actorStyle, value, ''] as const;
diff --git a/packages/kbn-xstate-utils/src/index.ts b/packages/kbn-xstate-utils/src/index.ts
index 3edf83e8a32c2..107585ba2096f 100644
--- a/packages/kbn-xstate-utils/src/index.ts
+++ b/packages/kbn-xstate-utils/src/index.ts
@@ -9,6 +9,5 @@
export * from './actions';
export * from './dev_tools';
-export * from './console_inspector';
export * from './notification_channel';
export * from './types';
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 e5ddfbe4dd037..dc2d2ad2c5de2 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
@@ -705,10 +705,4 @@ export const stackManagementSchema: MakeSchemaFrom = {
_meta: { description: 'Non-default value of setting.' },
},
},
- 'observability:newLogsOverview': {
- type: 'boolean',
- _meta: {
- description: 'Enable the new logs overview component.',
- },
- },
};
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 2acb487e7ed08..ef20ab223dfb6 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
@@ -56,7 +56,6 @@ export interface UsageStats {
'observability:logsExplorer:allowedDataViews': string[];
'observability:logSources': string[];
'observability:enableLogsStream': boolean;
- 'observability:newLogsOverview': boolean;
'observability:aiAssistantSimulatedFunctionCalling': boolean;
'observability:aiAssistantSearchConnectorIndexPattern': string;
'visualization:heatmap:maxBuckets': number;
diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json
index 830cffc17cf1c..958280d9eba00 100644
--- a/src/plugins/telemetry/schema/oss_plugins.json
+++ b/src/plugins/telemetry/schema/oss_plugins.json
@@ -10768,12 +10768,6 @@
"description": "Non-default value of setting."
}
},
- "observability:newLogsOverview": {
- "type": "boolean",
- "_meta": {
- "description": "Enable the new logs overview component."
- }
- },
"observability:searchExcludedDataTiers": {
"type": "array",
"items": {
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 4bc68d806f043..3df30d9cf8c30 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -1298,8 +1298,6 @@
"@kbn/observability-get-padded-alert-time-range-util/*": ["x-pack/packages/observability/get_padded_alert_time_range_util/*"],
"@kbn/observability-logs-explorer-plugin": ["x-pack/plugins/observability_solution/observability_logs_explorer"],
"@kbn/observability-logs-explorer-plugin/*": ["x-pack/plugins/observability_solution/observability_logs_explorer/*"],
- "@kbn/observability-logs-overview": ["x-pack/packages/observability/logs_overview"],
- "@kbn/observability-logs-overview/*": ["x-pack/packages/observability/logs_overview/*"],
"@kbn/observability-onboarding-e2e": ["x-pack/plugins/observability_solution/observability_onboarding/e2e"],
"@kbn/observability-onboarding-e2e/*": ["x-pack/plugins/observability_solution/observability_onboarding/e2e/*"],
"@kbn/observability-onboarding-plugin": ["x-pack/plugins/observability_solution/observability_onboarding"],
diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json
index 50f2b77b84ad7..a46e291093411 100644
--- a/x-pack/.i18nrc.json
+++ b/x-pack/.i18nrc.json
@@ -95,9 +95,6 @@
"xpack.observabilityLogsExplorer": "plugins/observability_solution/observability_logs_explorer",
"xpack.observability_onboarding": "plugins/observability_solution/observability_onboarding",
"xpack.observabilityShared": "plugins/observability_solution/observability_shared",
- "xpack.observabilityLogsOverview": [
- "packages/observability/logs_overview/src/components"
- ],
"xpack.osquery": ["plugins/osquery"],
"xpack.painlessLab": "plugins/painless_lab",
"xpack.profiling": ["plugins/observability_solution/profiling"],
diff --git a/x-pack/packages/observability/logs_overview/README.md b/x-pack/packages/observability/logs_overview/README.md
deleted file mode 100644
index 20d3f0f02b7df..0000000000000
--- a/x-pack/packages/observability/logs_overview/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# @kbn/observability-logs-overview
-
-Empty package generated by @kbn/generate
diff --git a/x-pack/packages/observability/logs_overview/index.ts b/x-pack/packages/observability/logs_overview/index.ts
deleted file mode 100644
index 057d1d3acd152..0000000000000
--- a/x-pack/packages/observability/logs_overview/index.ts
+++ /dev/null
@@ -1,21 +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.
- */
-
-export {
- LogsOverview,
- LogsOverviewErrorContent,
- LogsOverviewLoadingContent,
- type LogsOverviewDependencies,
- type LogsOverviewErrorContentProps,
- type LogsOverviewProps,
-} from './src/components/logs_overview';
-export type {
- DataViewLogsSourceConfiguration,
- IndexNameLogsSourceConfiguration,
- LogsSourceConfiguration,
- SharedSettingLogsSourceConfiguration,
-} from './src/utils/logs_source';
diff --git a/x-pack/packages/observability/logs_overview/jest.config.js b/x-pack/packages/observability/logs_overview/jest.config.js
deleted file mode 100644
index 2ee88ee990253..0000000000000
--- a/x-pack/packages/observability/logs_overview/jest.config.js
+++ /dev/null
@@ -1,12 +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.
- */
-
-module.exports = {
- preset: '@kbn/test',
- rootDir: '../../../..',
- roots: ['/x-pack/packages/observability/logs_overview'],
-};
diff --git a/x-pack/packages/observability/logs_overview/kibana.jsonc b/x-pack/packages/observability/logs_overview/kibana.jsonc
deleted file mode 100644
index 90b3375086720..0000000000000
--- a/x-pack/packages/observability/logs_overview/kibana.jsonc
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "type": "shared-browser",
- "id": "@kbn/observability-logs-overview",
- "owner": "@elastic/obs-ux-logs-team"
-}
diff --git a/x-pack/packages/observability/logs_overview/package.json b/x-pack/packages/observability/logs_overview/package.json
deleted file mode 100644
index 77a529e7e59f7..0000000000000
--- a/x-pack/packages/observability/logs_overview/package.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "name": "@kbn/observability-logs-overview",
- "private": true,
- "version": "1.0.0",
- "license": "Elastic License 2.0",
- "sideEffects": false
-}
diff --git a/x-pack/packages/observability/logs_overview/src/components/discover_link/discover_link.tsx b/x-pack/packages/observability/logs_overview/src/components/discover_link/discover_link.tsx
deleted file mode 100644
index fe108289985a9..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/components/discover_link/discover_link.tsx
+++ /dev/null
@@ -1,110 +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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
-import { EuiButton } from '@elastic/eui';
-import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
-import { FilterStateStore, buildCustomFilter } from '@kbn/es-query';
-import { i18n } from '@kbn/i18n';
-import { getRouterLinkProps } from '@kbn/router-utils';
-import type { SharePluginStart } from '@kbn/share-plugin/public';
-import React, { useCallback, useMemo } from 'react';
-import type { IndexNameLogsSourceConfiguration } from '../../utils/logs_source';
-
-export interface DiscoverLinkProps {
- documentFilters?: QueryDslQueryContainer[];
- logsSource: IndexNameLogsSourceConfiguration;
- timeRange: {
- start: string;
- end: string;
- };
- dependencies: DiscoverLinkDependencies;
-}
-
-export interface DiscoverLinkDependencies {
- share: SharePluginStart;
-}
-
-export const DiscoverLink = React.memo(
- ({ dependencies: { share }, documentFilters, logsSource, timeRange }: DiscoverLinkProps) => {
- const discoverLocatorParams = useMemo(
- () => ({
- dataViewSpec: {
- id: logsSource.indexName,
- name: logsSource.indexName,
- title: logsSource.indexName,
- timeFieldName: logsSource.timestampField,
- },
- timeRange: {
- from: timeRange.start,
- to: timeRange.end,
- },
- filters: documentFilters?.map((filter) =>
- buildCustomFilter(
- logsSource.indexName,
- filter,
- false,
- false,
- categorizedLogsFilterLabel,
- FilterStateStore.APP_STATE
- )
- ),
- }),
- [
- documentFilters,
- logsSource.indexName,
- logsSource.timestampField,
- timeRange.end,
- timeRange.start,
- ]
- );
-
- const discoverLocator = useMemo(
- () => share.url.locators.get('DISCOVER_APP_LOCATOR'),
- [share.url.locators]
- );
-
- const discoverUrl = useMemo(
- () => discoverLocator?.getRedirectUrl(discoverLocatorParams),
- [discoverLocatorParams, discoverLocator]
- );
-
- const navigateToDiscover = useCallback(() => {
- discoverLocator?.navigate(discoverLocatorParams);
- }, [discoverLocatorParams, discoverLocator]);
-
- const discoverLinkProps = getRouterLinkProps({
- href: discoverUrl,
- onClick: navigateToDiscover,
- });
-
- return (
-
- {discoverLinkTitle}
-
- );
- }
-);
-
-export const discoverLinkTitle = i18n.translate(
- 'xpack.observabilityLogsOverview.discoverLinkTitle',
- {
- defaultMessage: 'Open in Discover',
- }
-);
-
-export const categorizedLogsFilterLabel = i18n.translate(
- 'xpack.observabilityLogsOverview.categorizedLogsFilterLabel',
- {
- defaultMessage: 'Categorized log entries',
- }
-);
diff --git a/x-pack/packages/observability/logs_overview/src/components/discover_link/index.ts b/x-pack/packages/observability/logs_overview/src/components/discover_link/index.ts
deleted file mode 100644
index 738bf51d4529d..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/components/discover_link/index.ts
+++ /dev/null
@@ -1,8 +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.
- */
-
-export * from './discover_link';
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/index.ts b/x-pack/packages/observability/logs_overview/src/components/log_categories/index.ts
deleted file mode 100644
index 786475396237c..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/components/log_categories/index.ts
+++ /dev/null
@@ -1,8 +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.
- */
-
-export * from './log_categories';
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories.tsx
deleted file mode 100644
index 6204667827281..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories.tsx
+++ /dev/null
@@ -1,94 +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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
-import { ISearchGeneric } from '@kbn/search-types';
-import { createConsoleInspector } from '@kbn/xstate-utils';
-import { useMachine } from '@xstate5/react';
-import React, { useCallback } from 'react';
-import {
- categorizeLogsService,
- createCategorizeLogsServiceImplementations,
-} from '../../services/categorize_logs_service';
-import { IndexNameLogsSourceConfiguration } from '../../utils/logs_source';
-import { LogCategoriesErrorContent } from './log_categories_error_content';
-import { LogCategoriesLoadingContent } from './log_categories_loading_content';
-import {
- LogCategoriesResultContent,
- LogCategoriesResultContentDependencies,
-} from './log_categories_result_content';
-
-export interface LogCategoriesProps {
- dependencies: LogCategoriesDependencies;
- documentFilters?: QueryDslQueryContainer[];
- logsSource: IndexNameLogsSourceConfiguration;
- // The time range could be made optional if we want to support an internal
- // time range picker
- timeRange: {
- start: string;
- end: string;
- };
-}
-
-export type LogCategoriesDependencies = LogCategoriesResultContentDependencies & {
- search: ISearchGeneric;
-};
-
-export const LogCategories: React.FC = ({
- dependencies,
- documentFilters = [],
- logsSource,
- timeRange,
-}) => {
- const [categorizeLogsServiceState, sendToCategorizeLogsService] = useMachine(
- categorizeLogsService.provide(
- createCategorizeLogsServiceImplementations({ search: dependencies.search })
- ),
- {
- inspect: consoleInspector,
- input: {
- index: logsSource.indexName,
- startTimestamp: timeRange.start,
- endTimestamp: timeRange.end,
- timeField: logsSource.timestampField,
- messageField: logsSource.messageField,
- documentFilters,
- },
- }
- );
-
- const cancelOperation = useCallback(() => {
- sendToCategorizeLogsService({
- type: 'cancel',
- });
- }, [sendToCategorizeLogsService]);
-
- if (categorizeLogsServiceState.matches('done')) {
- return (
-
- );
- } else if (categorizeLogsServiceState.matches('failed')) {
- return ;
- } else if (categorizeLogsServiceState.matches('countingDocuments')) {
- return ;
- } else if (
- categorizeLogsServiceState.matches('fetchingSampledCategories') ||
- categorizeLogsServiceState.matches('fetchingRemainingCategories')
- ) {
- return ;
- } else {
- return null;
- }
-};
-
-const consoleInspector = createConsoleInspector();
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_control_bar.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_control_bar.tsx
deleted file mode 100644
index 4538b0ec2fd5d..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_control_bar.tsx
+++ /dev/null
@@ -1,44 +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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
-import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import type { SharePluginStart } from '@kbn/share-plugin/public';
-import React from 'react';
-import type { IndexNameLogsSourceConfiguration } from '../../utils/logs_source';
-import { DiscoverLink } from '../discover_link';
-
-export interface LogCategoriesControlBarProps {
- documentFilters?: QueryDslQueryContainer[];
- logsSource: IndexNameLogsSourceConfiguration;
- timeRange: {
- start: string;
- end: string;
- };
- dependencies: LogCategoriesControlBarDependencies;
-}
-
-export interface LogCategoriesControlBarDependencies {
- share: SharePluginStart;
-}
-
-export const LogCategoriesControlBar: React.FC = React.memo(
- ({ dependencies, documentFilters, logsSource, timeRange }) => {
- return (
-
-
-
-
-
- );
- }
-);
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_error_content.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_error_content.tsx
deleted file mode 100644
index 1a335e3265294..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_error_content.tsx
+++ /dev/null
@@ -1,44 +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 { EuiCodeBlock, EuiEmptyPrompt } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import React from 'react';
-
-export interface LogCategoriesErrorContentProps {
- error?: Error;
-}
-
-export const LogCategoriesErrorContent: React.FC = ({ error }) => {
- return (
- {logsOverviewErrorTitle}}
- body={
-
- {error?.stack ?? error?.toString() ?? unknownErrorDescription}
-
- }
- layout="vertical"
- />
- );
-};
-
-const logsOverviewErrorTitle = i18n.translate(
- 'xpack.observabilityLogsOverview.logCategories.errorTitle',
- {
- defaultMessage: 'Failed to categorize logs',
- }
-);
-
-const unknownErrorDescription = i18n.translate(
- 'xpack.observabilityLogsOverview.logCategories.unknownErrorDescription',
- {
- defaultMessage: 'An unspecified error occurred.',
- }
-);
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid.tsx
deleted file mode 100644
index d9e960685de99..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid.tsx
+++ /dev/null
@@ -1,182 +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 {
- EuiDataGrid,
- EuiDataGridColumnSortingConfig,
- EuiDataGridPaginationProps,
-} from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import { createConsoleInspector } from '@kbn/xstate-utils';
-import { useMachine } from '@xstate5/react';
-import _ from 'lodash';
-import React, { useMemo } from 'react';
-import { assign, setup } from 'xstate5';
-import { LogCategory } from '../../types';
-import {
- LogCategoriesGridCellDependencies,
- LogCategoriesGridColumnId,
- createCellContext,
- logCategoriesGridColumnIds,
- logCategoriesGridColumns,
- renderLogCategoriesGridCell,
-} from './log_categories_grid_cell';
-
-export interface LogCategoriesGridProps {
- dependencies: LogCategoriesGridDependencies;
- logCategories: LogCategory[];
-}
-
-export type LogCategoriesGridDependencies = LogCategoriesGridCellDependencies;
-
-export const LogCategoriesGrid: React.FC = ({
- dependencies,
- logCategories,
-}) => {
- const [gridState, dispatchGridEvent] = useMachine(gridStateService, {
- input: {
- visibleColumns: logCategoriesGridColumns.map(({ id }) => id),
- },
- inspect: consoleInspector,
- });
-
- const sortedLogCategories = useMemo(() => {
- const sortingCriteria = gridState.context.sortingColumns.map(
- ({ id, direction }): [(logCategory: LogCategory) => any, 'asc' | 'desc'] => {
- switch (id) {
- case 'count':
- return [(logCategory: LogCategory) => logCategory.documentCount, direction];
- case 'change_type':
- // TODO: use better sorting weight for change types
- return [(logCategory: LogCategory) => logCategory.change.type, direction];
- case 'change_time':
- return [
- (logCategory: LogCategory) =>
- 'timestamp' in logCategory.change ? logCategory.change.timestamp ?? '' : '',
- direction,
- ];
- default:
- return [_.identity, direction];
- }
- }
- );
- return _.orderBy(
- logCategories,
- sortingCriteria.map(([accessor]) => accessor),
- sortingCriteria.map(([, direction]) => direction)
- );
- }, [gridState.context.sortingColumns, logCategories]);
-
- return (
-
- dispatchGridEvent({ type: 'changeVisibleColumns', visibleColumns }),
- }}
- cellContext={createCellContext(sortedLogCategories, dependencies)}
- pagination={{
- ...gridState.context.pagination,
- onChangeItemsPerPage: (pageSize) => dispatchGridEvent({ type: 'changePageSize', pageSize }),
- onChangePage: (pageIndex) => dispatchGridEvent({ type: 'changePageIndex', pageIndex }),
- }}
- renderCellValue={renderLogCategoriesGridCell}
- rowCount={sortedLogCategories.length}
- sorting={{
- columns: gridState.context.sortingColumns,
- onSort: (sortingColumns) =>
- dispatchGridEvent({ type: 'changeSortingColumns', sortingColumns }),
- }}
- />
- );
-};
-
-const gridStateService = setup({
- types: {
- context: {} as {
- visibleColumns: string[];
- pagination: Pick;
- sortingColumns: LogCategoriesGridSortingConfig[];
- },
- events: {} as
- | {
- type: 'changePageSize';
- pageSize: number;
- }
- | {
- type: 'changePageIndex';
- pageIndex: number;
- }
- | {
- type: 'changeSortingColumns';
- sortingColumns: EuiDataGridColumnSortingConfig[];
- }
- | {
- type: 'changeVisibleColumns';
- visibleColumns: string[];
- },
- input: {} as {
- visibleColumns: string[];
- },
- },
-}).createMachine({
- id: 'logCategoriesGridState',
- context: ({ input }) => ({
- visibleColumns: input.visibleColumns,
- pagination: { pageIndex: 0, pageSize: 20, pageSizeOptions: [10, 20, 50] },
- sortingColumns: [{ id: 'change_time', direction: 'desc' }],
- }),
- on: {
- changePageSize: {
- actions: assign(({ context, event }) => ({
- pagination: {
- ...context.pagination,
- pageIndex: 0,
- pageSize: event.pageSize,
- },
- })),
- },
- changePageIndex: {
- actions: assign(({ context, event }) => ({
- pagination: {
- ...context.pagination,
- pageIndex: event.pageIndex,
- },
- })),
- },
- changeSortingColumns: {
- actions: assign(({ event }) => ({
- sortingColumns: event.sortingColumns.filter(
- (sortingConfig): sortingConfig is LogCategoriesGridSortingConfig =>
- (logCategoriesGridColumnIds as string[]).includes(sortingConfig.id)
- ),
- })),
- },
- changeVisibleColumns: {
- actions: assign(({ event }) => ({
- visibleColumns: event.visibleColumns,
- })),
- },
- },
-});
-
-const consoleInspector = createConsoleInspector();
-
-const logCategoriesGridLabel = i18n.translate(
- 'xpack.observabilityLogsOverview.logCategoriesGrid.euiDataGrid.logCategoriesLabel',
- { defaultMessage: 'Log categories' }
-);
-
-interface TypedEuiDataGridColumnSortingConfig
- extends EuiDataGridColumnSortingConfig {
- id: ColumnId;
-}
-
-type LogCategoriesGridSortingConfig =
- TypedEuiDataGridColumnSortingConfig;
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_cell.tsx
deleted file mode 100644
index d6ab4969eaf7b..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_cell.tsx
+++ /dev/null
@@ -1,99 +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 { EuiDataGridColumn, RenderCellValue } from '@elastic/eui';
-import React from 'react';
-import { LogCategory } from '../../types';
-import {
- LogCategoriesGridChangeTimeCell,
- LogCategoriesGridChangeTimeCellDependencies,
- logCategoriesGridChangeTimeColumn,
-} from './log_categories_grid_change_time_cell';
-import {
- LogCategoriesGridChangeTypeCell,
- logCategoriesGridChangeTypeColumn,
-} from './log_categories_grid_change_type_cell';
-import {
- LogCategoriesGridCountCell,
- logCategoriesGridCountColumn,
-} from './log_categories_grid_count_cell';
-import {
- LogCategoriesGridHistogramCell,
- LogCategoriesGridHistogramCellDependencies,
- logCategoriesGridHistoryColumn,
-} from './log_categories_grid_histogram_cell';
-import {
- LogCategoriesGridPatternCell,
- logCategoriesGridPatternColumn,
-} from './log_categories_grid_pattern_cell';
-
-export interface LogCategoriesGridCellContext {
- dependencies: LogCategoriesGridCellDependencies;
- logCategories: LogCategory[];
-}
-
-export type LogCategoriesGridCellDependencies = LogCategoriesGridHistogramCellDependencies &
- LogCategoriesGridChangeTimeCellDependencies;
-
-export const renderLogCategoriesGridCell: RenderCellValue = ({
- rowIndex,
- columnId,
- isExpanded,
- ...rest
-}) => {
- const { dependencies, logCategories } = getCellContext(rest);
-
- const logCategory = logCategories[rowIndex];
-
- switch (columnId as LogCategoriesGridColumnId) {
- case 'pattern':
- return ;
- case 'count':
- return ;
- case 'history':
- return (
-
- );
- case 'change_type':
- return ;
- case 'change_time':
- return (
-
- );
- default:
- return <>->;
- }
-};
-
-export const logCategoriesGridColumns = [
- logCategoriesGridPatternColumn,
- logCategoriesGridCountColumn,
- logCategoriesGridChangeTypeColumn,
- logCategoriesGridChangeTimeColumn,
- logCategoriesGridHistoryColumn,
-] satisfies EuiDataGridColumn[];
-
-export const logCategoriesGridColumnIds = logCategoriesGridColumns.map(({ id }) => id);
-
-export type LogCategoriesGridColumnId = (typeof logCategoriesGridColumns)[number]['id'];
-
-const cellContextKey = 'cellContext';
-
-const getCellContext = (cellContext: object): LogCategoriesGridCellContext =>
- (cellContextKey in cellContext
- ? cellContext[cellContextKey]
- : {}) as LogCategoriesGridCellContext;
-
-export const createCellContext = (
- logCategories: LogCategory[],
- dependencies: LogCategoriesGridCellDependencies
-): { [cellContextKey]: LogCategoriesGridCellContext } => ({
- [cellContextKey]: {
- dependencies,
- logCategories,
- },
-});
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_time_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_time_cell.tsx
deleted file mode 100644
index 5ad8cbdd49346..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_time_cell.tsx
+++ /dev/null
@@ -1,54 +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 { EuiDataGridColumn } from '@elastic/eui';
-import { SettingsStart } from '@kbn/core-ui-settings-browser';
-import { i18n } from '@kbn/i18n';
-import moment from 'moment';
-import React, { useMemo } from 'react';
-import { LogCategory } from '../../types';
-
-export const logCategoriesGridChangeTimeColumn = {
- id: 'change_time' as const,
- display: i18n.translate(
- 'xpack.observabilityLogsOverview.logCategoriesGrid.changeTimeColumnLabel',
- {
- defaultMessage: 'Change at',
- }
- ),
- isSortable: true,
- initialWidth: 220,
- schema: 'datetime',
-} satisfies EuiDataGridColumn;
-
-export interface LogCategoriesGridChangeTimeCellProps {
- dependencies: LogCategoriesGridChangeTimeCellDependencies;
- logCategory: LogCategory;
-}
-
-export interface LogCategoriesGridChangeTimeCellDependencies {
- uiSettings: SettingsStart;
-}
-
-export const LogCategoriesGridChangeTimeCell: React.FC = ({
- dependencies,
- logCategory,
-}) => {
- const dateFormat = useMemo(
- () => dependencies.uiSettings.client.get('dateFormat'),
- [dependencies.uiSettings.client]
- );
- if (!('timestamp' in logCategory.change && logCategory.change.timestamp != null)) {
- return null;
- }
-
- if (dateFormat) {
- return <>{moment(logCategory.change.timestamp).format(dateFormat)}>;
- } else {
- return <>{logCategory.change.timestamp}>;
- }
-};
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_type_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_type_cell.tsx
deleted file mode 100644
index af6349bd0e18c..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_type_cell.tsx
+++ /dev/null
@@ -1,108 +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 { EuiBadge, EuiDataGridColumn } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import React from 'react';
-import { LogCategory } from '../../types';
-
-export const logCategoriesGridChangeTypeColumn = {
- id: 'change_type' as const,
- display: i18n.translate(
- 'xpack.observabilityLogsOverview.logCategoriesGrid.changeTypeColumnLabel',
- {
- defaultMessage: 'Change type',
- }
- ),
- isSortable: true,
- initialWidth: 110,
-} satisfies EuiDataGridColumn;
-
-export interface LogCategoriesGridChangeTypeCellProps {
- logCategory: LogCategory;
-}
-
-export const LogCategoriesGridChangeTypeCell: React.FC = ({
- logCategory,
-}) => {
- switch (logCategory.change.type) {
- case 'dip':
- return {dipBadgeLabel} ;
- case 'spike':
- return {spikeBadgeLabel} ;
- case 'step':
- return {stepBadgeLabel} ;
- case 'distribution':
- return {distributionBadgeLabel} ;
- case 'rare':
- return {rareBadgeLabel} ;
- case 'trend':
- return {trendBadgeLabel} ;
- case 'other':
- return {otherBadgeLabel} ;
- case 'none':
- return <>->;
- default:
- return {unknownBadgeLabel} ;
- }
-};
-
-const dipBadgeLabel = i18n.translate(
- 'xpack.observabilityLogsOverview.logCategories.dipChangeTypeBadgeLabel',
- {
- defaultMessage: 'Dip',
- }
-);
-
-const spikeBadgeLabel = i18n.translate(
- 'xpack.observabilityLogsOverview.logCategories.spikeChangeTypeBadgeLabel',
- {
- defaultMessage: 'Spike',
- }
-);
-
-const stepBadgeLabel = i18n.translate(
- 'xpack.observabilityLogsOverview.logCategories.spikeChangeTypeBadgeLabel',
- {
- defaultMessage: 'Step',
- }
-);
-
-const distributionBadgeLabel = i18n.translate(
- 'xpack.observabilityLogsOverview.logCategories.distributionChangeTypeBadgeLabel',
- {
- defaultMessage: 'Distribution',
- }
-);
-
-const trendBadgeLabel = i18n.translate(
- 'xpack.observabilityLogsOverview.logCategories.spikeChangeTypeBadgeLabel',
- {
- defaultMessage: 'Trend',
- }
-);
-
-const otherBadgeLabel = i18n.translate(
- 'xpack.observabilityLogsOverview.logCategories.otherChangeTypeBadgeLabel',
- {
- defaultMessage: 'Other',
- }
-);
-
-const unknownBadgeLabel = i18n.translate(
- 'xpack.observabilityLogsOverview.logCategories.unknownChangeTypeBadgeLabel',
- {
- defaultMessage: 'Unknown',
- }
-);
-
-const rareBadgeLabel = i18n.translate(
- 'xpack.observabilityLogsOverview.logCategories.rareChangeTypeBadgeLabel',
- {
- defaultMessage: 'Rare',
- }
-);
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_count_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_count_cell.tsx
deleted file mode 100644
index f2247aab5212e..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_count_cell.tsx
+++ /dev/null
@@ -1,32 +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 { EuiDataGridColumn } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import { FormattedNumber } from '@kbn/i18n-react';
-import React from 'react';
-import { LogCategory } from '../../types';
-
-export const logCategoriesGridCountColumn = {
- id: 'count' as const,
- display: i18n.translate('xpack.observabilityLogsOverview.logCategoriesGrid.countColumnLabel', {
- defaultMessage: 'Events',
- }),
- isSortable: true,
- schema: 'numeric',
- initialWidth: 100,
-} satisfies EuiDataGridColumn;
-
-export interface LogCategoriesGridCountCellProps {
- logCategory: LogCategory;
-}
-
-export const LogCategoriesGridCountCell: React.FC = ({
- logCategory,
-}) => {
- return ;
-};
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_histogram_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_histogram_cell.tsx
deleted file mode 100644
index 2fb50b0f2f3b4..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_histogram_cell.tsx
+++ /dev/null
@@ -1,99 +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 {
- BarSeries,
- Chart,
- LineAnnotation,
- LineAnnotationStyle,
- PartialTheme,
- Settings,
- Tooltip,
- TooltipType,
-} from '@elastic/charts';
-import { EuiDataGridColumn } from '@elastic/eui';
-import { ChartsPluginStart } from '@kbn/charts-plugin/public';
-import { i18n } from '@kbn/i18n';
-import { RecursivePartial } from '@kbn/utility-types';
-import React from 'react';
-import { LogCategory, LogCategoryHistogramBucket } from '../../types';
-
-export const logCategoriesGridHistoryColumn = {
- id: 'history' as const,
- display: i18n.translate(
- 'xpack.observabilityLogsOverview.logCategoriesGrid.histogramColumnLabel',
- {
- defaultMessage: 'Timeline',
- }
- ),
- isSortable: false,
- initialWidth: 250,
- isExpandable: false,
-} satisfies EuiDataGridColumn;
-
-export interface LogCategoriesGridHistogramCellProps {
- dependencies: LogCategoriesGridHistogramCellDependencies;
- logCategory: LogCategory;
-}
-
-export interface LogCategoriesGridHistogramCellDependencies {
- charts: ChartsPluginStart;
-}
-
-export const LogCategoriesGridHistogramCell: React.FC = ({
- dependencies: { charts },
- logCategory,
-}) => {
- const baseTheme = charts.theme.useChartsBaseTheme();
- const sparklineTheme = charts.theme.useSparklineOverrides();
-
- return (
-
-
-
-
- {'timestamp' in logCategory.change && logCategory.change.timestamp != null ? (
-
- ) : null}
-
- );
-};
-
-const localThemeOverrides: PartialTheme = {
- scales: {
- histogramPadding: 0.1,
- },
- background: {
- color: 'transparent',
- },
-};
-
-const annotationStyle: RecursivePartial = {
- line: {
- strokeWidth: 2,
- },
-};
-
-const timestampAccessor = (histogram: LogCategoryHistogramBucket) =>
- new Date(histogram.timestamp).getTime();
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_pattern_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_pattern_cell.tsx
deleted file mode 100644
index d507487a99e3c..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_pattern_cell.tsx
+++ /dev/null
@@ -1,60 +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 { EuiDataGridColumn, useEuiTheme } from '@elastic/eui';
-import { css } from '@emotion/react';
-import { i18n } from '@kbn/i18n';
-import React, { useMemo } from 'react';
-import { LogCategory } from '../../types';
-
-export const logCategoriesGridPatternColumn = {
- id: 'pattern' as const,
- display: i18n.translate('xpack.observabilityLogsOverview.logCategoriesGrid.patternColumnLabel', {
- defaultMessage: 'Pattern',
- }),
- isSortable: false,
- schema: 'string',
-} satisfies EuiDataGridColumn;
-
-export interface LogCategoriesGridPatternCellProps {
- logCategory: LogCategory;
-}
-
-export const LogCategoriesGridPatternCell: React.FC = ({
- logCategory,
-}) => {
- const theme = useEuiTheme();
- const { euiTheme } = theme;
- const termsList = useMemo(() => logCategory.terms.split(' '), [logCategory.terms]);
-
- const commonStyle = css`
- display: inline-block;
- font-family: ${euiTheme.font.familyCode};
- margin-right: ${euiTheme.size.xs};
- `;
-
- const termStyle = css`
- ${commonStyle};
- `;
-
- const separatorStyle = css`
- ${commonStyle};
- color: ${euiTheme.colors.successText};
- `;
-
- return (
-
- *
- {termsList.map((term, index) => (
-
- {term}
- *
-
- ))}
-
- );
-};
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_loading_content.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_loading_content.tsx
deleted file mode 100644
index 0fde469fe717d..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_loading_content.tsx
+++ /dev/null
@@ -1,68 +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 { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui';
-import React from 'react';
-import { i18n } from '@kbn/i18n';
-
-export interface LogCategoriesLoadingContentProps {
- onCancel?: () => void;
- stage: 'counting' | 'categorizing';
-}
-
-export const LogCategoriesLoadingContent: React.FC = ({
- onCancel,
- stage,
-}) => {
- return (
- }
- title={
-
- {stage === 'counting'
- ? logCategoriesLoadingStateCountingTitle
- : logCategoriesLoadingStateCategorizingTitle}
-
- }
- actions={
- onCancel != null
- ? [
- {
- onCancel();
- }}
- >
- {logCategoriesLoadingStateCancelButtonLabel}
- ,
- ]
- : []
- }
- />
- );
-};
-
-const logCategoriesLoadingStateCountingTitle = i18n.translate(
- 'xpack.observabilityLogsOverview.logCategoriesGrid.loadingStageCountingTitle',
- {
- defaultMessage: 'Estimating log volume',
- }
-);
-
-const logCategoriesLoadingStateCategorizingTitle = i18n.translate(
- 'xpack.observabilityLogsOverview.logCategoriesGrid.loadingStageCategorizingTitle',
- {
- defaultMessage: 'Categorizing logs',
- }
-);
-
-const logCategoriesLoadingStateCancelButtonLabel = i18n.translate(
- 'xpack.observabilityLogsOverview.logCategoriesGrid.loadingStateCancelButtonLabel',
- {
- defaultMessage: 'Cancel',
- }
-);
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_result_content.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_result_content.tsx
deleted file mode 100644
index e16bdda7cb44a..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_result_content.tsx
+++ /dev/null
@@ -1,87 +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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
-import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import React from 'react';
-import { LogCategory } from '../../types';
-import { IndexNameLogsSourceConfiguration } from '../../utils/logs_source';
-import {
- LogCategoriesControlBar,
- LogCategoriesControlBarDependencies,
-} from './log_categories_control_bar';
-import { LogCategoriesGrid, LogCategoriesGridDependencies } from './log_categories_grid';
-
-export interface LogCategoriesResultContentProps {
- dependencies: LogCategoriesResultContentDependencies;
- documentFilters?: QueryDslQueryContainer[];
- logCategories: LogCategory[];
- logsSource: IndexNameLogsSourceConfiguration;
- timeRange: {
- start: string;
- end: string;
- };
-}
-
-export type LogCategoriesResultContentDependencies = LogCategoriesControlBarDependencies &
- LogCategoriesGridDependencies;
-
-export const LogCategoriesResultContent: React.FC = ({
- dependencies,
- documentFilters,
- logCategories,
- logsSource,
- timeRange,
-}) => {
- if (logCategories.length === 0) {
- return ;
- } else {
- return (
-
-
-
-
-
-
-
-
- );
- }
-};
-
-export const LogCategoriesEmptyResultContent: React.FC = () => {
- return (
- {emptyResultContentDescription}
}
- color="subdued"
- layout="horizontal"
- title={{emptyResultContentTitle} }
- titleSize="m"
- />
- );
-};
-
-const emptyResultContentTitle = i18n.translate(
- 'xpack.observabilityLogsOverview.logCategories.emptyResultContentTitle',
- {
- defaultMessage: 'No log categories found',
- }
-);
-
-const emptyResultContentDescription = i18n.translate(
- 'xpack.observabilityLogsOverview.logCategories.emptyResultContentDescription',
- {
- defaultMessage:
- 'No suitable documents within the time range. Try searching for a longer time period.',
- }
-);
diff --git a/x-pack/packages/observability/logs_overview/src/components/logs_overview/index.ts b/x-pack/packages/observability/logs_overview/src/components/logs_overview/index.ts
deleted file mode 100644
index 878f634f078ad..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/components/logs_overview/index.ts
+++ /dev/null
@@ -1,10 +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.
- */
-
-export * from './logs_overview';
-export * from './logs_overview_error_content';
-export * from './logs_overview_loading_content';
diff --git a/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview.tsx b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview.tsx
deleted file mode 100644
index 988656eb1571e..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview.tsx
+++ /dev/null
@@ -1,64 +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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
-import { type LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public';
-import React from 'react';
-import useAsync from 'react-use/lib/useAsync';
-import { LogsSourceConfiguration, normalizeLogsSource } from '../../utils/logs_source';
-import { LogCategories, LogCategoriesDependencies } from '../log_categories';
-import { LogsOverviewErrorContent } from './logs_overview_error_content';
-import { LogsOverviewLoadingContent } from './logs_overview_loading_content';
-
-export interface LogsOverviewProps {
- dependencies: LogsOverviewDependencies;
- documentFilters?: QueryDslQueryContainer[];
- logsSource?: LogsSourceConfiguration;
- timeRange: {
- start: string;
- end: string;
- };
-}
-
-export type LogsOverviewDependencies = LogCategoriesDependencies & {
- logsDataAccess: LogsDataAccessPluginStart;
-};
-
-export const LogsOverview: React.FC = React.memo(
- ({
- dependencies,
- documentFilters = defaultDocumentFilters,
- logsSource = defaultLogsSource,
- timeRange,
- }) => {
- const normalizedLogsSource = useAsync(
- () => normalizeLogsSource({ logsDataAccess: dependencies.logsDataAccess })(logsSource),
- [dependencies.logsDataAccess, logsSource]
- );
-
- if (normalizedLogsSource.loading) {
- return ;
- }
-
- if (normalizedLogsSource.error != null || normalizedLogsSource.value == null) {
- return ;
- }
-
- return (
-
- );
- }
-);
-
-const defaultDocumentFilters: QueryDslQueryContainer[] = [];
-
-const defaultLogsSource: LogsSourceConfiguration = { type: 'shared_setting' };
diff --git a/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_error_content.tsx b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_error_content.tsx
deleted file mode 100644
index 73586756bb908..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_error_content.tsx
+++ /dev/null
@@ -1,41 +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 { EuiCodeBlock, EuiEmptyPrompt } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import React from 'react';
-
-export interface LogsOverviewErrorContentProps {
- error?: Error;
-}
-
-export const LogsOverviewErrorContent: React.FC = ({ error }) => {
- return (
- {logsOverviewErrorTitle}}
- body={
-
- {error?.stack ?? error?.toString() ?? unknownErrorDescription}
-
- }
- layout="vertical"
- />
- );
-};
-
-const logsOverviewErrorTitle = i18n.translate('xpack.observabilityLogsOverview.errorTitle', {
- defaultMessage: 'Error',
-});
-
-const unknownErrorDescription = i18n.translate(
- 'xpack.observabilityLogsOverview.unknownErrorDescription',
- {
- defaultMessage: 'An unspecified error occurred.',
- }
-);
diff --git a/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_loading_content.tsx b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_loading_content.tsx
deleted file mode 100644
index 7645fdb90f0ac..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_loading_content.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import React from 'react';
-
-export const LogsOverviewLoadingContent: React.FC = ({}) => {
- return (
- }
- title={{logsOverviewLoadingTitle} }
- />
- );
-};
-
-const logsOverviewLoadingTitle = i18n.translate('xpack.observabilityLogsOverview.loadingTitle', {
- defaultMessage: 'Loading',
-});
diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_documents.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_documents.ts
deleted file mode 100644
index 7260efe63d435..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_documents.ts
+++ /dev/null
@@ -1,282 +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 { ISearchGeneric } from '@kbn/search-types';
-import { lastValueFrom } from 'rxjs';
-import { fromPromise } from 'xstate5';
-import { createRandomSamplerWrapper } from '@kbn/ml-random-sampler-utils';
-import { z } from '@kbn/zod';
-import { LogCategorizationParams } from './types';
-import { createCategorizationRequestParams } from './queries';
-import { LogCategory, LogCategoryChange } from '../../types';
-
-// the fraction of a category's histogram below which the category is considered rare
-const rarityThreshold = 0.2;
-const maxCategoriesCount = 1000;
-
-export const categorizeDocuments = ({ search }: { search: ISearchGeneric }) =>
- fromPromise<
- {
- categories: LogCategory[];
- hasReachedLimit: boolean;
- },
- LogCategorizationParams & {
- samplingProbability: number;
- ignoredCategoryTerms: string[];
- minDocsPerCategory: number;
- }
- >(
- async ({
- input: {
- index,
- endTimestamp,
- startTimestamp,
- timeField,
- messageField,
- samplingProbability,
- ignoredCategoryTerms,
- documentFilters = [],
- minDocsPerCategory,
- },
- signal,
- }) => {
- const randomSampler = createRandomSamplerWrapper({
- probability: samplingProbability,
- seed: 1,
- });
-
- const requestParams = createCategorizationRequestParams({
- index,
- timeField,
- messageField,
- startTimestamp,
- endTimestamp,
- randomSampler,
- additionalFilters: documentFilters,
- ignoredCategoryTerms,
- minDocsPerCategory,
- maxCategoriesCount,
- });
-
- const { rawResponse } = await lastValueFrom(
- search({ params: requestParams }, { abortSignal: signal })
- );
-
- if (rawResponse.aggregations == null) {
- throw new Error('No aggregations found in large categories response');
- }
-
- const logCategoriesAggResult = randomSampler.unwrap(rawResponse.aggregations);
-
- if (!('categories' in logCategoriesAggResult)) {
- throw new Error('No categorization aggregation found in large categories response');
- }
-
- const logCategories =
- (logCategoriesAggResult.categories.buckets as unknown[]).map(mapCategoryBucket) ?? [];
-
- return {
- categories: logCategories,
- hasReachedLimit: logCategories.length >= maxCategoriesCount,
- };
- }
- );
-
-const mapCategoryBucket = (bucket: any): LogCategory =>
- esCategoryBucketSchema
- .transform((parsedBucket) => ({
- change: mapChangePoint(parsedBucket),
- documentCount: parsedBucket.doc_count,
- histogram: parsedBucket.histogram,
- terms: parsedBucket.key,
- }))
- .parse(bucket);
-
-const mapChangePoint = ({ change, histogram }: EsCategoryBucket): LogCategoryChange => {
- switch (change.type) {
- case 'stationary':
- if (isRareInHistogram(histogram)) {
- return {
- type: 'rare',
- timestamp: findFirstNonZeroBucket(histogram)?.timestamp ?? histogram[0].timestamp,
- };
- } else {
- return {
- type: 'none',
- };
- }
- case 'dip':
- case 'spike':
- return {
- type: change.type,
- timestamp: change.bucket.key,
- };
- case 'step_change':
- return {
- type: 'step',
- timestamp: change.bucket.key,
- };
- case 'distribution_change':
- return {
- type: 'distribution',
- timestamp: change.bucket.key,
- };
- case 'trend_change':
- return {
- type: 'trend',
- timestamp: change.bucket.key,
- correlationCoefficient: change.details.r_value,
- };
- case 'unknown':
- return {
- type: 'unknown',
- rawChange: change.rawChange,
- };
- case 'non_stationary':
- default:
- return {
- type: 'other',
- };
- }
-};
-
-/**
- * The official types are lacking the change_point aggregation
- */
-const esChangePointBucketSchema = z.object({
- key: z.string().datetime(),
- doc_count: z.number(),
-});
-
-const esChangePointDetailsSchema = z.object({
- p_value: z.number(),
-});
-
-const esChangePointCorrelationSchema = esChangePointDetailsSchema.extend({
- r_value: z.number(),
-});
-
-const esChangePointSchema = z.union([
- z
- .object({
- bucket: esChangePointBucketSchema,
- type: z.object({
- dip: esChangePointDetailsSchema,
- }),
- })
- .transform(({ bucket, type: { dip: details } }) => ({
- type: 'dip' as const,
- bucket,
- details,
- })),
- z
- .object({
- bucket: esChangePointBucketSchema,
- type: z.object({
- spike: esChangePointDetailsSchema,
- }),
- })
- .transform(({ bucket, type: { spike: details } }) => ({
- type: 'spike' as const,
- bucket,
- details,
- })),
- z
- .object({
- bucket: esChangePointBucketSchema,
- type: z.object({
- step_change: esChangePointDetailsSchema,
- }),
- })
- .transform(({ bucket, type: { step_change: details } }) => ({
- type: 'step_change' as const,
- bucket,
- details,
- })),
- z
- .object({
- bucket: esChangePointBucketSchema,
- type: z.object({
- trend_change: esChangePointCorrelationSchema,
- }),
- })
- .transform(({ bucket, type: { trend_change: details } }) => ({
- type: 'trend_change' as const,
- bucket,
- details,
- })),
- z
- .object({
- bucket: esChangePointBucketSchema,
- type: z.object({
- distribution_change: esChangePointDetailsSchema,
- }),
- })
- .transform(({ bucket, type: { distribution_change: details } }) => ({
- type: 'distribution_change' as const,
- bucket,
- details,
- })),
- z
- .object({
- type: z.object({
- non_stationary: esChangePointCorrelationSchema.extend({
- trend: z.enum(['increasing', 'decreasing']),
- }),
- }),
- })
- .transform(({ type: { non_stationary: details } }) => ({
- type: 'non_stationary' as const,
- details,
- })),
- z
- .object({
- type: z.object({
- stationary: z.object({}),
- }),
- })
- .transform(() => ({ type: 'stationary' as const })),
- z
- .object({
- type: z.object({}),
- })
- .transform((value) => ({ type: 'unknown' as const, rawChange: JSON.stringify(value) })),
-]);
-
-const esHistogramSchema = z
- .object({
- buckets: z.array(
- z
- .object({
- key_as_string: z.string(),
- doc_count: z.number(),
- })
- .transform((bucket) => ({
- timestamp: bucket.key_as_string,
- documentCount: bucket.doc_count,
- }))
- ),
- })
- .transform(({ buckets }) => buckets);
-
-type EsHistogram = z.output;
-
-const esCategoryBucketSchema = z.object({
- key: z.string(),
- doc_count: z.number(),
- change: esChangePointSchema,
- histogram: esHistogramSchema,
-});
-
-type EsCategoryBucket = z.output;
-
-const isRareInHistogram = (histogram: EsHistogram): boolean =>
- histogram.filter((bucket) => bucket.documentCount > 0).length <
- histogram.length * rarityThreshold;
-
-const findFirstNonZeroBucket = (histogram: EsHistogram) =>
- histogram.find((bucket) => bucket.documentCount > 0);
diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_logs_service.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_logs_service.ts
deleted file mode 100644
index deeb758d2d737..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_logs_service.ts
+++ /dev/null
@@ -1,250 +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 { MachineImplementationsFrom, assign, setup } from 'xstate5';
-import { LogCategory } from '../../types';
-import { getPlaceholderFor } from '../../utils/xstate5_utils';
-import { categorizeDocuments } from './categorize_documents';
-import { countDocuments } from './count_documents';
-import { CategorizeLogsServiceDependencies, LogCategorizationParams } from './types';
-
-export const categorizeLogsService = setup({
- types: {
- input: {} as LogCategorizationParams,
- output: {} as {
- categories: LogCategory[];
- documentCount: number;
- hasReachedLimit: boolean;
- samplingProbability: number;
- },
- context: {} as {
- categories: LogCategory[];
- documentCount: number;
- error?: Error;
- hasReachedLimit: boolean;
- parameters: LogCategorizationParams;
- samplingProbability: number;
- },
- events: {} as {
- type: 'cancel';
- },
- },
- actors: {
- countDocuments: getPlaceholderFor(countDocuments),
- categorizeDocuments: getPlaceholderFor(categorizeDocuments),
- },
- actions: {
- storeError: assign((_, params: { error: unknown }) => ({
- error: params.error instanceof Error ? params.error : new Error(String(params.error)),
- })),
- storeCategories: assign(
- ({ context }, params: { categories: LogCategory[]; hasReachedLimit: boolean }) => ({
- categories: [...context.categories, ...params.categories],
- hasReachedLimit: params.hasReachedLimit,
- })
- ),
- storeDocumentCount: assign(
- (_, params: { documentCount: number; samplingProbability: number }) => ({
- documentCount: params.documentCount,
- samplingProbability: params.samplingProbability,
- })
- ),
- },
- guards: {
- hasTooFewDocuments: (_guardArgs, params: { documentCount: number }) => params.documentCount < 1,
- requiresSampling: (_guardArgs, params: { samplingProbability: number }) =>
- params.samplingProbability < 1,
- },
-}).createMachine({
- /** @xstate-layout N4IgpgJg5mDOIC5QGMCGAXMUD2AnAlgF5gAy2UsAdMtgK4B26+9UAItsrQLZiOwDEEbPTCVmAN2wBrUWkw4CxMhWp1GzNh2690sBBI4Z8wgNoAGALrmLiUAAdssfE2G2QAD0QBmMwA5KACy+AQFmob4AjABMwQBsADQgAJ6IkYEAnJkA7FmxZlERmQGxAL4liXJYeESk5FQ0DEws7Jw8fILCogYy1BhVirUqDerNWm26+vSScsb01iYRNkggDk4u9G6eCD7+QSFhftFxiSkIvgCsWZSxEVlRsbFZ52Zm515lFX0KNcr1ak2aVo6ARCERiKbSWRfapKOqqRoaFraPiTaZGUyWExRJb2RzOWabbx+QLBULhI7FE7eWL+F45GnRPIRZkfECVb6wob-RFjYH8MC4XB4Sh2AA2GAAZnguL15DDBn8EaMgSiDDMMVZLG5VvjXMstjsSftyTFKclEOdzgFKF5zukvA8zBFnl50udWez5b94SNAcjdPw0PRkGBRdZtXj1oTtsS9mTDqaEuaEBF8udKFkIr5fK6olkzOksgEPdCBt6JWB0MgABYaADKqC4YsgAGFS-g4B0wd0oXKBg2m6LW+24OHljqo-rEMzbpQos8-K7fC9CknTrF0rEbbb0oVMoWIgF3eU2e3OVQK1XaywB82IG2+x2BAKhbgReL0FLcDLPf3G3eH36J8x1xNYCSnFNmSuecXhzdJlydTcqQQLJfHSOc0PyLJN3SMxYiPEtH3PShLxret-yHe8RwEIMQzDLVx0jcDQC2GdoIXOCENXZDsyiOcAiiKJ0iiPDLi8V1CKA4jSOvKAACUwC4VBmA0QDvk7UEughHpfxqBSlJUlg1OqUcGNA3UNggrMs347IjzdaIvGQwSvECXI8k3Z43gEiJJI5BUSMrMiWH05T6FU6j+UFYUxUlaVZSksBQsMqBjIIUycRWJi9RY6dIn8KIAjsu1zkc5CAmiG1fBiaIzB8B0QmPT4iICmSNGS8KjMi2jQxArKwJyjw8pswriocqInOTLwIi3ASD1yQpswCd5WXobAIDgNxdPPCMBss3KEAAWjXRBDvTfcLsu9Jlr8r04WGAEkXGeBGL26MBOQzIt2ut4cwmirCt8W6yzhNqbwo4dH0216LOjTMIjnBdYhK1DYgdHjihtZbUIdWIXJuYGflBoLZI6iKoZe8zJwOw9KtGt1kbuTcsmQrwi0oeCQjzZ5blwt1Cek5TKN22GIIKZbAgKC45pyLyeLwtz4Kyabs1QgWAs0kXqaGhBxdcnzpaE2XXmch0MORmaBJeLwjbKMogA */
- id: 'categorizeLogs',
- context: ({ input }) => ({
- categories: [],
- documentCount: 0,
- hasReachedLimit: false,
- parameters: input,
- samplingProbability: 1,
- }),
- initial: 'countingDocuments',
- states: {
- countingDocuments: {
- invoke: {
- src: 'countDocuments',
- input: ({ context }) => context.parameters,
- onDone: [
- {
- target: 'done',
- guard: {
- type: 'hasTooFewDocuments',
- params: ({ event }) => event.output,
- },
- actions: [
- {
- type: 'storeDocumentCount',
- params: ({ event }) => event.output,
- },
- ],
- },
- {
- target: 'fetchingSampledCategories',
- guard: {
- type: 'requiresSampling',
- params: ({ event }) => event.output,
- },
- actions: [
- {
- type: 'storeDocumentCount',
- params: ({ event }) => event.output,
- },
- ],
- },
- {
- target: 'fetchingRemainingCategories',
- actions: [
- {
- type: 'storeDocumentCount',
- params: ({ event }) => event.output,
- },
- ],
- },
- ],
- onError: {
- target: 'failed',
- actions: [
- {
- type: 'storeError',
- params: ({ event }) => ({ error: event.error }),
- },
- ],
- },
- },
-
- on: {
- cancel: {
- target: 'failed',
- actions: [
- {
- type: 'storeError',
- params: () => ({ error: new Error('Counting cancelled') }),
- },
- ],
- },
- },
- },
-
- fetchingSampledCategories: {
- invoke: {
- src: 'categorizeDocuments',
- id: 'categorizeSampledCategories',
- input: ({ context }) => ({
- ...context.parameters,
- samplingProbability: context.samplingProbability,
- ignoredCategoryTerms: [],
- minDocsPerCategory: 10,
- }),
- onDone: {
- target: 'fetchingRemainingCategories',
- actions: [
- {
- type: 'storeCategories',
- params: ({ event }) => event.output,
- },
- ],
- },
- onError: {
- target: 'failed',
- actions: [
- {
- type: 'storeError',
- params: ({ event }) => ({ error: event.error }),
- },
- ],
- },
- },
-
- on: {
- cancel: {
- target: 'failed',
- actions: [
- {
- type: 'storeError',
- params: () => ({ error: new Error('Categorization cancelled') }),
- },
- ],
- },
- },
- },
-
- fetchingRemainingCategories: {
- invoke: {
- src: 'categorizeDocuments',
- id: 'categorizeRemainingCategories',
- input: ({ context }) => ({
- ...context.parameters,
- samplingProbability: 1,
- ignoredCategoryTerms: context.categories.map((category) => category.terms),
- minDocsPerCategory: 0,
- }),
- onDone: {
- target: 'done',
- actions: [
- {
- type: 'storeCategories',
- params: ({ event }) => event.output,
- },
- ],
- },
- onError: {
- target: 'failed',
- actions: [
- {
- type: 'storeError',
- params: ({ event }) => ({ error: event.error }),
- },
- ],
- },
- },
-
- on: {
- cancel: {
- target: 'failed',
- actions: [
- {
- type: 'storeError',
- params: () => ({ error: new Error('Categorization cancelled') }),
- },
- ],
- },
- },
- },
-
- failed: {
- type: 'final',
- },
-
- done: {
- type: 'final',
- },
- },
- output: ({ context }) => ({
- categories: context.categories,
- documentCount: context.documentCount,
- hasReachedLimit: context.hasReachedLimit,
- samplingProbability: context.samplingProbability,
- }),
-});
-
-export const createCategorizeLogsServiceImplementations = ({
- search,
-}: CategorizeLogsServiceDependencies): MachineImplementationsFrom<
- typeof categorizeLogsService
-> => ({
- actors: {
- categorizeDocuments: categorizeDocuments({ search }),
- countDocuments: countDocuments({ search }),
- },
-});
diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/count_documents.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/count_documents.ts
deleted file mode 100644
index 359f9ddac2bd8..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/count_documents.ts
+++ /dev/null
@@ -1,60 +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 { getSampleProbability } from '@kbn/ml-random-sampler-utils';
-import { ISearchGeneric } from '@kbn/search-types';
-import { lastValueFrom } from 'rxjs';
-import { fromPromise } from 'xstate5';
-import { LogCategorizationParams } from './types';
-import { createCategorizationQuery } from './queries';
-
-export const countDocuments = ({ search }: { search: ISearchGeneric }) =>
- fromPromise<
- {
- documentCount: number;
- samplingProbability: number;
- },
- LogCategorizationParams
- >(
- async ({
- input: { index, endTimestamp, startTimestamp, timeField, messageField, documentFilters },
- signal,
- }) => {
- const { rawResponse: totalHitsResponse } = await lastValueFrom(
- search(
- {
- params: {
- index,
- size: 0,
- track_total_hits: true,
- query: createCategorizationQuery({
- messageField,
- timeField,
- startTimestamp,
- endTimestamp,
- additionalFilters: documentFilters,
- }),
- },
- },
- { abortSignal: signal }
- )
- );
-
- const documentCount =
- totalHitsResponse.hits.total == null
- ? 0
- : typeof totalHitsResponse.hits.total === 'number'
- ? totalHitsResponse.hits.total
- : totalHitsResponse.hits.total.value;
- const samplingProbability = getSampleProbability(documentCount);
-
- return {
- documentCount,
- samplingProbability,
- };
- }
- );
diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/index.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/index.ts
deleted file mode 100644
index 149359b7d2015..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/index.ts
+++ /dev/null
@@ -1,8 +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.
- */
-
-export * from './categorize_logs_service';
diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/queries.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/queries.ts
deleted file mode 100644
index aef12da303bcc..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/queries.ts
+++ /dev/null
@@ -1,151 +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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
-import { calculateAuto } from '@kbn/calculate-auto';
-import { RandomSamplerWrapper } from '@kbn/ml-random-sampler-utils';
-import moment from 'moment';
-
-const isoTimestampFormat = "YYYY-MM-DD'T'HH:mm:ss.SSS'Z'";
-
-export const createCategorizationQuery = ({
- messageField,
- timeField,
- startTimestamp,
- endTimestamp,
- additionalFilters = [],
- ignoredCategoryTerms = [],
-}: {
- messageField: string;
- timeField: string;
- startTimestamp: string;
- endTimestamp: string;
- additionalFilters?: QueryDslQueryContainer[];
- ignoredCategoryTerms?: string[];
-}): QueryDslQueryContainer => {
- return {
- bool: {
- filter: [
- {
- exists: {
- field: messageField,
- },
- },
- {
- range: {
- [timeField]: {
- gte: startTimestamp,
- lte: endTimestamp,
- format: 'strict_date_time',
- },
- },
- },
- ...additionalFilters,
- ],
- must_not: ignoredCategoryTerms.map(createCategoryQuery(messageField)),
- },
- };
-};
-
-export const createCategorizationRequestParams = ({
- index,
- timeField,
- messageField,
- startTimestamp,
- endTimestamp,
- randomSampler,
- minDocsPerCategory = 0,
- additionalFilters = [],
- ignoredCategoryTerms = [],
- maxCategoriesCount = 1000,
-}: {
- startTimestamp: string;
- endTimestamp: string;
- index: string;
- timeField: string;
- messageField: string;
- randomSampler: RandomSamplerWrapper;
- minDocsPerCategory?: number;
- additionalFilters?: QueryDslQueryContainer[];
- ignoredCategoryTerms?: string[];
- maxCategoriesCount?: number;
-}) => {
- const startMoment = moment(startTimestamp, isoTimestampFormat);
- const endMoment = moment(endTimestamp, isoTimestampFormat);
- const fixedIntervalDuration = calculateAuto.atLeast(
- 24,
- moment.duration(endMoment.diff(startMoment))
- );
- const fixedIntervalSize = `${Math.ceil(fixedIntervalDuration?.asMinutes() ?? 1)}m`;
-
- return {
- index,
- size: 0,
- track_total_hits: false,
- query: createCategorizationQuery({
- messageField,
- timeField,
- startTimestamp,
- endTimestamp,
- additionalFilters,
- ignoredCategoryTerms,
- }),
- aggs: randomSampler.wrap({
- histogram: {
- date_histogram: {
- field: timeField,
- fixed_interval: fixedIntervalSize,
- extended_bounds: {
- min: startTimestamp,
- max: endTimestamp,
- },
- },
- },
- categories: {
- categorize_text: {
- field: messageField,
- size: maxCategoriesCount,
- categorization_analyzer: {
- tokenizer: 'standard',
- },
- ...(minDocsPerCategory > 0 ? { min_doc_count: minDocsPerCategory } : {}),
- },
- aggs: {
- histogram: {
- date_histogram: {
- field: timeField,
- fixed_interval: fixedIntervalSize,
- extended_bounds: {
- min: startTimestamp,
- max: endTimestamp,
- },
- },
- },
- change: {
- // @ts-expect-error the official types don't support the change_point aggregation
- change_point: {
- buckets_path: 'histogram>_count',
- },
- },
- },
- },
- }),
- };
-};
-
-export const createCategoryQuery =
- (messageField: string) =>
- (categoryTerms: string): QueryDslQueryContainer => ({
- match: {
- [messageField]: {
- query: categoryTerms,
- operator: 'AND' as const,
- fuzziness: 0,
- auto_generate_synonyms_phrase_query: false,
- },
- },
- });
diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/types.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/types.ts
deleted file mode 100644
index e094317a98d62..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/types.ts
+++ /dev/null
@@ -1,21 +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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
-import { ISearchGeneric } from '@kbn/search-types';
-
-export interface CategorizeLogsServiceDependencies {
- search: ISearchGeneric;
-}
-
-export interface LogCategorizationParams {
- documentFilters: QueryDslQueryContainer[];
- endTimestamp: string;
- index: string;
- messageField: string;
- startTimestamp: string;
- timeField: string;
-}
diff --git a/x-pack/packages/observability/logs_overview/src/types.ts b/x-pack/packages/observability/logs_overview/src/types.ts
deleted file mode 100644
index 4c3d27eca7e7c..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/types.ts
+++ /dev/null
@@ -1,74 +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.
- */
-
-export interface LogCategory {
- change: LogCategoryChange;
- documentCount: number;
- histogram: LogCategoryHistogramBucket[];
- terms: string;
-}
-
-export type LogCategoryChange =
- | LogCategoryNoChange
- | LogCategoryRareChange
- | LogCategorySpikeChange
- | LogCategoryDipChange
- | LogCategoryStepChange
- | LogCategoryDistributionChange
- | LogCategoryTrendChange
- | LogCategoryOtherChange
- | LogCategoryUnknownChange;
-
-export interface LogCategoryNoChange {
- type: 'none';
-}
-
-export interface LogCategoryRareChange {
- type: 'rare';
- timestamp: string;
-}
-
-export interface LogCategorySpikeChange {
- type: 'spike';
- timestamp: string;
-}
-
-export interface LogCategoryDipChange {
- type: 'dip';
- timestamp: string;
-}
-
-export interface LogCategoryStepChange {
- type: 'step';
- timestamp: string;
-}
-
-export interface LogCategoryTrendChange {
- type: 'trend';
- timestamp: string;
- correlationCoefficient: number;
-}
-
-export interface LogCategoryDistributionChange {
- type: 'distribution';
- timestamp: string;
-}
-
-export interface LogCategoryOtherChange {
- type: 'other';
- timestamp?: string;
-}
-
-export interface LogCategoryUnknownChange {
- type: 'unknown';
- rawChange: string;
-}
-
-export interface LogCategoryHistogramBucket {
- documentCount: number;
- timestamp: string;
-}
diff --git a/x-pack/packages/observability/logs_overview/src/utils/logs_source.ts b/x-pack/packages/observability/logs_overview/src/utils/logs_source.ts
deleted file mode 100644
index 0c8767c8702d4..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/utils/logs_source.ts
+++ /dev/null
@@ -1,60 +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 AbstractDataView } from '@kbn/data-views-plugin/common';
-import { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public';
-
-export type LogsSourceConfiguration =
- | SharedSettingLogsSourceConfiguration
- | IndexNameLogsSourceConfiguration
- | DataViewLogsSourceConfiguration;
-
-export interface SharedSettingLogsSourceConfiguration {
- type: 'shared_setting';
- timestampField?: string;
- messageField?: string;
-}
-
-export interface IndexNameLogsSourceConfiguration {
- type: 'index_name';
- indexName: string;
- timestampField: string;
- messageField: string;
-}
-
-export interface DataViewLogsSourceConfiguration {
- type: 'data_view';
- dataView: AbstractDataView;
- messageField?: string;
-}
-
-export const normalizeLogsSource =
- ({ logsDataAccess }: { logsDataAccess: LogsDataAccessPluginStart }) =>
- async (logsSource: LogsSourceConfiguration): Promise => {
- switch (logsSource.type) {
- case 'index_name':
- return logsSource;
- case 'shared_setting':
- const logSourcesFromSharedSettings =
- await logsDataAccess.services.logSourcesService.getLogSources();
- return {
- type: 'index_name',
- indexName: logSourcesFromSharedSettings
- .map((logSource) => logSource.indexPattern)
- .join(','),
- timestampField: logsSource.timestampField ?? '@timestamp',
- messageField: logsSource.messageField ?? 'message',
- };
- case 'data_view':
- return {
- type: 'index_name',
- indexName: logsSource.dataView.getIndexPattern(),
- timestampField: logsSource.dataView.timeFieldName ?? '@timestamp',
- messageField: logsSource.messageField ?? 'message',
- };
- }
- };
diff --git a/x-pack/packages/observability/logs_overview/src/utils/xstate5_utils.ts b/x-pack/packages/observability/logs_overview/src/utils/xstate5_utils.ts
deleted file mode 100644
index 3df0bf4ea3988..0000000000000
--- a/x-pack/packages/observability/logs_overview/src/utils/xstate5_utils.ts
+++ /dev/null
@@ -1,13 +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.
- */
-
-export const getPlaceholderFor = any>(
- implementationFactory: ImplementationFactory
-): ReturnType =>
- (() => {
- throw new Error('Not implemented');
- }) as ReturnType;
diff --git a/x-pack/packages/observability/logs_overview/tsconfig.json b/x-pack/packages/observability/logs_overview/tsconfig.json
deleted file mode 100644
index 886062ae8855f..0000000000000
--- a/x-pack/packages/observability/logs_overview/tsconfig.json
+++ /dev/null
@@ -1,39 +0,0 @@
-{
- "extends": "../../../../tsconfig.base.json",
- "compilerOptions": {
- "outDir": "target/types",
- "types": [
- "jest",
- "node",
- "react",
- "@kbn/ambient-ui-types",
- "@kbn/ambient-storybook-types",
- "@emotion/react/types/css-prop"
- ]
- },
- "include": [
- "**/*.ts",
- "**/*.tsx",
- ],
- "exclude": [
- "target/**/*"
- ],
- "kbn_references": [
- "@kbn/data-views-plugin",
- "@kbn/i18n",
- "@kbn/search-types",
- "@kbn/xstate-utils",
- "@kbn/core-ui-settings-browser",
- "@kbn/i18n-react",
- "@kbn/charts-plugin",
- "@kbn/utility-types",
- "@kbn/logs-data-access-plugin",
- "@kbn/ml-random-sampler-utils",
- "@kbn/zod",
- "@kbn/calculate-auto",
- "@kbn/discover-plugin",
- "@kbn/es-query",
- "@kbn/router-utils",
- "@kbn/share-plugin",
- ]
-}
diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_logs/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/service_logs/index.tsx
index a1dadbf186b91..4df52758ceda3 100644
--- a/x-pack/plugins/observability_solution/apm/public/components/app/service_logs/index.tsx
+++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_logs/index.tsx
@@ -5,36 +5,19 @@
* 2.0.
*/
-import React, { useMemo } from 'react';
+import React from 'react';
import moment from 'moment';
-import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { LogStream } from '@kbn/logs-shared-plugin/public';
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
-import { CONTAINER_ID, SERVICE_ENVIRONMENT, SERVICE_NAME } from '../../../../common/es_fields/apm';
+import { useFetcher } from '../../../hooks/use_fetcher';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
-import { useKibana } from '../../../context/kibana_context/use_kibana';
+import { APIReturnType } from '../../../services/rest/create_call_apm_api';
+
+import { CONTAINER_ID, SERVICE_ENVIRONMENT, SERVICE_NAME } from '../../../../common/es_fields/apm';
import { useAnyOfApmParams } from '../../../hooks/use_apm_params';
-import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import { useTimeRange } from '../../../hooks/use_time_range';
-import { APIReturnType } from '../../../services/rest/create_call_apm_api';
export function ServiceLogs() {
- const {
- services: {
- logsShared: { LogsOverview },
- },
- } = useKibana();
-
- const isLogsOverviewEnabled = LogsOverview.useIsEnabled();
-
- if (isLogsOverviewEnabled) {
- return ;
- } else {
- return ;
- }
-}
-
-export function ClassicServiceLogsStream() {
const { serviceName } = useApmServiceContext();
const {
@@ -75,54 +58,6 @@ export function ClassicServiceLogsStream() {
);
}
-export function ServiceLogsOverview() {
- const {
- services: { logsShared },
- } = useKibana();
- const { serviceName } = useApmServiceContext();
- const {
- query: { environment, kuery, rangeFrom, rangeTo },
- } = useAnyOfApmParams('/services/{serviceName}/logs');
- const { start, end } = useTimeRange({ rangeFrom, rangeTo });
- const timeRange = useMemo(() => ({ start, end }), [start, end]);
-
- const { data: logFilters, status } = useFetcher(
- async (callApmApi) => {
- if (start == null || end == null) {
- return;
- }
-
- const { containerIds } = await callApmApi(
- 'GET /internal/apm/services/{serviceName}/infrastructure_attributes',
- {
- params: {
- path: { serviceName },
- query: {
- environment,
- kuery,
- start,
- end,
- },
- },
- }
- );
-
- return [getInfrastructureFilter({ containerIds, environment, serviceName })];
- },
- [environment, kuery, serviceName, start, end]
- );
-
- if (status === FETCH_STATUS.SUCCESS) {
- return ;
- } else if (status === FETCH_STATUS.FAILURE) {
- return (
-
- );
- } else {
- return ;
- }
-}
-
export function getInfrastructureKQLFilter({
data,
serviceName,
@@ -149,99 +84,3 @@ export function getInfrastructureKQLFilter({
return [serviceNameAndEnvironmentCorrelation, ...containerIdCorrelation].join(' or ');
}
-
-export function getInfrastructureFilter({
- containerIds,
- environment,
- serviceName,
-}: {
- containerIds: string[];
- environment: string;
- serviceName: string;
-}): QueryDslQueryContainer {
- return {
- bool: {
- should: [
- ...getServiceShouldClauses({ environment, serviceName }),
- ...getContainerShouldClauses({ containerIds }),
- ],
- minimum_should_match: 1,
- },
- };
-}
-
-export function getServiceShouldClauses({
- environment,
- serviceName,
-}: {
- environment: string;
- serviceName: string;
-}): QueryDslQueryContainer[] {
- const serviceNameFilter: QueryDslQueryContainer = {
- term: {
- [SERVICE_NAME]: serviceName,
- },
- };
-
- if (environment === ENVIRONMENT_ALL.value) {
- return [serviceNameFilter];
- } else {
- return [
- {
- bool: {
- filter: [
- serviceNameFilter,
- {
- term: {
- [SERVICE_ENVIRONMENT]: environment,
- },
- },
- ],
- },
- },
- {
- bool: {
- filter: [serviceNameFilter],
- must_not: [
- {
- exists: {
- field: SERVICE_ENVIRONMENT,
- },
- },
- ],
- },
- },
- ];
- }
-}
-
-export function getContainerShouldClauses({
- containerIds = [],
-}: {
- containerIds: string[];
-}): QueryDslQueryContainer[] {
- if (containerIds.length === 0) {
- return [];
- }
-
- return [
- {
- bool: {
- filter: [
- {
- terms: {
- [CONTAINER_ID]: containerIds,
- },
- },
- ],
- must_not: [
- {
- term: {
- [SERVICE_NAME]: '*',
- },
- },
- ],
- },
- },
- ];
-}
diff --git a/x-pack/plugins/observability_solution/apm/public/components/routing/service_detail/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/routing/service_detail/index.tsx
index 8a4a1c32877c5..d746e0464fd40 100644
--- a/x-pack/plugins/observability_solution/apm/public/components/routing/service_detail/index.tsx
+++ b/x-pack/plugins/observability_solution/apm/public/components/routing/service_detail/index.tsx
@@ -330,7 +330,7 @@ export const serviceDetailRoute = {
}),
element: ,
searchBarOptions: {
- showQueryInput: false,
+ showUnifiedSearchBar: false,
},
}),
'/services/{serviceName}/infrastructure': {
diff --git a/x-pack/plugins/observability_solution/apm/public/plugin.ts b/x-pack/plugins/observability_solution/apm/public/plugin.ts
index b21bdedac9ef8..9a9f45f42a39e 100644
--- a/x-pack/plugins/observability_solution/apm/public/plugin.ts
+++ b/x-pack/plugins/observability_solution/apm/public/plugin.ts
@@ -69,7 +69,6 @@ import { from } from 'rxjs';
import { map } from 'rxjs';
import type { CloudSetup } from '@kbn/cloud-plugin/public';
import type { ServerlessPluginStart } from '@kbn/serverless/public';
-import { LogsSharedClientStartExports } from '@kbn/logs-shared-plugin/public';
import type { ConfigSchema } from '.';
import { registerApmRuleTypes } from './components/alerting/rule_types/register_apm_rule_types';
import { registerEmbeddables } from './embeddable/register_embeddables';
@@ -143,7 +142,6 @@ export interface ApmPluginStartDeps {
dashboard: DashboardStart;
metricsDataAccess: MetricsDataPluginStart;
uiSettings: IUiSettingsClient;
- logsShared: LogsSharedClientStartExports;
}
const applicationsTitle = i18n.translate('xpack.apm.navigation.rootTitle', {
diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx b/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx
index 78443c9a6ec81..27344ccd1f108 100644
--- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx
+++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx
@@ -5,37 +5,21 @@
* 2.0.
*/
-import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import React, { useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { LogStream } from '@kbn/logs-shared-plugin/public';
-import React, { useMemo } from 'react';
+import { i18n } from '@kbn/i18n';
import { InfraLoadingPanel } from '../../../../../../components/loading';
-import { useKibanaContextForPlugin } from '../../../../../../hooks/use_kibana';
-import { useLogViewReference } from '../../../../../../hooks/use_log_view_reference';
-import { buildCombinedAssetFilter } from '../../../../../../utils/filters/build';
import { useHostsViewContext } from '../../../hooks/use_hosts_view';
-import { useLogsSearchUrlState } from '../../../hooks/use_logs_search_url_state';
import { useUnifiedSearchContext } from '../../../hooks/use_unified_search';
+import { useLogsSearchUrlState } from '../../../hooks/use_logs_search_url_state';
import { LogsLinkToStream } from './logs_link_to_stream';
import { LogsSearchBar } from './logs_search_bar';
+import { buildCombinedAssetFilter } from '../../../../../../utils/filters/build';
+import { useLogViewReference } from '../../../../../../hooks/use_log_view_reference';
export const LogsTabContent = () => {
- const {
- services: {
- logsShared: { LogsOverview },
- },
- } = useKibanaContextForPlugin();
- const isLogsOverviewEnabled = LogsOverview.useIsEnabled();
- if (isLogsOverviewEnabled) {
- return ;
- } else {
- return ;
- }
-};
-
-export const LogsTabLogStreamContent = () => {
const [filterQuery] = useLogsSearchUrlState();
const { getDateRangeAsTimestamp } = useUnifiedSearchContext();
const { from, to } = useMemo(() => getDateRangeAsTimestamp(), [getDateRangeAsTimestamp]);
@@ -69,7 +53,22 @@ export const LogsTabLogStreamContent = () => {
}, [filterQuery.query, hostNodes]);
if (loading || logViewLoading || !logView) {
- return ;
+ return (
+
+
+
+ }
+ />
+
+
+ );
}
return (
@@ -85,7 +84,6 @@ export const LogsTabLogStreamContent = () => {
query={logsLinkToStreamQuery}
logView={logView}
/>
- ]
@@ -114,53 +112,3 @@ const createHostsFilterQueryParam = (hostNodes: string[]): string => {
return hostsQueryParam;
};
-
-const LogsTabLogsOverviewContent = () => {
- const {
- services: {
- logsShared: { LogsOverview },
- },
- } = useKibanaContextForPlugin();
-
- const { parsedDateRange } = useUnifiedSearchContext();
- const timeRange = useMemo(
- () => ({ start: parsedDateRange.from, end: parsedDateRange.to }),
- [parsedDateRange.from, parsedDateRange.to]
- );
-
- const { hostNodes, loading, error } = useHostsViewContext();
- const logFilters = useMemo(
- () => [
- buildCombinedAssetFilter({
- field: 'host.name',
- values: hostNodes.map((p) => p.name),
- }).query as QueryDslQueryContainer,
- ],
- [hostNodes]
- );
-
- if (loading) {
- return ;
- } else if (error != null) {
- return ;
- } else {
- return ;
- }
-};
-
-const LogsTabLoadingContent = () => (
-
-
-
- }
- />
-
-
-);
diff --git a/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc b/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc
index 10c8fe32cfe9c..ea93fd326dac7 100644
--- a/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc
+++ b/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc
@@ -9,14 +9,13 @@
"browser": true,
"configPath": ["xpack", "logs_shared"],
"requiredPlugins": [
- "charts",
"data",
"dataViews",
"discoverShared",
- "logsDataAccess",
+ "usageCollection",
"observabilityShared",
"share",
- "usageCollection",
+ "logsDataAccess"
],
"optionalPlugins": [
"observabilityAIAssistant",
diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/index.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/index.tsx
deleted file mode 100644
index 627cdc8447eea..0000000000000
--- a/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/index.tsx
+++ /dev/null
@@ -1,8 +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.
- */
-
-export * from './logs_overview';
diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.mock.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.mock.tsx
deleted file mode 100644
index 435766bff793d..0000000000000
--- a/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.mock.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import type {
- LogsOverviewProps,
- SelfContainedLogsOverviewComponent,
- SelfContainedLogsOverviewHelpers,
-} from './logs_overview';
-
-export const createLogsOverviewMock = () => {
- const LogsOverviewMock = jest.fn(LogsOverviewMockImpl) as unknown as ILogsOverviewMock;
-
- LogsOverviewMock.useIsEnabled = jest.fn(() => true);
-
- LogsOverviewMock.ErrorContent = jest.fn(() =>
);
-
- LogsOverviewMock.LoadingContent = jest.fn(() =>
);
-
- return LogsOverviewMock;
-};
-
-const LogsOverviewMockImpl = (_props: LogsOverviewProps) => {
- return
;
-};
-
-type ILogsOverviewMock = jest.Mocked &
- jest.Mocked;
diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.tsx
deleted file mode 100644
index 7b60aee5be57c..0000000000000
--- a/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.tsx
+++ /dev/null
@@ -1,70 +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 { OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID } from '@kbn/management-settings-ids';
-import type {
- LogsOverviewProps as FullLogsOverviewProps,
- LogsOverviewDependencies,
- LogsOverviewErrorContentProps,
-} from '@kbn/observability-logs-overview';
-import { dynamic } from '@kbn/shared-ux-utility';
-import React from 'react';
-import useObservable from 'react-use/lib/useObservable';
-
-const LazyLogsOverview = dynamic(() =>
- import('@kbn/observability-logs-overview').then((mod) => ({ default: mod.LogsOverview }))
-);
-
-const LazyLogsOverviewErrorContent = dynamic(() =>
- import('@kbn/observability-logs-overview').then((mod) => ({
- default: mod.LogsOverviewErrorContent,
- }))
-);
-
-const LazyLogsOverviewLoadingContent = dynamic(() =>
- import('@kbn/observability-logs-overview').then((mod) => ({
- default: mod.LogsOverviewLoadingContent,
- }))
-);
-
-export type LogsOverviewProps = Omit;
-
-export interface SelfContainedLogsOverviewHelpers {
- useIsEnabled: () => boolean;
- ErrorContent: React.ComponentType;
- LoadingContent: React.ComponentType;
-}
-
-export type SelfContainedLogsOverviewComponent = React.ComponentType;
-
-export type SelfContainedLogsOverview = SelfContainedLogsOverviewComponent &
- SelfContainedLogsOverviewHelpers;
-
-export const createLogsOverview = (
- dependencies: LogsOverviewDependencies
-): SelfContainedLogsOverview => {
- const SelfContainedLogsOverview = (props: LogsOverviewProps) => {
- return ;
- };
-
- const isEnabled$ = dependencies.uiSettings.client.get$(
- OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID,
- defaultIsEnabled
- );
-
- SelfContainedLogsOverview.useIsEnabled = (): boolean => {
- return useObservable(isEnabled$, defaultIsEnabled);
- };
-
- SelfContainedLogsOverview.ErrorContent = LazyLogsOverviewErrorContent;
-
- SelfContainedLogsOverview.LoadingContent = LazyLogsOverviewLoadingContent;
-
- return SelfContainedLogsOverview;
-};
-
-const defaultIsEnabled = false;
diff --git a/x-pack/plugins/observability_solution/logs_shared/public/index.ts b/x-pack/plugins/observability_solution/logs_shared/public/index.ts
index 3d601c9936f2d..a602b25786116 100644
--- a/x-pack/plugins/observability_solution/logs_shared/public/index.ts
+++ b/x-pack/plugins/observability_solution/logs_shared/public/index.ts
@@ -50,7 +50,6 @@ export type {
UpdatedDateRange,
VisibleInterval,
} from './components/logging/log_text_stream/scrollable_log_text_stream_view';
-export type { LogsOverviewProps } from './components/logs_overview';
export const WithSummary = dynamic(() => import('./containers/logs/log_summary/with_summary'));
export const LogEntryFlyout = dynamic(
diff --git a/x-pack/plugins/observability_solution/logs_shared/public/mocks.tsx b/x-pack/plugins/observability_solution/logs_shared/public/mocks.tsx
index ffb867abbcc17..a9b0ebd6a6aa3 100644
--- a/x-pack/plugins/observability_solution/logs_shared/public/mocks.tsx
+++ b/x-pack/plugins/observability_solution/logs_shared/public/mocks.tsx
@@ -6,14 +6,12 @@
*/
import { createLogAIAssistantMock } from './components/log_ai_assistant/log_ai_assistant.mock';
-import { createLogsOverviewMock } from './components/logs_overview/logs_overview.mock';
import { createLogViewsServiceStartMock } from './services/log_views/log_views_service.mock';
import { LogsSharedClientStartExports } from './types';
export const createLogsSharedPluginStartMock = (): jest.Mocked => ({
logViews: createLogViewsServiceStartMock(),
LogAIAssistant: createLogAIAssistantMock(),
- LogsOverview: createLogsOverviewMock(),
});
export const _ensureTypeCompatibility = (): LogsSharedClientStartExports =>
diff --git a/x-pack/plugins/observability_solution/logs_shared/public/plugin.ts b/x-pack/plugins/observability_solution/logs_shared/public/plugin.ts
index fc17e9b17cc82..d6f4ac81fe266 100644
--- a/x-pack/plugins/observability_solution/logs_shared/public/plugin.ts
+++ b/x-pack/plugins/observability_solution/logs_shared/public/plugin.ts
@@ -12,7 +12,6 @@ import {
TraceLogsLocatorDefinition,
} from '../common/locators';
import { createLogAIAssistant, createLogsAIAssistantRenderer } from './components/log_ai_assistant';
-import { createLogsOverview } from './components/logs_overview';
import { LogViewsService } from './services/log_views';
import {
LogsSharedClientCoreSetup,
@@ -52,16 +51,8 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass {
}
public start(core: CoreStart, plugins: LogsSharedClientStartDeps) {
- const { http, settings } = core;
- const {
- charts,
- data,
- dataViews,
- discoverShared,
- logsDataAccess,
- observabilityAIAssistant,
- share,
- } = plugins;
+ const { http } = core;
+ const { data, dataViews, discoverShared, observabilityAIAssistant, logsDataAccess } = plugins;
const logViews = this.logViews.start({
http,
@@ -70,18 +61,9 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass {
search: data.search,
});
- const LogsOverview = createLogsOverview({
- charts,
- logsDataAccess,
- search: data.search.search,
- uiSettings: settings,
- share,
- });
-
if (!observabilityAIAssistant) {
return {
logViews,
- LogsOverview,
};
}
@@ -95,7 +77,6 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass {
return {
logViews,
LogAIAssistant,
- LogsOverview,
};
}
diff --git a/x-pack/plugins/observability_solution/logs_shared/public/types.ts b/x-pack/plugins/observability_solution/logs_shared/public/types.ts
index 4237c28c621b8..58b180ee8b6ef 100644
--- a/x-pack/plugins/observability_solution/logs_shared/public/types.ts
+++ b/x-pack/plugins/observability_solution/logs_shared/public/types.ts
@@ -5,19 +5,19 @@
* 2.0.
*/
-import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
import type { CoreSetup, CoreStart, Plugin as PluginClass } from '@kbn/core/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { DiscoverSharedPublicStart } from '@kbn/discover-shared-plugin/public';
-import type { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public';
+import { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public';
import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public';
import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
-import type { LogsSharedLocators } from '../common/locators';
+
+import { LogsSharedLocators } from '../common/locators';
import type { LogAIAssistantProps } from './components/log_ai_assistant/log_ai_assistant';
-import type { SelfContainedLogsOverview } from './components/logs_overview';
-import type { LogViewsServiceSetup, LogViewsServiceStart } from './services/log_views';
+// import type { OsqueryPluginStart } from '../../osquery/public';
+import { LogViewsServiceSetup, LogViewsServiceStart } from './services/log_views';
// Our own setup and start contract values
export interface LogsSharedClientSetupExports {
@@ -28,7 +28,6 @@ export interface LogsSharedClientSetupExports {
export interface LogsSharedClientStartExports {
logViews: LogViewsServiceStart;
LogAIAssistant?: (props: Omit) => JSX.Element;
- LogsOverview: SelfContainedLogsOverview;
}
export interface LogsSharedClientSetupDeps {
@@ -36,7 +35,6 @@ export interface LogsSharedClientSetupDeps {
}
export interface LogsSharedClientStartDeps {
- charts: ChartsPluginStart;
data: DataPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
discoverShared: DiscoverSharedPublicStart;
diff --git a/x-pack/plugins/observability_solution/logs_shared/server/feature_flags.ts b/x-pack/plugins/observability_solution/logs_shared/server/feature_flags.ts
deleted file mode 100644
index 0298416bd3f26..0000000000000
--- a/x-pack/plugins/observability_solution/logs_shared/server/feature_flags.ts
+++ /dev/null
@@ -1,33 +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 { schema } from '@kbn/config-schema';
-import { UiSettingsParams } from '@kbn/core-ui-settings-common';
-import { i18n } from '@kbn/i18n';
-import { OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID } from '@kbn/management-settings-ids';
-
-const technicalPreviewLabel = i18n.translate('xpack.logsShared.technicalPreviewSettingLabel', {
- defaultMessage: 'Technical Preview',
-});
-
-export const featureFlagUiSettings: Record = {
- [OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID]: {
- category: ['observability'],
- name: i18n.translate('xpack.logsShared.newLogsOverviewSettingName', {
- defaultMessage: 'New logs overview',
- }),
- value: false,
- description: i18n.translate('xpack.logsShared.newLogsOverviewSettingDescription', {
- defaultMessage: '{technicalPreviewLabel} Enable the new logs overview experience.',
-
- values: { technicalPreviewLabel: `[${technicalPreviewLabel}] ` },
- }),
- type: 'boolean',
- schema: schema.boolean(),
- requiresPageReload: true,
- },
-};
diff --git a/x-pack/plugins/observability_solution/logs_shared/server/plugin.ts b/x-pack/plugins/observability_solution/logs_shared/server/plugin.ts
index d1f6399104fc2..7c97e175ed64f 100644
--- a/x-pack/plugins/observability_solution/logs_shared/server/plugin.ts
+++ b/x-pack/plugins/observability_solution/logs_shared/server/plugin.ts
@@ -5,19 +5,8 @@
* 2.0.
*/
-import { CoreStart, Logger, Plugin, PluginInitializerContext } from '@kbn/core/server';
-import { defaultLogViewId } from '../common/log_views';
-import { LogsSharedConfig } from '../common/plugin_config';
-import { registerDeprecations } from './deprecations';
-import { featureFlagUiSettings } from './feature_flags';
-import { KibanaFramework } from './lib/adapters/framework/kibana_framework_adapter';
-import { LogsSharedKibanaLogEntriesAdapter } from './lib/adapters/log_entries/kibana_log_entries_adapter';
-import { LogsSharedLogEntriesDomain } from './lib/domains/log_entries_domain';
-import { LogsSharedBackendLibs, LogsSharedDomainLibs } from './lib/logs_shared_types';
-import { initLogsSharedServer } from './logs_shared_server';
-import { logViewSavedObjectType } from './saved_objects';
-import { LogEntriesService } from './services/log_entries';
-import { LogViewsService } from './services/log_views';
+import { PluginInitializerContext, CoreStart, Plugin, Logger } from '@kbn/core/server';
+
import {
LogsSharedPluginCoreSetup,
LogsSharedPluginSetup,
@@ -26,6 +15,17 @@ import {
LogsSharedServerPluginStartDeps,
UsageCollector,
} from './types';
+import { logViewSavedObjectType } from './saved_objects';
+import { initLogsSharedServer } from './logs_shared_server';
+import { LogViewsService } from './services/log_views';
+import { KibanaFramework } from './lib/adapters/framework/kibana_framework_adapter';
+import { LogsSharedBackendLibs, LogsSharedDomainLibs } from './lib/logs_shared_types';
+import { LogsSharedLogEntriesDomain } from './lib/domains/log_entries_domain';
+import { LogsSharedKibanaLogEntriesAdapter } from './lib/adapters/log_entries/kibana_log_entries_adapter';
+import { LogEntriesService } from './services/log_entries';
+import { LogsSharedConfig } from '../common/plugin_config';
+import { registerDeprecations } from './deprecations';
+import { defaultLogViewId } from '../common/log_views';
export class LogsSharedPlugin
implements
@@ -88,8 +88,6 @@ export class LogsSharedPlugin
registerDeprecations({ core });
- core.uiSettings.register(featureFlagUiSettings);
-
return {
...domainLibs,
logViews,
diff --git a/x-pack/plugins/observability_solution/logs_shared/tsconfig.json b/x-pack/plugins/observability_solution/logs_shared/tsconfig.json
index 788f55c9b6fc5..38cbba7c252c0 100644
--- a/x-pack/plugins/observability_solution/logs_shared/tsconfig.json
+++ b/x-pack/plugins/observability_solution/logs_shared/tsconfig.json
@@ -44,9 +44,5 @@
"@kbn/logs-data-access-plugin",
"@kbn/core-deprecations-common",
"@kbn/core-deprecations-server",
- "@kbn/management-settings-ids",
- "@kbn/observability-logs-overview",
- "@kbn/charts-plugin",
- "@kbn/core-ui-settings-common",
]
}
diff --git a/yarn.lock b/yarn.lock
index 019de6121540e..54a38b2c0e5d3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5879,10 +5879,6 @@
version "0.0.0"
uid ""
-"@kbn/observability-logs-overview@link:x-pack/packages/observability/logs_overview":
- version "0.0.0"
- uid ""
-
"@kbn/observability-onboarding-e2e@link:x-pack/plugins/observability_solution/observability_onboarding/e2e":
version "0.0.0"
uid ""
@@ -12109,14 +12105,6 @@
use-isomorphic-layout-effect "^1.1.2"
use-sync-external-store "^1.0.0"
-"@xstate5/react@npm:@xstate/react@^4.1.2":
- version "4.1.2"
- resolved "https://registry.yarnpkg.com/@xstate/react/-/react-4.1.2.tgz#4bfcdf2d9e9ef1eaea7388d1896649345e6679cd"
- integrity sha512-orAidFrKCrU0ZwN5l/ABPlBfW2ziRDT2RrYoktRlZ0WRoLvA2E/uAC1JpZt43mCLtc8jrdwYCgJiqx1V8NvGTw==
- dependencies:
- use-isomorphic-layout-effect "^1.1.2"
- use-sync-external-store "^1.2.0"
-
"@xtuc/ieee754@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
@@ -32812,11 +32800,6 @@ xpath@^0.0.33:
resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.33.tgz#5136b6094227c5df92002e7c3a13516a5074eb07"
integrity sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA==
-"xstate5@npm:xstate@^5.18.1", xstate@^5.18.1:
- version "5.18.1"
- resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.18.1.tgz#c4d43ceaba6e6c31705d36bd96e285de4be4f7f4"
- integrity sha512-m02IqcCQbaE/kBQLunwub/5i8epvkD2mFutnL17Oeg1eXTShe1sRF4D5mhv1dlaFO4vbW5gRGRhraeAD5c938g==
-
xstate@^4.38.2:
version "4.38.2"
resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.2.tgz#1b74544fc9c8c6c713ba77f81c6017e65aa89804"
From bd6533f30b58fc831670d400f25a61321379902c Mon Sep 17 00:00:00 2001
From: Jen Huang
Date: Wed, 9 Oct 2024 13:08:18 -0700
Subject: [PATCH 16/87] [UII] Add types to return content packages correctly
(#195505)
## Summary
Related to #192484. This PR adding [new content package types and
schemas](https://github.com/elastic/package-spec/pull/777) so that
content packages can be returned correctly from EPR to unblock
development of those packages.
The only current content package is `kubernetes_otel`. You will need to
bump up the max allowed spec version and search with beta (prerelease)
packages enabled to find it:
```
xpack.fleet.internal.registry.spec.max: '3.4'
```
Tests will come with the rest of work for #192484
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
oas_docs/bundle.json | 144 +++++++++++++++++-
oas_docs/bundle.serverless.json | 144 +++++++++++++++++-
.../output/kibana.serverless.staging.yaml | 90 +++++++++++
oas_docs/output/kibana.serverless.yaml | 90 +++++++++++
oas_docs/output/kibana.staging.yaml | 90 +++++++++++
oas_docs/output/kibana.yaml | 90 +++++++++++
.../fleet/common/types/models/package_spec.ts | 9 +-
.../fleet/server/types/rest_spec/epm.ts | 13 +-
8 files changed, 655 insertions(+), 15 deletions(-)
diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json
index 6cc3990de1b51..e52362ff13a6a 100644
--- a/oas_docs/bundle.json
+++ b/oas_docs/bundle.json
@@ -19328,6 +19328,27 @@
"description": {
"type": "string"
},
+ "discovery": {
+ "additionalProperties": true,
+ "properties": {
+ "fields": {
+ "items": {
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "type": "object"
+ },
"download": {
"type": "string"
},
@@ -19716,7 +19737,8 @@
"type": {
"enum": [
"integration",
- "input"
+ "input",
+ "content"
],
"type": "string"
},
@@ -19793,6 +19815,27 @@
"description": {
"type": "string"
},
+ "discovery": {
+ "additionalProperties": true,
+ "properties": {
+ "fields": {
+ "items": {
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "type": "object"
+ },
"download": {
"type": "string"
},
@@ -20181,7 +20224,8 @@
"type": {
"enum": [
"integration",
- "input"
+ "input",
+ "content"
],
"type": "string"
},
@@ -21769,6 +21813,27 @@
"description": {
"type": "string"
},
+ "discovery": {
+ "additionalProperties": true,
+ "properties": {
+ "fields": {
+ "items": {
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "type": "object"
+ },
"download": {
"type": "string"
},
@@ -22197,7 +22262,8 @@
"type": {
"enum": [
"integration",
- "input"
+ "input",
+ "content"
],
"type": "string"
},
@@ -22329,6 +22395,27 @@
"description": {
"type": "string"
},
+ "discovery": {
+ "additionalProperties": true,
+ "properties": {
+ "fields": {
+ "items": {
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "type": "object"
+ },
"download": {
"type": "string"
},
@@ -22757,7 +22844,8 @@
"type": {
"enum": [
"integration",
- "input"
+ "input",
+ "content"
],
"type": "string"
},
@@ -23279,6 +23367,27 @@
"description": {
"type": "string"
},
+ "discovery": {
+ "additionalProperties": true,
+ "properties": {
+ "fields": {
+ "items": {
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "type": "object"
+ },
"download": {
"type": "string"
},
@@ -23707,7 +23816,8 @@
"type": {
"enum": [
"integration",
- "input"
+ "input",
+ "content"
],
"type": "string"
},
@@ -23827,6 +23937,27 @@
"description": {
"type": "string"
},
+ "discovery": {
+ "additionalProperties": true,
+ "properties": {
+ "fields": {
+ "items": {
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "type": "object"
+ },
"download": {
"type": "string"
},
@@ -24255,7 +24386,8 @@
"type": {
"enum": [
"integration",
- "input"
+ "input",
+ "content"
],
"type": "string"
},
diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json
index 6fcc247e1fb22..531ab412ce1bf 100644
--- a/oas_docs/bundle.serverless.json
+++ b/oas_docs/bundle.serverless.json
@@ -19328,6 +19328,27 @@
"description": {
"type": "string"
},
+ "discovery": {
+ "additionalProperties": true,
+ "properties": {
+ "fields": {
+ "items": {
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "type": "object"
+ },
"download": {
"type": "string"
},
@@ -19716,7 +19737,8 @@
"type": {
"enum": [
"integration",
- "input"
+ "input",
+ "content"
],
"type": "string"
},
@@ -19793,6 +19815,27 @@
"description": {
"type": "string"
},
+ "discovery": {
+ "additionalProperties": true,
+ "properties": {
+ "fields": {
+ "items": {
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "type": "object"
+ },
"download": {
"type": "string"
},
@@ -20181,7 +20224,8 @@
"type": {
"enum": [
"integration",
- "input"
+ "input",
+ "content"
],
"type": "string"
},
@@ -21769,6 +21813,27 @@
"description": {
"type": "string"
},
+ "discovery": {
+ "additionalProperties": true,
+ "properties": {
+ "fields": {
+ "items": {
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "type": "object"
+ },
"download": {
"type": "string"
},
@@ -22197,7 +22262,8 @@
"type": {
"enum": [
"integration",
- "input"
+ "input",
+ "content"
],
"type": "string"
},
@@ -22329,6 +22395,27 @@
"description": {
"type": "string"
},
+ "discovery": {
+ "additionalProperties": true,
+ "properties": {
+ "fields": {
+ "items": {
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "type": "object"
+ },
"download": {
"type": "string"
},
@@ -22757,7 +22844,8 @@
"type": {
"enum": [
"integration",
- "input"
+ "input",
+ "content"
],
"type": "string"
},
@@ -23279,6 +23367,27 @@
"description": {
"type": "string"
},
+ "discovery": {
+ "additionalProperties": true,
+ "properties": {
+ "fields": {
+ "items": {
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "type": "object"
+ },
"download": {
"type": "string"
},
@@ -23707,7 +23816,8 @@
"type": {
"enum": [
"integration",
- "input"
+ "input",
+ "content"
],
"type": "string"
},
@@ -23827,6 +23937,27 @@
"description": {
"type": "string"
},
+ "discovery": {
+ "additionalProperties": true,
+ "properties": {
+ "fields": {
+ "items": {
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "type": "object"
+ },
"download": {
"type": "string"
},
@@ -24255,7 +24386,8 @@
"type": {
"enum": [
"integration",
- "input"
+ "input",
+ "content"
],
"type": "string"
},
diff --git a/oas_docs/output/kibana.serverless.staging.yaml b/oas_docs/output/kibana.serverless.staging.yaml
index bd2f5c597ecc4..69b783c6ccc44 100644
--- a/oas_docs/output/kibana.serverless.staging.yaml
+++ b/oas_docs/output/kibana.serverless.staging.yaml
@@ -18990,6 +18990,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
format_version:
@@ -19270,6 +19284,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
@@ -19322,6 +19337,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
format_version:
@@ -19602,6 +19631,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
@@ -20574,6 +20604,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
elasticsearch:
@@ -20881,6 +20925,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
@@ -20970,6 +21015,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
elasticsearch:
@@ -21277,6 +21336,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
@@ -21629,6 +21689,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
elasticsearch:
@@ -21936,6 +22010,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
@@ -22017,6 +22092,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
elasticsearch:
@@ -22324,6 +22413,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml
index bd2f5c597ecc4..69b783c6ccc44 100644
--- a/oas_docs/output/kibana.serverless.yaml
+++ b/oas_docs/output/kibana.serverless.yaml
@@ -18990,6 +18990,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
format_version:
@@ -19270,6 +19284,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
@@ -19322,6 +19337,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
format_version:
@@ -19602,6 +19631,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
@@ -20574,6 +20604,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
elasticsearch:
@@ -20881,6 +20925,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
@@ -20970,6 +21015,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
elasticsearch:
@@ -21277,6 +21336,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
@@ -21629,6 +21689,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
elasticsearch:
@@ -21936,6 +22010,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
@@ -22017,6 +22092,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
elasticsearch:
@@ -22324,6 +22413,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
diff --git a/oas_docs/output/kibana.staging.yaml b/oas_docs/output/kibana.staging.yaml
index 544315cd12646..bc0828a44b619 100644
--- a/oas_docs/output/kibana.staging.yaml
+++ b/oas_docs/output/kibana.staging.yaml
@@ -22419,6 +22419,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
format_version:
@@ -22699,6 +22713,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
@@ -22751,6 +22766,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
format_version:
@@ -23031,6 +23060,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
@@ -24003,6 +24033,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
elasticsearch:
@@ -24310,6 +24354,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
@@ -24399,6 +24444,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
elasticsearch:
@@ -24706,6 +24765,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
@@ -25058,6 +25118,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
elasticsearch:
@@ -25365,6 +25439,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
@@ -25446,6 +25521,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
elasticsearch:
@@ -25753,6 +25842,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml
index 544315cd12646..bc0828a44b619 100644
--- a/oas_docs/output/kibana.yaml
+++ b/oas_docs/output/kibana.yaml
@@ -22419,6 +22419,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
format_version:
@@ -22699,6 +22713,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
@@ -22751,6 +22766,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
format_version:
@@ -23031,6 +23060,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
@@ -24003,6 +24033,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
elasticsearch:
@@ -24310,6 +24354,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
@@ -24399,6 +24444,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
elasticsearch:
@@ -24706,6 +24765,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
@@ -25058,6 +25118,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
elasticsearch:
@@ -25365,6 +25439,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
@@ -25446,6 +25521,20 @@ paths:
type: array
description:
type: string
+ discovery:
+ additionalProperties: true
+ type: object
+ properties:
+ fields:
+ items:
+ additionalProperties: true
+ type: object
+ properties:
+ name:
+ type: string
+ required:
+ - name
+ type: array
download:
type: string
elasticsearch:
@@ -25753,6 +25842,7 @@ paths:
enum:
- integration
- input
+ - content
type: string
vars:
items:
diff --git a/x-pack/plugins/fleet/common/types/models/package_spec.ts b/x-pack/plugins/fleet/common/types/models/package_spec.ts
index 24a592490137c..18c10e4617417 100644
--- a/x-pack/plugins/fleet/common/types/models/package_spec.ts
+++ b/x-pack/plugins/fleet/common/types/models/package_spec.ts
@@ -18,7 +18,7 @@ export interface PackageSpecManifest {
source?: {
license: string;
};
- type?: 'integration' | 'input';
+ type?: PackageSpecPackageType;
release?: 'experimental' | 'beta' | 'ga';
categories?: Array;
conditions?: PackageSpecConditions;
@@ -35,6 +35,11 @@ export interface PackageSpecManifest {
privileges?: { root?: boolean };
};
asset_tags?: PackageSpecTags[];
+ discovery?: {
+ fields?: Array<{
+ name: string;
+ }>;
+ };
}
export interface PackageSpecTags {
text: string;
@@ -42,7 +47,7 @@ export interface PackageSpecTags {
asset_ids?: string[];
}
-export type PackageSpecPackageType = 'integration' | 'input';
+export type PackageSpecPackageType = 'integration' | 'input' | 'content';
export type PackageSpecCategory =
| 'advanced_analytics_ueba'
diff --git a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts
index 2dc9606a5432d..f08ccd9ff1248 100644
--- a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts
+++ b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts
@@ -163,7 +163,13 @@ export const PackageInfoSchema = schema
release: schema.maybe(
schema.oneOf([schema.literal('ga'), schema.literal('beta'), schema.literal('experimental')])
),
- type: schema.maybe(schema.oneOf([schema.literal('integration'), schema.literal('input')])),
+ type: schema.maybe(
+ schema.oneOf([
+ schema.literal('integration'),
+ schema.literal('input'),
+ schema.literal('content'),
+ ])
+ ),
path: schema.maybe(schema.string()),
download: schema.maybe(schema.string()),
internal: schema.maybe(schema.boolean()),
@@ -192,6 +198,11 @@ export const PackageInfoSchema = schema
format_version: schema.maybe(schema.string()),
vars: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))),
latestVersion: schema.maybe(schema.string()),
+ discovery: schema.maybe(
+ schema.object({
+ fields: schema.maybe(schema.arrayOf(schema.object({ name: schema.string() }))),
+ })
+ ),
})
// sometimes package list response contains extra properties, e.g. installed_kibana
.extendsDeep({
From 554ec2e321b6245a56224e0f65ef0fa70bb5425d Mon Sep 17 00:00:00 2001
From: Robert Oskamp
Date: Wed, 9 Oct 2024 22:28:19 +0200
Subject: [PATCH 17/87] Tag pipelines related to Kibana serverless release
(#195631)
## Summary
This PR tags all pipelines that are related to the Kibana serverless
release (including requirements like on-merge and artifacts build) with
`kibana-serverless-release`. This will allow us to easily find these
pipelines in Buildkite.
---
.../kibana-artifacts-container-image.yml | 1 +
.../kibana-es-serverless-snapshots.yml | 1 +
.buildkite/pipeline-resource-definitions/kibana-on-merge.yml | 1 +
.../kibana-serverless-emergency-release.yml | 1 +
.../kibana-serverless-quality-gates-emergency.yml | 1 +
.../kibana-serverless-quality-gates.yml | 1 +
.../kibana-serverless-release-testing.yml | 1 +
.../pipeline-resource-definitions/kibana-serverless-release.yml | 1 +
8 files changed, 8 insertions(+)
diff --git a/.buildkite/pipeline-resource-definitions/kibana-artifacts-container-image.yml b/.buildkite/pipeline-resource-definitions/kibana-artifacts-container-image.yml
index 37bc5ee59ff0b..eb86f8d7aab2a 100644
--- a/.buildkite/pipeline-resource-definitions/kibana-artifacts-container-image.yml
+++ b/.buildkite/pipeline-resource-definitions/kibana-artifacts-container-image.yml
@@ -44,3 +44,4 @@ spec:
access_level: MANAGE_BUILD_AND_READ
tags:
- kibana
+ - kibana-serverless-release
diff --git a/.buildkite/pipeline-resource-definitions/kibana-es-serverless-snapshots.yml b/.buildkite/pipeline-resource-definitions/kibana-es-serverless-snapshots.yml
index 684e2e07fb187..6ba182ccd393e 100644
--- a/.buildkite/pipeline-resource-definitions/kibana-es-serverless-snapshots.yml
+++ b/.buildkite/pipeline-resource-definitions/kibana-es-serverless-snapshots.yml
@@ -55,3 +55,4 @@ spec:
branch: main
tags:
- kibana
+ - kibana-serverless-release
diff --git a/.buildkite/pipeline-resource-definitions/kibana-on-merge.yml b/.buildkite/pipeline-resource-definitions/kibana-on-merge.yml
index 5e6622e6da513..e524adc786c0e 100644
--- a/.buildkite/pipeline-resource-definitions/kibana-on-merge.yml
+++ b/.buildkite/pipeline-resource-definitions/kibana-on-merge.yml
@@ -49,3 +49,4 @@ spec:
access_level: MANAGE_BUILD_AND_READ
tags:
- kibana
+ - kibana-serverless-release
diff --git a/.buildkite/pipeline-resource-definitions/kibana-serverless-emergency-release.yml b/.buildkite/pipeline-resource-definitions/kibana-serverless-emergency-release.yml
index c51e44432596d..62b05bc49dae6 100644
--- a/.buildkite/pipeline-resource-definitions/kibana-serverless-emergency-release.yml
+++ b/.buildkite/pipeline-resource-definitions/kibana-serverless-emergency-release.yml
@@ -30,3 +30,4 @@ spec:
access_level: READ_ONLY
tags:
- kibana
+ - kibana-serverless-release
diff --git a/.buildkite/pipeline-resource-definitions/kibana-serverless-quality-gates-emergency.yml b/.buildkite/pipeline-resource-definitions/kibana-serverless-quality-gates-emergency.yml
index 267db48ba6d90..ef04fd324b31a 100644
--- a/.buildkite/pipeline-resource-definitions/kibana-serverless-quality-gates-emergency.yml
+++ b/.buildkite/pipeline-resource-definitions/kibana-serverless-quality-gates-emergency.yml
@@ -33,3 +33,4 @@ spec:
access_level: READ_ONLY
tags:
- kibana
+ - kibana-serverless-release
diff --git a/.buildkite/pipeline-resource-definitions/kibana-serverless-quality-gates.yml b/.buildkite/pipeline-resource-definitions/kibana-serverless-quality-gates.yml
index 8d4e7f35cd6fe..e9ea3d02b8968 100644
--- a/.buildkite/pipeline-resource-definitions/kibana-serverless-quality-gates.yml
+++ b/.buildkite/pipeline-resource-definitions/kibana-serverless-quality-gates.yml
@@ -33,3 +33,4 @@ spec:
access_level: READ_ONLY
tags:
- kibana
+ - kibana-serverless-release
diff --git a/.buildkite/pipeline-resource-definitions/kibana-serverless-release-testing.yml b/.buildkite/pipeline-resource-definitions/kibana-serverless-release-testing.yml
index 0a033e72d53b8..5276871fa1c9f 100644
--- a/.buildkite/pipeline-resource-definitions/kibana-serverless-release-testing.yml
+++ b/.buildkite/pipeline-resource-definitions/kibana-serverless-release-testing.yml
@@ -46,3 +46,4 @@ spec:
access_level: MANAGE_BUILD_AND_READ
tags:
- kibana
+ - kibana-serverless-release
diff --git a/.buildkite/pipeline-resource-definitions/kibana-serverless-release.yml b/.buildkite/pipeline-resource-definitions/kibana-serverless-release.yml
index 7a35ea3ad1ec8..e1457f10420f7 100644
--- a/.buildkite/pipeline-resource-definitions/kibana-serverless-release.yml
+++ b/.buildkite/pipeline-resource-definitions/kibana-serverless-release.yml
@@ -48,3 +48,4 @@ spec:
branch: main
tags:
- kibana
+ - kibana-serverless-release
From 9221ab19e86ca7d3215205110fc709f7ba4739af Mon Sep 17 00:00:00 2001
From: Ying Mao
Date: Wed, 9 Oct 2024 17:01:16 -0400
Subject: [PATCH 18/87] [Response Ops][Alerting] Refactor `ExecutionHandler`
stage 2 (#193807)
Resolves https://github.com/elastic/kibana/issues/186534
## Summary
This PR splits the for-loop in the `ActionScheduler.run` function into
the appropriate scheduler classes. Previously, each scheduler had a
`generateExecutables` function that would return an array of executables
and the `ActionScheduler` would loop through the array and convert the
executable to a scheduleable action depending on whether it was a
per-alert action, summary action or system action. This refactor renames
`generateExecutables` into `getActionsToSchedule` and moves the logic to
convert the executables into a schedulable action into the appropriate
scheduler class.
## To Verify
Create some rules with per-alert and summary and system actions and
verify they are triggered as expected.
---------
Co-authored-by: Elastic Machine
---
.../server/create_execute_function.test.ts | 90 +++
.../actions/server/create_execute_function.ts | 4 +
.../alerting_event_logger.test.ts | 9 +
.../alerting_event_logger.ts | 3 +
.../action_scheduler/action_scheduler.test.ts | 66 +-
.../action_scheduler/action_scheduler.ts | 503 ++-------------
.../lib/build_rule_url.test.ts | 141 ++++
.../action_scheduler/lib/build_rule_url.ts | 65 ++
.../lib/format_action_to_enqueue.test.ts | 222 +++++++
.../lib/format_action_to_enqueue.ts | 48 ++
.../{ => lib}/get_summarized_alerts.test.ts | 6 +-
.../{ => lib}/get_summarized_alerts.ts | 4 +-
.../task_runner/action_scheduler/lib/index.ts | 20 +
.../{ => lib}/rule_action_helper.test.ts | 2 +-
.../{ => lib}/rule_action_helper.ts | 2 +-
.../lib/should_schedule_action.test.ts | 195 ++++++
.../lib/should_schedule_action.ts | 70 ++
.../per_alert_action_scheduler.test.ts | 610 ++++++++++++------
.../schedulers/per_alert_action_scheduler.ts | 136 +++-
.../summary_action_scheduler.test.ts | 319 ++++++---
.../schedulers/summary_action_scheduler.ts | 121 +++-
.../system_action_scheduler.test.ts | 297 +++++++--
.../schedulers/system_action_scheduler.ts | 118 +++-
.../task_runner/action_scheduler/types.ts | 21 +-
.../alerting/server/task_runner/fixtures.ts | 10 +-
.../server/task_runner/task_runner.test.ts | 34 +-
26 files changed, 2287 insertions(+), 829 deletions(-)
create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.test.ts
create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.ts
create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.test.ts
create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.ts
rename x-pack/plugins/alerting/server/task_runner/action_scheduler/{ => lib}/get_summarized_alerts.test.ts (95%)
rename x-pack/plugins/alerting/server/task_runner/action_scheduler/{ => lib}/get_summarized_alerts.ts (98%)
create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/index.ts
rename x-pack/plugins/alerting/server/task_runner/action_scheduler/{ => lib}/rule_action_helper.test.ts (99%)
rename x-pack/plugins/alerting/server/task_runner/action_scheduler/{ => lib}/rule_action_helper.ts (99%)
create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/should_schedule_action.test.ts
create mode 100644 x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/should_schedule_action.ts
diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts
index a1ab85933d9bc..7be187743e634 100644
--- a/x-pack/plugins/actions/server/create_execute_function.test.ts
+++ b/x-pack/plugins/actions/server/create_execute_function.test.ts
@@ -1088,6 +1088,7 @@ describe('bulkExecute()', () => {
"actionTypeId": "mock-action",
"id": "123",
"response": "queuedActionsLimitError",
+ "uuid": undefined,
},
],
}
@@ -1099,4 +1100,93 @@ describe('bulkExecute()', () => {
]
`);
});
+
+ test('passes through action uuid if provided', async () => {
+ mockTaskManager.aggregate.mockResolvedValue({
+ took: 1,
+ timed_out: false,
+ _shards: { total: 1, successful: 1, skipped: 0, failed: 0 },
+ hits: { total: { value: 2, relation: 'eq' }, max_score: null, hits: [] },
+ aggregations: {},
+ });
+ mockActionsConfig.getMaxQueued.mockReturnValueOnce(3);
+ const executeFn = createBulkExecutionEnqueuerFunction({
+ taskManager: mockTaskManager,
+ actionTypeRegistry: actionTypeRegistryMock.create(),
+ isESOCanEncrypt: true,
+ inMemoryConnectors: [],
+ configurationUtilities: mockActionsConfig,
+ logger: mockLogger,
+ });
+ savedObjectsClient.bulkGet.mockResolvedValueOnce({
+ saved_objects: [
+ { id: '123', type: 'action', attributes: { actionTypeId: 'mock-action' }, references: [] },
+ ],
+ });
+ savedObjectsClient.bulkCreate.mockResolvedValueOnce({
+ saved_objects: [
+ { id: '234', type: 'action_task_params', attributes: { actionId: '123' }, references: [] },
+ ],
+ });
+ expect(
+ await executeFn(savedObjectsClient, [
+ {
+ id: '123',
+ params: { baz: false },
+ spaceId: 'default',
+ executionId: '123abc',
+ apiKey: null,
+ source: asHttpRequestExecutionSource(request),
+ actionTypeId: 'mock-action',
+ uuid: 'aaa',
+ },
+ {
+ id: '123',
+ params: { baz: false },
+ spaceId: 'default',
+ executionId: '456xyz',
+ apiKey: null,
+ source: asHttpRequestExecutionSource(request),
+ actionTypeId: 'mock-action',
+ uuid: 'bbb',
+ },
+ ])
+ ).toMatchInlineSnapshot(`
+ Object {
+ "errors": true,
+ "items": Array [
+ Object {
+ "actionTypeId": "mock-action",
+ "id": "123",
+ "response": "success",
+ "uuid": "aaa",
+ },
+ Object {
+ "actionTypeId": "mock-action",
+ "id": "123",
+ "response": "queuedActionsLimitError",
+ "uuid": "bbb",
+ },
+ ],
+ }
+ `);
+ expect(mockTaskManager.bulkSchedule).toHaveBeenCalledTimes(1);
+ expect(mockTaskManager.bulkSchedule.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ Array [
+ Object {
+ "params": Object {
+ "actionTaskParamsId": "234",
+ "spaceId": "default",
+ },
+ "scope": Array [
+ "actions",
+ ],
+ "state": Object {},
+ "taskType": "actions:mock-action",
+ },
+ ],
+ ]
+ `);
+ });
});
diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts
index e8f9c859747ff..a92bff9719559 100644
--- a/x-pack/plugins/actions/server/create_execute_function.ts
+++ b/x-pack/plugins/actions/server/create_execute_function.ts
@@ -31,6 +31,7 @@ interface CreateExecuteFunctionOptions {
export interface ExecuteOptions
extends Pick {
id: string;
+ uuid?: string;
spaceId: string;
apiKey: string | null;
executionId: string;
@@ -71,6 +72,7 @@ export interface ExecutionResponse {
export interface ExecutionResponseItem {
id: string;
+ uuid?: string;
actionTypeId: string;
response: ExecutionResponseType;
}
@@ -197,12 +199,14 @@ export function createBulkExecutionEnqueuerFunction({
items: runnableActions
.map((a) => ({
id: a.id,
+ uuid: a.uuid,
actionTypeId: a.actionTypeId,
response: ExecutionResponseType.SUCCESS,
}))
.concat(
actionsOverLimit.map((a) => ({
id: a.id,
+ uuid: a.uuid,
actionTypeId: a.actionTypeId,
response: ExecutionResponseType.QUEUED_ACTIONS_LIMIT_ERROR,
}))
diff --git a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts
index 82e8663bd6bf8..082d5ea6381df 100644
--- a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts
+++ b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts
@@ -807,6 +807,15 @@ describe('AlertingEventLogger', () => {
expect(eventLogger.logEvent).toHaveBeenCalledWith(event);
});
+
+ test('should log action event with uuid', () => {
+ alertingEventLogger.initialize({ context: ruleContext, runDate, ruleData });
+ alertingEventLogger.logAction({ ...action, uuid: 'abcdefg' });
+
+ const event = createActionExecuteRecord(ruleContext, ruleData, [alertSO], action);
+
+ expect(eventLogger.logEvent).toHaveBeenCalledWith(event);
+ });
});
describe('done()', () => {
diff --git a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts
index f29e1e00473b2..1607f6090b10c 100644
--- a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts
+++ b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts
@@ -78,6 +78,9 @@ interface AlertOpts {
export interface ActionOpts {
id: string;
+ // uuid is typed as optional but in reality it is always
+ // populated - https://github.com/elastic/kibana/issues/195255
+ uuid?: string;
typeId: string;
alertId?: string;
alertGroup?: string;
diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.test.ts
index 600f6aedbe039..b6f250b47205e 100644
--- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.test.ts
+++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.test.ts
@@ -60,7 +60,9 @@ const defaultSchedulerContext = getDefaultSchedulerContext(
const defaultExecutionResponse = {
errors: false,
- items: [{ actionTypeId: 'test', id: '1', response: ExecutionResponseType.SUCCESS }],
+ items: [
+ { actionTypeId: 'test', id: '1', uuid: '111-111', response: ExecutionResponseType.SUCCESS },
+ ],
};
let ruleRunMetricsStore: RuleRunMetricsStore;
@@ -99,7 +101,7 @@ describe('Action Scheduler', () => {
});
afterAll(() => clock.restore());
- test('enqueues execution per selected action', async () => {
+ test('schedules execution per selected action', async () => {
const alerts = generateAlert({ id: 1 });
const actionScheduler = new ActionScheduler(getSchedulerContext());
await actionScheduler.run(alerts);
@@ -138,6 +140,7 @@ describe('Action Scheduler', () => {
"type": "SAVED_OBJECT",
},
"spaceId": "test1",
+ "uuid": "111-111",
},
],
]
@@ -146,6 +149,7 @@ describe('Action Scheduler', () => {
expect(alertingEventLogger.logAction).toHaveBeenCalledTimes(1);
expect(alertingEventLogger.logAction).toHaveBeenNthCalledWith(1, {
id: '1',
+ uuid: '111-111',
typeId: 'test',
alertId: '1',
alertGroup: 'default',
@@ -368,6 +372,7 @@ describe('Action Scheduler', () => {
"type": "SAVED_OBJECT",
},
"spaceId": "test1",
+ "uuid": "111-111",
},
],
]
@@ -409,6 +414,7 @@ describe('Action Scheduler', () => {
"type": "SAVED_OBJECT",
},
"spaceId": "test1",
+ "uuid": "111-111",
},
],
]
@@ -437,11 +443,13 @@ describe('Action Scheduler', () => {
{
actionTypeId: 'test2',
id: '1',
+ uuid: '111-111',
response: ExecutionResponseType.SUCCESS,
},
{
actionTypeId: 'test2',
id: '2',
+ uuid: '222-222',
response: ExecutionResponseType.SUCCESS,
},
],
@@ -508,20 +516,23 @@ describe('Action Scheduler', () => {
actionsClient.bulkEnqueueExecution.mockResolvedValueOnce({
errors: false,
items: [
- { actionTypeId: 'test', id: '1', response: ExecutionResponseType.SUCCESS },
+ { actionTypeId: 'test', id: '1', uuid: '222-222', response: ExecutionResponseType.SUCCESS },
{
actionTypeId: 'test-action-type-id',
id: '2',
+ uuid: '222-222',
response: ExecutionResponseType.SUCCESS,
},
{
actionTypeId: 'another-action-type-id',
id: '4',
+ uuid: '444-444',
response: ExecutionResponseType.SUCCESS,
},
{
actionTypeId: 'another-action-type-id',
id: '5',
+ uuid: '555-555',
response: ExecutionResponseType.SUCCESS,
},
],
@@ -537,6 +548,7 @@ describe('Action Scheduler', () => {
contextVal: 'My other {{context.value}} goes here',
stateVal: 'My other {{state.value}} goes here',
},
+ uuid: '222-222',
},
{
id: '3',
@@ -547,6 +559,7 @@ describe('Action Scheduler', () => {
contextVal: '{{context.value}} goes here',
stateVal: '{{state.value}} goes here',
},
+ uuid: '333-333',
},
{
id: '4',
@@ -557,6 +570,7 @@ describe('Action Scheduler', () => {
contextVal: '{{context.value}} goes here',
stateVal: '{{state.value}} goes here',
},
+ uuid: '444-444',
},
{
id: '5',
@@ -567,6 +581,7 @@ describe('Action Scheduler', () => {
contextVal: '{{context.value}} goes here',
stateVal: '{{state.value}} goes here',
},
+ uuid: '555-555',
},
];
const actionScheduler = new ActionScheduler(
@@ -612,16 +627,19 @@ describe('Action Scheduler', () => {
{
actionTypeId: 'test',
id: '1',
+ uuid: '111-111',
response: ExecutionResponseType.SUCCESS,
},
{
actionTypeId: 'test',
id: '2',
+ uuid: '222-222',
response: ExecutionResponseType.SUCCESS,
},
{
actionTypeId: 'test',
id: '3',
+ uuid: '333-333',
response: ExecutionResponseType.QUEUED_ACTIONS_LIMIT_ERROR,
},
],
@@ -636,6 +654,7 @@ describe('Action Scheduler', () => {
contextVal: 'My other {{context.value}} goes here',
stateVal: 'My other {{state.value}} goes here',
},
+ uuid: '111-111',
},
{
id: '2',
@@ -646,6 +665,7 @@ describe('Action Scheduler', () => {
contextVal: 'My other {{context.value}} goes here',
stateVal: 'My other {{state.value}} goes here',
},
+ uuid: '222-222',
},
{
id: '3',
@@ -656,6 +676,7 @@ describe('Action Scheduler', () => {
contextVal: '{{context.value}} goes here',
stateVal: '{{state.value}} goes here',
},
+ uuid: '333-333',
},
];
const actionScheduler = new ActionScheduler(
@@ -679,7 +700,7 @@ describe('Action Scheduler', () => {
test('schedules alerts with recovered actions', async () => {
const actions = [
{
- id: '1',
+ id: 'action-2',
group: 'recovered',
actionTypeId: 'test',
params: {
@@ -689,6 +710,7 @@ describe('Action Scheduler', () => {
alertVal:
'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
},
+ uuid: '222-222',
},
];
const actionScheduler = new ActionScheduler(
@@ -711,7 +733,7 @@ describe('Action Scheduler', () => {
"apiKey": "MTIzOmFiYw==",
"consumer": "rule-consumer",
"executionId": "5f6aa57d-3e22-484e-bae8-cbed868f4d28",
- "id": "1",
+ "id": "action-2",
"params": Object {
"alertVal": "My 1 name-of-alert test1 tag-A,tag-B 1 goes here",
"contextVal": "My goes here",
@@ -734,6 +756,7 @@ describe('Action Scheduler', () => {
"type": "SAVED_OBJECT",
},
"spaceId": "test1",
+ "uuid": "222-222",
},
],
]
@@ -883,6 +906,7 @@ describe('Action Scheduler', () => {
{
actionTypeId: 'testActionTypeId',
id: '1',
+ uuid: '111-111',
response: ExecutionResponseType.SUCCESS,
},
],
@@ -914,6 +938,7 @@ describe('Action Scheduler', () => {
message:
'New: {{alerts.new.count}} Ongoing: {{alerts.ongoing.count}} Recovered: {{alerts.recovered.count}}',
},
+ uuid: '111-111',
},
],
},
@@ -957,6 +982,7 @@ describe('Action Scheduler', () => {
"type": "SAVED_OBJECT",
},
"spaceId": "test1",
+ "uuid": "111-111",
},
],
]
@@ -964,6 +990,7 @@ describe('Action Scheduler', () => {
expect(alertingEventLogger.logAction).toBeCalledWith({
alertSummary: { new: 1, ongoing: 0, recovered: 0 },
id: '1',
+ uuid: '111-111',
typeId: 'testActionTypeId',
});
});
@@ -1012,6 +1039,7 @@ describe('Action Scheduler', () => {
{
actionTypeId: 'testActionTypeId',
id: '1',
+ uuid: '111-111',
response: ExecutionResponseType.SUCCESS,
},
],
@@ -1095,6 +1123,7 @@ describe('Action Scheduler', () => {
"type": "SAVED_OBJECT",
},
"spaceId": "test1",
+ "uuid": "111-111",
},
],
]
@@ -1102,6 +1131,7 @@ describe('Action Scheduler', () => {
expect(alertingEventLogger.logAction).toBeCalledWith({
alertSummary: { new: 1, ongoing: 0, recovered: 0 },
id: '1',
+ uuid: '111-111',
typeId: 'testActionTypeId',
});
});
@@ -1256,10 +1286,11 @@ describe('Action Scheduler', () => {
actionsClient.bulkEnqueueExecution.mockResolvedValueOnce({
errors: false,
items: [
- { actionTypeId: 'test', id: '1', response: ExecutionResponseType.SUCCESS },
+ { actionTypeId: 'test', id: '1', uuid: '111-111', response: ExecutionResponseType.SUCCESS },
{
actionTypeId: 'test',
id: '2',
+ uuid: '222-222',
response: ExecutionResponseType.SUCCESS,
},
],
@@ -1276,6 +1307,7 @@ describe('Action Scheduler', () => {
alertVal:
'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
},
+ uuid: '111-111',
},
{
id: '2',
@@ -1288,6 +1320,7 @@ describe('Action Scheduler', () => {
alertVal:
'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
},
+ uuid: '222-222',
},
];
const actionScheduler = new ActionScheduler(
@@ -1333,6 +1366,7 @@ describe('Action Scheduler', () => {
"type": "SAVED_OBJECT",
},
"spaceId": "test1",
+ "uuid": "111-111",
},
Object {
"actionTypeId": "test",
@@ -1362,6 +1396,7 @@ describe('Action Scheduler', () => {
"type": "SAVED_OBJECT",
},
"spaceId": "test1",
+ "uuid": "222-222",
},
],
]
@@ -1448,6 +1483,7 @@ describe('Action Scheduler', () => {
{
actionTypeId: 'testActionTypeId',
id: '1',
+ uuid: '111-111',
response: ExecutionResponseType.SUCCESS,
},
],
@@ -1518,6 +1554,7 @@ describe('Action Scheduler', () => {
{
actionTypeId: 'testActionTypeId',
id: '1',
+ uuid: '111-111',
response: ExecutionResponseType.SUCCESS,
},
],
@@ -1541,7 +1578,7 @@ describe('Action Scheduler', () => {
actions: [
{
id: '1',
- uuid: '111',
+ uuid: '111-111',
group: 'default',
actionTypeId: 'testActionTypeId',
frequency: {
@@ -1587,17 +1624,19 @@ describe('Action Scheduler', () => {
],
source: { source: { id: '1', type: RULE_SAVED_OBJECT_TYPE }, type: 'SAVED_OBJECT' },
spaceId: 'test1',
+ uuid: '111-111',
},
]);
expect(alertingEventLogger.logAction).toHaveBeenCalledWith({
alertGroup: 'default',
alertId: '1',
id: '1',
+ uuid: '111-111',
typeId: 'testActionTypeId',
});
expect(defaultSchedulerContext.logger.debug).toHaveBeenCalledTimes(1);
expect(defaultSchedulerContext.logger.debug).toHaveBeenCalledWith(
- '(2) alerts have been filtered out for: testActionTypeId:111'
+ '(2) alerts have been filtered out for: testActionTypeId:111-111'
);
});
@@ -1840,6 +1879,7 @@ describe('Action Scheduler', () => {
"type": "SAVED_OBJECT",
},
"spaceId": "test1",
+ "uuid": "111-111",
},
Object {
"actionTypeId": "test",
@@ -1869,6 +1909,7 @@ describe('Action Scheduler', () => {
"type": "SAVED_OBJECT",
},
"spaceId": "test1",
+ "uuid": "111-111",
},
Object {
"actionTypeId": "test",
@@ -1898,6 +1939,7 @@ describe('Action Scheduler', () => {
"type": "SAVED_OBJECT",
},
"spaceId": "test1",
+ "uuid": "111-111",
},
],
]
@@ -2261,12 +2303,13 @@ describe('Action Scheduler', () => {
const executorParams = getSchedulerContext({
rule: {
...defaultSchedulerContext.rule,
+ actions: [],
systemActions: [
{
id: '1',
actionTypeId: '.test-system-action',
params: actionsParams,
- uui: 'test',
+ uuid: 'test',
},
],
},
@@ -2360,6 +2403,7 @@ describe('Action Scheduler', () => {
"type": "SAVED_OBJECT",
},
"spaceId": "test1",
+ "uuid": "test",
},
],
]
@@ -2368,6 +2412,7 @@ describe('Action Scheduler', () => {
expect(alertingEventLogger.logAction).toBeCalledWith({
alertSummary: { new: 1, ongoing: 0, recovered: 0 },
id: '1',
+ uuid: 'test',
typeId: '.test-system-action',
});
});
@@ -2387,6 +2432,7 @@ describe('Action Scheduler', () => {
const executorParams = getSchedulerContext({
rule: {
...defaultSchedulerContext.rule,
+ actions: [],
systemActions: [
{
id: 'action-id',
@@ -2443,6 +2489,7 @@ describe('Action Scheduler', () => {
},
rule: {
...defaultSchedulerContext.rule,
+ actions: [],
systemActions: [
{
id: 'action-id',
@@ -2477,6 +2524,7 @@ describe('Action Scheduler', () => {
const executorParams = getSchedulerContext({
rule: {
...defaultSchedulerContext.rule,
+ actions: [],
systemActions: [
{
id: '1',
diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.ts
index 3b804ce3da413..44822657ba86f 100644
--- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.ts
+++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/action_scheduler.ts
@@ -5,8 +5,6 @@
* 2.0.
*/
-import { getRuleDetailsRoute, triggersActionsRoute } from '@kbn/rule-data-utils';
-import { asSavedObjectExecutionSource } from '@kbn/actions-plugin/server';
import {
createTaskRunError,
isEphemeralTaskRejectedDueToCapacityError,
@@ -19,77 +17,21 @@ import {
} from '@kbn/actions-plugin/server/create_execute_function';
import { ActionsCompletion } from '@kbn/alerting-state-types';
import { chunk } from 'lodash';
-import { CombinedSummarizedAlerts, ThrottledActions } from '../../types';
-import { injectActionParams } from '../inject_action_params';
-import { ActionSchedulerOptions, IActionScheduler, RuleUrl } from './types';
-import {
- transformActionParams,
- TransformActionParamsOptions,
- transformSummaryActionParams,
-} from '../transform_action_params';
+import { ThrottledActions } from '../../types';
+import { ActionSchedulerOptions, ActionsToSchedule, IActionScheduler } from './types';
import { Alert } from '../../alert';
import {
AlertInstanceContext,
AlertInstanceState,
- RuleAction,
RuleTypeParams,
RuleTypeState,
- SanitizedRule,
RuleAlertData,
- RuleSystemAction,
} from '../../../common';
-import {
- generateActionHash,
- getSummaryActionsFromTaskState,
- getSummaryActionTimeBounds,
- isActionOnInterval,
-} from './rule_action_helper';
-import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects';
-import { ConnectorAdapter } from '../../connector_adapters/types';
+import { getSummaryActionsFromTaskState } from './lib';
import { withAlertingSpan } from '../lib';
import * as schedulers from './schedulers';
-interface LogAction {
- id: string;
- typeId: string;
- alertId?: string;
- alertGroup?: string;
- alertSummary?: {
- new: number;
- ongoing: number;
- recovered: number;
- };
-}
-
-interface RunSummarizedActionArgs {
- action: RuleAction;
- summarizedAlerts: CombinedSummarizedAlerts;
- spaceId: string;
- bulkActions: EnqueueExecutionOptions[];
-}
-
-interface RunSystemActionArgs {
- action: RuleSystemAction;
- connectorAdapter: ConnectorAdapter;
- summarizedAlerts: CombinedSummarizedAlerts;
- rule: SanitizedRule;
- ruleProducer: string;
- spaceId: string;
- bulkActions: EnqueueExecutionOptions[];
-}
-
-interface RunActionArgs<
- State extends AlertInstanceState,
- Context extends AlertInstanceContext,
- ActionGroupIds extends string,
- RecoveryActionGroupId extends string
-> {
- action: RuleAction;
- alert: Alert;
- ruleId: string;
- spaceId: string;
- bulkActions: EnqueueExecutionOptions[];
-}
+const BULK_SCHEDULE_CHUNK_SIZE = 1000;
export interface RunResult {
throttledSummaryActions: ThrottledActions;
@@ -110,9 +52,6 @@ export class ActionScheduler<
> = [];
private ephemeralActionsToSchedule: number;
- private CHUNK_SIZE = 1000;
- private ruleTypeActionGroups?: Map;
- private previousStartedAt: Date | null;
constructor(
private readonly context: ActionSchedulerOptions<
@@ -127,11 +66,6 @@ export class ActionScheduler<
>
) {
this.ephemeralActionsToSchedule = context.taskRunnerContext.maxEphemeralActionsPerRule;
- this.ruleTypeActionGroups = new Map(
- context.ruleType.actionGroups.map((actionGroup) => [actionGroup.id, actionGroup.name])
- );
- this.previousStartedAt = context.previousStartedAt;
-
for (const [_, scheduler] of Object.entries(schedulers)) {
this.schedulers.push(new scheduler(context));
}
@@ -148,148 +82,30 @@ export class ActionScheduler<
summaryActions: this.context.taskInstance.state?.summaryActions,
});
- const executables = [];
+ const allActionsToScheduleResult: ActionsToSchedule[] = [];
for (const scheduler of this.schedulers) {
- executables.push(
- ...(await scheduler.generateExecutables({ alerts, throttledSummaryActions }))
+ allActionsToScheduleResult.push(
+ ...(await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions }))
);
}
- if (executables.length === 0) {
+ if (allActionsToScheduleResult.length === 0) {
return { throttledSummaryActions };
}
- const {
- CHUNK_SIZE,
- context: {
- logger,
- alertingEventLogger,
- ruleRunMetricsStore,
- taskRunnerContext: { actionsConfigMap },
- taskInstance: {
- params: { spaceId, alertId: ruleId },
- },
- },
- } = this;
-
- const logActions: Record = {};
- const bulkActions: EnqueueExecutionOptions[] = [];
- let bulkActionsResponse: ExecutionResponseItem[] = [];
+ const bulkScheduleRequest: EnqueueExecutionOptions[] = [];
- this.context.ruleRunMetricsStore.incrementNumberOfGeneratedActions(executables.length);
-
- for (const { action, alert, summarizedAlerts } of executables) {
- const { actionTypeId } = action;
-
- ruleRunMetricsStore.incrementNumberOfGeneratedActionsByConnectorType(actionTypeId);
- if (ruleRunMetricsStore.hasReachedTheExecutableActionsLimit(actionsConfigMap)) {
- ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({
- actionTypeId,
- status: ActionsCompletion.PARTIAL,
- });
- logger.debug(
- `Rule "${this.context.rule.id}" skipped scheduling action "${action.id}" because the maximum number of allowed actions has been reached.`
- );
- break;
- }
-
- if (
- ruleRunMetricsStore.hasReachedTheExecutableActionsLimitByConnectorType({
- actionTypeId,
- actionsConfigMap,
- })
- ) {
- if (!ruleRunMetricsStore.hasConnectorTypeReachedTheLimit(actionTypeId)) {
- logger.debug(
- `Rule "${this.context.rule.id}" skipped scheduling action "${action.id}" because the maximum number of allowed actions for connector type ${actionTypeId} has been reached.`
- );
- }
- ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({
- actionTypeId,
- status: ActionsCompletion.PARTIAL,
- });
- continue;
- }
-
- if (!this.isExecutableAction(action)) {
- this.context.logger.warn(
- `Rule "${this.context.taskInstance.params.alertId}" skipped scheduling action "${action.id}" because it is disabled`
- );
- continue;
- }
-
- ruleRunMetricsStore.incrementNumberOfTriggeredActions();
- ruleRunMetricsStore.incrementNumberOfTriggeredActionsByConnectorType(actionTypeId);
-
- if (!this.isSystemAction(action) && summarizedAlerts) {
- const defaultAction = action as RuleAction;
- if (isActionOnInterval(action)) {
- throttledSummaryActions[defaultAction.uuid!] = { date: new Date().toISOString() };
- }
-
- logActions[defaultAction.id] = await this.runSummarizedAction({
- action,
- summarizedAlerts,
- spaceId,
- bulkActions,
- });
- } else if (summarizedAlerts && this.isSystemAction(action)) {
- const hasConnectorAdapter = this.context.taskRunnerContext.connectorAdapterRegistry.has(
- action.actionTypeId
- );
- /**
- * System actions without an adapter
- * cannot be executed
- *
- */
- if (!hasConnectorAdapter) {
- this.context.logger.warn(
- `Rule "${this.context.taskInstance.params.alertId}" skipped scheduling system action "${action.id}" because no connector adapter is configured`
- );
-
- continue;
- }
-
- const connectorAdapter = this.context.taskRunnerContext.connectorAdapterRegistry.get(
- action.actionTypeId
- );
- logActions[action.id] = await this.runSystemAction({
- action,
- connectorAdapter,
- summarizedAlerts,
- rule: this.context.rule,
- ruleProducer: this.context.ruleType.producer,
- spaceId,
- bulkActions,
- });
- } else if (!this.isSystemAction(action) && alert) {
- const defaultAction = action as RuleAction;
- logActions[defaultAction.id] = await this.runAction({
- action,
- spaceId,
- alert,
- ruleId,
- bulkActions,
- });
-
- const actionGroup = defaultAction.group;
- if (!this.isRecoveredAlert(actionGroup)) {
- if (isActionOnInterval(action)) {
- alert.updateLastScheduledActions(
- defaultAction.group as ActionGroupIds,
- generateActionHash(action),
- defaultAction.uuid
- );
- } else {
- alert.updateLastScheduledActions(defaultAction.group as ActionGroupIds);
- }
- alert.unscheduleActions();
- }
- }
+ for (const result of allActionsToScheduleResult) {
+ await this.runActionAsEphemeralOrAddToBulkScheduleRequest({
+ enqueueOptions: result.actionToEnqueue,
+ bulkScheduleRequest,
+ });
}
- if (!!bulkActions.length) {
- for (const c of chunk(bulkActions, CHUNK_SIZE)) {
+ let bulkScheduleResponse: ExecutionResponseItem[] = [];
+
+ if (!!bulkScheduleRequest.length) {
+ for (const c of chunk(bulkScheduleRequest, BULK_SCHEDULE_CHUNK_SIZE)) {
let enqueueResponse;
try {
enqueueResponse = await withAlertingSpan('alerting:bulk-enqueue-actions', () =>
@@ -302,7 +118,7 @@ export class ActionScheduler<
throw createTaskRunError(e, TaskErrorSource.FRAMEWORK);
}
if (enqueueResponse.errors) {
- bulkActionsResponse = bulkActionsResponse.concat(
+ bulkScheduleResponse = bulkScheduleResponse.concat(
enqueueResponse.items.filter(
(i) => i.response === ExecutionResponseType.QUEUED_ACTIONS_LIMIT_ERROR
)
@@ -311,280 +127,53 @@ export class ActionScheduler<
}
}
- if (!!bulkActionsResponse.length) {
- for (const r of bulkActionsResponse) {
+ const actionsToNotLog: string[] = [];
+ if (!!bulkScheduleResponse.length) {
+ for (const r of bulkScheduleResponse) {
if (r.response === ExecutionResponseType.QUEUED_ACTIONS_LIMIT_ERROR) {
- ruleRunMetricsStore.setHasReachedQueuedActionsLimit(true);
- ruleRunMetricsStore.decrementNumberOfTriggeredActions();
- ruleRunMetricsStore.decrementNumberOfTriggeredActionsByConnectorType(r.actionTypeId);
- ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({
+ this.context.ruleRunMetricsStore.setHasReachedQueuedActionsLimit(true);
+ this.context.ruleRunMetricsStore.decrementNumberOfTriggeredActions();
+ this.context.ruleRunMetricsStore.decrementNumberOfTriggeredActionsByConnectorType(
+ r.actionTypeId
+ );
+ this.context.ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({
actionTypeId: r.actionTypeId,
status: ActionsCompletion.PARTIAL,
});
- logger.debug(
+ this.context.logger.debug(
`Rule "${this.context.rule.id}" skipped scheduling action "${r.id}" because the maximum number of queued actions has been reached.`
);
- delete logActions[r.id];
+ const uuid = r.uuid;
+ // uuid is typed as optional but in reality it is always
+ // populated - https://github.com/elastic/kibana/issues/195255
+ if (uuid) {
+ actionsToNotLog.push(uuid);
+ }
}
}
}
- const logActionsValues = Object.values(logActions);
- if (!!logActionsValues.length) {
- for (const action of logActionsValues) {
- alertingEventLogger.logAction(action);
- }
- }
-
- return { throttledSummaryActions };
- }
-
- private async runSummarizedAction({
- action,
- summarizedAlerts,
- spaceId,
- bulkActions,
- }: RunSummarizedActionArgs): Promise {
- const { start, end } = getSummaryActionTimeBounds(
- action,
- this.context.rule.schedule,
- this.previousStartedAt
+ const actionsToLog = allActionsToScheduleResult.filter(
+ (result) => result.actionToLog.uuid && !actionsToNotLog.includes(result.actionToLog.uuid)
);
- const ruleUrl = this.buildRuleUrl(spaceId, start, end);
- const actionToRun = {
- ...action,
- params: injectActionParams({
- actionTypeId: action.actionTypeId,
- ruleUrl,
- ruleName: this.context.rule.name,
- actionParams: transformSummaryActionParams({
- alerts: summarizedAlerts,
- rule: this.context.rule,
- ruleTypeId: this.context.ruleType.id,
- actionId: action.id,
- actionParams: action.params,
- spaceId,
- actionsPlugin: this.context.taskRunnerContext.actionsPlugin,
- actionTypeId: action.actionTypeId,
- kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl,
- ruleUrl: ruleUrl?.absoluteUrl,
- }),
- }),
- };
- await this.actionRunOrAddToBulk({
- enqueueOptions: this.getEnqueueOptions(actionToRun),
- bulkActions,
- });
-
- return {
- id: action.id,
- typeId: action.actionTypeId,
- alertSummary: {
- new: summarizedAlerts.new.count,
- ongoing: summarizedAlerts.ongoing.count,
- recovered: summarizedAlerts.recovered.count,
- },
- };
- }
-
- private async runSystemAction({
- action,
- spaceId,
- connectorAdapter,
- summarizedAlerts,
- rule,
- ruleProducer,
- bulkActions,
- }: RunSystemActionArgs): Promise {
- const ruleUrl = this.buildRuleUrl(spaceId);
-
- const connectorAdapterActionParams = connectorAdapter.buildActionParams({
- alerts: summarizedAlerts,
- rule: {
- id: rule.id,
- tags: rule.tags,
- name: rule.name,
- consumer: rule.consumer,
- producer: ruleProducer,
- },
- ruleUrl: ruleUrl?.absoluteUrl,
- spaceId,
- params: action.params,
- });
-
- const actionToRun = Object.assign(action, { params: connectorAdapterActionParams });
-
- await this.actionRunOrAddToBulk({
- enqueueOptions: this.getEnqueueOptions(actionToRun),
- bulkActions,
- });
-
- return {
- id: action.id,
- typeId: action.actionTypeId,
- alertSummary: {
- new: summarizedAlerts.new.count,
- ongoing: summarizedAlerts.ongoing.count,
- recovered: summarizedAlerts.recovered.count,
- },
- };
- }
-
- private async runAction({
- action,
- spaceId,
- alert,
- ruleId,
- bulkActions,
- }: RunActionArgs): Promise {
- const ruleUrl = this.buildRuleUrl(spaceId);
- const executableAlert = alert!;
- const actionGroup = action.group as ActionGroupIds;
- const transformActionParamsOptions: TransformActionParamsOptions = {
- actionsPlugin: this.context.taskRunnerContext.actionsPlugin,
- alertId: ruleId,
- alertType: this.context.ruleType.id,
- actionTypeId: action.actionTypeId,
- alertName: this.context.rule.name,
- spaceId,
- tags: this.context.rule.tags,
- alertInstanceId: executableAlert.getId(),
- alertUuid: executableAlert.getUuid(),
- alertActionGroup: actionGroup,
- alertActionGroupName: this.ruleTypeActionGroups!.get(actionGroup)!,
- context: executableAlert.getContext(),
- actionId: action.id,
- state: executableAlert.getState(),
- kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl,
- alertParams: this.context.rule.params,
- actionParams: action.params,
- flapping: executableAlert.getFlapping(),
- ruleUrl: ruleUrl?.absoluteUrl,
- };
-
- if (executableAlert.isAlertAsData()) {
- transformActionParamsOptions.aadAlert = executableAlert.getAlertAsData();
- }
- const actionToRun = {
- ...action,
- params: injectActionParams({
- actionTypeId: action.actionTypeId,
- ruleUrl,
- ruleName: this.context.rule.name,
- actionParams: transformActionParams(transformActionParamsOptions),
- }),
- };
-
- await this.actionRunOrAddToBulk({
- enqueueOptions: this.getEnqueueOptions(actionToRun),
- bulkActions,
- });
-
- return {
- id: action.id,
- typeId: action.actionTypeId,
- alertId: alert.getId(),
- alertGroup: action.group,
- };
- }
-
- private isExecutableAction(action: RuleAction | RuleSystemAction) {
- return this.context.taskRunnerContext.actionsPlugin.isActionExecutable(
- action.id,
- action.actionTypeId,
- {
- notifyUsage: true,
+ if (!!actionsToLog.length) {
+ for (const action of actionsToLog) {
+ this.context.alertingEventLogger.logAction(action.actionToLog);
}
- );
- }
-
- private isSystemAction(action?: RuleAction | RuleSystemAction): action is RuleSystemAction {
- return this.context.taskRunnerContext.actionsPlugin.isSystemActionConnector(action?.id ?? '');
- }
-
- private isRecoveredAlert(actionGroup: string) {
- return actionGroup === this.context.ruleType.recoveryActionGroup.id;
- }
-
- private buildRuleUrl(spaceId: string, start?: number, end?: number): RuleUrl | undefined {
- if (!this.context.taskRunnerContext.kibanaBaseUrl) {
- return;
}
- const relativePath = this.context.ruleType.getViewInAppRelativeUrl
- ? this.context.ruleType.getViewInAppRelativeUrl({ rule: this.context.rule, start, end })
- : `${triggersActionsRoute}${getRuleDetailsRoute(this.context.rule.id)}`;
-
- try {
- const basePathname = new URL(this.context.taskRunnerContext.kibanaBaseUrl).pathname;
- const basePathnamePrefix = basePathname !== '/' ? `${basePathname}` : '';
- const spaceIdSegment = spaceId !== 'default' ? `/s/${spaceId}` : '';
-
- const ruleUrl = new URL(
- [basePathnamePrefix, spaceIdSegment, relativePath].join(''),
- this.context.taskRunnerContext.kibanaBaseUrl
- );
-
- return {
- absoluteUrl: ruleUrl.toString(),
- kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl,
- basePathname: basePathnamePrefix,
- spaceIdSegment,
- relativePath,
- };
- } catch (error) {
- this.context.logger.debug(
- `Rule "${this.context.rule.id}" encountered an error while constructing the rule.url variable: ${error.message}`
- );
- return;
- }
- }
-
- private getEnqueueOptions(action: RuleAction | RuleSystemAction): EnqueueExecutionOptions {
- const {
- context: {
- apiKey,
- ruleConsumer,
- executionId,
- taskInstance: {
- params: { spaceId, alertId: ruleId },
- },
- },
- } = this;
-
- const namespace = spaceId === 'default' ? {} : { namespace: spaceId };
- return {
- id: action.id,
- params: action.params,
- spaceId,
- apiKey: apiKey ?? null,
- consumer: ruleConsumer,
- source: asSavedObjectExecutionSource({
- id: ruleId,
- type: RULE_SAVED_OBJECT_TYPE,
- }),
- executionId,
- relatedSavedObjects: [
- {
- id: ruleId,
- type: RULE_SAVED_OBJECT_TYPE,
- namespace: namespace.namespace,
- typeId: this.context.ruleType.id,
- },
- ],
- actionTypeId: action.actionTypeId,
- };
+ return { throttledSummaryActions };
}
- private async actionRunOrAddToBulk({
+ private async runActionAsEphemeralOrAddToBulkScheduleRequest({
enqueueOptions,
- bulkActions,
+ bulkScheduleRequest,
}: {
enqueueOptions: EnqueueExecutionOptions;
- bulkActions: EnqueueExecutionOptions[];
+ bulkScheduleRequest: EnqueueExecutionOptions[];
}) {
if (
this.context.taskRunnerContext.supportsEphemeralTasks &&
@@ -595,11 +184,11 @@ export class ActionScheduler<
await this.context.actionsClient!.ephemeralEnqueuedExecution(enqueueOptions);
} catch (err) {
if (isEphemeralTaskRejectedDueToCapacityError(err)) {
- bulkActions.push(enqueueOptions);
+ bulkScheduleRequest.push(enqueueOptions);
}
}
} else {
- bulkActions.push(enqueueOptions);
+ bulkScheduleRequest.push(enqueueOptions);
}
}
}
diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.test.ts
new file mode 100644
index 0000000000000..cb1f3c60fd992
--- /dev/null
+++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.test.ts
@@ -0,0 +1,141 @@
+/*
+ * 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 { loggingSystemMock } from '@kbn/core-logging-server-mocks';
+import { buildRuleUrl } from './build_rule_url';
+import { getRule } from '../test_fixtures';
+
+const logger = loggingSystemMock.create().get();
+const rule = getRule();
+
+describe('buildRuleUrl', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('should return undefined if kibanaBaseUrl is not provided', () => {
+ expect(
+ buildRuleUrl({
+ kibanaBaseUrl: undefined,
+ logger,
+ rule,
+ spaceId: 'default',
+ })
+ ).toBeUndefined();
+ });
+
+ test('should return the expected URL', () => {
+ expect(
+ buildRuleUrl({
+ kibanaBaseUrl: 'http://localhost:5601',
+ logger,
+ rule,
+ spaceId: 'default',
+ })
+ ).toEqual({
+ absoluteUrl:
+ 'http://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/1',
+ basePathname: '',
+ kibanaBaseUrl: 'http://localhost:5601',
+ relativePath: '/app/management/insightsAndAlerting/triggersActions/rule/1',
+ spaceIdSegment: '',
+ });
+ });
+
+ test('should return the expected URL for custom space', () => {
+ expect(
+ buildRuleUrl({
+ kibanaBaseUrl: 'http://localhost:5601',
+ logger,
+ rule,
+ spaceId: 'my-special-space',
+ })
+ ).toEqual({
+ absoluteUrl:
+ 'http://localhost:5601/s/my-special-space/app/management/insightsAndAlerting/triggersActions/rule/1',
+ basePathname: '',
+ kibanaBaseUrl: 'http://localhost:5601',
+ relativePath: '/app/management/insightsAndAlerting/triggersActions/rule/1',
+ spaceIdSegment: '/s/my-special-space',
+ });
+ });
+
+ test('should return the expected URL when getViewInAppRelativeUrl is defined', () => {
+ expect(
+ buildRuleUrl({
+ getViewInAppRelativeUrl: ({ rule: r }) => `/app/test/my-custom-rule-page/${r.id}`,
+ kibanaBaseUrl: 'http://localhost:5601',
+ logger,
+ rule,
+ spaceId: 'default',
+ })
+ ).toEqual({
+ absoluteUrl: 'http://localhost:5601/app/test/my-custom-rule-page/1',
+ basePathname: '',
+ kibanaBaseUrl: 'http://localhost:5601',
+ relativePath: '/app/test/my-custom-rule-page/1',
+ spaceIdSegment: '',
+ });
+ });
+
+ test('should return the expected URL when start, end and getViewInAppRelativeUrl is defined', () => {
+ expect(
+ buildRuleUrl({
+ end: 987654321,
+ getViewInAppRelativeUrl: ({ rule: r, start: s, end: e }) =>
+ `/app/test/my-custom-rule-page/${r.id}?start=${s}&end=${e}`,
+ kibanaBaseUrl: 'http://localhost:5601',
+ logger,
+ rule,
+ start: 123456789,
+ spaceId: 'default',
+ })
+ ).toEqual({
+ absoluteUrl:
+ 'http://localhost:5601/app/test/my-custom-rule-page/1?start=123456789&end=987654321',
+ basePathname: '',
+ kibanaBaseUrl: 'http://localhost:5601',
+ relativePath: '/app/test/my-custom-rule-page/1?start=123456789&end=987654321',
+ spaceIdSegment: '',
+ });
+ });
+
+ test('should return the expected URL when start and end are defined but getViewInAppRelativeUrl is undefined', () => {
+ expect(
+ buildRuleUrl({
+ end: 987654321,
+ kibanaBaseUrl: 'http://localhost:5601',
+ logger,
+ rule,
+ start: 123456789,
+ spaceId: 'default',
+ })
+ ).toEqual({
+ absoluteUrl:
+ 'http://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/1',
+ basePathname: '',
+ kibanaBaseUrl: 'http://localhost:5601',
+ relativePath: '/app/management/insightsAndAlerting/triggersActions/rule/1',
+ spaceIdSegment: '',
+ });
+ });
+
+ test('should return undefined if base url is invalid', () => {
+ expect(
+ buildRuleUrl({
+ kibanaBaseUrl: 'foo-url',
+ logger,
+ rule,
+ spaceId: 'default',
+ })
+ ).toBeUndefined();
+
+ expect(logger.debug).toHaveBeenCalledWith(
+ `Rule "1" encountered an error while constructing the rule.url variable: Invalid URL: foo-url`
+ );
+ });
+});
diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.ts
new file mode 100644
index 0000000000000..3df27a512c7f9
--- /dev/null
+++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/build_rule_url.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 { Logger } from '@kbn/logging';
+import { RuleTypeParams, SanitizedRule } from '@kbn/alerting-types';
+import { getRuleDetailsRoute, triggersActionsRoute } from '@kbn/rule-data-utils';
+import { GetViewInAppRelativeUrlFn } from '../../../types';
+
+interface BuildRuleUrlOpts {
+ end?: number;
+ getViewInAppRelativeUrl?: GetViewInAppRelativeUrlFn;
+ kibanaBaseUrl: string | undefined;
+ logger: Logger;
+ rule: SanitizedRule;
+ spaceId: string;
+ start?: number;
+}
+
+interface BuildRuleUrlResult {
+ absoluteUrl: string;
+ basePathname: string;
+ kibanaBaseUrl: string;
+ relativePath: string;
+ spaceIdSegment: string;
+}
+
+export const buildRuleUrl = (
+ opts: BuildRuleUrlOpts
+): BuildRuleUrlResult | undefined => {
+ if (!opts.kibanaBaseUrl) {
+ return;
+ }
+
+ const relativePath = opts.getViewInAppRelativeUrl
+ ? opts.getViewInAppRelativeUrl({ rule: opts.rule, start: opts.start, end: opts.end })
+ : `${triggersActionsRoute}${getRuleDetailsRoute(opts.rule.id)}`;
+
+ try {
+ const basePathname = new URL(opts.kibanaBaseUrl).pathname;
+ const basePathnamePrefix = basePathname !== '/' ? `${basePathname}` : '';
+ const spaceIdSegment = opts.spaceId !== 'default' ? `/s/${opts.spaceId}` : '';
+
+ const ruleUrl = new URL(
+ [basePathnamePrefix, spaceIdSegment, relativePath].join(''),
+ opts.kibanaBaseUrl
+ );
+
+ return {
+ absoluteUrl: ruleUrl.toString(),
+ kibanaBaseUrl: opts.kibanaBaseUrl,
+ basePathname: basePathnamePrefix,
+ spaceIdSegment,
+ relativePath,
+ };
+ } catch (error) {
+ opts.logger.debug(
+ `Rule "${opts.rule.id}" encountered an error while constructing the rule.url variable: ${error.message}`
+ );
+ return;
+ }
+};
diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.test.ts
new file mode 100644
index 0000000000000..02ff513c5b639
--- /dev/null
+++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.test.ts
@@ -0,0 +1,222 @@
+/*
+ * 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 { RULE_SAVED_OBJECT_TYPE } from '../../..';
+import { formatActionToEnqueue } from './format_action_to_enqueue';
+
+describe('formatActionToEnqueue', () => {
+ test('should format a rule action as expected', () => {
+ expect(
+ formatActionToEnqueue({
+ action: {
+ id: '1',
+ group: 'default',
+ actionTypeId: 'test',
+ params: {
+ foo: true,
+ contextVal: 'My {{context.value}} goes here',
+ stateVal: 'My {{state.value}} goes here',
+ alertVal:
+ 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
+ },
+ uuid: '111-111',
+ },
+ apiKey: 'MTIzOmFiYw==',
+ executionId: '123',
+ ruleConsumer: 'rule-consumer',
+ ruleId: 'aaa',
+ ruleTypeId: 'security-rule',
+ spaceId: 'default',
+ })
+ ).toEqual({
+ id: '1',
+ uuid: '111-111',
+ params: {
+ foo: true,
+ contextVal: 'My {{context.value}} goes here',
+ stateVal: 'My {{state.value}} goes here',
+ alertVal:
+ 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
+ },
+ spaceId: 'default',
+ apiKey: 'MTIzOmFiYw==',
+ consumer: 'rule-consumer',
+ source: {
+ source: {
+ id: 'aaa',
+ type: RULE_SAVED_OBJECT_TYPE,
+ },
+ type: 'SAVED_OBJECT',
+ },
+ executionId: '123',
+ relatedSavedObjects: [
+ {
+ id: 'aaa',
+ type: RULE_SAVED_OBJECT_TYPE,
+ namespace: undefined,
+ typeId: 'security-rule',
+ },
+ ],
+ actionTypeId: 'test',
+ });
+ });
+
+ test('should format a rule action with null apiKey as expected', () => {
+ expect(
+ formatActionToEnqueue({
+ action: {
+ id: '1',
+ group: 'default',
+ actionTypeId: 'test',
+ params: {
+ foo: true,
+ contextVal: 'My {{context.value}} goes here',
+ stateVal: 'My {{state.value}} goes here',
+ alertVal:
+ 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
+ },
+ uuid: '111-111',
+ },
+ apiKey: null,
+ executionId: '123',
+ ruleConsumer: 'rule-consumer',
+ ruleId: 'aaa',
+ ruleTypeId: 'security-rule',
+ spaceId: 'default',
+ })
+ ).toEqual({
+ id: '1',
+ uuid: '111-111',
+ params: {
+ foo: true,
+ contextVal: 'My {{context.value}} goes here',
+ stateVal: 'My {{state.value}} goes here',
+ alertVal:
+ 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
+ },
+ spaceId: 'default',
+ apiKey: null,
+ consumer: 'rule-consumer',
+ source: {
+ source: {
+ id: 'aaa',
+ type: RULE_SAVED_OBJECT_TYPE,
+ },
+ type: 'SAVED_OBJECT',
+ },
+ executionId: '123',
+ relatedSavedObjects: [
+ {
+ id: 'aaa',
+ type: RULE_SAVED_OBJECT_TYPE,
+ namespace: undefined,
+ typeId: 'security-rule',
+ },
+ ],
+ actionTypeId: 'test',
+ });
+ });
+
+ test('should format a rule action in a custom space as expected', () => {
+ expect(
+ formatActionToEnqueue({
+ action: {
+ id: '1',
+ group: 'default',
+ actionTypeId: 'test',
+ params: {
+ foo: true,
+ contextVal: 'My {{context.value}} goes here',
+ stateVal: 'My {{state.value}} goes here',
+ alertVal:
+ 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
+ },
+ uuid: '111-111',
+ },
+ apiKey: 'MTIzOmFiYw==',
+ executionId: '123',
+ ruleConsumer: 'rule-consumer',
+ ruleId: 'aaa',
+ ruleTypeId: 'security-rule',
+ spaceId: 'my-special-space',
+ })
+ ).toEqual({
+ id: '1',
+ uuid: '111-111',
+ params: {
+ foo: true,
+ contextVal: 'My {{context.value}} goes here',
+ stateVal: 'My {{state.value}} goes here',
+ alertVal:
+ 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
+ },
+ spaceId: 'my-special-space',
+ apiKey: 'MTIzOmFiYw==',
+ consumer: 'rule-consumer',
+ source: {
+ source: {
+ id: 'aaa',
+ type: RULE_SAVED_OBJECT_TYPE,
+ },
+ type: 'SAVED_OBJECT',
+ },
+ executionId: '123',
+ relatedSavedObjects: [
+ {
+ id: 'aaa',
+ type: RULE_SAVED_OBJECT_TYPE,
+ namespace: 'my-special-space',
+ typeId: 'security-rule',
+ },
+ ],
+ actionTypeId: 'test',
+ });
+ });
+
+ test('should format a system action as expected', () => {
+ expect(
+ formatActionToEnqueue({
+ action: {
+ id: '1',
+ actionTypeId: '.test-system-action',
+ params: { myParams: 'test' },
+ uuid: 'xxxyyyyzzzz',
+ },
+ apiKey: 'MTIzOmFiYw==',
+ executionId: '123',
+ ruleConsumer: 'rule-consumer',
+ ruleId: 'aaa',
+ ruleTypeId: 'security-rule',
+ spaceId: 'default',
+ })
+ ).toEqual({
+ id: '1',
+ uuid: 'xxxyyyyzzzz',
+ params: { myParams: 'test' },
+ spaceId: 'default',
+ apiKey: 'MTIzOmFiYw==',
+ consumer: 'rule-consumer',
+ source: {
+ source: {
+ id: 'aaa',
+ type: RULE_SAVED_OBJECT_TYPE,
+ },
+ type: 'SAVED_OBJECT',
+ },
+ executionId: '123',
+ relatedSavedObjects: [
+ {
+ id: 'aaa',
+ type: RULE_SAVED_OBJECT_TYPE,
+ namespace: undefined,
+ typeId: 'security-rule',
+ },
+ ],
+ actionTypeId: '.test-system-action',
+ });
+ });
+});
diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.ts
new file mode 100644
index 0000000000000..af560a19ab9be
--- /dev/null
+++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/format_action_to_enqueue.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { RuleAction, RuleSystemAction } from '@kbn/alerting-types';
+import { asSavedObjectExecutionSource } from '@kbn/actions-plugin/server';
+import { RULE_SAVED_OBJECT_TYPE } from '../../..';
+
+interface FormatActionToEnqueueOpts {
+ action: RuleAction | RuleSystemAction;
+ apiKey: string | null;
+ executionId: string;
+ ruleConsumer: string;
+ ruleId: string;
+ ruleTypeId: string;
+ spaceId: string;
+}
+
+export const formatActionToEnqueue = (opts: FormatActionToEnqueueOpts) => {
+ const { action, apiKey, executionId, ruleConsumer, ruleId, ruleTypeId, spaceId } = opts;
+
+ const namespace = spaceId === 'default' ? {} : { namespace: spaceId };
+ return {
+ id: action.id,
+ uuid: action.uuid,
+ params: action.params,
+ spaceId,
+ apiKey: apiKey ?? null,
+ consumer: ruleConsumer,
+ source: asSavedObjectExecutionSource({
+ id: ruleId,
+ type: RULE_SAVED_OBJECT_TYPE,
+ }),
+ executionId,
+ relatedSavedObjects: [
+ {
+ id: ruleId,
+ type: RULE_SAVED_OBJECT_TYPE,
+ namespace: namespace.namespace,
+ typeId: ruleTypeId,
+ },
+ ],
+ actionTypeId: action.actionTypeId,
+ };
+};
diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/get_summarized_alerts.test.ts
similarity index 95%
rename from x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.test.ts
rename to x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/get_summarized_alerts.test.ts
index 9afd0647094eb..036c49c51d1be 100644
--- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.test.ts
+++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/get_summarized_alerts.test.ts
@@ -6,10 +6,10 @@
*/
import { getSummarizedAlerts } from './get_summarized_alerts';
-import { alertsClientMock } from '../../alerts_client/alerts_client.mock';
-import { mockAAD } from '../fixtures';
+import { alertsClientMock } from '../../../alerts_client/alerts_client.mock';
+import { mockAAD } from '../../fixtures';
import { ALERT_UUID } from '@kbn/rule-data-utils';
-import { generateAlert } from './test_fixtures';
+import { generateAlert } from '../test_fixtures';
import { getErrorSource } from '@kbn/task-manager-plugin/server/task_running';
const alertsClient = alertsClientMock.create();
diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/get_summarized_alerts.ts
similarity index 98%
rename from x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.ts
rename to x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/get_summarized_alerts.ts
index df667a3e20775..00e155856d946 100644
--- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/get_summarized_alerts.ts
+++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/get_summarized_alerts.ts
@@ -7,13 +7,13 @@
import { ALERT_UUID } from '@kbn/rule-data-utils';
import { createTaskRunError, TaskErrorSource } from '@kbn/task-manager-plugin/server';
-import { GetSummarizedAlertsParams, IAlertsClient } from '../../alerts_client/types';
+import { GetSummarizedAlertsParams, IAlertsClient } from '../../../alerts_client/types';
import {
AlertInstanceContext,
AlertInstanceState,
CombinedSummarizedAlerts,
RuleAlertData,
-} from '../../types';
+} from '../../../types';
interface GetSummarizedAlertsOpts<
State extends AlertInstanceState,
diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/index.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/index.ts
new file mode 100644
index 0000000000000..1bd78f302d00c
--- /dev/null
+++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/index.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.
+ */
+
+export { buildRuleUrl } from './build_rule_url';
+export { formatActionToEnqueue } from './format_action_to_enqueue';
+export { getSummarizedAlerts } from './get_summarized_alerts';
+export {
+ isSummaryAction,
+ isActionOnInterval,
+ isSummaryActionThrottled,
+ generateActionHash,
+ getSummaryActionsFromTaskState,
+ getSummaryActionTimeBounds,
+ logNumberOfFilteredAlerts,
+} from './rule_action_helper';
+export { shouldScheduleAction } from './should_schedule_action';
diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/rule_action_helper.test.ts
similarity index 99%
rename from x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.test.ts
rename to x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/rule_action_helper.test.ts
index cc8a0a1b0cde5..1adb68a951351 100644
--- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.test.ts
+++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/rule_action_helper.test.ts
@@ -7,7 +7,7 @@
import { Logger } from '@kbn/logging';
import { loggingSystemMock } from '@kbn/core/server/mocks';
-import { RuleAction } from '../../types';
+import { RuleAction } from '../../../types';
import {
generateActionHash,
getSummaryActionsFromTaskState,
diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/rule_action_helper.ts
similarity index 99%
rename from x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.ts
rename to x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/rule_action_helper.ts
index 67223b0728689..c3ef79b3086d8 100644
--- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/rule_action_helper.ts
+++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/rule_action_helper.ts
@@ -12,7 +12,7 @@ import {
RuleAction,
RuleNotifyWhenTypeValues,
ThrottledActions,
-} from '../../../common';
+} from '../../../../common';
export const isSummaryAction = (action?: RuleAction) => {
return action?.frequency?.summary ?? false;
diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/should_schedule_action.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/should_schedule_action.test.ts
new file mode 100644
index 0000000000000..7ebd65fab005d
--- /dev/null
+++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/should_schedule_action.test.ts
@@ -0,0 +1,195 @@
+/*
+ * 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 { loggingSystemMock } from '@kbn/core-logging-server-mocks';
+import { shouldScheduleAction } from './should_schedule_action';
+import { ruleRunMetricsStoreMock } from '../../../lib/rule_run_metrics_store.mock';
+import { ActionsCompletion } from '@kbn/alerting-state-types';
+
+const logger = loggingSystemMock.create().get();
+const ruleRunMetricsStore = ruleRunMetricsStoreMock.create();
+
+describe('shouldScheduleAction', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('should return false if the the limit of executable actions has been reached', () => {
+ ruleRunMetricsStore.hasReachedTheExecutableActionsLimit.mockReturnValueOnce(true);
+ expect(
+ shouldScheduleAction({
+ action: {
+ id: '1',
+ group: 'default',
+ actionTypeId: 'test-action-type-id',
+ params: {
+ foo: true,
+ contextVal: 'My {{context.value}} goes here',
+ stateVal: 'My {{state.value}} goes here',
+ alertVal:
+ 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
+ },
+ uuid: '111-111',
+ },
+ actionsConfigMap: {
+ default: { max: 4 },
+ 'test-action-type-id': { max: 2 },
+ },
+ isActionExecutable: () => true,
+ logger,
+ ruleId: '1',
+ ruleRunMetricsStore,
+ })
+ ).toEqual(false);
+
+ expect(ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType).toHaveBeenCalledWith({
+ actionTypeId: 'test-action-type-id',
+ status: ActionsCompletion.PARTIAL,
+ });
+ expect(logger.debug).toHaveBeenCalledWith(
+ `Rule "1" skipped scheduling action "1" because the maximum number of allowed actions has been reached.`
+ );
+ });
+
+ test('should return false if the the limit of executable actions for this action type has been reached', () => {
+ ruleRunMetricsStore.hasReachedTheExecutableActionsLimitByConnectorType.mockReturnValueOnce(
+ true
+ );
+ ruleRunMetricsStore.hasConnectorTypeReachedTheLimit.mockReturnValueOnce(true);
+ expect(
+ shouldScheduleAction({
+ action: {
+ id: '1',
+ group: 'default',
+ actionTypeId: 'test-action-type-id',
+ params: {
+ foo: true,
+ contextVal: 'My {{context.value}} goes here',
+ stateVal: 'My {{state.value}} goes here',
+ alertVal:
+ 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
+ },
+ uuid: '111-111',
+ },
+ actionsConfigMap: {
+ default: { max: 4 },
+ 'test-action-type-id': { max: 2 },
+ },
+ isActionExecutable: () => true,
+ logger,
+ ruleId: '1',
+ ruleRunMetricsStore,
+ })
+ ).toEqual(false);
+
+ expect(ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType).toHaveBeenCalledWith({
+ actionTypeId: 'test-action-type-id',
+ status: ActionsCompletion.PARTIAL,
+ });
+ expect(logger.debug).not.toHaveBeenCalled();
+ });
+
+ test('should return false and log if the the limit of executable actions for this action type has been reached', () => {
+ ruleRunMetricsStore.hasReachedTheExecutableActionsLimitByConnectorType.mockReturnValueOnce(
+ true
+ );
+ ruleRunMetricsStore.hasConnectorTypeReachedTheLimit.mockReturnValueOnce(false);
+ expect(
+ shouldScheduleAction({
+ action: {
+ id: '1',
+ group: 'default',
+ actionTypeId: 'test-action-type-id',
+ params: {
+ foo: true,
+ contextVal: 'My {{context.value}} goes here',
+ stateVal: 'My {{state.value}} goes here',
+ alertVal:
+ 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
+ },
+ uuid: '111-111',
+ },
+ actionsConfigMap: {
+ default: { max: 4 },
+ 'test-action-type-id': { max: 2 },
+ },
+ isActionExecutable: () => true,
+ logger,
+ ruleId: '1',
+ ruleRunMetricsStore,
+ })
+ ).toEqual(false);
+
+ expect(ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType).toHaveBeenCalledWith({
+ actionTypeId: 'test-action-type-id',
+ status: ActionsCompletion.PARTIAL,
+ });
+ expect(logger.debug).toHaveBeenCalledWith(
+ `Rule "1" skipped scheduling action "1" because the maximum number of allowed actions for connector type test-action-type-id has been reached.`
+ );
+ });
+
+ test('should return false the action is not executable', () => {
+ expect(
+ shouldScheduleAction({
+ action: {
+ id: '1',
+ group: 'default',
+ actionTypeId: 'test-action-type-id',
+ params: {
+ foo: true,
+ contextVal: 'My {{context.value}} goes here',
+ stateVal: 'My {{state.value}} goes here',
+ alertVal:
+ 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
+ },
+ uuid: '111-111',
+ },
+ actionsConfigMap: {
+ default: { max: 4 },
+ 'test-action-type-id': { max: 2 },
+ },
+ isActionExecutable: () => false,
+ logger,
+ ruleId: '1',
+ ruleRunMetricsStore,
+ })
+ ).toEqual(false);
+
+ expect(logger.warn).toHaveBeenCalledWith(
+ `Rule "1" skipped scheduling action "1" because it is disabled`
+ );
+ });
+
+ test('should return true if the action is executable and no limits have been reached', () => {
+ expect(
+ shouldScheduleAction({
+ action: {
+ id: '1',
+ group: 'default',
+ actionTypeId: 'test-action-type-id',
+ params: {
+ foo: true,
+ contextVal: 'My {{context.value}} goes here',
+ stateVal: 'My {{state.value}} goes here',
+ alertVal:
+ 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
+ },
+ uuid: '111-111',
+ },
+ actionsConfigMap: {
+ default: { max: 4 },
+ 'test-action-type-id': { max: 2 },
+ },
+ isActionExecutable: () => true,
+ logger,
+ ruleId: '1',
+ ruleRunMetricsStore,
+ })
+ ).toEqual(true);
+ });
+});
diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/should_schedule_action.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/should_schedule_action.ts
new file mode 100644
index 0000000000000..99fa3c42ad3df
--- /dev/null
+++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/lib/should_schedule_action.ts
@@ -0,0 +1,70 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { Logger } from '@kbn/logging';
+import { ActionsCompletion } from '@kbn/alerting-state-types';
+import { RuleAction, RuleSystemAction } from '@kbn/alerting-types';
+import { RuleRunMetricsStore } from '../../../lib/rule_run_metrics_store';
+import { ActionsConfigMap } from '../../../lib/get_actions_config_map';
+
+interface ShouldScheduleActionOpts {
+ action: RuleAction | RuleSystemAction;
+ actionsConfigMap: ActionsConfigMap;
+ isActionExecutable(
+ actionId: string,
+ actionTypeId: string,
+ options?: { notifyUsage: boolean }
+ ): boolean;
+ logger: Logger;
+ ruleId: string;
+ ruleRunMetricsStore: RuleRunMetricsStore;
+}
+
+export const shouldScheduleAction = (opts: ShouldScheduleActionOpts): boolean => {
+ const { actionsConfigMap, action, logger, ruleRunMetricsStore } = opts;
+
+ // keep track of how many actions we want to schedule by connector type
+ ruleRunMetricsStore.incrementNumberOfGeneratedActionsByConnectorType(action.actionTypeId);
+
+ if (ruleRunMetricsStore.hasReachedTheExecutableActionsLimit(actionsConfigMap)) {
+ ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({
+ actionTypeId: action.actionTypeId,
+ status: ActionsCompletion.PARTIAL,
+ });
+ logger.debug(
+ `Rule "${opts.ruleId}" skipped scheduling action "${action.id}" because the maximum number of allowed actions has been reached.`
+ );
+ return false;
+ }
+
+ if (
+ ruleRunMetricsStore.hasReachedTheExecutableActionsLimitByConnectorType({
+ actionTypeId: action.actionTypeId,
+ actionsConfigMap,
+ })
+ ) {
+ if (!ruleRunMetricsStore.hasConnectorTypeReachedTheLimit(action.actionTypeId)) {
+ logger.debug(
+ `Rule "${opts.ruleId}" skipped scheduling action "${action.id}" because the maximum number of allowed actions for connector type ${action.actionTypeId} has been reached.`
+ );
+ }
+ ruleRunMetricsStore.setTriggeredActionsStatusByConnectorType({
+ actionTypeId: action.actionTypeId,
+ status: ActionsCompletion.PARTIAL,
+ });
+ return false;
+ }
+
+ if (!opts.isActionExecutable(action.id, action.actionTypeId, { notifyUsage: true })) {
+ logger.warn(
+ `Rule "${opts.ruleId}" skipped scheduling action "${action.id}" because it is disabled`
+ );
+ return false;
+ }
+
+ return true;
+};
diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.test.ts
index 53e75245d94d0..99a693133a2a6 100644
--- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.test.ts
+++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.test.ts
@@ -16,6 +16,12 @@ import { PerAlertActionScheduler } from './per_alert_action_scheduler';
import { getRule, getRuleType, getDefaultSchedulerContext, generateAlert } from '../test_fixtures';
import { SanitizedRuleAction } from '@kbn/alerting-types';
import { ALERT_UUID } from '@kbn/rule-data-utils';
+import { Alert } from '../../../alert';
+import {
+ ActionsCompletion,
+ AlertInstanceContext,
+ AlertInstanceState,
+} from '@kbn/alerting-state-types';
const alertingEventLogger = alertingEventLoggerMock.create();
const actionsClient = actionsClientMock.create();
@@ -25,9 +31,10 @@ const logger = loggingSystemMock.create().get();
let ruleRunMetricsStore: RuleRunMetricsStore;
const rule = getRule({
+ id: 'rule-id-1',
actions: [
{
- id: '1',
+ id: 'action-1',
group: 'default',
actionTypeId: 'test',
frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null },
@@ -41,7 +48,7 @@ const rule = getRule({
uuid: '111-111',
},
{
- id: '2',
+ id: 'action-2',
group: 'default',
actionTypeId: 'test',
frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null },
@@ -55,7 +62,7 @@ const rule = getRule({
uuid: '222-222',
},
{
- id: '3',
+ id: 'action-3',
group: 'default',
actionTypeId: 'test',
frequency: { summary: true, notifyWhen: 'onActiveAlert' },
@@ -84,6 +91,21 @@ const getSchedulerContext = (params = {}) => {
return { ...defaultSchedulerContext, rule, ...params, ruleRunMetricsStore };
};
+const getResult = (actionId: string, alertId: string, actionUuid: string) => ({
+ actionToEnqueue: {
+ actionTypeId: 'test',
+ apiKey: 'MTIzOmFiYw==',
+ consumer: 'rule-consumer',
+ executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28',
+ id: actionId,
+ uuid: actionUuid,
+ relatedSavedObjects: [{ id: 'rule-id-1', namespace: 'test1', type: 'alert', typeId: 'test' }],
+ source: { source: { id: 'rule-id-1', type: 'alert' }, type: 'SAVED_OBJECT' },
+ spaceId: 'test1',
+ },
+ actionToLog: { alertGroup: 'default', alertId, id: actionId, uuid: actionUuid, typeId: 'test' },
+});
+
let clock: sinon.SinonFakeTimers;
describe('Per-Alert Action Scheduler', () => {
@@ -93,6 +115,7 @@ describe('Per-Alert Action Scheduler', () => {
beforeEach(() => {
jest.resetAllMocks();
+ jest.clearAllMocks();
mockActionsPlugin.isActionTypeEnabled.mockReturnValue(true);
mockActionsPlugin.isActionExecutable.mockReturnValue(true);
mockActionsPlugin.getActionsClientWithRequest.mockResolvedValue(actionsClient);
@@ -163,67 +186,93 @@ describe('Per-Alert Action Scheduler', () => {
expect(scheduler.actions).toEqual([actions[0]]);
expect(logger.error).toHaveBeenCalledTimes(1);
expect(logger.error).toHaveBeenCalledWith(
- `Skipping action \"2\" for rule \"1\" because the rule type \"Test\" does not support alert-as-data.`
+ `Skipping action \"2\" for rule \"rule-id-1\" because the rule type \"Test\" does not support alert-as-data.`
);
});
- describe('generateExecutables', () => {
- const newAlert1 = generateAlert({ id: 1 });
- const newAlert2 = generateAlert({ id: 2 });
- const alerts = { ...newAlert1, ...newAlert2 };
+ describe('getActionsToSchedule', () => {
+ let newAlert1: Record<
+ string,
+ Alert
+ >;
+ let newAlert2: Record<
+ string,
+ Alert
+ >;
+ let alerts: Record<
+ string,
+ Alert
+ >;
+
+ beforeEach(() => {
+ newAlert1 = generateAlert({ id: 1 });
+ newAlert2 = generateAlert({ id: 2 });
+ alerts = { ...newAlert1, ...newAlert2 };
+ });
- test('should generate executable for each alert and each action', async () => {
+ test('should create action to schedule for each alert and each action', async () => {
+ // 2 per-alert actions * 2 alerts = 4 actions to schedule
const scheduler = new PerAlertActionScheduler(getSchedulerContext());
- const executables = await scheduler.generateExecutables({
- alerts,
- throttledSummaryActions: {},
- });
+ const results = await scheduler.getActionsToSchedule({ alerts });
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
expect(logger.debug).not.toHaveBeenCalled();
- expect(executables).toHaveLength(4);
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 4,
+ numberOfTriggeredActions: 4,
+ });
- expect(executables).toEqual([
- { action: rule.actions[0], alert: alerts['1'] },
- { action: rule.actions[0], alert: alerts['2'] },
- { action: rule.actions[1], alert: alerts['1'] },
- { action: rule.actions[1], alert: alerts['2'] },
+ expect(results).toHaveLength(4);
+ expect(results).toEqual([
+ getResult('action-1', '1', '111-111'),
+ getResult('action-1', '2', '111-111'),
+ getResult('action-2', '1', '222-222'),
+ getResult('action-2', '2', '222-222'),
]);
});
- test('should skip generating executable when alert has maintenance window', async () => {
+ test('should skip creating actions to schedule when alert has maintenance window', async () => {
+ // 2 per-alert actions * 2 alerts = 4 actions to schedule
+ // but alert 1 has maintenance window, so only actions for alert 2 should be scheduled
const scheduler = new PerAlertActionScheduler(getSchedulerContext());
const newAlertWithMaintenanceWindow = generateAlert({
id: 1,
maintenanceWindowIds: ['mw-1'],
});
const alertsWithMaintenanceWindow = { ...newAlertWithMaintenanceWindow, ...newAlert2 };
- const executables = await scheduler.generateExecutables({
- alerts: alertsWithMaintenanceWindow,
- throttledSummaryActions: {},
- });
+ const results = await scheduler.getActionsToSchedule({ alerts: alertsWithMaintenanceWindow });
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
expect(logger.debug).toHaveBeenCalledTimes(2);
expect(logger.debug).toHaveBeenNthCalledWith(
1,
- `no scheduling of summary actions \"1\" for rule \"1\": has active maintenance windows mw-1.`
+ `no scheduling of summary actions \"action-1\" for rule \"rule-id-1\": has active maintenance windows mw-1.`
);
expect(logger.debug).toHaveBeenNthCalledWith(
2,
- `no scheduling of summary actions \"2\" for rule \"1\": has active maintenance windows mw-1.`
+ `no scheduling of summary actions \"action-2\" for rule \"rule-id-1\": has active maintenance windows mw-1.`
);
- expect(executables).toHaveLength(2);
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 2,
+ numberOfTriggeredActions: 2,
+ });
- expect(executables).toEqual([
- { action: rule.actions[0], alert: alerts['2'] },
- { action: rule.actions[1], alert: alerts['2'] },
+ expect(results).toHaveLength(2);
+ expect(results).toEqual([
+ getResult('action-1', '2', '111-111'),
+ getResult('action-2', '2', '222-222'),
]);
});
- test('should skip generating executable when alert has invalid action group', async () => {
+ test('should skip creating actions to schedule when alert has invalid action group', async () => {
+ // 2 per-alert actions * 2 alerts = 4 actions to schedule
+ // but alert 1 has invalid action group, so only actions for alert 2 should be scheduled
const scheduler = new PerAlertActionScheduler(getSchedulerContext());
const newAlertInvalidActionGroup = generateAlert({
id: 1,
@@ -231,9 +280,8 @@ describe('Per-Alert Action Scheduler', () => {
group: 'invalid',
});
const alertsWithInvalidActionGroup = { ...newAlertInvalidActionGroup, ...newAlert2 };
- const executables = await scheduler.generateExecutables({
+ const results = await scheduler.getActionsToSchedule({
alerts: alertsWithInvalidActionGroup,
- throttledSummaryActions: {},
});
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
@@ -247,15 +295,23 @@ describe('Per-Alert Action Scheduler', () => {
`Invalid action group \"invalid\" for rule \"test\".`
);
- expect(executables).toHaveLength(2);
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 2,
+ numberOfTriggeredActions: 2,
+ });
- expect(executables).toEqual([
- { action: rule.actions[0], alert: alerts['2'] },
- { action: rule.actions[1], alert: alerts['2'] },
+ expect(results).toHaveLength(2);
+ expect(results).toEqual([
+ getResult('action-1', '2', '111-111'),
+ getResult('action-2', '2', '222-222'),
]);
});
- test('should skip generating executable when alert has pending recovered count greater than 0 and notifyWhen is onActiveAlert', async () => {
+ test('should skip creating actions to schedule when alert has pending recovered count greater than 0 and notifyWhen is onActiveAlert', async () => {
+ // 2 per-alert actions * 2 alerts = 4 actions to schedule
+ // but alert 1 has a pending recovered count > 0 & notifyWhen is onActiveAlert, so only actions for alert 2 should be scheduled
const scheduler = new PerAlertActionScheduler(getSchedulerContext());
const newAlertWithPendingRecoveredCount = generateAlert({
id: 1,
@@ -265,23 +321,31 @@ describe('Per-Alert Action Scheduler', () => {
...newAlertWithPendingRecoveredCount,
...newAlert2,
};
- const executables = await scheduler.generateExecutables({
+ const results = await scheduler.getActionsToSchedule({
alerts: alertsWithPendingRecoveredCount,
- throttledSummaryActions: {},
});
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
- expect(executables).toHaveLength(2);
- expect(executables).toEqual([
- { action: rule.actions[0], alert: alerts['2'] },
- { action: rule.actions[1], alert: alerts['2'] },
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 2,
+ numberOfTriggeredActions: 2,
+ });
+
+ expect(results).toHaveLength(2);
+ expect(results).toEqual([
+ getResult('action-1', '2', '111-111'),
+ getResult('action-2', '2', '222-222'),
]);
});
- test('should skip generating executable when alert has pending recovered count greater than 0 and notifyWhen is onThrottleInterval', async () => {
+ test('should skip creating actions to schedule when alert has pending recovered count greater than 0 and notifyWhen is onThrottleInterval', async () => {
+ // 2 per-alert actions * 2 alerts = 4 actions to schedule
+ // but alert 1 has a pending recovered count > 0 & notifyWhen is onThrottleInterval, so only actions for alert 2 should be scheduled
const onThrottleIntervalAction: SanitizedRuleAction = {
- id: '2',
+ id: 'action-4',
group: 'default',
actionTypeId: 'test',
frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '1h' },
@@ -292,43 +356,45 @@ describe('Per-Alert Action Scheduler', () => {
alertVal:
'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
},
- uuid: '222-222',
+ uuid: '444-444',
};
const scheduler = new PerAlertActionScheduler({
...getSchedulerContext(),
rule: { ...rule, actions: [rule.actions[0], onThrottleIntervalAction] },
});
- const newAlertWithPendingRecoveredCount = generateAlert({
- id: 1,
- pendingRecoveredCount: 3,
- });
+ const newAlertWithPendingRecoveredCount = generateAlert({ id: 1, pendingRecoveredCount: 3 });
const alertsWithPendingRecoveredCount = {
...newAlertWithPendingRecoveredCount,
...newAlert2,
};
- const executables = await scheduler.generateExecutables({
+ const results = await scheduler.getActionsToSchedule({
alerts: alertsWithPendingRecoveredCount,
- throttledSummaryActions: {},
});
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
- expect(executables).toHaveLength(2);
- expect(executables).toEqual([
- { action: rule.actions[0], alert: alerts['2'] },
- { action: onThrottleIntervalAction, alert: alerts['2'] },
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 2,
+ numberOfTriggeredActions: 2,
+ });
+
+ expect(results).toHaveLength(2);
+ expect(results).toEqual([
+ getResult('action-1', '2', '111-111'),
+ getResult('action-4', '2', '444-444'),
]);
});
- test('should skip generating executable when alert is muted', async () => {
+ test('should skip creating actions to schedule when alert is muted', async () => {
+ // 2 per-alert actions * 2 alerts = 4 actions to schedule
+ // but alert 2 is muted, so only actions for alert 1 should be scheduled
const scheduler = new PerAlertActionScheduler({
...getSchedulerContext(),
rule: { ...rule, mutedInstanceIds: ['2'] },
});
- const executables = await scheduler.generateExecutables({
- alerts,
- throttledSummaryActions: {},
- });
+ const results = await scheduler.getActionsToSchedule({ alerts });
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
expect(logger.debug).toHaveBeenCalledTimes(1);
@@ -336,20 +402,27 @@ describe('Per-Alert Action Scheduler', () => {
1,
`skipping scheduling of actions for '2' in rule rule-label: rule is muted`
);
- expect(executables).toHaveLength(2);
- // @ts-expect-error private variable
- expect(scheduler.skippedAlerts).toEqual({ '2': { reason: 'muted' } });
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 2,
+ numberOfTriggeredActions: 2,
+ });
- expect(executables).toEqual([
- { action: rule.actions[0], alert: alerts['1'] },
- { action: rule.actions[1], alert: alerts['1'] },
+ expect(results).toHaveLength(2);
+ expect(results).toEqual([
+ getResult('action-1', '1', '111-111'),
+ getResult('action-2', '1', '222-222'),
]);
+
+ // @ts-expect-error private variable
+ expect(scheduler.skippedAlerts).toEqual({ '2': { reason: 'muted' } });
});
- test('should skip generating executable when alert action group has not changed and notifyWhen is onActionGroupChange', async () => {
+ test('should skip creating actions to schedule when alert action group has not changed and notifyWhen is onActionGroupChange', async () => {
const onActionGroupChangeAction: SanitizedRuleAction = {
- id: '2',
+ id: 'action-4',
group: 'default',
actionTypeId: 'test',
frequency: { summary: false, notifyWhen: 'onActionGroupChange', throttle: null },
@@ -360,7 +433,7 @@ describe('Per-Alert Action Scheduler', () => {
alertVal:
'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
},
- uuid: '222-222',
+ uuid: '444-444',
};
const activeAlert1 = generateAlert({
@@ -380,10 +453,7 @@ describe('Per-Alert Action Scheduler', () => {
rule: { ...rule, actions: [rule.actions[0], onActionGroupChangeAction] },
});
- const executables = await scheduler.generateExecutables({
- alerts: alertsWithOngoingAlert,
- throttledSummaryActions: {},
- });
+ const results = await scheduler.getActionsToSchedule({ alerts: alertsWithOngoingAlert });
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
expect(logger.debug).toHaveBeenCalledTimes(1);
@@ -391,21 +461,28 @@ describe('Per-Alert Action Scheduler', () => {
1,
`skipping scheduling of actions for '2' in rule rule-label: alert is active but action group has not changed`
);
- expect(executables).toHaveLength(3);
- // @ts-expect-error private variable
- expect(scheduler.skippedAlerts).toEqual({ '2': { reason: 'actionGroupHasNotChanged' } });
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(3);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(3);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 3,
+ numberOfTriggeredActions: 3,
+ });
- expect(executables).toEqual([
- { action: rule.actions[0], alert: alertsWithOngoingAlert['1'] },
- { action: rule.actions[0], alert: alertsWithOngoingAlert['2'] },
- { action: onActionGroupChangeAction, alert: alertsWithOngoingAlert['1'] },
+ expect(results).toHaveLength(3);
+ expect(results).toEqual([
+ getResult('action-1', '1', '111-111'),
+ getResult('action-1', '2', '111-111'),
+ getResult('action-4', '1', '444-444'),
]);
+
+ // @ts-expect-error private variable
+ expect(scheduler.skippedAlerts).toEqual({ '2': { reason: 'actionGroupHasNotChanged' } });
});
- test('should skip generating executable when throttle interval has not passed and notifyWhen is onThrottleInterval', async () => {
+ test('should skip creating actions to schedule when throttle interval has not passed and notifyWhen is onThrottleInterval', async () => {
const onThrottleIntervalAction: SanitizedRuleAction = {
- id: '2',
+ id: 'action-5',
group: 'default',
actionTypeId: 'test',
frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '1h' },
@@ -416,13 +493,13 @@ describe('Per-Alert Action Scheduler', () => {
alertVal:
'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
},
- uuid: '222-222',
+ uuid: '555-555',
};
const activeAlert2 = generateAlert({
id: 2,
lastScheduledActionsGroup: 'default',
- throttledActions: { '222-222': { date: '1969-12-31T23:10:00.000Z' } },
+ throttledActions: { '555-555': { date: '1969-12-31T23:10:00.000Z' } },
});
const alertsWithOngoingAlert = { ...newAlert1, ...activeAlert2 };
@@ -431,10 +508,7 @@ describe('Per-Alert Action Scheduler', () => {
rule: { ...rule, actions: [rule.actions[0], onThrottleIntervalAction] },
});
- const executables = await scheduler.generateExecutables({
- alerts: alertsWithOngoingAlert,
- throttledSummaryActions: {},
- });
+ const results = await scheduler.getActionsToSchedule({ alerts: alertsWithOngoingAlert });
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
expect(logger.debug).toHaveBeenCalledTimes(1);
@@ -442,21 +516,28 @@ describe('Per-Alert Action Scheduler', () => {
1,
`skipping scheduling of actions for '2' in rule rule-label: rule is throttled`
);
- expect(executables).toHaveLength(3);
- // @ts-expect-error private variable
- expect(scheduler.skippedAlerts).toEqual({ '2': { reason: 'throttled' } });
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(3);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(3);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 3,
+ numberOfTriggeredActions: 3,
+ });
- expect(executables).toEqual([
- { action: rule.actions[0], alert: alertsWithOngoingAlert['1'] },
- { action: rule.actions[0], alert: alertsWithOngoingAlert['2'] },
- { action: onThrottleIntervalAction, alert: alertsWithOngoingAlert['1'] },
+ expect(results).toHaveLength(3);
+ expect(results).toEqual([
+ getResult('action-1', '1', '111-111'),
+ getResult('action-1', '2', '111-111'),
+ getResult('action-5', '1', '555-555'),
]);
+
+ // @ts-expect-error private variable
+ expect(scheduler.skippedAlerts).toEqual({ '2': { reason: 'throttled' } });
});
- test('should not skip generating executable when throttle interval has passed and notifyWhen is onThrottleInterval', async () => {
+ test('should not skip creating actions to schedule when throttle interval has passed and notifyWhen is onThrottleInterval', async () => {
const onThrottleIntervalAction: SanitizedRuleAction = {
- id: '2',
+ id: 'action-5',
group: 'default',
actionTypeId: 'test',
frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '1h' },
@@ -467,7 +548,7 @@ describe('Per-Alert Action Scheduler', () => {
alertVal:
'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
},
- uuid: '222-222',
+ uuid: '555-555',
};
const activeAlert2 = generateAlert({
@@ -482,24 +563,28 @@ describe('Per-Alert Action Scheduler', () => {
rule: { ...rule, actions: [rule.actions[0], onThrottleIntervalAction] },
});
- const executables = await scheduler.generateExecutables({
- alerts: alertsWithOngoingAlert,
- throttledSummaryActions: {},
- });
+ const results = await scheduler.getActionsToSchedule({ alerts: alertsWithOngoingAlert });
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
expect(logger.debug).not.toHaveBeenCalled();
- expect(executables).toHaveLength(4);
- // @ts-expect-error private variable
- expect(scheduler.skippedAlerts).toEqual({});
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 4,
+ numberOfTriggeredActions: 4,
+ });
- expect(executables).toEqual([
- { action: rule.actions[0], alert: alertsWithOngoingAlert['1'] },
- { action: rule.actions[0], alert: alertsWithOngoingAlert['2'] },
- { action: onThrottleIntervalAction, alert: alertsWithOngoingAlert['1'] },
- { action: onThrottleIntervalAction, alert: alertsWithOngoingAlert['2'] },
+ expect(results).toHaveLength(4);
+ expect(results).toEqual([
+ getResult('action-1', '1', '111-111'),
+ getResult('action-1', '2', '111-111'),
+ getResult('action-5', '1', '555-555'),
+ getResult('action-5', '2', '555-555'),
]);
+
+ // @ts-expect-error private variable
+ expect(scheduler.skippedAlerts).toEqual({});
});
test('should query for summarized alerts if useAlertDataForTemplate is true', async () => {
@@ -517,7 +602,7 @@ describe('Per-Alert Action Scheduler', () => {
};
alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts);
const actionWithUseAlertDataForTemplate: SanitizedRuleAction = {
- id: '1',
+ id: 'action-6',
group: 'default',
actionTypeId: 'test',
frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null },
@@ -528,33 +613,36 @@ describe('Per-Alert Action Scheduler', () => {
alertVal:
'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
},
- uuid: '111-111',
+ uuid: '666-666',
useAlertDataForTemplate: true,
};
const scheduler = new PerAlertActionScheduler({
...getSchedulerContext(),
rule: { ...rule, actions: [rule.actions[0], actionWithUseAlertDataForTemplate] },
});
- const executables = await scheduler.generateExecutables({
- alerts,
- throttledSummaryActions: {},
- });
+ const results = await scheduler.getActionsToSchedule({ alerts });
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1);
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({
excludedAlertInstanceIds: [],
executionUuid: defaultSchedulerContext.executionId,
- ruleId: '1',
+ ruleId: 'rule-id-1',
spaceId: 'test1',
});
- expect(executables).toHaveLength(4);
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 4,
+ numberOfTriggeredActions: 4,
+ });
- expect(executables).toEqual([
- { action: rule.actions[0], alert: alerts['1'] },
- { action: rule.actions[0], alert: alerts['2'] },
- { action: actionWithUseAlertDataForTemplate, alert: alerts['1'] },
- { action: actionWithUseAlertDataForTemplate, alert: alerts['2'] },
+ expect(results).toHaveLength(4);
+ expect(results).toEqual([
+ getResult('action-1', '1', '111-111'),
+ getResult('action-1', '2', '111-111'),
+ getResult('action-6', '1', '666-666'),
+ getResult('action-6', '2', '666-666'),
]);
});
@@ -573,7 +661,7 @@ describe('Per-Alert Action Scheduler', () => {
};
alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts);
const actionWithUseAlertDataForTemplate: SanitizedRuleAction = {
- id: '1',
+ id: 'action-6',
group: 'default',
actionTypeId: 'test',
frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '1h' },
@@ -584,34 +672,37 @@ describe('Per-Alert Action Scheduler', () => {
alertVal:
'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
},
- uuid: '111-111',
+ uuid: '666-666',
useAlertDataForTemplate: true,
};
const scheduler = new PerAlertActionScheduler({
...getSchedulerContext(),
rule: { ...rule, actions: [rule.actions[0], actionWithUseAlertDataForTemplate] },
});
- const executables = await scheduler.generateExecutables({
- alerts,
- throttledSummaryActions: {},
- });
+ const results = await scheduler.getActionsToSchedule({ alerts });
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1);
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({
excludedAlertInstanceIds: [],
- ruleId: '1',
+ ruleId: 'rule-id-1',
spaceId: 'test1',
start: new Date('1969-12-31T23:00:00.000Z'),
end: new Date('1970-01-01T00:00:00.000Z'),
});
- expect(executables).toHaveLength(4);
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 4,
+ numberOfTriggeredActions: 4,
+ });
- expect(executables).toEqual([
- { action: rule.actions[0], alert: alerts['1'] },
- { action: rule.actions[0], alert: alerts['2'] },
- { action: actionWithUseAlertDataForTemplate, alert: alerts['1'] },
- { action: actionWithUseAlertDataForTemplate, alert: alerts['2'] },
+ expect(results).toHaveLength(4);
+ expect(results).toEqual([
+ getResult('action-1', '1', '111-111'),
+ getResult('action-1', '2', '111-111'),
+ getResult('action-6', '1', '666-666'),
+ getResult('action-6', '2', '666-666'),
]);
});
@@ -630,7 +721,7 @@ describe('Per-Alert Action Scheduler', () => {
};
alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts);
const actionWithAlertsFilter: SanitizedRuleAction = {
- id: '1',
+ id: 'action-7',
group: 'default',
actionTypeId: 'test',
frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null },
@@ -641,34 +732,37 @@ describe('Per-Alert Action Scheduler', () => {
alertVal:
'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
},
- uuid: '111-111',
+ uuid: '777-777',
alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } },
};
const scheduler = new PerAlertActionScheduler({
...getSchedulerContext(),
rule: { ...rule, actions: [rule.actions[0], actionWithAlertsFilter] },
});
- const executables = await scheduler.generateExecutables({
- alerts,
- throttledSummaryActions: {},
- });
+ const results = await scheduler.getActionsToSchedule({ alerts });
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1);
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({
excludedAlertInstanceIds: [],
executionUuid: defaultSchedulerContext.executionId,
- ruleId: '1',
+ ruleId: 'rule-id-1',
spaceId: 'test1',
alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } },
});
- expect(executables).toHaveLength(4);
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 4,
+ numberOfTriggeredActions: 4,
+ });
- expect(executables).toEqual([
- { action: rule.actions[0], alert: alerts['1'] },
- { action: rule.actions[0], alert: alerts['2'] },
- { action: actionWithAlertsFilter, alert: alerts['1'] },
- { action: actionWithAlertsFilter, alert: alerts['2'] },
+ expect(results).toHaveLength(4);
+ expect(results).toEqual([
+ getResult('action-1', '1', '111-111'),
+ getResult('action-1', '2', '111-111'),
+ getResult('action-7', '1', '777-777'),
+ getResult('action-7', '2', '777-777'),
]);
});
@@ -687,7 +781,7 @@ describe('Per-Alert Action Scheduler', () => {
};
alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts);
const actionWithAlertsFilter: SanitizedRuleAction = {
- id: '1',
+ id: 'action-7',
group: 'default',
actionTypeId: 'test',
frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '6h' },
@@ -698,39 +792,42 @@ describe('Per-Alert Action Scheduler', () => {
alertVal:
'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
},
- uuid: '111-111',
+ uuid: '777-777',
alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } },
};
const scheduler = new PerAlertActionScheduler({
...getSchedulerContext(),
rule: { ...rule, actions: [rule.actions[0], actionWithAlertsFilter] },
});
- const executables = await scheduler.generateExecutables({
- alerts,
- throttledSummaryActions: {},
- });
+ const results = await scheduler.getActionsToSchedule({ alerts });
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1);
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({
excludedAlertInstanceIds: [],
- ruleId: '1',
+ ruleId: 'rule-id-1',
spaceId: 'test1',
alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } },
start: new Date('1969-12-31T18:00:00.000Z'),
end: new Date('1970-01-01T00:00:00.000Z'),
});
- expect(executables).toHaveLength(4);
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 4,
+ numberOfTriggeredActions: 4,
+ });
- expect(executables).toEqual([
- { action: rule.actions[0], alert: alerts['1'] },
- { action: rule.actions[0], alert: alerts['2'] },
- { action: actionWithAlertsFilter, alert: alerts['1'] },
- { action: actionWithAlertsFilter, alert: alerts['2'] },
+ expect(results).toHaveLength(4);
+ expect(results).toEqual([
+ getResult('action-1', '1', '111-111'),
+ getResult('action-1', '2', '111-111'),
+ getResult('action-7', '1', '777-777'),
+ getResult('action-7', '2', '777-777'),
]);
});
- test('should skip generating executable if alert does not match any alerts in summarized alerts', async () => {
+ test('should skip creating actions to schedule if alert does not match any alerts in summarized alerts', async () => {
alertsClient.getProcessedAlerts.mockReturnValue(alerts);
const summarizedAlerts = {
new: {
@@ -745,7 +842,7 @@ describe('Per-Alert Action Scheduler', () => {
};
alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts);
const actionWithAlertsFilter: SanitizedRuleAction = {
- id: '1',
+ id: 'action-8',
group: 'default',
actionTypeId: 'test',
frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null },
@@ -756,33 +853,36 @@ describe('Per-Alert Action Scheduler', () => {
alertVal:
'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
},
- uuid: '111-111',
+ uuid: '888-888',
alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } },
};
const scheduler = new PerAlertActionScheduler({
...getSchedulerContext(),
rule: { ...rule, actions: [rule.actions[0], actionWithAlertsFilter] },
});
- const executables = await scheduler.generateExecutables({
- alerts,
- throttledSummaryActions: {},
- });
+ const results = await scheduler.getActionsToSchedule({ alerts });
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1);
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({
excludedAlertInstanceIds: [],
executionUuid: defaultSchedulerContext.executionId,
- ruleId: '1',
+ ruleId: 'rule-id-1',
spaceId: 'test1',
alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } },
});
- expect(executables).toHaveLength(3);
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(3);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(3);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 3,
+ numberOfTriggeredActions: 3,
+ });
- expect(executables).toEqual([
- { action: rule.actions[0], alert: alerts['1'] },
- { action: rule.actions[0], alert: alerts['2'] },
- { action: actionWithAlertsFilter, alert: alerts['1'] },
+ expect(results).toHaveLength(3);
+ expect(results).toEqual([
+ getResult('action-1', '1', '111-111'),
+ getResult('action-1', '2', '111-111'),
+ getResult('action-8', '1', '888-888'),
]);
});
@@ -801,7 +901,7 @@ describe('Per-Alert Action Scheduler', () => {
};
alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts);
const actionWithAlertsFilter: SanitizedRuleAction = {
- id: '1',
+ id: 'action-9',
group: 'default',
actionTypeId: 'test',
frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null },
@@ -812,38 +912,168 @@ describe('Per-Alert Action Scheduler', () => {
alertVal:
'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
},
- uuid: '111-111',
+ uuid: '999-999',
alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } },
};
const scheduler = new PerAlertActionScheduler({
...getSchedulerContext(),
rule: { ...rule, actions: [rule.actions[0], actionWithAlertsFilter] },
});
- const executables = await scheduler.generateExecutables({
- alerts,
- throttledSummaryActions: {},
- });
+ const results = await scheduler.getActionsToSchedule({ alerts });
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1);
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({
excludedAlertInstanceIds: [],
executionUuid: defaultSchedulerContext.executionId,
- ruleId: '1',
+ ruleId: 'rule-id-1',
spaceId: 'test1',
alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', filters: [] } },
});
- expect(executables).toHaveLength(4);
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(4);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 4,
+ numberOfTriggeredActions: 4,
+ });
+
+ expect(results).toHaveLength(4);
+ expect(results).toEqual([
+ getResult('action-1', '1', '111-111'),
+ getResult('action-1', '2', '111-111'),
+ getResult('action-9', '1', '999-999'),
+ getResult('action-9', '2', '999-999'),
+ ]);
expect(alerts['1'].getAlertAsData()).not.toBeUndefined();
expect(alerts['2'].getAlertAsData()).not.toBeUndefined();
+ });
+
+ test('should skip creating actions to schedule if overall max actions limit exceeded', async () => {
+ const defaultContext = getSchedulerContext();
+ const scheduler = new PerAlertActionScheduler({
+ ...defaultContext,
+ taskRunnerContext: {
+ ...defaultContext.taskRunnerContext,
+ actionsConfigMap: {
+ default: { max: 3 },
+ },
+ },
+ });
+ const results = await scheduler.getActionsToSchedule({ alerts });
+
+ expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
- expect(executables).toEqual([
- { action: rule.actions[0], alert: alerts['1'] },
- { action: rule.actions[0], alert: alerts['2'] },
- { action: actionWithAlertsFilter, alert: alerts['1'] },
- { action: actionWithAlertsFilter, alert: alerts['2'] },
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(3);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 4,
+ numberOfTriggeredActions: 3,
+ triggeredActionsStatus: ActionsCompletion.PARTIAL,
+ });
+
+ expect(logger.debug).toHaveBeenCalledWith(
+ `Rule "rule-id-1" skipped scheduling action "action-2" because the maximum number of allowed actions has been reached.`
+ );
+
+ expect(results).toHaveLength(3);
+ expect(results).toEqual([
+ getResult('action-1', '1', '111-111'),
+ getResult('action-1', '2', '111-111'),
+ getResult('action-2', '1', '222-222'),
]);
});
+
+ test('should skip creating actions to schedule if connector type max actions limit exceeded', async () => {
+ const defaultContext = getSchedulerContext();
+ const scheduler = new PerAlertActionScheduler({
+ ...defaultContext,
+ taskRunnerContext: {
+ ...defaultContext.taskRunnerContext,
+ actionsConfigMap: {
+ default: { max: 1000 },
+ test: { max: 1 },
+ },
+ },
+ });
+ const results = await scheduler.getActionsToSchedule({ alerts });
+
+ expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
+
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(4);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 4,
+ numberOfTriggeredActions: 1,
+ triggeredActionsStatus: ActionsCompletion.PARTIAL,
+ });
+
+ expect(logger.debug).toHaveBeenCalledWith(
+ `Rule "rule-id-1" skipped scheduling action "action-1" because the maximum number of allowed actions for connector type test has been reached.`
+ );
+
+ expect(results).toHaveLength(1);
+ expect(results).toEqual([getResult('action-1', '1', '111-111')]);
+ });
+
+ test('should correctly update last scheduled actions for alert when action is "onActiveAlert"', async () => {
+ const alert = new Alert('1', {
+ state: { test: true },
+ meta: {},
+ });
+ alert.scheduleActions('default');
+ const scheduler = new PerAlertActionScheduler({
+ ...getSchedulerContext(),
+ rule: { ...rule, actions: [rule.actions[0]] },
+ });
+
+ expect(alert.getLastScheduledActions()).toBeUndefined();
+ expect(alert.hasScheduledActions()).toBe(true);
+ await scheduler.getActionsToSchedule({ alerts: { '1': alert } });
+
+ expect(alert.getLastScheduledActions()).toEqual({
+ date: '1970-01-01T00:00:00.000Z',
+ group: 'default',
+ });
+ expect(alert.hasScheduledActions()).toBe(false);
+ });
+
+ test('should correctly update last scheduled actions for alert', async () => {
+ const alert = new Alert('1', {
+ state: { test: true },
+ meta: {},
+ });
+ alert.scheduleActions('default');
+ const onThrottleIntervalAction: SanitizedRuleAction = {
+ id: 'action-4',
+ group: 'default',
+ actionTypeId: 'test',
+ frequency: { summary: false, notifyWhen: 'onThrottleInterval', throttle: '1h' },
+ params: {
+ foo: true,
+ contextVal: 'My {{context.value}} goes here',
+ stateVal: 'My {{state.value}} goes here',
+ alertVal:
+ 'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
+ },
+ uuid: '222-222',
+ };
+
+ expect(alert.getLastScheduledActions()).toBeUndefined();
+ expect(alert.hasScheduledActions()).toBe(true);
+ const scheduler = new PerAlertActionScheduler({
+ ...getSchedulerContext(),
+ rule: { ...rule, actions: [onThrottleIntervalAction] },
+ });
+
+ await scheduler.getActionsToSchedule({ alerts: { '1': alert } });
+
+ expect(alert.getLastScheduledActions()).toEqual({
+ date: '1970-01-01T00:00:00.000Z',
+ group: 'default',
+ actions: { '222-222': { date: '1970-01-01T00:00:00.000Z' } },
+ });
+ expect(alert.hasScheduledActions()).toBe(false);
+ });
});
});
diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts
index 602d3c31688c1..b35d86dff0105 100644
--- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts
+++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/per_alert_action_scheduler.ts
@@ -12,19 +12,24 @@ import { RuleTypeState, RuleAlertData, parseDuration } from '../../../../common'
import { GetSummarizedAlertsParams } from '../../../alerts_client/types';
import { AlertHit } from '../../../types';
import { Alert } from '../../../alert';
-import { getSummarizedAlerts } from '../get_summarized_alerts';
import {
+ buildRuleUrl,
+ formatActionToEnqueue,
generateActionHash,
+ getSummarizedAlerts,
isActionOnInterval,
isSummaryAction,
logNumberOfFilteredAlerts,
-} from '../rule_action_helper';
+ shouldScheduleAction,
+} from '../lib';
import {
ActionSchedulerOptions,
- Executable,
- GenerateExecutablesOpts,
+ ActionsToSchedule,
+ GetActionsToScheduleOpts,
IActionScheduler,
} from '../types';
+import { TransformActionParamsOptions, transformActionParams } from '../../transform_action_params';
+import { injectActionParams } from '../../inject_action_params';
enum Reasons {
MUTED = 'muted',
@@ -90,12 +95,16 @@ export class PerAlertActionScheduler<
return 2;
}
- public async generateExecutables({
+ public async getActionsToSchedule({
alerts,
- }: GenerateExecutablesOpts): Promise<
- Array>
+ }: GetActionsToScheduleOpts): Promise<
+ ActionsToSchedule[]
> {
- const executables = [];
+ const executables: Array<{
+ action: RuleAction;
+ alert: Alert;
+ }> = [];
+ const results: ActionsToSchedule[] = [];
const alertsArray = Object.entries(alerts);
for (const action of this.actions) {
@@ -104,7 +113,7 @@ export class PerAlertActionScheduler<
if (action.useAlertDataForTemplate || action.alertsFilter) {
const optionsBase = {
spaceId: this.context.taskInstance.params.spaceId,
- ruleId: this.context.taskInstance.params.alertId,
+ ruleId: this.context.rule.id,
excludedAlertInstanceIds: this.context.rule.mutedInstanceIds,
alertsFilter: action.alertsFilter,
};
@@ -135,7 +144,7 @@ export class PerAlertActionScheduler<
if (alertMaintenanceWindowIds.length !== 0) {
this.context.logger.debug(
`no scheduling of summary actions "${action.id}" for rule "${
- this.context.taskInstance.params.alertId
+ this.context.rule.id
}": has active maintenance windows ${alertMaintenanceWindowIds.join(', ')}.`
);
continue;
@@ -185,7 +194,112 @@ export class PerAlertActionScheduler<
}
}
- return executables;
+ if (executables.length === 0) return [];
+
+ this.context.ruleRunMetricsStore.incrementNumberOfGeneratedActions(executables.length);
+
+ const ruleUrl = buildRuleUrl({
+ getViewInAppRelativeUrl: this.context.ruleType.getViewInAppRelativeUrl,
+ kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl,
+ logger: this.context.logger,
+ rule: this.context.rule,
+ spaceId: this.context.taskInstance.params.spaceId,
+ });
+
+ for (const { action, alert } of executables) {
+ const { actionTypeId } = action;
+
+ if (
+ !shouldScheduleAction({
+ action,
+ actionsConfigMap: this.context.taskRunnerContext.actionsConfigMap,
+ isActionExecutable: this.context.taskRunnerContext.actionsPlugin.isActionExecutable,
+ logger: this.context.logger,
+ ruleId: this.context.rule.id,
+ ruleRunMetricsStore: this.context.ruleRunMetricsStore,
+ })
+ ) {
+ continue;
+ }
+
+ this.context.ruleRunMetricsStore.incrementNumberOfTriggeredActions();
+ this.context.ruleRunMetricsStore.incrementNumberOfTriggeredActionsByConnectorType(
+ actionTypeId
+ );
+
+ const actionGroup = action.group as ActionGroupIds;
+ const transformActionParamsOptions: TransformActionParamsOptions = {
+ actionsPlugin: this.context.taskRunnerContext.actionsPlugin,
+ alertId: this.context.rule.id,
+ alertType: this.context.ruleType.id,
+ actionTypeId: action.actionTypeId,
+ alertName: this.context.rule.name,
+ spaceId: this.context.taskInstance.params.spaceId,
+ tags: this.context.rule.tags,
+ alertInstanceId: alert.getId(),
+ alertUuid: alert.getUuid(),
+ alertActionGroup: actionGroup,
+ alertActionGroupName: this.ruleTypeActionGroups!.get(actionGroup)!,
+ context: alert.getContext(),
+ actionId: action.id,
+ state: alert.getState(),
+ kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl,
+ alertParams: this.context.rule.params,
+ actionParams: action.params,
+ flapping: alert.getFlapping(),
+ ruleUrl: ruleUrl?.absoluteUrl,
+ };
+
+ if (alert.isAlertAsData()) {
+ transformActionParamsOptions.aadAlert = alert.getAlertAsData();
+ }
+
+ const actionToRun = {
+ ...action,
+ params: injectActionParams({
+ actionTypeId: action.actionTypeId,
+ ruleUrl,
+ ruleName: this.context.rule.name,
+ actionParams: transformActionParams(transformActionParamsOptions),
+ }),
+ };
+
+ results.push({
+ actionToEnqueue: formatActionToEnqueue({
+ action: actionToRun,
+ apiKey: this.context.apiKey,
+ executionId: this.context.executionId,
+ ruleConsumer: this.context.ruleConsumer,
+ ruleId: this.context.rule.id,
+ ruleTypeId: this.context.ruleType.id,
+ spaceId: this.context.taskInstance.params.spaceId,
+ }),
+ actionToLog: {
+ id: action.id,
+ // uuid is typed as optional but in reality it is always
+ // populated - https://github.com/elastic/kibana/issues/195255
+ uuid: action.uuid,
+ typeId: action.actionTypeId,
+ alertId: alert.getId(),
+ alertGroup: action.group,
+ },
+ });
+
+ if (!this.isRecoveredAlert(actionGroup)) {
+ if (isActionOnInterval(action)) {
+ alert.updateLastScheduledActions(
+ action.group as ActionGroupIds,
+ generateActionHash(action),
+ action.uuid
+ );
+ } else {
+ alert.updateLastScheduledActions(action.group as ActionGroupIds);
+ }
+ alert.unscheduleActions();
+ }
+ }
+
+ return results;
}
private isAlertMuted(alertId: string) {
diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.test.ts
index 600dd0e1951d5..fc810fc4ef34c 100644
--- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.test.ts
+++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.test.ts
@@ -20,6 +20,8 @@ import {
getErrorSource,
TaskErrorSource,
} from '@kbn/task-manager-plugin/server/task_running/errors';
+import { CombinedSummarizedAlerts } from '../../../types';
+import { ActionsCompletion } from '@kbn/alerting-state-types';
const alertingEventLogger = alertingEventLoggerMock.create();
const actionsClient = actionsClientMock.create();
@@ -29,9 +31,10 @@ const logger = loggingSystemMock.create().get();
let ruleRunMetricsStore: RuleRunMetricsStore;
const rule = getRule({
+ id: 'rule-id-1',
actions: [
{
- id: '1',
+ id: 'action-1',
group: 'default',
actionTypeId: 'test',
frequency: { summary: false, notifyWhen: 'onActiveAlert', throttle: null },
@@ -45,7 +48,7 @@ const rule = getRule({
uuid: '111-111',
},
{
- id: '2',
+ id: 'action-2',
group: 'default',
actionTypeId: 'test',
frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null },
@@ -59,7 +62,7 @@ const rule = getRule({
uuid: '222-222',
},
{
- id: '3',
+ id: 'action-3',
group: 'default',
actionTypeId: 'test',
frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null },
@@ -88,6 +91,30 @@ const getSchedulerContext = (params = {}) => {
return { ...defaultSchedulerContext, rule, ...params, ruleRunMetricsStore };
};
+const getResult = (actionId: string, actionUuid: string, summary: CombinedSummarizedAlerts) => ({
+ actionToEnqueue: {
+ actionTypeId: 'test',
+ apiKey: 'MTIzOmFiYw==',
+ consumer: 'rule-consumer',
+ executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28',
+ id: actionId,
+ uuid: actionUuid,
+ relatedSavedObjects: [{ id: 'rule-id-1', namespace: 'test1', type: 'alert', typeId: 'test' }],
+ source: { source: { id: 'rule-id-1', type: 'alert' }, type: 'SAVED_OBJECT' },
+ spaceId: 'test1',
+ },
+ actionToLog: {
+ alertSummary: {
+ new: summary.new.count,
+ ongoing: summary.ongoing.count,
+ recovered: summary.recovered.count,
+ },
+ id: actionId,
+ uuid: actionUuid,
+ typeId: 'test',
+ },
+});
+
let clock: sinon.SinonFakeTimers;
describe('Summary Action Scheduler', () => {
@@ -127,21 +154,21 @@ describe('Summary Action Scheduler', () => {
expect(logger.error).toHaveBeenCalledTimes(2);
expect(logger.error).toHaveBeenNthCalledWith(
1,
- `Skipping action \"2\" for rule \"1\" because the rule type \"Test\" does not support alert-as-data.`
+ `Skipping action \"action-2\" for rule \"rule-id-1\" because the rule type \"Test\" does not support alert-as-data.`
);
expect(logger.error).toHaveBeenNthCalledWith(
2,
- `Skipping action \"3\" for rule \"1\" because the rule type \"Test\" does not support alert-as-data.`
+ `Skipping action \"action-3\" for rule \"rule-id-1\" because the rule type \"Test\" does not support alert-as-data.`
);
});
- describe('generateExecutables', () => {
+ describe('getActionsToSchedule', () => {
const newAlert1 = generateAlert({ id: 1 });
const newAlert2 = generateAlert({ id: 2 });
const alerts = { ...newAlert1, ...newAlert2 };
const summaryActionWithAlertFilter: RuleAction = {
- id: '2',
+ id: 'action-3',
group: 'default',
actionTypeId: 'test',
frequency: {
@@ -157,11 +184,11 @@ describe('Summary Action Scheduler', () => {
'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
},
alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', dsl: '{}', filters: [] } },
- uuid: '222-222',
+ uuid: '333-333',
};
const summaryActionWithThrottle: RuleAction = {
- id: '2',
+ id: 'action-4',
group: 'default',
actionTypeId: 'test',
frequency: {
@@ -176,10 +203,10 @@ describe('Summary Action Scheduler', () => {
alertVal:
'My {{rule.id}} {{rule.name}} {{rule.spaceId}} {{rule.tags}} {{alert.id}} goes here',
},
- uuid: '222-222',
+ uuid: '444-444',
};
- test('should generate executable for summary action when summary action is per rule run', async () => {
+ test('should create action to schedule for summary action when summary action is per rule run', async () => {
alertsClient.getProcessedAlerts.mockReturnValue(alerts);
const summarizedAlerts = {
new: { count: 2, data: [mockAAD, mockAAD] },
@@ -188,37 +215,43 @@ describe('Summary Action Scheduler', () => {
};
alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts);
+ const throttledSummaryActions = {};
const scheduler = new SummaryActionScheduler(getSchedulerContext());
- const executables = await scheduler.generateExecutables({
- alerts,
- throttledSummaryActions: {},
- });
+ const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions });
+ expect(throttledSummaryActions).toEqual({});
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2);
expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, {
excludedAlertInstanceIds: [],
executionUuid: defaultSchedulerContext.executionId,
- ruleId: '1',
+ ruleId: 'rule-id-1',
spaceId: 'test1',
});
expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, {
excludedAlertInstanceIds: [],
executionUuid: defaultSchedulerContext.executionId,
- ruleId: '1',
+ ruleId: 'rule-id-1',
spaceId: 'test1',
});
expect(logger.debug).not.toHaveBeenCalled();
- expect(executables).toHaveLength(2);
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 2,
+ numberOfTriggeredActions: 2,
+ });
+
+ expect(results).toHaveLength(2);
const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } };
- expect(executables).toEqual([
- { action: rule.actions[1], summarizedAlerts: finalSummary },
- { action: rule.actions[2], summarizedAlerts: finalSummary },
+ expect(results).toEqual([
+ getResult('action-2', '222-222', finalSummary),
+ getResult('action-3', '333-333', finalSummary),
]);
});
- test('should generate executable for summary action when summary action has alertsFilter', async () => {
+ test('should create actions to schedule for summary action when summary action has alertsFilter', async () => {
alertsClient.getProcessedAlerts.mockReturnValue(alerts);
const summarizedAlerts = {
new: { count: 2, data: [mockAAD, mockAAD] },
@@ -232,30 +265,34 @@ describe('Summary Action Scheduler', () => {
rule: { ...rule, actions: [summaryActionWithAlertFilter] },
});
- const executables = await scheduler.generateExecutables({
- alerts,
- throttledSummaryActions: {},
- });
+ const throttledSummaryActions = {};
+ const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions });
+ expect(throttledSummaryActions).toEqual({});
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1);
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({
excludedAlertInstanceIds: [],
executionUuid: defaultSchedulerContext.executionId,
- ruleId: '1',
+ ruleId: 'rule-id-1',
spaceId: 'test1',
alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', dsl: '{}', filters: [] } },
});
expect(logger.debug).not.toHaveBeenCalled();
- expect(executables).toHaveLength(1);
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(1);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 1,
+ numberOfTriggeredActions: 1,
+ });
+
+ expect(results).toHaveLength(1);
const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } };
- expect(executables).toEqual([
- { action: summaryActionWithAlertFilter, summarizedAlerts: finalSummary },
- ]);
+ expect(results).toEqual([getResult('action-3', '333-333', finalSummary)]);
});
- test('should generate executable for summary action when summary action is throttled with no throttle history', async () => {
+ test('should create actions to schedule for summary action when summary action is throttled with no throttle history', async () => {
alertsClient.getProcessedAlerts.mockReturnValue(alerts);
const summarizedAlerts = {
new: { count: 2, data: [mockAAD, mockAAD] },
@@ -269,48 +306,52 @@ describe('Summary Action Scheduler', () => {
rule: { ...rule, actions: [summaryActionWithThrottle] },
});
- const executables = await scheduler.generateExecutables({
- alerts,
- throttledSummaryActions: {},
- });
+ const throttledSummaryActions = {};
+ const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions });
+ expect(throttledSummaryActions).toEqual({ '444-444': { date: '1970-01-01T00:00:00.000Z' } });
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1);
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({
excludedAlertInstanceIds: [],
- ruleId: '1',
+ ruleId: 'rule-id-1',
spaceId: 'test1',
start: new Date('1969-12-31T00:00:00.000Z'),
end: new Date(),
});
expect(logger.debug).not.toHaveBeenCalled();
- expect(executables).toHaveLength(1);
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(1);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 1,
+ numberOfTriggeredActions: 1,
+ });
+
+ expect(results).toHaveLength(1);
const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } };
- expect(executables).toEqual([
- { action: summaryActionWithThrottle, summarizedAlerts: finalSummary },
- ]);
+ expect(results).toEqual([getResult('action-4', '444-444', finalSummary)]);
});
- test('should skip generating executable for summary action when summary action is throttled', async () => {
+ test('should skip creating actions to schedule for summary action when summary action is throttled', async () => {
const scheduler = new SummaryActionScheduler({
...getSchedulerContext(),
rule: { ...rule, actions: [summaryActionWithThrottle] },
});
- const executables = await scheduler.generateExecutables({
- alerts,
- throttledSummaryActions: {
- '222-222': { date: '1969-12-31T13:00:00.000Z' },
- },
- });
+ const throttledSummaryActions = { '444-444': { date: '1969-12-31T13:00:00.000Z' } };
+ const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions });
+ expect(throttledSummaryActions).toEqual({ '444-444': { date: '1969-12-31T13:00:00.000Z' } });
expect(alertsClient.getSummarizedAlerts).not.toHaveBeenCalled();
expect(logger.debug).toHaveBeenCalledWith(
- `skipping scheduling the action 'test:2', summary action is still being throttled`
+ `skipping scheduling the action 'test:action-4', summary action is still being throttled`
);
- expect(executables).toHaveLength(0);
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(0);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(0);
+
+ expect(results).toHaveLength(0);
});
test('should remove new alerts from summary if suppressed by maintenance window', async () => {
@@ -332,22 +373,21 @@ describe('Summary Action Scheduler', () => {
alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts);
const scheduler = new SummaryActionScheduler(getSchedulerContext());
- const executables = await scheduler.generateExecutables({
- alerts,
- throttledSummaryActions: {},
- });
+ const throttledSummaryActions = {};
+ const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions });
+ expect(throttledSummaryActions).toEqual({});
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2);
expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, {
excludedAlertInstanceIds: [],
executionUuid: defaultSchedulerContext.executionId,
- ruleId: '1',
+ ruleId: 'rule-id-1',
spaceId: 'test1',
});
expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, {
excludedAlertInstanceIds: [],
executionUuid: defaultSchedulerContext.executionId,
- ruleId: '1',
+ ruleId: 'rule-id-1',
spaceId: 'test1',
});
expect(logger.debug).toHaveBeenCalledTimes(2);
@@ -360,7 +400,14 @@ describe('Summary Action Scheduler', () => {
`(1) alert has been filtered out for: test:333-333`
);
- expect(executables).toHaveLength(2);
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(2);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 2,
+ numberOfTriggeredActions: 2,
+ });
+
+ expect(results).toHaveLength(2);
const finalSummary = {
all: { count: 1, data: [newAADAlerts[1]] },
@@ -368,13 +415,13 @@ describe('Summary Action Scheduler', () => {
ongoing: { count: 0, data: [] },
recovered: { count: 0, data: [] },
};
- expect(executables).toEqual([
- { action: rule.actions[1], summarizedAlerts: finalSummary },
- { action: rule.actions[2], summarizedAlerts: finalSummary },
+ expect(results).toEqual([
+ getResult('action-2', '222-222', finalSummary),
+ getResult('action-3', '333-333', finalSummary),
]);
});
- test('should generate executable for summary action and log when alerts have been filtered out by action condition', async () => {
+ test('should create alerts to schedule for summary action and log when alerts have been filtered out by action condition', async () => {
alertsClient.getProcessedAlerts.mockReturnValue(alerts);
const summarizedAlerts = {
new: { count: 1, data: [mockAAD] },
@@ -388,33 +435,37 @@ describe('Summary Action Scheduler', () => {
rule: { ...rule, actions: [summaryActionWithAlertFilter] },
});
- const executables = await scheduler.generateExecutables({
- alerts,
- throttledSummaryActions: {},
- });
+ const throttledSummaryActions = {};
+ const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions });
+ expect(throttledSummaryActions).toEqual({});
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1);
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({
excludedAlertInstanceIds: [],
executionUuid: defaultSchedulerContext.executionId,
- ruleId: '1',
+ ruleId: 'rule-id-1',
spaceId: 'test1',
alertsFilter: { query: { kql: 'kibana.alert.rule.name:foo', dsl: '{}', filters: [] } },
});
expect(logger.debug).toHaveBeenCalledTimes(1);
expect(logger.debug).toHaveBeenCalledWith(
- `(1) alert has been filtered out for: test:222-222`
+ `(1) alert has been filtered out for: test:333-333`
);
- expect(executables).toHaveLength(1);
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(1);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 1,
+ numberOfTriggeredActions: 1,
+ });
+
+ expect(results).toHaveLength(1);
const finalSummary = { ...summarizedAlerts, all: { count: 1, data: [mockAAD] } };
- expect(executables).toEqual([
- { action: summaryActionWithAlertFilter, summarizedAlerts: finalSummary },
- ]);
+ expect(results).toEqual([getResult('action-3', '333-333', finalSummary)]);
});
- test('should skip generating executable for summary action when no alerts found', async () => {
+ test('should skip creating actions to schedule for summary action when no alerts found', async () => {
alertsClient.getProcessedAlerts.mockReturnValue(alerts);
const summarizedAlerts = {
new: { count: 0, data: [] },
@@ -428,22 +479,23 @@ describe('Summary Action Scheduler', () => {
rule: { ...rule, actions: [summaryActionWithThrottle] },
});
- const executables = await scheduler.generateExecutables({
- alerts,
- throttledSummaryActions: {},
- });
+ const throttledSummaryActions = {};
+ const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions });
+ expect(throttledSummaryActions).toEqual({});
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1);
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({
excludedAlertInstanceIds: [],
- ruleId: '1',
+ ruleId: 'rule-id-1',
spaceId: 'test1',
start: new Date('1969-12-31T00:00:00.000Z'),
end: new Date(),
});
expect(logger.debug).not.toHaveBeenCalled();
- expect(executables).toHaveLength(0);
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(0);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(0);
+ expect(results).toHaveLength(0);
});
test('should throw framework error if getSummarizedAlerts throws error', async () => {
@@ -455,14 +507,117 @@ describe('Summary Action Scheduler', () => {
const scheduler = new SummaryActionScheduler(getSchedulerContext());
try {
- await scheduler.generateExecutables({
- alerts,
- throttledSummaryActions: {},
- });
+ await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions: {} });
} catch (err) {
expect(err.message).toEqual(`no alerts for you`);
expect(getErrorSource(err)).toBe(TaskErrorSource.FRAMEWORK);
}
});
+
+ test('should skip creating actions to schedule if overall max actions limit exceeded', async () => {
+ alertsClient.getProcessedAlerts.mockReturnValue(alerts);
+ const summarizedAlerts = {
+ new: { count: 2, data: [mockAAD, mockAAD] },
+ ongoing: { count: 0, data: [] },
+ recovered: { count: 0, data: [] },
+ };
+ alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts);
+
+ const defaultContext = getSchedulerContext();
+ const scheduler = new SummaryActionScheduler({
+ ...defaultContext,
+ taskRunnerContext: {
+ ...defaultContext.taskRunnerContext,
+ actionsConfigMap: {
+ default: { max: 1 },
+ },
+ },
+ });
+ const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions: {} });
+
+ expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2);
+ expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, {
+ excludedAlertInstanceIds: [],
+ executionUuid: defaultSchedulerContext.executionId,
+ ruleId: 'rule-id-1',
+ spaceId: 'test1',
+ });
+ expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, {
+ excludedAlertInstanceIds: [],
+ executionUuid: defaultSchedulerContext.executionId,
+ ruleId: 'rule-id-1',
+ spaceId: 'test1',
+ });
+
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 2,
+ numberOfTriggeredActions: 1,
+ triggeredActionsStatus: ActionsCompletion.PARTIAL,
+ });
+
+ expect(logger.debug).toHaveBeenCalledWith(
+ `Rule "rule-id-1" skipped scheduling action "action-3" because the maximum number of allowed actions has been reached.`
+ );
+
+ expect(results).toHaveLength(1);
+
+ const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } };
+ expect(results).toEqual([getResult('action-2', '222-222', finalSummary)]);
+ });
+
+ test('should skip creating actions to schedule if connector type max actions limit exceeded', async () => {
+ alertsClient.getProcessedAlerts.mockReturnValue(alerts);
+ const summarizedAlerts = {
+ new: { count: 2, data: [mockAAD, mockAAD] },
+ ongoing: { count: 0, data: [] },
+ recovered: { count: 0, data: [] },
+ };
+ alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts);
+
+ const defaultContext = getSchedulerContext();
+ const scheduler = new SummaryActionScheduler({
+ ...defaultContext,
+ taskRunnerContext: {
+ ...defaultContext.taskRunnerContext,
+ actionsConfigMap: {
+ default: { max: 1000 },
+ test: { max: 1 },
+ },
+ },
+ });
+ const results = await scheduler.getActionsToSchedule({ alerts, throttledSummaryActions: {} });
+
+ expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2);
+ expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, {
+ excludedAlertInstanceIds: [],
+ executionUuid: defaultSchedulerContext.executionId,
+ ruleId: 'rule-id-1',
+ spaceId: 'test1',
+ });
+ expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, {
+ excludedAlertInstanceIds: [],
+ executionUuid: defaultSchedulerContext.executionId,
+ ruleId: 'rule-id-1',
+ spaceId: 'test1',
+ });
+
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('test')).toEqual({
+ numberOfGeneratedActions: 2,
+ numberOfTriggeredActions: 1,
+ triggeredActionsStatus: ActionsCompletion.PARTIAL,
+ });
+
+ expect(logger.debug).toHaveBeenCalledWith(
+ `Rule "rule-id-1" skipped scheduling action "action-3" because the maximum number of allowed actions for connector type test has been reached.`
+ );
+
+ expect(results).toHaveLength(1);
+ const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } };
+ expect(results).toEqual([getResult('action-2', '222-222', finalSummary)]);
+ });
});
});
diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.ts
index 9b67c37e6216e..050eea352f0d5 100644
--- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.ts
+++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/summary_action_scheduler.ts
@@ -8,21 +8,28 @@
import { AlertInstanceState, AlertInstanceContext } from '@kbn/alerting-state-types';
import { RuleAction, RuleTypeParams } from '@kbn/alerting-types';
import { compact } from 'lodash';
+import { CombinedSummarizedAlerts } from '../../../types';
import { RuleTypeState, RuleAlertData, parseDuration } from '../../../../common';
import { GetSummarizedAlertsParams } from '../../../alerts_client/types';
-import { getSummarizedAlerts } from '../get_summarized_alerts';
import {
+ buildRuleUrl,
+ formatActionToEnqueue,
+ getSummarizedAlerts,
+ getSummaryActionTimeBounds,
isActionOnInterval,
isSummaryAction,
isSummaryActionThrottled,
logNumberOfFilteredAlerts,
-} from '../rule_action_helper';
+ shouldScheduleAction,
+} from '../lib';
import {
ActionSchedulerOptions,
- Executable,
- GenerateExecutablesOpts,
+ ActionsToSchedule,
+ GetActionsToScheduleOpts,
IActionScheduler,
} from '../types';
+import { injectActionParams } from '../../inject_action_params';
+import { transformSummaryActionParams } from '../../transform_action_params';
export class SummaryActionScheduler<
Params extends RuleTypeParams,
@@ -73,13 +80,18 @@ export class SummaryActionScheduler<
return 0;
}
- public async generateExecutables({
+ public async getActionsToSchedule({
alerts,
throttledSummaryActions,
- }: GenerateExecutablesOpts): Promise<
- Array>
+ }: GetActionsToScheduleOpts): Promise<
+ ActionsToSchedule[]
> {
- const executables = [];
+ const executables: Array<{
+ action: RuleAction;
+ summarizedAlerts: CombinedSummarizedAlerts;
+ }> = [];
+ const results: ActionsToSchedule[] = [];
+
for (const action of this.actions) {
if (
// if summary action is throttled, we won't send any notifications
@@ -88,7 +100,7 @@ export class SummaryActionScheduler<
const actionHasThrottleInterval = isActionOnInterval(action);
const optionsBase = {
spaceId: this.context.taskInstance.params.spaceId,
- ruleId: this.context.taskInstance.params.alertId,
+ ruleId: this.context.rule.id,
excludedAlertInstanceIds: this.context.rule.mutedInstanceIds,
alertsFilter: action.alertsFilter,
};
@@ -122,6 +134,95 @@ export class SummaryActionScheduler<
}
}
- return executables;
+ if (executables.length === 0) return [];
+
+ this.context.ruleRunMetricsStore.incrementNumberOfGeneratedActions(executables.length);
+
+ for (const { action, summarizedAlerts } of executables) {
+ const { actionTypeId } = action;
+
+ if (
+ !shouldScheduleAction({
+ action,
+ actionsConfigMap: this.context.taskRunnerContext.actionsConfigMap,
+ isActionExecutable: this.context.taskRunnerContext.actionsPlugin.isActionExecutable,
+ logger: this.context.logger,
+ ruleId: this.context.rule.id,
+ ruleRunMetricsStore: this.context.ruleRunMetricsStore,
+ })
+ ) {
+ continue;
+ }
+
+ this.context.ruleRunMetricsStore.incrementNumberOfTriggeredActions();
+ this.context.ruleRunMetricsStore.incrementNumberOfTriggeredActionsByConnectorType(
+ actionTypeId
+ );
+
+ if (isActionOnInterval(action) && throttledSummaryActions) {
+ throttledSummaryActions[action.uuid!] = { date: new Date().toISOString() };
+ }
+
+ const { start, end } = getSummaryActionTimeBounds(
+ action,
+ this.context.rule.schedule,
+ this.context.previousStartedAt
+ );
+
+ const ruleUrl = buildRuleUrl({
+ end,
+ getViewInAppRelativeUrl: this.context.ruleType.getViewInAppRelativeUrl,
+ kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl,
+ logger: this.context.logger,
+ rule: this.context.rule,
+ spaceId: this.context.taskInstance.params.spaceId,
+ start,
+ });
+
+ const actionToRun = {
+ ...action,
+ params: injectActionParams({
+ actionTypeId: action.actionTypeId,
+ ruleUrl,
+ ruleName: this.context.rule.name,
+ actionParams: transformSummaryActionParams({
+ alerts: summarizedAlerts,
+ rule: this.context.rule,
+ ruleTypeId: this.context.ruleType.id,
+ actionId: action.id,
+ actionParams: action.params,
+ spaceId: this.context.taskInstance.params.spaceId,
+ actionsPlugin: this.context.taskRunnerContext.actionsPlugin,
+ actionTypeId: action.actionTypeId,
+ kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl,
+ ruleUrl: ruleUrl?.absoluteUrl,
+ }),
+ }),
+ };
+
+ results.push({
+ actionToEnqueue: formatActionToEnqueue({
+ action: actionToRun,
+ apiKey: this.context.apiKey,
+ executionId: this.context.executionId,
+ ruleConsumer: this.context.ruleConsumer,
+ ruleId: this.context.rule.id,
+ ruleTypeId: this.context.ruleType.id,
+ spaceId: this.context.taskInstance.params.spaceId,
+ }),
+ actionToLog: {
+ id: action.id,
+ uuid: action.uuid,
+ typeId: action.actionTypeId,
+ alertSummary: {
+ new: summarizedAlerts.new.count,
+ ongoing: summarizedAlerts.ongoing.count,
+ recovered: summarizedAlerts.recovered.count,
+ },
+ },
+ });
+ }
+
+ return results;
}
}
diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.test.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.test.ts
index fd4db6ce34678..28bf58a30c689 100644
--- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.test.ts
+++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.test.ts
@@ -12,6 +12,12 @@ import { alertsClientMock } from '../../../alerts_client/alerts_client.mock';
import { alertingEventLoggerMock } from '../../../lib/alerting_event_logger/alerting_event_logger.mock';
import { RuleRunMetricsStore } from '../../../lib/rule_run_metrics_store';
import { mockAAD } from '../../fixtures';
+import { Alert } from '../../../alert';
+import {
+ ActionsCompletion,
+ AlertInstanceContext,
+ AlertInstanceState,
+} from '@kbn/alerting-state-types';
import { getRule, getRuleType, getDefaultSchedulerContext, generateAlert } from '../test_fixtures';
import { SystemActionScheduler } from './system_action_scheduler';
import { ALERT_UUID } from '@kbn/rule-data-utils';
@@ -19,6 +25,8 @@ import {
getErrorSource,
TaskErrorSource,
} from '@kbn/task-manager-plugin/server/task_running/errors';
+import { CombinedSummarizedAlerts } from '../../../types';
+import { schema } from '@kbn/config-schema';
const alertingEventLogger = alertingEventLoggerMock.create();
const actionsClient = actionsClientMock.create();
@@ -28,12 +36,13 @@ const logger = loggingSystemMock.create().get();
let ruleRunMetricsStore: RuleRunMetricsStore;
const rule = getRule({
+ id: 'rule-id-1',
systemActions: [
{
- id: '1',
+ id: 'system-action-1',
actionTypeId: '.test-system-action',
params: { myParams: 'test' },
- uui: 'test',
+ uuid: 'xxx-xxx',
},
],
});
@@ -46,11 +55,43 @@ const defaultSchedulerContext = getDefaultSchedulerContext(
alertsClient
);
+const actionsParams = { myParams: 'test' };
+const buildActionParams = jest.fn().mockReturnValue({ ...actionsParams, foo: 'bar' });
+defaultSchedulerContext.taskRunnerContext.connectorAdapterRegistry.register({
+ connectorTypeId: '.test-system-action',
+ ruleActionParamsSchema: schema.object({}),
+ buildActionParams,
+});
+
// @ts-ignore
const getSchedulerContext = (params = {}) => {
return { ...defaultSchedulerContext, rule, ...params, ruleRunMetricsStore };
};
+const getResult = (actionId: string, actionUuid: string, summary: CombinedSummarizedAlerts) => ({
+ actionToEnqueue: {
+ actionTypeId: '.test-system-action',
+ apiKey: 'MTIzOmFiYw==',
+ consumer: 'rule-consumer',
+ executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28',
+ id: actionId,
+ uuid: actionUuid,
+ relatedSavedObjects: [{ id: 'rule-id-1', namespace: 'test1', type: 'alert', typeId: 'test' }],
+ source: { source: { id: 'rule-id-1', type: 'alert' }, type: 'SAVED_OBJECT' },
+ spaceId: 'test1',
+ },
+ actionToLog: {
+ alertSummary: {
+ new: summary.new.count,
+ ongoing: summary.ongoing.count,
+ recovered: summary.recovered.count,
+ },
+ id: actionId,
+ uuid: actionUuid,
+ typeId: '.test-system-action',
+ },
+});
+
let clock: sinon.SinonFakeTimers;
describe('System Action Scheduler', () => {
@@ -88,13 +129,29 @@ describe('System Action Scheduler', () => {
expect(scheduler.actions).toHaveLength(0);
});
- describe('generateExecutables', () => {
- const newAlert1 = generateAlert({ id: 1 });
- const newAlert2 = generateAlert({ id: 2 });
- const alerts = { ...newAlert1, ...newAlert2 };
+ describe('getActionsToSchedule', () => {
+ let newAlert1: Record<
+ string,
+ Alert
+ >;
+ let newAlert2: Record<
+ string,
+ Alert
+ >;
+ let alerts: Record<
+ string,
+ Alert
+ >;
- test('should generate executable for each system action', async () => {
+ beforeEach(() => {
+ newAlert1 = generateAlert({ id: 1 });
+ newAlert2 = generateAlert({ id: 2 });
+ alerts = { ...newAlert1, ...newAlert2 };
+ });
+
+ test('should create actions to schedule for each system action', async () => {
alertsClient.getProcessedAlerts.mockReturnValue(alerts);
+
const summarizedAlerts = {
new: { count: 2, data: [mockAAD, mockAAD] },
ongoing: { count: 0, data: [] },
@@ -103,25 +160,27 @@ describe('System Action Scheduler', () => {
alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts);
const scheduler = new SystemActionScheduler(getSchedulerContext());
- const executables = await scheduler.generateExecutables({
- alerts,
- throttledSummaryActions: {},
- });
+ const results = await scheduler.getActionsToSchedule({ alerts });
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1);
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({
excludedAlertInstanceIds: [],
executionUuid: defaultSchedulerContext.executionId,
- ruleId: '1',
+ ruleId: 'rule-id-1',
spaceId: 'test1',
});
- expect(executables).toHaveLength(1);
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(1);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('.test-system-action')).toEqual({
+ numberOfGeneratedActions: 1,
+ numberOfTriggeredActions: 1,
+ });
+
+ expect(results).toHaveLength(1);
const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } };
- expect(executables).toEqual([
- { action: rule.systemActions?.[0], summarizedAlerts: finalSummary },
- ]);
+ expect(results).toEqual([getResult('system-action-1', 'xxx-xxx', finalSummary)]);
});
test('should remove new alerts from summary if suppressed by maintenance window', async () => {
@@ -141,22 +200,26 @@ describe('System Action Scheduler', () => {
recovered: { count: 0, data: [] },
};
alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts);
- const scheduler = new SystemActionScheduler(getSchedulerContext());
- const executables = await scheduler.generateExecutables({
- alerts,
- throttledSummaryActions: {},
- });
+ const scheduler = new SystemActionScheduler(getSchedulerContext());
+ const results = await scheduler.getActionsToSchedule({ alerts });
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1);
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({
excludedAlertInstanceIds: [],
executionUuid: defaultSchedulerContext.executionId,
- ruleId: '1',
+ ruleId: 'rule-id-1',
spaceId: 'test1',
});
- expect(executables).toHaveLength(1);
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(1);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('.test-system-action')).toEqual({
+ numberOfGeneratedActions: 1,
+ numberOfTriggeredActions: 1,
+ });
+
+ expect(results).toHaveLength(1);
const finalSummary = {
all: { count: 1, data: [newAADAlerts[1]] },
@@ -164,12 +227,10 @@ describe('System Action Scheduler', () => {
ongoing: { count: 0, data: [] },
recovered: { count: 0, data: [] },
};
- expect(executables).toEqual([
- { action: rule.systemActions?.[0], summarizedAlerts: finalSummary },
- ]);
+ expect(results).toEqual([getResult('system-action-1', 'xxx-xxx', finalSummary)]);
});
- test('should skip generating executable for summary action when no alerts found', async () => {
+ test('should skip creating actions to schedule for summary action when no alerts found', async () => {
alertsClient.getProcessedAlerts.mockReturnValue(alerts);
const summarizedAlerts = {
new: { count: 0, data: [] },
@@ -179,21 +240,20 @@ describe('System Action Scheduler', () => {
alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts);
const scheduler = new SystemActionScheduler(getSchedulerContext());
-
- const executables = await scheduler.generateExecutables({
- alerts,
- throttledSummaryActions: {},
- });
+ const results = await scheduler.getActionsToSchedule({ alerts });
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1);
expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({
excludedAlertInstanceIds: [],
executionUuid: defaultSchedulerContext.executionId,
- ruleId: '1',
+ ruleId: 'rule-id-1',
spaceId: 'test1',
});
- expect(executables).toHaveLength(0);
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(0);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(0);
+
+ expect(results).toHaveLength(0);
});
test('should throw framework error if getSummarizedAlerts throws error', async () => {
@@ -205,14 +265,175 @@ describe('System Action Scheduler', () => {
const scheduler = new SystemActionScheduler(getSchedulerContext());
try {
- await scheduler.generateExecutables({
- alerts,
- throttledSummaryActions: {},
- });
+ await scheduler.getActionsToSchedule({ alerts });
} catch (err) {
expect(err.message).toEqual(`no alerts for you`);
expect(getErrorSource(err)).toBe(TaskErrorSource.FRAMEWORK);
}
});
+
+ test('should skip creating actions to schedule if overall max actions limit exceeded', async () => {
+ const anotherSystemAction = {
+ id: 'system-action-1',
+ actionTypeId: '.test-system-action',
+ params: { myParams: 'foo' },
+ uuid: 'yyy-yyy',
+ };
+
+ alertsClient.getProcessedAlerts.mockReturnValue(alerts);
+ const summarizedAlerts = {
+ new: { count: 2, data: [mockAAD, mockAAD] },
+ ongoing: { count: 0, data: [] },
+ recovered: { count: 0, data: [] },
+ };
+ alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts);
+
+ const defaultContext = getSchedulerContext();
+ const scheduler = new SystemActionScheduler({
+ ...defaultContext,
+ rule: { ...rule, systemActions: [rule.systemActions?.[0]!, anotherSystemAction] },
+ taskRunnerContext: {
+ ...defaultContext.taskRunnerContext,
+ actionsConfigMap: {
+ default: { max: 1 },
+ },
+ },
+ });
+ const results = await scheduler.getActionsToSchedule({ alerts });
+
+ expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2);
+ expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, {
+ excludedAlertInstanceIds: [],
+ executionUuid: defaultSchedulerContext.executionId,
+ ruleId: 'rule-id-1',
+ spaceId: 'test1',
+ });
+ expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, {
+ excludedAlertInstanceIds: [],
+ executionUuid: defaultSchedulerContext.executionId,
+ ruleId: 'rule-id-1',
+ spaceId: 'test1',
+ });
+
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('.test-system-action')).toEqual({
+ numberOfGeneratedActions: 2,
+ numberOfTriggeredActions: 1,
+ triggeredActionsStatus: ActionsCompletion.PARTIAL,
+ });
+
+ expect(logger.debug).toHaveBeenCalledWith(
+ `Rule "rule-id-1" skipped scheduling action "system-action-1" because the maximum number of allowed actions has been reached.`
+ );
+
+ expect(results).toHaveLength(1);
+
+ const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } };
+ expect(results).toEqual([getResult('system-action-1', 'xxx-xxx', finalSummary)]);
+ });
+
+ test('should skip creating actions to schedule if connector type max actions limit exceeded', async () => {
+ const anotherSystemAction = {
+ id: 'system-action-1',
+ actionTypeId: '.test-system-action',
+ params: { myParams: 'foo' },
+ uuid: 'yyy-yyy',
+ };
+
+ alertsClient.getProcessedAlerts.mockReturnValue(alerts);
+ const summarizedAlerts = {
+ new: { count: 2, data: [mockAAD, mockAAD] },
+ ongoing: { count: 0, data: [] },
+ recovered: { count: 0, data: [] },
+ };
+ alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts);
+
+ const defaultContext = getSchedulerContext();
+ const scheduler = new SystemActionScheduler({
+ ...defaultContext,
+ rule: { ...rule, systemActions: [rule.systemActions?.[0]!, anotherSystemAction] },
+ taskRunnerContext: {
+ ...defaultContext.taskRunnerContext,
+ actionsConfigMap: {
+ default: { max: 1000 },
+ '.test-system-action': { max: 1 },
+ },
+ },
+ });
+ const results = await scheduler.getActionsToSchedule({ alerts });
+
+ expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(2);
+ expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(1, {
+ excludedAlertInstanceIds: [],
+ executionUuid: defaultSchedulerContext.executionId,
+ ruleId: 'rule-id-1',
+ spaceId: 'test1',
+ });
+ expect(alertsClient.getSummarizedAlerts).toHaveBeenNthCalledWith(2, {
+ excludedAlertInstanceIds: [],
+ executionUuid: defaultSchedulerContext.executionId,
+ ruleId: 'rule-id-1',
+ spaceId: 'test1',
+ });
+
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(2);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(1);
+ expect(ruleRunMetricsStore.getStatusByConnectorType('.test-system-action')).toEqual({
+ numberOfGeneratedActions: 2,
+ numberOfTriggeredActions: 1,
+ triggeredActionsStatus: ActionsCompletion.PARTIAL,
+ });
+
+ expect(logger.debug).toHaveBeenCalledWith(
+ `Rule "rule-id-1" skipped scheduling action "system-action-1" because the maximum number of allowed actions for connector type .test-system-action has been reached.`
+ );
+
+ expect(results).toHaveLength(1);
+
+ const finalSummary = { ...summarizedAlerts, all: { count: 2, data: [mockAAD, mockAAD] } };
+ expect(results).toEqual([getResult('system-action-1', 'xxx-xxx', finalSummary)]);
+ });
+
+ test('should skip creating actions to schedule if no connector adapter exists for connector type', async () => {
+ const differentSystemAction = {
+ id: 'different-action-1',
+ actionTypeId: '.test-bad-system-action',
+ params: { myParams: 'foo' },
+ uuid: 'zzz-zzz',
+ };
+
+ alertsClient.getProcessedAlerts.mockReturnValue(alerts);
+ const summarizedAlerts = {
+ new: { count: 2, data: [mockAAD, mockAAD] },
+ ongoing: { count: 0, data: [] },
+ recovered: { count: 0, data: [] },
+ };
+ alertsClient.getSummarizedAlerts.mockResolvedValue(summarizedAlerts);
+
+ const defaultContext = getSchedulerContext();
+ const scheduler = new SystemActionScheduler({
+ ...defaultContext,
+ rule: { ...rule, systemActions: [differentSystemAction] },
+ });
+ const results = await scheduler.getActionsToSchedule({ alerts });
+
+ expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledTimes(1);
+ expect(alertsClient.getSummarizedAlerts).toHaveBeenCalledWith({
+ excludedAlertInstanceIds: [],
+ executionUuid: defaultSchedulerContext.executionId,
+ ruleId: 'rule-id-1',
+ spaceId: 'test1',
+ });
+
+ expect(ruleRunMetricsStore.getNumberOfGeneratedActions()).toEqual(1);
+ expect(ruleRunMetricsStore.getNumberOfTriggeredActions()).toEqual(0);
+
+ expect(logger.warn).toHaveBeenCalledWith(
+ `Rule "rule-id-1" skipped scheduling system action "different-action-1" because no connector adapter is configured`
+ );
+
+ expect(results).toHaveLength(0);
+ });
});
});
diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.ts
index b923baf8fbf38..0c5cceb0f0a52 100644
--- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.ts
+++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/schedulers/system_action_scheduler.ts
@@ -7,13 +7,19 @@
import { AlertInstanceState, AlertInstanceContext } from '@kbn/alerting-state-types';
import { RuleSystemAction, RuleTypeParams } from '@kbn/alerting-types';
+import { CombinedSummarizedAlerts } from '../../../types';
import { RuleTypeState, RuleAlertData } from '../../../../common';
import { GetSummarizedAlertsParams } from '../../../alerts_client/types';
-import { getSummarizedAlerts } from '../get_summarized_alerts';
+import {
+ buildRuleUrl,
+ formatActionToEnqueue,
+ getSummarizedAlerts,
+ shouldScheduleAction,
+} from '../lib';
import {
ActionSchedulerOptions,
- Executable,
- GenerateExecutablesOpts,
+ ActionsToSchedule,
+ GetActionsToScheduleOpts,
IActionScheduler,
} from '../types';
@@ -53,14 +59,19 @@ export class SystemActionScheduler<
return 1;
}
- public async generateExecutables(
- _: GenerateExecutablesOpts
- ): Promise>> {
- const executables = [];
+ public async getActionsToSchedule(
+ _: GetActionsToScheduleOpts
+ ): Promise {
+ const executables: Array<{
+ action: RuleSystemAction;
+ summarizedAlerts: CombinedSummarizedAlerts;
+ }> = [];
+ const results: ActionsToSchedule[] = [];
+
for (const action of this.actions) {
const options: GetSummarizedAlertsParams = {
spaceId: this.context.taskInstance.params.spaceId,
- ruleId: this.context.taskInstance.params.alertId,
+ ruleId: this.context.rule.id,
excludedAlertInstanceIds: this.context.rule.mutedInstanceIds,
executionUuid: this.context.executionId,
};
@@ -75,6 +86,95 @@ export class SystemActionScheduler<
}
}
- return executables;
+ if (executables.length === 0) return [];
+
+ this.context.ruleRunMetricsStore.incrementNumberOfGeneratedActions(executables.length);
+
+ const ruleUrl = buildRuleUrl({
+ getViewInAppRelativeUrl: this.context.ruleType.getViewInAppRelativeUrl,
+ kibanaBaseUrl: this.context.taskRunnerContext.kibanaBaseUrl,
+ logger: this.context.logger,
+ rule: this.context.rule,
+ spaceId: this.context.taskInstance.params.spaceId,
+ });
+
+ for (const { action, summarizedAlerts } of executables) {
+ const { actionTypeId } = action;
+
+ if (
+ !shouldScheduleAction({
+ action,
+ actionsConfigMap: this.context.taskRunnerContext.actionsConfigMap,
+ isActionExecutable: this.context.taskRunnerContext.actionsPlugin.isActionExecutable,
+ logger: this.context.logger,
+ ruleId: this.context.rule.id,
+ ruleRunMetricsStore: this.context.ruleRunMetricsStore,
+ })
+ ) {
+ continue;
+ }
+
+ const hasConnectorAdapter = this.context.taskRunnerContext.connectorAdapterRegistry.has(
+ action.actionTypeId
+ );
+
+ // System actions without an adapter cannot be executed
+ if (!hasConnectorAdapter) {
+ this.context.logger.warn(
+ `Rule "${this.context.rule.id}" skipped scheduling system action "${action.id}" because no connector adapter is configured`
+ );
+
+ continue;
+ }
+
+ this.context.ruleRunMetricsStore.incrementNumberOfTriggeredActions();
+ this.context.ruleRunMetricsStore.incrementNumberOfTriggeredActionsByConnectorType(
+ actionTypeId
+ );
+
+ const connectorAdapter = this.context.taskRunnerContext.connectorAdapterRegistry.get(
+ action.actionTypeId
+ );
+
+ const connectorAdapterActionParams = connectorAdapter.buildActionParams({
+ alerts: summarizedAlerts,
+ rule: {
+ id: this.context.rule.id,
+ tags: this.context.rule.tags,
+ name: this.context.rule.name,
+ consumer: this.context.rule.consumer,
+ producer: this.context.ruleType.producer,
+ },
+ ruleUrl: ruleUrl?.absoluteUrl,
+ spaceId: this.context.taskInstance.params.spaceId,
+ params: action.params,
+ });
+
+ const actionToRun = Object.assign(action, { params: connectorAdapterActionParams });
+
+ results.push({
+ actionToEnqueue: formatActionToEnqueue({
+ action: actionToRun,
+ apiKey: this.context.apiKey,
+ executionId: this.context.executionId,
+ ruleConsumer: this.context.ruleConsumer,
+ ruleId: this.context.rule.id,
+ ruleTypeId: this.context.ruleType.id,
+ spaceId: this.context.taskInstance.params.spaceId,
+ }),
+ actionToLog: {
+ id: action.id,
+ uuid: action.uuid,
+ typeId: action.actionTypeId,
+ alertSummary: {
+ new: summarizedAlerts.new.count,
+ ongoing: summarizedAlerts.ongoing.count,
+ recovered: summarizedAlerts.recovered.count,
+ },
+ },
+ });
+ }
+
+ return results;
}
}
diff --git a/x-pack/plugins/alerting/server/task_runner/action_scheduler/types.ts b/x-pack/plugins/alerting/server/task_runner/action_scheduler/types.ts
index efcb51fcb2698..b90ffb88d541b 100644
--- a/x-pack/plugins/alerting/server/task_runner/action_scheduler/types.ts
+++ b/x-pack/plugins/alerting/server/task_runner/action_scheduler/types.ts
@@ -8,6 +8,7 @@
import type { Logger } from '@kbn/core/server';
import { PublicMethodsOf } from '@kbn/utility-types';
import { ActionsClient } from '@kbn/actions-plugin/server/actions_client';
+import { ExecuteOptions as EnqueueExecutionOptions } from '@kbn/actions-plugin/server/create_execute_function';
import { IAlertsClient } from '../../alerts_client/types';
import { Alert } from '../../alert';
import {
@@ -24,7 +25,10 @@ import {
import { NormalizedRuleType } from '../../rule_type_registry';
import { CombinedSummarizedAlerts, RawRule } from '../../types';
import { RuleRunMetricsStore } from '../../lib/rule_run_metrics_store';
-import { AlertingEventLogger } from '../../lib/alerting_event_logger/alerting_event_logger';
+import {
+ ActionOpts,
+ AlertingEventLogger,
+} from '../../lib/alerting_event_logger/alerting_event_logger';
import { RuleTaskInstance, TaskRunnerContext } from '../types';
export interface ActionSchedulerOptions<
@@ -80,14 +84,19 @@ export type Executable<
}
);
-export interface GenerateExecutablesOpts<
+export interface GetActionsToScheduleOpts<
State extends AlertInstanceState,
Context extends AlertInstanceContext,
ActionGroupIds extends string,
RecoveryActionGroupId extends string
> {
alerts: Record>;
- throttledSummaryActions: ThrottledActions;
+ throttledSummaryActions?: ThrottledActions;
+}
+
+export interface ActionsToSchedule {
+ actionToEnqueue: EnqueueExecutionOptions;
+ actionToLog: ActionOpts;
}
export interface IActionScheduler<
@@ -97,9 +106,9 @@ export interface IActionScheduler<
RecoveryActionGroupId extends string
> {
get priority(): number;
- generateExecutables(
- opts: GenerateExecutablesOpts
- ): Promise>>;
+ getActionsToSchedule(
+ opts: GetActionsToScheduleOpts
+ ): Promise;
}
export interface RuleUrl {
diff --git a/x-pack/plugins/alerting/server/task_runner/fixtures.ts b/x-pack/plugins/alerting/server/task_runner/fixtures.ts
index 5174aa9b965ec..d820f2690caeb 100644
--- a/x-pack/plugins/alerting/server/task_runner/fixtures.ts
+++ b/x-pack/plugins/alerting/server/task_runner/fixtures.ts
@@ -21,7 +21,7 @@ import {
import { getDefaultMonitoring } from '../lib/monitoring';
import { UntypedNormalizedRuleType } from '../rule_type_registry';
import { EVENT_LOG_ACTIONS } from '../plugin';
-import { RawRule } from '../types';
+import { AlertHit, RawRule } from '../types';
import { RULE_SAVED_OBJECT_TYPE } from '../saved_objects';
interface GeneratorParams {
@@ -349,9 +349,10 @@ export const generateAlertOpts = ({
};
};
-export const generateActionOpts = ({ id, alertGroup, alertId }: GeneratorParams = {}) => ({
+export const generateActionOpts = ({ id, alertGroup, alertId, uuid }: GeneratorParams = {}) => ({
id: id ?? '1',
typeId: 'action',
+ uuid: uuid ?? '111-111',
alertId: alertId ?? '1',
alertGroup: alertGroup ?? 'default',
});
@@ -403,11 +404,13 @@ export const generateRunnerResult = ({
export const generateEnqueueFunctionInput = ({
id = '1',
+ uuid = '111-111',
isBulk = false,
isResolved,
foo,
actionTypeId,
}: {
+ uuid?: string;
id: string;
isBulk?: boolean;
isResolved?: boolean;
@@ -419,6 +422,7 @@ export const generateEnqueueFunctionInput = ({
apiKey: 'MTIzOmFiYw==',
executionId: '5f6aa57d-3e22-484e-bae8-cbed868f4d28',
id,
+ uuid,
params: {
...(isResolved !== undefined ? { isResolved } : {}),
...(foo !== undefined ? { foo } : {}),
@@ -504,4 +508,4 @@ export const mockAAD = {
},
},
},
-};
+} as unknown as AlertHit;
diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts
index eb531f0e00b88..b6e59402ba4c6 100644
--- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts
+++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts
@@ -1420,7 +1420,7 @@ describe('Task Runner', () => {
expect(alertingEventLogger.logAction).toHaveBeenNthCalledWith(1, generateActionOpts({}));
expect(alertingEventLogger.logAction).toHaveBeenNthCalledWith(
2,
- generateActionOpts({ id: '2', alertId: '2', alertGroup: 'recovered' })
+ generateActionOpts({ id: '2', alertId: '2', alertGroup: 'recovered', uuid: '222-222' })
);
expect(enqueueFunction).toHaveBeenCalledTimes(isBulk ? 1 : 2);
@@ -1428,7 +1428,12 @@ describe('Task Runner', () => {
isBulk
? [
generateEnqueueFunctionInput({ isBulk: false, id: '1', foo: true }),
- generateEnqueueFunctionInput({ isBulk: false, id: '2', isResolved: true }),
+ generateEnqueueFunctionInput({
+ isBulk: false,
+ id: '2',
+ isResolved: true,
+ uuid: '222-222',
+ }),
]
: generateEnqueueFunctionInput({ isBulk: false, id: '1', foo: true })
);
@@ -1645,7 +1650,12 @@ describe('Task Runner', () => {
isBulk
? [
generateEnqueueFunctionInput({ isBulk: false, id: '1', foo: true }),
- generateEnqueueFunctionInput({ isBulk: false, id: '2', isResolved: true }),
+ generateEnqueueFunctionInput({
+ isBulk: false,
+ id: '2',
+ isResolved: true,
+ uuid: '222-222',
+ }),
]
: generateEnqueueFunctionInput({ isBulk: false, id: '1', foo: true })
);
@@ -2891,26 +2901,31 @@ describe('Task Runner', () => {
{
group: 'default',
id: '1',
+ uuid: '111-111',
actionTypeId: 'action',
},
{
group: 'default',
id: '2',
+ uuid: '222-222',
actionTypeId: 'action',
},
{
group: 'default',
id: '3',
+ uuid: '333-333',
actionTypeId: 'action',
},
{
group: 'default',
id: '4',
+ uuid: '444-444',
actionTypeId: 'action',
},
{
group: 'default',
id: '5',
+ uuid: '555-555',
actionTypeId: 'action',
},
];
@@ -2975,7 +2990,7 @@ describe('Task Runner', () => {
})
);
- expect(logger.debug).toHaveBeenCalledTimes(7);
+ expect(logger.debug).toHaveBeenCalledTimes(8);
expect(logger.debug).nthCalledWith(
3,
@@ -3012,11 +3027,11 @@ describe('Task Runner', () => {
expect(alertingEventLogger.logAction).toHaveBeenNthCalledWith(1, generateActionOpts({}));
expect(alertingEventLogger.logAction).toHaveBeenNthCalledWith(
2,
- generateActionOpts({ id: '2' })
+ generateActionOpts({ id: '2', uuid: '222-222' })
);
expect(alertingEventLogger.logAction).toHaveBeenNthCalledWith(
3,
- generateActionOpts({ id: '3' })
+ generateActionOpts({ id: '3', uuid: '333-333' })
);
});
@@ -3061,26 +3076,31 @@ describe('Task Runner', () => {
{
group: 'default',
id: '1',
+ uuid: '111-111',
actionTypeId: '.server-log',
},
{
group: 'default',
id: '2',
+ uuid: '222-222',
actionTypeId: '.server-log',
},
{
group: 'default',
id: '3',
+ uuid: '333-333',
actionTypeId: '.server-log',
},
{
group: 'default',
id: '4',
+ uuid: '444-444',
actionTypeId: 'any-action',
},
{
group: 'default',
id: '5',
+ uuid: '555-555',
actionTypeId: 'any-action',
},
] as RuleAction[],
@@ -3176,7 +3196,7 @@ describe('Task Runner', () => {
status: 'warning',
errorReason: `maxExecutableActions`,
logAlert: 4,
- logAction: 3,
+ logAction: 5,
});
});
From 742cd1336e71d2236608450e2a6a77b3ce9b3c4c Mon Sep 17 00:00:00 2001
From: Ying Mao
Date: Wed, 9 Oct 2024 17:01:49 -0400
Subject: [PATCH 19/87] Fixes Failing test: Jest Integration
Tests.x-pack/plugins/task_manager/server/integration_tests - unrecognized
task types should be no workload aggregator errors when there are removed
task types (#195496)
Resolves https://github.com/elastic/kibana/issues/194208
## Summary
The original integration test was checking for the (non) existence of
any error logs on startup when there are removed task types, which was
not specific enough because there were occasionally error logs like
```
"Task SLO:ORPHAN_SUMMARIES-CLEANUP-TASK \"SLO:ORPHAN_SUMMARIES-CLEANUP-TASK:1.0.0\" failed: ResponseError: search_phase_execution_exception
```
so this PR updates the integration test to check specifically for
workload aggregator error logs
Co-authored-by: Elastic Machine
---
.../server/integration_tests/removed_types.test.ts | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/x-pack/plugins/task_manager/server/integration_tests/removed_types.test.ts b/x-pack/plugins/task_manager/server/integration_tests/removed_types.test.ts
index 69bf717b95fc6..aeb182c4794e6 100644
--- a/x-pack/plugins/task_manager/server/integration_tests/removed_types.test.ts
+++ b/x-pack/plugins/task_manager/server/integration_tests/removed_types.test.ts
@@ -121,7 +121,15 @@ describe('unrecognized task types', () => {
// so we want to wait that long to let it refresh
await new Promise((r) => setTimeout(r, 5100));
- expect(errorLogSpy).not.toHaveBeenCalled();
+ const errorLogCalls = errorLogSpy.mock.calls[0];
+
+ // if there are any error logs, none of them should be workload aggregator errors
+ if (errorLogCalls) {
+ // should be no workload aggregator errors
+ for (const elog of errorLogCalls) {
+ expect(elog).not.toMatch(/^\[WorkloadAggregator\]: Error: Unsupported task type/i);
+ }
+ }
});
});
From 8f8e9883e0a8e78a632418a0677980f758450351 Mon Sep 17 00:00:00 2001
From: Kevin Lacabane
Date: Wed, 9 Oct 2024 23:15:33 +0200
Subject: [PATCH 20/87] [eem] remove history transforms (#193999)
### Summary
Remove history and backfill transforms, leaving latest transform in
place.
Notable changes to latest transform:
- it does not read from history output anymore but source indices
defined on the definition
- it defines a `latest.lookbackPeriod` to limit the amount of data
ingested, which defaults to 24h
- each metadata aggregation now accepts a
`metadata.aggregation.lookbackPeriod` which defaults to the
`latest.lookbackPeriod`
- `entity.firstSeenTimestamp` is removed. this should be temporary until
we have a solution for
https://github.com/elastic/elastic-entity-model/issues/174
- latest metrics used to get the latest pre-computed value from history
data, but is it now aggregating over the `lookbackPeriod` in the source
indices (which can be filtered down with `metrics.filter`)
- `latest` block on the entity definition is now mandatory
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Mark Hopkin
---
.../check_registered_types.test.ts | 2 +-
.../schema/__snapshots__/common.test.ts.snap | 12 +-
.../src/schema/common.test.ts | 30 +-
.../kbn-entities-schema/src/schema/common.ts | 10 +-
.../kbn-entities-schema/src/schema/entity.ts | 1 -
.../src/schema/entity_definition.ts | 39 ++-
.../common/constants_entities.ts | 8 +-
.../built_in/containers_from_ecs_data.ts | 4 +-
.../entities/built_in/hosts_from_ecs_data.ts | 4 +-
.../built_in/services_from_ecs_data.ts | 5 +-
.../create_and_install_ingest_pipeline.ts | 40 +--
.../entities/create_and_install_transform.ts | 45 +--
.../lib/entities/delete_ingest_pipeline.ts | 24 +-
.../server/lib/entities/delete_transforms.ts | 57 ++-
.../lib/entities/find_entity_definition.ts | 33 +-
.../lib/entities/helpers/calculate_offset.ts | 34 --
.../fixtures/builtin_entity_definition.ts | 3 +-
.../helpers/fixtures/entity_definition.ts | 9 +-
.../entity_definition_with_backfill.ts | 51 ---
.../lib/entities/helpers/fixtures/index.ts | 1 -
.../entities/helpers/is_backfill_enabled.ts | 12 -
.../generate_history_processors.test.ts.snap | 327 ------------------
.../generate_latest_processors.test.ts.snap | 140 ++++++--
.../generate_history_processors.test.ts | 21 --
.../generate_history_processors.ts | 222 ------------
.../generate_latest_processors.ts | 90 +++--
.../install_entity_definition.test.ts | 133 ++++---
.../lib/entities/install_entity_definition.ts | 114 ++----
.../lib/entities/save_entity_definition.ts | 2 +-
.../server/lib/entities/start_transforms.ts | 37 +-
.../server/lib/entities/stop_transforms.ts | 65 ++--
.../entities_history_template.test.ts.snap | 152 --------
.../entities_history_template.test.ts | 21 --
.../templates/entities_history_template.ts | 96 -----
.../templates/entities_latest_template.ts | 15 +-
.../generate_history_transform.test.ts.snap | 305 ----------------
.../generate_latest_transform.test.ts.snap | 150 ++++----
.../generate_history_transform.test.ts | 24 --
.../transform/generate_history_transform.ts | 178 ----------
.../transform/generate_latest_transform.ts | 97 ++++--
.../generate_metadata_aggregations.test.ts | 144 +-------
.../generate_metadata_aggregations.ts | 54 +--
.../transform/generate_metric_aggregations.ts | 30 +-
.../transform/validate_transform_ids.ts | 16 +-
.../entities/uninstall_entity_definition.ts | 18 +-
.../server/lib/entity_client.ts | 2 +-
.../server/lib/manage_index_templates.ts | 39 ++-
.../server/routes/enablement/disable.ts | 5 +-
.../server/routes/enablement/enable.ts | 9 +-
.../server/routes/entities/reset.ts | 30 +-
.../server/saved_objects/entity_definition.ts | 33 ++
.../templates/components/helpers.test.ts | 31 --
.../server/templates/components/helpers.ts | 35 --
x-pack/plugins/entity_manager/tsconfig.json | 1 +
.../server/services/get_entities.ts | 1 -
.../entity_store/definition.ts | 6 +-
.../apis/entity_manager/definitions.ts | 18 +-
.../trial_license_complete_tier/engine.ts | 12 +-
.../engine_nondefault_spaces.ts | 10 +-
59 files changed, 712 insertions(+), 2395 deletions(-)
delete mode 100644 x-pack/plugins/entity_manager/server/lib/entities/helpers/calculate_offset.ts
delete mode 100644 x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/entity_definition_with_backfill.ts
delete mode 100644 x-pack/plugins/entity_manager/server/lib/entities/helpers/is_backfill_enabled.ts
delete mode 100644 x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap
delete mode 100644 x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.test.ts
delete mode 100644 x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts
delete mode 100644 x-pack/plugins/entity_manager/server/lib/entities/templates/__snapshots__/entities_history_template.test.ts.snap
delete mode 100644 x-pack/plugins/entity_manager/server/lib/entities/templates/entities_history_template.test.ts
delete mode 100644 x-pack/plugins/entity_manager/server/lib/entities/templates/entities_history_template.ts
delete mode 100644 x-pack/plugins/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap
delete mode 100644 x-pack/plugins/entity_manager/server/lib/entities/transform/generate_history_transform.test.ts
delete mode 100644 x-pack/plugins/entity_manager/server/lib/entities/transform/generate_history_transform.ts
delete mode 100644 x-pack/plugins/entity_manager/server/templates/components/helpers.test.ts
delete mode 100644 x-pack/plugins/entity_manager/server/templates/components/helpers.ts
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 c8803a1fbd071..7736e1ad7e90b 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
@@ -91,7 +91,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"endpoint:unified-user-artifact-manifest": "71c7fcb52c658b21ea2800a6b6a76972ae1c776e",
"endpoint:user-artifact-manifest": "1c3533161811a58772e30cdc77bac4631da3ef2b",
"enterprise_search_telemetry": "9ac912e1417fc8681e0cd383775382117c9e3d3d",
- "entity-definition": "61be3e95966045122b55e181bb39658b1dc9bbe9",
+ "entity-definition": "e3811fd5fbb878d170067c0d6897a2e63010af36",
"entity-discovery-api-key": "c267a65c69171d1804362155c1378365f5acef88",
"entity-engine-status": "0738aa1a06d3361911740f8f166071ea43a00927",
"epm-packages": "8042d4a1522f6c4e6f5486e791b3ffe3a22f88fd",
diff --git a/x-pack/packages/kbn-entities-schema/src/schema/__snapshots__/common.test.ts.snap b/x-pack/packages/kbn-entities-schema/src/schema/__snapshots__/common.test.ts.snap
index 9210d3b9991cf..766ce1c70ac3a 100644
--- a/x-pack/packages/kbn-entities-schema/src/schema/__snapshots__/common.test.ts.snap
+++ b/x-pack/packages/kbn-entities-schema/src/schema/__snapshots__/common.test.ts.snap
@@ -78,7 +78,8 @@ exports[`schemas metadataSchema should parse successfully with a source and desi
Object {
"data": Object {
"aggregation": Object {
- "limit": 1000,
+ "limit": 10,
+ "lookbackPeriod": undefined,
"type": "terms",
},
"destination": "hostName",
@@ -92,7 +93,8 @@ exports[`schemas metadataSchema should parse successfully with an valid string 1
Object {
"data": Object {
"aggregation": Object {
- "limit": 1000,
+ "limit": 10,
+ "lookbackPeriod": undefined,
"type": "terms",
},
"destination": "host.name",
@@ -106,7 +108,8 @@ exports[`schemas metadataSchema should parse successfully with just a source 1`]
Object {
"data": Object {
"aggregation": Object {
- "limit": 1000,
+ "limit": 10,
+ "lookbackPeriod": undefined,
"type": "terms",
},
"destination": "host.name",
@@ -120,7 +123,8 @@ exports[`schemas metadataSchema should parse successfully with valid object 1`]
Object {
"data": Object {
"aggregation": Object {
- "limit": 1000,
+ "limit": 10,
+ "lookbackPeriod": undefined,
"type": "terms",
},
"destination": "hostName",
diff --git a/x-pack/packages/kbn-entities-schema/src/schema/common.test.ts b/x-pack/packages/kbn-entities-schema/src/schema/common.test.ts
index 1a737ac3f4d9b..210e34943bd40 100644
--- a/x-pack/packages/kbn-entities-schema/src/schema/common.test.ts
+++ b/x-pack/packages/kbn-entities-schema/src/schema/common.test.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { durationSchema, metadataSchema, semVerSchema, historySettingsSchema } from './common';
+import { durationSchema, metadataSchema, semVerSchema } from './common';
describe('schemas', () => {
describe('metadataSchema', () => {
@@ -66,7 +66,7 @@ describe('schemas', () => {
expect(result.data).toEqual({
source: 'host.name',
destination: 'hostName',
- aggregation: { type: 'terms', limit: 1000 },
+ aggregation: { type: 'terms', limit: 10, lookbackPeriod: undefined },
});
});
@@ -139,30 +139,4 @@ describe('schemas', () => {
expect(result).toMatchSnapshot();
});
});
-
- describe('historySettingsSchema', () => {
- it('should return default values when not defined', () => {
- let result = historySettingsSchema.safeParse(undefined);
- expect(result.success).toBeTruthy();
- expect(result.data).toEqual({ lookbackPeriod: '1h' });
-
- result = historySettingsSchema.safeParse({ syncDelay: '1m' });
- expect(result.success).toBeTruthy();
- expect(result.data).toEqual({ syncDelay: '1m', lookbackPeriod: '1h' });
- });
-
- it('should return user defined values when defined', () => {
- const result = historySettingsSchema.safeParse({
- lookbackPeriod: '30m',
- syncField: 'event.ingested',
- syncDelay: '5m',
- });
- expect(result.success).toBeTruthy();
- expect(result.data).toEqual({
- lookbackPeriod: '30m',
- syncField: 'event.ingested',
- syncDelay: '5m',
- });
- });
- });
});
diff --git a/x-pack/packages/kbn-entities-schema/src/schema/common.ts b/x-pack/packages/kbn-entities-schema/src/schema/common.ts
index aa54dbd16c9aa..caecf48d88aac 100644
--- a/x-pack/packages/kbn-entities-schema/src/schema/common.ts
+++ b/x-pack/packages/kbn-entities-schema/src/schema/common.ts
@@ -85,7 +85,11 @@ export const keyMetricSchema = z.object({
export type KeyMetric = z.infer;
export const metadataAggregation = z.union([
- z.object({ type: z.literal('terms'), limit: z.number().default(1000) }),
+ z.object({
+ type: z.literal('terms'),
+ limit: z.number().default(10),
+ lookbackPeriod: z.optional(durationSchema),
+ }),
z.object({
type: z.literal('top_value'),
sort: z.record(z.string(), z.union([z.literal('asc'), z.literal('desc')])),
@@ -99,13 +103,13 @@ export const metadataSchema = z
destination: z.optional(z.string()),
aggregation: z
.optional(metadataAggregation)
- .default({ type: z.literal('terms').value, limit: 1000 }),
+ .default({ type: z.literal('terms').value, limit: 10, lookbackPeriod: undefined }),
})
.or(
z.string().transform((value) => ({
source: value,
destination: value,
- aggregation: { type: z.literal('terms').value, limit: 1000 },
+ aggregation: { type: z.literal('terms').value, limit: 10, lookbackPeriod: undefined },
}))
)
.transform((metadata) => ({
diff --git a/x-pack/packages/kbn-entities-schema/src/schema/entity.ts b/x-pack/packages/kbn-entities-schema/src/schema/entity.ts
index eae6873356c14..3eb87a797ef21 100644
--- a/x-pack/packages/kbn-entities-schema/src/schema/entity.ts
+++ b/x-pack/packages/kbn-entities-schema/src/schema/entity.ts
@@ -35,7 +35,6 @@ export const entityLatestSchema = z
entity: entityBaseSchema.merge(
z.object({
lastSeenTimestamp: z.string(),
- firstSeenTimestamp: z.string(),
})
),
})
diff --git a/x-pack/packages/kbn-entities-schema/src/schema/entity_definition.ts b/x-pack/packages/kbn-entities-schema/src/schema/entity_definition.ts
index 74be36cc5d802..d9d8e6b610013 100644
--- a/x-pack/packages/kbn-entities-schema/src/schema/entity_definition.ts
+++ b/x-pack/packages/kbn-entities-schema/src/schema/entity_definition.ts
@@ -14,8 +14,6 @@ import {
durationSchema,
identityFieldsSchema,
semVerSchema,
- historySettingsSchema,
- durationSchemaWithMinimum,
} from './common';
export const entityDefinitionSchema = z.object({
@@ -32,22 +30,17 @@ export const entityDefinitionSchema = z.object({
metrics: z.optional(z.array(keyMetricSchema)),
staticFields: z.optional(z.record(z.string(), z.string())),
managed: z.optional(z.boolean()).default(false),
- history: z.object({
+ latest: z.object({
timestampField: z.string(),
- interval: durationSchemaWithMinimum(1),
- settings: historySettingsSchema,
+ lookbackPeriod: z.optional(durationSchema).default('24h'),
+ settings: z.optional(
+ z.object({
+ syncField: z.optional(z.string()),
+ syncDelay: z.optional(durationSchema),
+ frequency: z.optional(durationSchema),
+ })
+ ),
}),
- latest: z.optional(
- z.object({
- settings: z.optional(
- z.object({
- syncField: z.optional(z.string()),
- syncDelay: z.optional(durationSchema),
- frequency: z.optional(durationSchema),
- })
- ),
- })
- ),
installStatus: z.optional(
z.union([
z.literal('installing'),
@@ -57,6 +50,18 @@ export const entityDefinitionSchema = z.object({
])
),
installStartedAt: z.optional(z.string()),
+ installedComponents: z.optional(
+ z.array(
+ z.object({
+ type: z.union([
+ z.literal('transform'),
+ z.literal('ingest_pipeline'),
+ z.literal('template'),
+ ]),
+ id: z.string(),
+ })
+ )
+ ),
});
export const entityDefinitionUpdateSchema = entityDefinitionSchema
@@ -69,7 +74,7 @@ export const entityDefinitionUpdateSchema = entityDefinitionSchema
.partial()
.merge(
z.object({
- history: z.optional(entityDefinitionSchema.shape.history.partial()),
+ latest: z.optional(entityDefinitionSchema.shape.latest.partial()),
version: semVerSchema,
})
);
diff --git a/x-pack/plugins/entity_manager/common/constants_entities.ts b/x-pack/plugins/entity_manager/common/constants_entities.ts
index c53847afbb548..c17e6f33918c6 100644
--- a/x-pack/plugins/entity_manager/common/constants_entities.ts
+++ b/x-pack/plugins/entity_manager/common/constants_entities.ts
@@ -33,8 +33,6 @@ export const ENTITY_LATEST_PREFIX_V1 =
`${ENTITY_BASE_PREFIX}-${ENTITY_SCHEMA_VERSION_V1}-${ENTITY_LATEST}` as const;
// Transform constants
-export const ENTITY_DEFAULT_HISTORY_FREQUENCY = '1m';
-export const ENTITY_DEFAULT_HISTORY_SYNC_DELAY = '60s';
-export const ENTITY_DEFAULT_LATEST_FREQUENCY = '30s';
-export const ENTITY_DEFAULT_LATEST_SYNC_DELAY = '1s';
-export const ENTITY_DEFAULT_METADATA_LIMIT = 1000;
+export const ENTITY_DEFAULT_LATEST_FREQUENCY = '1m';
+export const ENTITY_DEFAULT_LATEST_SYNC_DELAY = '60s';
+export const ENTITY_DEFAULT_METADATA_LIMIT = 10;
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/containers_from_ecs_data.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/containers_from_ecs_data.ts
index 6ce76e127c8e8..e3356c4826ae8 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/containers_from_ecs_data.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/containers_from_ecs_data.ts
@@ -20,9 +20,9 @@ export const builtInContainersFromEcsEntityDefinition: EntityDefinition =
indexPatterns: ['filebeat-*', 'logs-*', 'metrics-*', 'metricbeat-*'],
identityFields: ['container.id'],
displayNameTemplate: '{{container.id}}',
- history: {
+ latest: {
timestampField: '@timestamp',
- interval: '5m',
+ lookbackPeriod: '10m',
settings: {
frequency: '5m',
},
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/hosts_from_ecs_data.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/hosts_from_ecs_data.ts
index 56f83d5fbaed6..5d7a30093419e 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/hosts_from_ecs_data.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/hosts_from_ecs_data.ts
@@ -19,9 +19,9 @@ export const builtInHostsFromEcsEntityDefinition: EntityDefinition = entityDefin
indexPatterns: ['filebeat-*', 'logs-*', 'metrics-*', 'metricbeat-*'],
identityFields: ['host.name'],
displayNameTemplate: '{{host.name}}',
- history: {
+ latest: {
timestampField: '@timestamp',
- interval: '5m',
+ lookbackPeriod: '10m',
settings: {
frequency: '5m',
},
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/services_from_ecs_data.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/services_from_ecs_data.ts
index 6caa209da02ca..d6aa4d08ad221 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/services_from_ecs_data.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/services_from_ecs_data.ts
@@ -18,11 +18,10 @@ export const builtInServicesFromEcsEntityDefinition: EntityDefinition =
type: 'service',
managed: true,
indexPatterns: ['logs-*', 'filebeat*', 'traces-apm*'],
- history: {
+ latest: {
timestampField: '@timestamp',
- interval: '1m',
+ lookbackPeriod: '10m',
settings: {
- lookbackPeriod: '10m',
frequency: '2m',
syncDelay: '2m',
},
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/create_and_install_ingest_pipeline.ts b/x-pack/plugins/entity_manager/server/lib/entities/create_and_install_ingest_pipeline.ts
index 360f416cd5a00..0b3900363c0c8 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/create_and_install_ingest_pipeline.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/create_and_install_ingest_pipeline.ts
@@ -7,46 +7,15 @@
import { ElasticsearchClient, Logger } from '@kbn/core/server';
import { EntityDefinition } from '@kbn/entities-schema';
-import {
- generateHistoryIngestPipelineId,
- generateLatestIngestPipelineId,
-} from './helpers/generate_component_id';
+import { generateLatestIngestPipelineId } from './helpers/generate_component_id';
import { retryTransientEsErrors } from './helpers/retry';
-import { generateHistoryProcessors } from './ingest_pipeline/generate_history_processors';
import { generateLatestProcessors } from './ingest_pipeline/generate_latest_processors';
-export async function createAndInstallHistoryIngestPipeline(
+export async function createAndInstallIngestPipelines(
esClient: ElasticsearchClient,
definition: EntityDefinition,
logger: Logger
-) {
- try {
- const historyProcessors = generateHistoryProcessors(definition);
- const historyId = generateHistoryIngestPipelineId(definition);
- await retryTransientEsErrors(
- () =>
- esClient.ingest.putPipeline({
- id: historyId,
- processors: historyProcessors,
- _meta: {
- definitionVersion: definition.version,
- managed: definition.managed,
- },
- }),
- { logger }
- );
- } catch (e) {
- logger.error(
- `Cannot create entity history ingest pipelines for [${definition.id}] entity defintion`
- );
- throw e;
- }
-}
-export async function createAndInstallLatestIngestPipeline(
- esClient: ElasticsearchClient,
- definition: EntityDefinition,
- logger: Logger
-) {
+): Promise> {
try {
const latestProcessors = generateLatestProcessors(definition);
const latestId = generateLatestIngestPipelineId(definition);
@@ -62,9 +31,10 @@ export async function createAndInstallLatestIngestPipeline(
}),
{ logger }
);
+ return [{ type: 'ingest_pipeline', id: latestId }];
} catch (e) {
logger.error(
- `Cannot create entity latest ingest pipelines for [${definition.id}] entity defintion`
+ `Cannot create entity latest ingest pipelines for [${definition.id}] entity definition`
);
throw e;
}
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/create_and_install_transform.ts b/x-pack/plugins/entity_manager/server/lib/entities/create_and_install_transform.ts
index d6379773479fc..779e0994a33b8 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/create_and_install_transform.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/create_and_install_transform.ts
@@ -9,57 +9,20 @@ import { ElasticsearchClient, Logger } from '@kbn/core/server';
import { EntityDefinition } from '@kbn/entities-schema';
import { retryTransientEsErrors } from './helpers/retry';
import { generateLatestTransform } from './transform/generate_latest_transform';
-import {
- generateBackfillHistoryTransform,
- generateHistoryTransform,
-} from './transform/generate_history_transform';
-export async function createAndInstallHistoryTransform(
+export async function createAndInstallTransforms(
esClient: ElasticsearchClient,
definition: EntityDefinition,
logger: Logger
-) {
- try {
- const historyTransform = generateHistoryTransform(definition);
- await retryTransientEsErrors(() => esClient.transform.putTransform(historyTransform), {
- logger,
- });
- } catch (e) {
- logger.error(`Cannot create entity history transform for [${definition.id}] entity definition`);
- throw e;
- }
-}
-
-export async function createAndInstallHistoryBackfillTransform(
- esClient: ElasticsearchClient,
- definition: EntityDefinition,
- logger: Logger
-) {
- try {
- const historyTransform = generateBackfillHistoryTransform(definition);
- await retryTransientEsErrors(() => esClient.transform.putTransform(historyTransform), {
- logger,
- });
- } catch (e) {
- logger.error(
- `Cannot create entity history backfill transform for [${definition.id}] entity definition`
- );
- throw e;
- }
-}
-
-export async function createAndInstallLatestTransform(
- esClient: ElasticsearchClient,
- definition: EntityDefinition,
- logger: Logger
-) {
+): Promise> {
try {
const latestTransform = generateLatestTransform(definition);
await retryTransientEsErrors(() => esClient.transform.putTransform(latestTransform), {
logger,
});
+ return [{ type: 'transform', id: latestTransform.transform_id }];
} catch (e) {
- logger.error(`Cannot create entity latest transform for [${definition.id}] entity definition`);
+ logger.error(`Cannot create entity history transform for [${definition.id}] entity definition`);
throw e;
}
}
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/delete_ingest_pipeline.ts b/x-pack/plugins/entity_manager/server/lib/entities/delete_ingest_pipeline.ts
index f4c46d8447d8f..a3b910dd4cb5e 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/delete_ingest_pipeline.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/delete_ingest_pipeline.ts
@@ -7,24 +7,24 @@
import { ElasticsearchClient, Logger } from '@kbn/core/server';
import { EntityDefinition } from '@kbn/entities-schema';
-import {
- generateHistoryIngestPipelineId,
- generateLatestIngestPipelineId,
-} from './helpers/generate_component_id';
import { retryTransientEsErrors } from './helpers/retry';
+import { generateLatestIngestPipelineId } from './helpers/generate_component_id';
-export async function deleteHistoryIngestPipeline(
+export async function deleteIngestPipelines(
esClient: ElasticsearchClient,
definition: EntityDefinition,
logger: Logger
) {
try {
- const historyPipelineId = generateHistoryIngestPipelineId(definition);
- await retryTransientEsErrors(() =>
- esClient.ingest.deletePipeline({ id: historyPipelineId }, { ignore: [404] })
+ await Promise.all(
+ (definition.installedComponents ?? [])
+ .filter(({ type }) => type === 'ingest_pipeline')
+ .map(({ id }) =>
+ retryTransientEsErrors(() => esClient.ingest.deletePipeline({ id }, { ignore: [404] }))
+ )
);
} catch (e) {
- logger.error(`Unable to delete history ingest pipeline [${definition.id}]: ${e}`);
+ logger.error(`Unable to delete ingest pipelines for definition [${definition.id}]: ${e}`);
throw e;
}
}
@@ -35,9 +35,11 @@ export async function deleteLatestIngestPipeline(
logger: Logger
) {
try {
- const latestPipelineId = generateLatestIngestPipelineId(definition);
await retryTransientEsErrors(() =>
- esClient.ingest.deletePipeline({ id: latestPipelineId }, { ignore: [404] })
+ esClient.ingest.deletePipeline(
+ { id: generateLatestIngestPipelineId(definition) },
+ { ignore: [404] }
+ )
);
} catch (e) {
logger.error(`Unable to delete latest ingest pipeline [${definition.id}]: ${e}`);
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/delete_transforms.ts b/x-pack/plugins/entity_manager/server/lib/entities/delete_transforms.ts
index a66c0998c014d..79b83998d38db 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/delete_transforms.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/delete_transforms.ts
@@ -7,14 +7,8 @@
import { ElasticsearchClient, Logger } from '@kbn/core/server';
import { EntityDefinition } from '@kbn/entities-schema';
-
-import {
- generateHistoryTransformId,
- generateHistoryBackfillTransformId,
- generateLatestTransformId,
-} from './helpers/generate_component_id';
import { retryTransientEsErrors } from './helpers/retry';
-import { isBackfillEnabled } from './helpers/is_backfill_enabled';
+import { generateLatestTransformId } from './helpers/generate_component_id';
export async function deleteTransforms(
esClient: ElasticsearchClient,
@@ -22,37 +16,42 @@ export async function deleteTransforms(
logger: Logger
) {
try {
- const historyTransformId = generateHistoryTransformId(definition);
- const latestTransformId = generateLatestTransformId(definition);
- await retryTransientEsErrors(
- () =>
- esClient.transform.deleteTransform(
- { transform_id: historyTransformId, force: true },
- { ignore: [404] }
- ),
- { logger }
+ await Promise.all(
+ (definition.installedComponents ?? [])
+ .filter(({ type }) => type === 'transform')
+ .map(({ id }) =>
+ retryTransientEsErrors(
+ () =>
+ esClient.transform.deleteTransform(
+ { transform_id: id, force: true },
+ { ignore: [404] }
+ ),
+ { logger }
+ )
+ )
);
- if (isBackfillEnabled(definition)) {
- const historyBackfillTransformId = generateHistoryBackfillTransformId(definition);
- await retryTransientEsErrors(
- () =>
- esClient.transform.deleteTransform(
- { transform_id: historyBackfillTransformId, force: true },
- { ignore: [404] }
- ),
- { logger }
- );
- }
+ } catch (e) {
+ logger.error(`Cannot delete transforms for definition [${definition.id}]: ${e}`);
+ throw e;
+ }
+}
+
+export async function deleteLatestTransform(
+ esClient: ElasticsearchClient,
+ definition: EntityDefinition,
+ logger: Logger
+) {
+ try {
await retryTransientEsErrors(
() =>
esClient.transform.deleteTransform(
- { transform_id: latestTransformId, force: true },
+ { transform_id: generateLatestTransformId(definition), force: true },
{ ignore: [404] }
),
{ logger }
);
} catch (e) {
- logger.error(`Cannot delete history transform [${definition.id}]: ${e}`);
+ logger.error(`Cannot delete latest transform for definition [${definition.id}]: ${e}`);
throw e;
}
}
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/find_entity_definition.ts b/x-pack/plugins/entity_manager/server/lib/entities/find_entity_definition.ts
index d1d84f27414af..cfbb5a5ef5556 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/find_entity_definition.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/find_entity_definition.ts
@@ -10,18 +10,8 @@ import { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/serve
import { EntityDefinition } from '@kbn/entities-schema';
import { NodesIngestTotal } from '@elastic/elasticsearch/lib/api/types';
import { SO_ENTITY_DEFINITION_TYPE } from '../../saved_objects';
-import {
- generateHistoryTransformId,
- generateHistoryBackfillTransformId,
- generateHistoryIngestPipelineId,
- generateHistoryIndexTemplateId,
- generateLatestTransformId,
- generateLatestIngestPipelineId,
- generateLatestIndexTemplateId,
-} from './helpers/generate_component_id';
import { BUILT_IN_ID_PREFIX } from './built_in';
import { EntityDefinitionState, EntityDefinitionWithState } from './types';
-import { isBackfillEnabled } from './helpers/is_backfill_enabled';
export async function findEntityDefinitions({
soClient,
@@ -120,11 +110,9 @@ async function getTransformState({
definition: EntityDefinition;
esClient: ElasticsearchClient;
}) {
- const transformIds = [
- generateHistoryTransformId(definition),
- generateLatestTransformId(definition),
- ...(isBackfillEnabled(definition) ? [generateHistoryBackfillTransformId(definition)] : []),
- ];
+ const transformIds = (definition.installedComponents ?? [])
+ .filter(({ type }) => type === 'transform')
+ .map(({ id }) => id);
const transformStats = await Promise.all(
transformIds.map((id) => esClient.transform.getTransformStats({ transform_id: id }))
@@ -152,10 +140,10 @@ async function getIngestPipelineState({
definition: EntityDefinition;
esClient: ElasticsearchClient;
}) {
- const ingestPipelineIds = [
- generateHistoryIngestPipelineId(definition),
- generateLatestIngestPipelineId(definition),
- ];
+ const ingestPipelineIds = (definition.installedComponents ?? [])
+ .filter(({ type }) => type === 'ingest_pipeline')
+ .map(({ id }) => id);
+
const [ingestPipelines, ingestPipelinesStats] = await Promise.all([
esClient.ingest.getPipeline({ id: ingestPipelineIds.join(',') }, { ignore: [404] }),
esClient.nodes.stats({
@@ -193,10 +181,9 @@ async function getIndexTemplatesState({
definition: EntityDefinition;
esClient: ElasticsearchClient;
}) {
- const indexTemplatesIds = [
- generateLatestIndexTemplateId(definition),
- generateHistoryIndexTemplateId(definition),
- ];
+ const indexTemplatesIds = (definition.installedComponents ?? [])
+ .filter(({ type }) => type === 'template')
+ .map(({ id }) => id);
const templates = await Promise.all(
indexTemplatesIds.map((id) =>
esClient.indices
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/helpers/calculate_offset.ts b/x-pack/plugins/entity_manager/server/lib/entities/helpers/calculate_offset.ts
deleted file mode 100644
index 3eba710561abf..0000000000000
--- a/x-pack/plugins/entity_manager/server/lib/entities/helpers/calculate_offset.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { EntityDefinition } from '@kbn/entities-schema';
-import moment from 'moment';
-import {
- ENTITY_DEFAULT_HISTORY_FREQUENCY,
- ENTITY_DEFAULT_HISTORY_SYNC_DELAY,
-} from '../../../../common/constants_entities';
-
-const durationToSeconds = (dateMath: string) => {
- const parts = dateMath.match(/(\d+)([m|s|h|d])/);
- if (!parts) {
- throw new Error(`Invalid date math supplied: ${dateMath}`);
- }
- const value = parseInt(parts[1], 10);
- const unit = parts[2] as 'm' | 's' | 'h' | 'd';
- return moment.duration(value, unit).asSeconds();
-};
-
-export function calculateOffset(definition: EntityDefinition) {
- const syncDelay = durationToSeconds(
- definition.history.settings.syncDelay || ENTITY_DEFAULT_HISTORY_SYNC_DELAY
- );
- const frequency =
- durationToSeconds(definition.history.settings.frequency || ENTITY_DEFAULT_HISTORY_FREQUENCY) *
- 2;
-
- return syncDelay + frequency;
-}
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/builtin_entity_definition.ts b/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/builtin_entity_definition.ts
index 5092e2caa5d78..b1e506150fb60 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/builtin_entity_definition.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/builtin_entity_definition.ts
@@ -13,9 +13,8 @@ export const builtInEntityDefinition = entityDefinitionSchema.parse({
type: 'service',
indexPatterns: ['kbn-data-forge-fake_stack.*'],
managed: true,
- history: {
+ latest: {
timestampField: '@timestamp',
- interval: '1m',
},
identityFields: ['log.logger', { field: 'event.category', optional: true }],
displayNameTemplate: '{{log.logger}}{{#event.category}}:{{.}}{{/event.category}}',
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/entity_definition.ts b/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/entity_definition.ts
index 940e209260c54..00ab9ac7759af 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/entity_definition.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/entity_definition.ts
@@ -12,13 +12,12 @@ export const rawEntityDefinition = {
name: 'Services for Admin Console',
type: 'service',
indexPatterns: ['kbn-data-forge-fake_stack.*'],
- history: {
+ latest: {
timestampField: '@timestamp',
- interval: '1m',
+ lookbackPeriod: '10m',
settings: {
- lookbackPeriod: '10m',
- frequency: '2m',
- syncDelay: '2m',
+ frequency: '30s',
+ syncDelay: '10s',
},
},
identityFields: ['log.logger', { field: 'event.category', optional: true }],
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/entity_definition_with_backfill.ts b/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/entity_definition_with_backfill.ts
deleted file mode 100644
index 66a79825fbfb0..0000000000000
--- a/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/entity_definition_with_backfill.ts
+++ /dev/null
@@ -1,51 +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 { entityDefinitionSchema } from '@kbn/entities-schema';
-export const entityDefinitionWithBackfill = entityDefinitionSchema.parse({
- id: 'admin-console-services-backfill',
- version: '999.999.999',
- name: 'Services for Admin Console',
- type: 'service',
- indexPatterns: ['kbn-data-forge-fake_stack.*'],
- history: {
- timestampField: '@timestamp',
- interval: '1m',
- settings: {
- backfillSyncDelay: '15m',
- backfillLookbackPeriod: '72h',
- backfillFrequency: '5m',
- },
- },
- identityFields: ['log.logger', { field: 'event.category', optional: true }],
- displayNameTemplate: '{{log.logger}}{{#event.category}}:{{.}}{{/event.category}}',
- metadata: ['tags', 'host.name', 'host.os.name', { source: '_index', destination: 'sourceIndex' }],
- metrics: [
- {
- name: 'logRate',
- equation: 'A',
- metrics: [
- {
- name: 'A',
- aggregation: 'doc_count',
- filter: 'log.level: *',
- },
- ],
- },
- {
- name: 'errorRate',
- equation: 'A',
- metrics: [
- {
- name: 'A',
- aggregation: 'doc_count',
- filter: 'log.level: "ERROR"',
- },
- ],
- },
- ],
-});
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/index.ts b/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/index.ts
index c24dcee1f8cf7..e841b1c8e23dd 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/index.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/helpers/fixtures/index.ts
@@ -6,5 +6,4 @@
*/
export { entityDefinition } from './entity_definition';
-export { entityDefinitionWithBackfill } from './entity_definition_with_backfill';
export { builtInEntityDefinition } from './builtin_entity_definition';
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/helpers/is_backfill_enabled.ts b/x-pack/plugins/entity_manager/server/lib/entities/helpers/is_backfill_enabled.ts
deleted file mode 100644
index 4c34f5d3c0256..0000000000000
--- a/x-pack/plugins/entity_manager/server/lib/entities/helpers/is_backfill_enabled.ts
+++ /dev/null
@@ -1,12 +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 { EntityDefinition } from '@kbn/entities-schema';
-
-export function isBackfillEnabled(definition: EntityDefinition) {
- return definition.history.settings.backfillSyncDelay != null;
-}
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap b/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap
deleted file mode 100644
index c2e4605e5f909..0000000000000
--- a/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap
+++ /dev/null
@@ -1,327 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`generateHistoryProcessors(definition) should generate a valid pipeline for builtin definition 1`] = `
-Array [
- Object {
- "set": Object {
- "field": "event.ingested",
- "value": "{{{_ingest.timestamp}}}",
- },
- },
- Object {
- "set": Object {
- "field": "entity.type",
- "value": "service",
- },
- },
- Object {
- "set": Object {
- "field": "entity.definitionId",
- "value": "builtin_mock_entity_definition",
- },
- },
- Object {
- "set": Object {
- "field": "entity.definitionVersion",
- "value": "1.0.0",
- },
- },
- Object {
- "set": Object {
- "field": "entity.schemaVersion",
- "value": "v1",
- },
- },
- Object {
- "set": Object {
- "field": "entity.identityFields",
- "value": Array [
- "log.logger",
- "event.category",
- ],
- },
- },
- Object {
- "script": Object {
- "description": "Generated the entity.id field",
- "source": "// This function will recursively collect all the values of a HashMap of HashMaps
-Collection collectValues(HashMap subject) {
- Collection values = new ArrayList();
- // Iterate through the values
- for(Object value: subject.values()) {
- // If the value is a HashMap, recurse
- if (value instanceof HashMap) {
- values.addAll(collectValues((HashMap) value));
- } else {
- values.add(String.valueOf(value));
- }
- }
- return values;
-}
-// Create the string builder
-StringBuilder entityId = new StringBuilder();
-if (ctx[\\"entity\\"][\\"identity\\"] != null) {
- // Get the values as a collection
- Collection values = collectValues(ctx[\\"entity\\"][\\"identity\\"]);
- // Convert to a list and sort
- List sortedValues = new ArrayList(values);
- Collections.sort(sortedValues);
- // Create comma delimited string
- for(String instanceValue: sortedValues) {
- entityId.append(instanceValue);
- entityId.append(\\":\\");
- }
- // Assign the entity.id
- ctx[\\"entity\\"][\\"id\\"] = entityId.length() > 0 ? entityId.substring(0, entityId.length() - 1) : \\"unknown\\";
-}",
- },
- },
- Object {
- "fingerprint": Object {
- "fields": Array [
- "entity.id",
- ],
- "method": "MurmurHash3",
- "target_field": "entity.id",
- },
- },
- Object {
- "script": Object {
- "source": "if (ctx.entity?.metadata?.tags != null) {
- ctx.tags = ctx.entity.metadata.tags.keySet();
-}
-if (ctx.entity?.metadata?.host?.name != null) {
- if (ctx.host == null) {
- ctx.host = new HashMap();
- }
- ctx.host.name = ctx.entity.metadata.host.name.keySet();
-}
-if (ctx.entity?.metadata?.host?.os?.name != null) {
- if (ctx.host == null) {
- ctx.host = new HashMap();
- }
- if (ctx.host.os == null) {
- ctx.host.os = new HashMap();
- }
- ctx.host.os.name = ctx.entity.metadata.host.os.name.keySet();
-}
-if (ctx.entity?.metadata?.sourceIndex != null) {
- ctx.sourceIndex = ctx.entity.metadata.sourceIndex.keySet();
-}",
- },
- },
- Object {
- "remove": Object {
- "field": "entity.metadata",
- "ignore_missing": true,
- },
- },
- Object {
- "set": Object {
- "field": "log.logger",
- "if": "ctx.entity?.identity?.log?.logger != null",
- "value": "{{entity.identity.log.logger}}",
- },
- },
- Object {
- "set": Object {
- "field": "event.category",
- "if": "ctx.entity?.identity?.event?.category != null",
- "value": "{{entity.identity.event.category}}",
- },
- },
- Object {
- "remove": Object {
- "field": "entity.identity",
- "ignore_missing": true,
- },
- },
- Object {
- "date_index_name": Object {
- "date_formats": Array [
- "UNIX_MS",
- "ISO8601",
- "yyyy-MM-dd'T'HH:mm:ss.SSSXX",
- ],
- "date_rounding": "M",
- "field": "@timestamp",
- "index_name_prefix": ".entities.v1.history.builtin_mock_entity_definition.",
- },
- },
-]
-`;
-
-exports[`generateHistoryProcessors(definition) should generate a valid pipeline for custom definition 1`] = `
-Array [
- Object {
- "set": Object {
- "field": "event.ingested",
- "value": "{{{_ingest.timestamp}}}",
- },
- },
- Object {
- "set": Object {
- "field": "entity.type",
- "value": "service",
- },
- },
- Object {
- "set": Object {
- "field": "entity.definitionId",
- "value": "admin-console-services",
- },
- },
- Object {
- "set": Object {
- "field": "entity.definitionVersion",
- "value": "1.0.0",
- },
- },
- Object {
- "set": Object {
- "field": "entity.schemaVersion",
- "value": "v1",
- },
- },
- Object {
- "set": Object {
- "field": "entity.identityFields",
- "value": Array [
- "log.logger",
- "event.category",
- ],
- },
- },
- Object {
- "script": Object {
- "description": "Generated the entity.id field",
- "source": "// This function will recursively collect all the values of a HashMap of HashMaps
-Collection collectValues(HashMap subject) {
- Collection values = new ArrayList();
- // Iterate through the values
- for(Object value: subject.values()) {
- // If the value is a HashMap, recurse
- if (value instanceof HashMap) {
- values.addAll(collectValues((HashMap) value));
- } else {
- values.add(String.valueOf(value));
- }
- }
- return values;
-}
-// Create the string builder
-StringBuilder entityId = new StringBuilder();
-if (ctx[\\"entity\\"][\\"identity\\"] != null) {
- // Get the values as a collection
- Collection values = collectValues(ctx[\\"entity\\"][\\"identity\\"]);
- // Convert to a list and sort
- List sortedValues = new ArrayList(values);
- Collections.sort(sortedValues);
- // Create comma delimited string
- for(String instanceValue: sortedValues) {
- entityId.append(instanceValue);
- entityId.append(\\":\\");
- }
- // Assign the entity.id
- ctx[\\"entity\\"][\\"id\\"] = entityId.length() > 0 ? entityId.substring(0, entityId.length() - 1) : \\"unknown\\";
-}",
- },
- },
- Object {
- "fingerprint": Object {
- "fields": Array [
- "entity.id",
- ],
- "method": "MurmurHash3",
- "target_field": "entity.id",
- },
- },
- Object {
- "script": Object {
- "source": "if (ctx.entity?.metadata?.tags != null) {
- ctx.tags = ctx.entity.metadata.tags.keySet();
-}
-if (ctx.entity?.metadata?.host?.name != null) {
- if (ctx.host == null) {
- ctx.host = new HashMap();
- }
- ctx.host.name = ctx.entity.metadata.host.name.keySet();
-}
-if (ctx.entity?.metadata?.host?.os?.name != null) {
- if (ctx.host == null) {
- ctx.host = new HashMap();
- }
- if (ctx.host.os == null) {
- ctx.host.os = new HashMap();
- }
- ctx.host.os.name = ctx.entity.metadata.host.os.name.keySet();
-}
-if (ctx.entity?.metadata?.sourceIndex != null) {
- ctx.sourceIndex = ctx.entity.metadata.sourceIndex.keySet();
-}",
- },
- },
- Object {
- "remove": Object {
- "field": "entity.metadata",
- "ignore_missing": true,
- },
- },
- Object {
- "set": Object {
- "field": "log.logger",
- "if": "ctx.entity?.identity?.log?.logger != null",
- "value": "{{entity.identity.log.logger}}",
- },
- },
- Object {
- "set": Object {
- "field": "event.category",
- "if": "ctx.entity?.identity?.event?.category != null",
- "value": "{{entity.identity.event.category}}",
- },
- },
- Object {
- "remove": Object {
- "field": "entity.identity",
- "ignore_missing": true,
- },
- },
- Object {
- "date_index_name": Object {
- "date_formats": Array [
- "UNIX_MS",
- "ISO8601",
- "yyyy-MM-dd'T'HH:mm:ss.SSSXX",
- ],
- "date_rounding": "M",
- "field": "@timestamp",
- "index_name_prefix": ".entities.v1.history.admin-console-services.",
- },
- },
- Object {
- "pipeline": Object {
- "ignore_missing_pipeline": true,
- "name": "admin-console-services@platform",
- },
- },
- Object {
- "pipeline": Object {
- "ignore_missing_pipeline": true,
- "name": "admin-console-services-history@platform",
- },
- },
- Object {
- "pipeline": Object {
- "ignore_missing_pipeline": true,
- "name": "admin-console-services@custom",
- },
- },
- Object {
- "pipeline": Object {
- "ignore_missing_pipeline": true,
- "name": "admin-console-services-history@custom",
- },
- },
-]
-`;
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap b/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap
index f277b3ac84ab8..218deda422fe2 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap
+++ b/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap
@@ -43,16 +43,60 @@ Array [
},
Object {
"script": Object {
- "source": "if (ctx.entity?.metadata?.tags.data != null) {
+ "description": "Generated the entity.id field",
+ "source": "// This function will recursively collect all the values of a HashMap of HashMaps
+Collection collectValues(HashMap subject) {
+ Collection values = new ArrayList();
+ // Iterate through the values
+ for(Object value: subject.values()) {
+ // If the value is a HashMap, recurse
+ if (value instanceof HashMap) {
+ values.addAll(collectValues((HashMap) value));
+ } else {
+ values.add(String.valueOf(value));
+ }
+ }
+ return values;
+}
+// Create the string builder
+StringBuilder entityId = new StringBuilder();
+if (ctx[\\"entity\\"][\\"identity\\"] != null) {
+ // Get the values as a collection
+ Collection values = collectValues(ctx[\\"entity\\"][\\"identity\\"]);
+ // Convert to a list and sort
+ List sortedValues = new ArrayList(values);
+ Collections.sort(sortedValues);
+ // Create comma delimited string
+ for(String instanceValue: sortedValues) {
+ entityId.append(instanceValue);
+ entityId.append(\\":\\");
+ }
+ // Assign the entity.id
+ ctx[\\"entity\\"][\\"id\\"] = entityId.length() > 0 ? entityId.substring(0, entityId.length() - 1) : \\"unknown\\";
+}",
+ },
+ },
+ Object {
+ "fingerprint": Object {
+ "fields": Array [
+ "entity.id",
+ ],
+ "method": "MurmurHash3",
+ "target_field": "entity.id",
+ },
+ },
+ Object {
+ "script": Object {
+ "source": "if (ctx.entity?.metadata?.tags?.data != null) {
ctx.tags = ctx.entity.metadata.tags.data.keySet();
}
-if (ctx.entity?.metadata?.host?.name.data != null) {
+if (ctx.entity?.metadata?.host?.name?.data != null) {
if (ctx.host == null) {
ctx.host = new HashMap();
}
ctx.host.name = ctx.entity.metadata.host.name.data.keySet();
}
-if (ctx.entity?.metadata?.host?.os?.name.data != null) {
+if (ctx.entity?.metadata?.host?.os?.name?.data != null) {
if (ctx.host == null) {
ctx.host = new HashMap();
}
@@ -61,7 +105,7 @@ if (ctx.entity?.metadata?.host?.os?.name.data != null) {
}
ctx.host.os.name = ctx.entity.metadata.host.os.name.data.keySet();
}
-if (ctx.entity?.metadata?.sourceIndex.data != null) {
+if (ctx.entity?.metadata?.sourceIndex?.data != null) {
ctx.sourceIndex = ctx.entity.metadata.sourceIndex.data.keySet();
}",
},
@@ -72,28 +116,18 @@ if (ctx.entity?.metadata?.sourceIndex.data != null) {
"ignore_missing": true,
},
},
- Object {
- "dot_expander": Object {
- "field": "log.logger",
- "path": "entity.identity.log.logger.top_metric",
- },
- },
Object {
"set": Object {
"field": "log.logger",
- "value": "{{entity.identity.log.logger.top_metric.log.logger}}",
- },
- },
- Object {
- "dot_expander": Object {
- "field": "event.category",
- "path": "entity.identity.event.category.top_metric",
+ "if": "ctx.entity?.identity?.log?.logger != null",
+ "value": "{{entity.identity.log.logger}}",
},
},
Object {
"set": Object {
"field": "event.category",
- "value": "{{entity.identity.event.category.top_metric.event.category}}",
+ "if": "ctx.entity?.identity?.event?.category != null",
+ "value": "{{entity.identity.event.category}}",
},
},
Object {
@@ -160,16 +194,60 @@ Array [
},
Object {
"script": Object {
- "source": "if (ctx.entity?.metadata?.tags.data != null) {
+ "description": "Generated the entity.id field",
+ "source": "// This function will recursively collect all the values of a HashMap of HashMaps
+Collection collectValues(HashMap subject) {
+ Collection values = new ArrayList();
+ // Iterate through the values
+ for(Object value: subject.values()) {
+ // If the value is a HashMap, recurse
+ if (value instanceof HashMap) {
+ values.addAll(collectValues((HashMap) value));
+ } else {
+ values.add(String.valueOf(value));
+ }
+ }
+ return values;
+}
+// Create the string builder
+StringBuilder entityId = new StringBuilder();
+if (ctx[\\"entity\\"][\\"identity\\"] != null) {
+ // Get the values as a collection
+ Collection values = collectValues(ctx[\\"entity\\"][\\"identity\\"]);
+ // Convert to a list and sort
+ List sortedValues = new ArrayList(values);
+ Collections.sort(sortedValues);
+ // Create comma delimited string
+ for(String instanceValue: sortedValues) {
+ entityId.append(instanceValue);
+ entityId.append(\\":\\");
+ }
+ // Assign the entity.id
+ ctx[\\"entity\\"][\\"id\\"] = entityId.length() > 0 ? entityId.substring(0, entityId.length() - 1) : \\"unknown\\";
+}",
+ },
+ },
+ Object {
+ "fingerprint": Object {
+ "fields": Array [
+ "entity.id",
+ ],
+ "method": "MurmurHash3",
+ "target_field": "entity.id",
+ },
+ },
+ Object {
+ "script": Object {
+ "source": "if (ctx.entity?.metadata?.tags?.data != null) {
ctx.tags = ctx.entity.metadata.tags.data.keySet();
}
-if (ctx.entity?.metadata?.host?.name.data != null) {
+if (ctx.entity?.metadata?.host?.name?.data != null) {
if (ctx.host == null) {
ctx.host = new HashMap();
}
ctx.host.name = ctx.entity.metadata.host.name.data.keySet();
}
-if (ctx.entity?.metadata?.host?.os?.name.data != null) {
+if (ctx.entity?.metadata?.host?.os?.name?.data != null) {
if (ctx.host == null) {
ctx.host = new HashMap();
}
@@ -178,7 +256,7 @@ if (ctx.entity?.metadata?.host?.os?.name.data != null) {
}
ctx.host.os.name = ctx.entity.metadata.host.os.name.data.keySet();
}
-if (ctx.entity?.metadata?.sourceIndex.data != null) {
+if (ctx.entity?.metadata?.sourceIndex?.data != null) {
ctx.sourceIndex = ctx.entity.metadata.sourceIndex.data.keySet();
}",
},
@@ -189,28 +267,18 @@ if (ctx.entity?.metadata?.sourceIndex.data != null) {
"ignore_missing": true,
},
},
- Object {
- "dot_expander": Object {
- "field": "log.logger",
- "path": "entity.identity.log.logger.top_metric",
- },
- },
Object {
"set": Object {
"field": "log.logger",
- "value": "{{entity.identity.log.logger.top_metric.log.logger}}",
- },
- },
- Object {
- "dot_expander": Object {
- "field": "event.category",
- "path": "entity.identity.event.category.top_metric",
+ "if": "ctx.entity?.identity?.log?.logger != null",
+ "value": "{{entity.identity.log.logger}}",
},
},
Object {
"set": Object {
"field": "event.category",
- "value": "{{entity.identity.event.category.top_metric.event.category}}",
+ "if": "ctx.entity?.identity?.event?.category != null",
+ "value": "{{entity.identity.event.category}}",
},
},
Object {
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.test.ts b/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.test.ts
deleted file mode 100644
index 717241b89143d..0000000000000
--- a/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.test.ts
+++ /dev/null
@@ -1,21 +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 { entityDefinition, builtInEntityDefinition } from '../helpers/fixtures';
-import { generateHistoryProcessors } from './generate_history_processors';
-
-describe('generateHistoryProcessors(definition)', () => {
- it('should generate a valid pipeline for custom definition', () => {
- const processors = generateHistoryProcessors(entityDefinition);
- expect(processors).toMatchSnapshot();
- });
-
- it('should generate a valid pipeline for builtin definition', () => {
- const processors = generateHistoryProcessors(builtInEntityDefinition);
- expect(processors).toMatchSnapshot();
- });
-});
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts b/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts
deleted file mode 100644
index d51ab0be75db1..0000000000000
--- a/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts
+++ /dev/null
@@ -1,222 +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 { EntityDefinition, ENTITY_SCHEMA_VERSION_V1, MetadataField } from '@kbn/entities-schema';
-import {
- initializePathScript,
- cleanScript,
-} from '../helpers/ingest_pipeline_script_processor_helpers';
-import { generateHistoryIndexName } from '../helpers/generate_component_id';
-import { isBuiltinDefinition } from '../helpers/is_builtin_definition';
-
-function getMetadataSourceField({ aggregation, destination, source }: MetadataField) {
- if (aggregation.type === 'terms') {
- return `ctx.entity.metadata.${destination}.keySet()`;
- } else if (aggregation.type === 'top_value') {
- return `ctx.entity.metadata.${destination}.top_value["${source}"]`;
- }
-}
-
-function mapDestinationToPainless(metadata: MetadataField) {
- const field = metadata.destination;
- return `
- ${initializePathScript(field)}
- ctx.${field} = ${getMetadataSourceField(metadata)};
- `;
-}
-
-function createMetadataPainlessScript(definition: EntityDefinition) {
- if (!definition.metadata) {
- return '';
- }
-
- return definition.metadata.reduce((acc, metadata) => {
- const { destination, source } = metadata;
- const optionalFieldPath = destination.replaceAll('.', '?.');
-
- if (metadata.aggregation.type === 'terms') {
- const next = `
- if (ctx.entity?.metadata?.${optionalFieldPath} != null) {
- ${mapDestinationToPainless(metadata)}
- }
- `;
- return `${acc}\n${next}`;
- } else if (metadata.aggregation.type === 'top_value') {
- const next = `
- if (ctx.entity?.metadata?.${optionalFieldPath}?.top_value["${source}"] != null) {
- ${mapDestinationToPainless(metadata)}
- }
- `;
- return `${acc}\n${next}`;
- }
-
- return acc;
- }, '');
-}
-
-function liftIdentityFieldsToDocumentRoot(definition: EntityDefinition) {
- return definition.identityFields.map((key) => ({
- set: {
- if: `ctx.entity?.identity?.${key.field.replaceAll('.', '?.')} != null`,
- field: key.field,
- value: `{{entity.identity.${key.field}}}`,
- },
- }));
-}
-
-function getCustomIngestPipelines(definition: EntityDefinition) {
- if (isBuiltinDefinition(definition)) {
- return [];
- }
-
- return [
- {
- pipeline: {
- ignore_missing_pipeline: true,
- name: `${definition.id}@platform`,
- },
- },
- {
- pipeline: {
- ignore_missing_pipeline: true,
- name: `${definition.id}-history@platform`,
- },
- },
- {
- pipeline: {
- ignore_missing_pipeline: true,
- name: `${definition.id}@custom`,
- },
- },
- {
- pipeline: {
- ignore_missing_pipeline: true,
- name: `${definition.id}-history@custom`,
- },
- },
- ];
-}
-
-export function generateHistoryProcessors(definition: EntityDefinition) {
- return [
- {
- set: {
- field: 'event.ingested',
- value: '{{{_ingest.timestamp}}}',
- },
- },
- {
- set: {
- field: 'entity.type',
- value: definition.type,
- },
- },
- {
- set: {
- field: 'entity.definitionId',
- value: definition.id,
- },
- },
- {
- set: {
- field: 'entity.definitionVersion',
- value: definition.version,
- },
- },
- {
- set: {
- field: 'entity.schemaVersion',
- value: ENTITY_SCHEMA_VERSION_V1,
- },
- },
- {
- set: {
- field: 'entity.identityFields',
- value: definition.identityFields.map((identityField) => identityField.field),
- },
- },
- {
- script: {
- description: 'Generated the entity.id field',
- source: cleanScript(`
- // This function will recursively collect all the values of a HashMap of HashMaps
- Collection collectValues(HashMap subject) {
- Collection values = new ArrayList();
- // Iterate through the values
- for(Object value: subject.values()) {
- // If the value is a HashMap, recurse
- if (value instanceof HashMap) {
- values.addAll(collectValues((HashMap) value));
- } else {
- values.add(String.valueOf(value));
- }
- }
- return values;
- }
-
- // Create the string builder
- StringBuilder entityId = new StringBuilder();
-
- if (ctx["entity"]["identity"] != null) {
- // Get the values as a collection
- Collection values = collectValues(ctx["entity"]["identity"]);
-
- // Convert to a list and sort
- List sortedValues = new ArrayList(values);
- Collections.sort(sortedValues);
-
- // Create comma delimited string
- for(String instanceValue: sortedValues) {
- entityId.append(instanceValue);
- entityId.append(":");
- }
-
- // Assign the entity.id
- ctx["entity"]["id"] = entityId.length() > 0 ? entityId.substring(0, entityId.length() - 1) : "unknown";
- }
- `),
- },
- },
- {
- fingerprint: {
- fields: ['entity.id'],
- target_field: 'entity.id',
- method: 'MurmurHash3',
- },
- },
- ...(definition.staticFields != null
- ? Object.keys(definition.staticFields).map((field) => ({
- set: { field, value: definition.staticFields![field] },
- }))
- : []),
- ...(definition.metadata != null
- ? [{ script: { source: cleanScript(createMetadataPainlessScript(definition)) } }]
- : []),
- {
- remove: {
- field: 'entity.metadata',
- ignore_missing: true,
- },
- },
- ...liftIdentityFieldsToDocumentRoot(definition),
- {
- remove: {
- field: 'entity.identity',
- ignore_missing: true,
- },
- },
- {
- date_index_name: {
- field: '@timestamp',
- index_name_prefix: `${generateHistoryIndexName(definition)}.`,
- date_rounding: 'M',
- date_formats: ['UNIX_MS', 'ISO8601', "yyyy-MM-dd'T'HH:mm:ss.SSSXX"],
- },
- },
- ...getCustomIngestPipelines(definition),
- ];
-}
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts b/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts
index 16823221fffb3..0e3812de2e320 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts
@@ -17,7 +17,7 @@ function getMetadataSourceField({ aggregation, destination, source }: MetadataFi
if (aggregation.type === 'terms') {
return `ctx.entity.metadata.${destination}.data.keySet()`;
} else if (aggregation.type === 'top_value') {
- return `ctx.entity.metadata.${destination}.top_value["${destination}"]`;
+ return `ctx.entity.metadata.${destination}.top_value["${source}"]`;
}
}
@@ -35,19 +35,19 @@ function createMetadataPainlessScript(definition: EntityDefinition) {
}
return definition.metadata.reduce((acc, metadata) => {
- const destination = metadata.destination;
+ const { destination, source } = metadata;
const optionalFieldPath = destination.replaceAll('.', '?.');
if (metadata.aggregation.type === 'terms') {
const next = `
- if (ctx.entity?.metadata?.${optionalFieldPath}.data != null) {
+ if (ctx.entity?.metadata?.${optionalFieldPath}?.data != null) {
${mapDestinationToPainless(metadata)}
}
`;
return `${acc}\n${next}`;
} else if (metadata.aggregation.type === 'top_value') {
const next = `
- if (ctx.entity?.metadata?.${optionalFieldPath}?.top_value["${destination}"] != null) {
+ if (ctx.entity?.metadata?.${optionalFieldPath}?.top_value["${source}"] != null) {
${mapDestinationToPainless(metadata)}
}
`;
@@ -59,30 +59,13 @@ function createMetadataPainlessScript(definition: EntityDefinition) {
}
function liftIdentityFieldsToDocumentRoot(definition: EntityDefinition) {
- return definition.identityFields
- .map((identityField) => {
- const setProcessor = {
- set: {
- field: identityField.field,
- value: `{{entity.identity.${identityField.field}.top_metric.${identityField.field}}}`,
- },
- };
-
- if (!identityField.field.includes('.')) {
- return [setProcessor];
- }
-
- return [
- {
- dot_expander: {
- field: identityField.field,
- path: `entity.identity.${identityField.field}.top_metric`,
- },
- },
- setProcessor,
- ];
- })
- .flat();
+ return definition.identityFields.map((key) => ({
+ set: {
+ if: `ctx.entity?.identity?.${key.field.replaceAll('.', '?.')} != null`,
+ field: key.field,
+ value: `{{entity.identity.${key.field}}}`,
+ },
+ }));
}
function getCustomIngestPipelines(definition: EntityDefinition) {
@@ -156,6 +139,55 @@ export function generateLatestProcessors(definition: EntityDefinition) {
value: definition.identityFields.map((identityField) => identityField.field),
},
},
+ {
+ script: {
+ description: 'Generated the entity.id field',
+ source: cleanScript(`
+ // This function will recursively collect all the values of a HashMap of HashMaps
+ Collection collectValues(HashMap subject) {
+ Collection values = new ArrayList();
+ // Iterate through the values
+ for(Object value: subject.values()) {
+ // If the value is a HashMap, recurse
+ if (value instanceof HashMap) {
+ values.addAll(collectValues((HashMap) value));
+ } else {
+ values.add(String.valueOf(value));
+ }
+ }
+ return values;
+ }
+
+ // Create the string builder
+ StringBuilder entityId = new StringBuilder();
+
+ if (ctx["entity"]["identity"] != null) {
+ // Get the values as a collection
+ Collection values = collectValues(ctx["entity"]["identity"]);
+
+ // Convert to a list and sort
+ List sortedValues = new ArrayList(values);
+ Collections.sort(sortedValues);
+
+ // Create comma delimited string
+ for(String instanceValue: sortedValues) {
+ entityId.append(instanceValue);
+ entityId.append(":");
+ }
+
+ // Assign the entity.id
+ ctx["entity"]["id"] = entityId.length() > 0 ? entityId.substring(0, entityId.length() - 1) : "unknown";
+ }
+ `),
+ },
+ },
+ {
+ fingerprint: {
+ fields: ['entity.id'],
+ target_field: 'entity.id',
+ method: 'MurmurHash3',
+ },
+ },
...(definition.staticFields != null
? Object.keys(definition.staticFields).map((field) => ({
set: { field, value: definition.staticFields![field] },
@@ -177,8 +209,8 @@ export function generateLatestProcessors(definition: EntityDefinition) {
ignore_missing: true,
},
},
+ // This must happen AFTER we lift the identity fields into the root of the document
{
- // This must happen AFTER we lift the identity fields into the root of the document
set: {
field: 'entity.displayName',
value: definition.displayNameTemplate,
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/install_entity_definition.test.ts b/x-pack/plugins/entity_manager/server/lib/entities/install_entity_definition.test.ts
index 5cee21dc43a07..e07670c58fd9b 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/install_entity_definition.test.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/install_entity_definition.test.ts
@@ -19,19 +19,23 @@ import {
} from './install_entity_definition';
import { SO_ENTITY_DEFINITION_TYPE } from '../../saved_objects';
import {
- generateHistoryIndexTemplateId,
- generateHistoryIngestPipelineId,
- generateHistoryTransformId,
generateLatestIndexTemplateId,
generateLatestIngestPipelineId,
generateLatestTransformId,
} from './helpers/generate_component_id';
-import { generateHistoryTransform } from './transform/generate_history_transform';
import { generateLatestTransform } from './transform/generate_latest_transform';
import { entityDefinition as mockEntityDefinition } from './helpers/fixtures/entity_definition';
import { EntityDefinitionIdInvalid } from './errors/entity_definition_id_invalid';
import { EntityIdConflict } from './errors/entity_id_conflict_error';
+const getExpectedInstalledComponents = (definition: EntityDefinition) => {
+ return [
+ { type: 'template', id: generateLatestIndexTemplateId(definition) },
+ { type: 'ingest_pipeline', id: generateLatestIngestPipelineId(definition) },
+ { type: 'transform', id: generateLatestTransformId(definition) },
+ ];
+};
+
const assertHasCreatedDefinition = (
definition: EntityDefinition,
soClient: SavedObjectsClientContract,
@@ -44,6 +48,7 @@ const assertHasCreatedDefinition = (
...definition,
installStatus: 'installing',
installStartedAt: expect.any(String),
+ installedComponents: [],
},
{
id: definition.id,
@@ -54,29 +59,17 @@ const assertHasCreatedDefinition = (
expect(soClient.update).toBeCalledTimes(1);
expect(soClient.update).toBeCalledWith(SO_ENTITY_DEFINITION_TYPE, definition.id, {
installStatus: 'installed',
+ installedComponents: getExpectedInstalledComponents(definition),
});
- expect(esClient.indices.putIndexTemplate).toBeCalledTimes(2);
- expect(esClient.indices.putIndexTemplate).toBeCalledWith(
- expect.objectContaining({
- name: `entities_v1_history_${definition.id}_index_template`,
- })
- );
+ expect(esClient.indices.putIndexTemplate).toBeCalledTimes(1);
expect(esClient.indices.putIndexTemplate).toBeCalledWith(
expect.objectContaining({
name: `entities_v1_latest_${definition.id}_index_template`,
})
);
- expect(esClient.ingest.putPipeline).toBeCalledTimes(2);
- expect(esClient.ingest.putPipeline).toBeCalledWith({
- id: generateHistoryIngestPipelineId(definition),
- processors: expect.anything(),
- _meta: {
- definitionVersion: definition.version,
- managed: definition.managed,
- },
- });
+ expect(esClient.ingest.putPipeline).toBeCalledTimes(1);
expect(esClient.ingest.putPipeline).toBeCalledWith({
id: generateLatestIngestPipelineId(definition),
processors: expect.anything(),
@@ -86,8 +79,7 @@ const assertHasCreatedDefinition = (
},
});
- expect(esClient.transform.putTransform).toBeCalledTimes(2);
- expect(esClient.transform.putTransform).toBeCalledWith(generateHistoryTransform(definition));
+ expect(esClient.transform.putTransform).toBeCalledTimes(1);
expect(esClient.transform.putTransform).toBeCalledWith(generateLatestTransform(definition));
};
@@ -101,32 +93,21 @@ const assertHasUpgradedDefinition = (
...definition,
installStatus: 'upgrading',
installStartedAt: expect.any(String),
+ installedComponents: getExpectedInstalledComponents(definition),
});
expect(soClient.update).toBeCalledWith(SO_ENTITY_DEFINITION_TYPE, definition.id, {
installStatus: 'installed',
+ installedComponents: getExpectedInstalledComponents(definition),
});
- expect(esClient.indices.putIndexTemplate).toBeCalledTimes(2);
- expect(esClient.indices.putIndexTemplate).toBeCalledWith(
- expect.objectContaining({
- name: `entities_v1_history_${definition.id}_index_template`,
- })
- );
+ expect(esClient.indices.putIndexTemplate).toBeCalledTimes(1);
expect(esClient.indices.putIndexTemplate).toBeCalledWith(
expect.objectContaining({
name: `entities_v1_latest_${definition.id}_index_template`,
})
);
- expect(esClient.ingest.putPipeline).toBeCalledTimes(2);
- expect(esClient.ingest.putPipeline).toBeCalledWith({
- id: generateHistoryIngestPipelineId(definition),
- processors: expect.anything(),
- _meta: {
- definitionVersion: definition.version,
- managed: definition.managed,
- },
- });
+ expect(esClient.ingest.putPipeline).toBeCalledTimes(1);
expect(esClient.ingest.putPipeline).toBeCalledWith({
id: generateLatestIngestPipelineId(definition),
processors: expect.anything(),
@@ -136,8 +117,7 @@ const assertHasUpgradedDefinition = (
},
});
- expect(esClient.transform.putTransform).toBeCalledTimes(2);
- expect(esClient.transform.putTransform).toBeCalledWith(generateHistoryTransform(definition));
+ expect(esClient.transform.putTransform).toBeCalledTimes(1);
expect(esClient.transform.putTransform).toBeCalledWith(generateLatestTransform(definition));
};
@@ -148,13 +128,7 @@ const assertHasDeletedDefinition = (
) => {
assertHasDeletedTransforms(definition, esClient);
- expect(esClient.ingest.deletePipeline).toBeCalledTimes(2);
- expect(esClient.ingest.deletePipeline).toBeCalledWith(
- {
- id: generateHistoryIngestPipelineId(definition),
- },
- { ignore: [404] }
- );
+ expect(esClient.ingest.deletePipeline).toBeCalledTimes(1);
expect(esClient.ingest.deletePipeline).toBeCalledWith(
{
id: generateLatestIngestPipelineId(definition),
@@ -162,13 +136,7 @@ const assertHasDeletedDefinition = (
{ ignore: [404] }
);
- expect(esClient.indices.deleteIndexTemplate).toBeCalledTimes(2);
- expect(esClient.indices.deleteIndexTemplate).toBeCalledWith(
- {
- name: generateHistoryIndexTemplateId(definition),
- },
- { ignore: [404] }
- );
+ expect(esClient.indices.deleteIndexTemplate).toBeCalledTimes(1);
expect(esClient.indices.deleteIndexTemplate).toBeCalledWith(
{
name: generateLatestIndexTemplateId(definition),
@@ -184,33 +152,21 @@ const assertHasDeletedTransforms = (
definition: EntityDefinition,
esClient: ElasticsearchClient
) => {
- expect(esClient.transform.stopTransform).toBeCalledTimes(2);
- expect(esClient.transform.stopTransform).toBeCalledWith(
- expect.objectContaining({
- transform_id: generateHistoryTransformId(definition),
- }),
- expect.anything()
- );
- expect(esClient.transform.deleteTransform).toBeCalledWith(
- expect.objectContaining({
- transform_id: generateHistoryTransformId(definition),
- }),
- expect.anything()
- );
+ expect(esClient.transform.stopTransform).toBeCalledTimes(1);
expect(esClient.transform.stopTransform).toBeCalledWith(
expect.objectContaining({
transform_id: generateLatestTransformId(definition),
}),
expect.anything()
);
+
+ expect(esClient.transform.deleteTransform).toBeCalledTimes(1);
expect(esClient.transform.deleteTransform).toBeCalledWith(
expect.objectContaining({
transform_id: generateLatestTransformId(definition),
}),
expect.anything()
);
-
- expect(esClient.transform.deleteTransform).toBeCalledTimes(2);
};
describe('install_entity_definition', () => {
@@ -223,7 +179,7 @@ describe('install_entity_definition', () => {
installEntityDefinition({
esClient,
soClient,
- definition: { id: 'a'.repeat(40) } as EntityDefinition,
+ definition: { id: 'a'.repeat(50) } as EntityDefinition,
logger: loggerMock.create(),
})
).rejects.toThrow(EntityDefinitionIdInvalid);
@@ -242,6 +198,7 @@ describe('install_entity_definition', () => {
attributes: {
...mockEntityDefinition,
installStatus: 'installed',
+ installedComponents: [],
},
},
],
@@ -264,6 +221,12 @@ describe('install_entity_definition', () => {
const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser;
const soClient = savedObjectsClientMock.create();
soClient.find.mockResolvedValue({ saved_objects: [], total: 0, page: 1, per_page: 10 });
+ soClient.update.mockResolvedValue({
+ id: mockEntityDefinition.id,
+ type: 'entity-definition',
+ references: [],
+ attributes: {},
+ });
await installEntityDefinition({
esClient,
@@ -300,6 +263,12 @@ describe('install_entity_definition', () => {
const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser;
const soClient = savedObjectsClientMock.create();
soClient.find.mockResolvedValue({ saved_objects: [], total: 0, page: 1, per_page: 10 });
+ soClient.update.mockResolvedValue({
+ id: mockEntityDefinition.id,
+ type: 'entity-definition',
+ references: [],
+ attributes: {},
+ });
await installBuiltInEntityDefinitions({
esClient,
@@ -329,6 +298,7 @@ describe('install_entity_definition', () => {
attributes: {
...mockEntityDefinition,
installStatus: 'installed',
+ installedComponents: getExpectedInstalledComponents(mockEntityDefinition),
},
},
],
@@ -336,6 +306,12 @@ describe('install_entity_definition', () => {
page: 1,
per_page: 10,
});
+ soClient.update.mockResolvedValue({
+ id: mockEntityDefinition.id,
+ type: 'entity-definition',
+ references: [],
+ attributes: {},
+ });
await installBuiltInEntityDefinitions({
esClient,
@@ -367,6 +343,7 @@ describe('install_entity_definition', () => {
attributes: {
...mockEntityDefinition,
installStatus: 'installed',
+ installedComponents: getExpectedInstalledComponents(mockEntityDefinition),
},
},
],
@@ -374,6 +351,12 @@ describe('install_entity_definition', () => {
page: 1,
per_page: 10,
});
+ soClient.update.mockResolvedValue({
+ id: mockEntityDefinition.id,
+ type: 'entity-definition',
+ references: [],
+ attributes: {},
+ });
await installBuiltInEntityDefinitions({
esClient,
@@ -407,6 +390,7 @@ describe('install_entity_definition', () => {
// upgrading for 1h
installStatus: 'upgrading',
installStartedAt: moment().subtract(1, 'hour').toISOString(),
+ installedComponents: getExpectedInstalledComponents(mockEntityDefinition),
},
},
],
@@ -414,6 +398,12 @@ describe('install_entity_definition', () => {
page: 1,
per_page: 10,
});
+ soClient.update.mockResolvedValue({
+ id: mockEntityDefinition.id,
+ type: 'entity-definition',
+ references: [],
+ attributes: {},
+ });
await installBuiltInEntityDefinitions({
esClient,
@@ -442,6 +432,7 @@ describe('install_entity_definition', () => {
...mockEntityDefinition,
installStatus: 'failed',
installStartedAt: new Date().toISOString(),
+ installedComponents: getExpectedInstalledComponents(mockEntityDefinition),
},
},
],
@@ -449,6 +440,12 @@ describe('install_entity_definition', () => {
page: 1,
per_page: 10,
});
+ soClient.update.mockResolvedValue({
+ id: mockEntityDefinition.id,
+ type: 'entity-definition',
+ references: [],
+ attributes: {},
+ });
await installBuiltInEntityDefinitions({
esClient,
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/install_entity_definition.ts b/x-pack/plugins/entity_manager/server/lib/entities/install_entity_definition.ts
index 7d6dee4fb2ced..b4adedaf10374 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/install_entity_definition.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/install_entity_definition.ts
@@ -10,39 +10,25 @@ import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import { EntityDefinition, EntityDefinitionUpdate } from '@kbn/entities-schema';
import { Logger } from '@kbn/logging';
-import {
- generateHistoryIndexTemplateId,
- generateLatestIndexTemplateId,
-} from './helpers/generate_component_id';
-import {
- createAndInstallHistoryIngestPipeline,
- createAndInstallLatestIngestPipeline,
-} from './create_and_install_ingest_pipeline';
-import {
- createAndInstallHistoryBackfillTransform,
- createAndInstallHistoryTransform,
- createAndInstallLatestTransform,
-} from './create_and_install_transform';
+import { generateLatestIndexTemplateId } from './helpers/generate_component_id';
+import { createAndInstallIngestPipelines } from './create_and_install_ingest_pipeline';
+import { createAndInstallTransforms } from './create_and_install_transform';
import { validateDefinitionCanCreateValidTransformIds } from './transform/validate_transform_ids';
import { deleteEntityDefinition } from './delete_entity_definition';
-import { deleteHistoryIngestPipeline, deleteLatestIngestPipeline } from './delete_ingest_pipeline';
+import { deleteLatestIngestPipeline } from './delete_ingest_pipeline';
import { findEntityDefinitionById } from './find_entity_definition';
import {
entityDefinitionExists,
saveEntityDefinition,
updateEntityDefinition,
} from './save_entity_definition';
-
-import { isBackfillEnabled } from './helpers/is_backfill_enabled';
-import { deleteTemplate, upsertTemplate } from '../manage_index_templates';
-import { generateEntitiesLatestIndexTemplateConfig } from './templates/entities_latest_template';
-import { generateEntitiesHistoryIndexTemplateConfig } from './templates/entities_history_template';
+import { createAndInstallTemplates, deleteTemplate } from '../manage_index_templates';
import { EntityIdConflict } from './errors/entity_id_conflict_error';
import { EntityDefinitionNotFound } from './errors/entity_not_found';
import { mergeEntityDefinitionUpdate } from './helpers/merge_definition_update';
import { EntityDefinitionWithState } from './types';
-import { stopTransforms } from './stop_transforms';
-import { deleteTransforms } from './delete_transforms';
+import { stopLatestTransform, stopTransforms } from './stop_transforms';
+import { deleteLatestTransform, deleteTransforms } from './delete_transforms';
export interface InstallDefinitionParams {
esClient: ElasticsearchClient;
@@ -51,16 +37,6 @@ export interface InstallDefinitionParams {
logger: Logger;
}
-const throwIfRejected = (values: Array | PromiseRejectedResult>) => {
- const rejectedPromise = values.find(
- (value) => value.status === 'rejected'
- ) as PromiseRejectedResult;
- if (rejectedPromise) {
- throw new Error(rejectedPromise.reason);
- }
- return values;
-};
-
// install an entity definition from scratch with all its required components
// after verifying that the definition id is valid and available.
// attempt to remove all installed components if the installation fails.
@@ -72,42 +48,35 @@ export async function installEntityDefinition({
}: InstallDefinitionParams): Promise {
validateDefinitionCanCreateValidTransformIds(definition);
- try {
- if (await entityDefinitionExists(soClient, definition.id)) {
- throw new EntityIdConflict(
- `Entity definition with [${definition.id}] already exists.`,
- definition
- );
- }
+ if (await entityDefinitionExists(soClient, definition.id)) {
+ throw new EntityIdConflict(
+ `Entity definition with [${definition.id}] already exists.`,
+ definition
+ );
+ }
+ try {
const entityDefinition = await saveEntityDefinition(soClient, {
...definition,
installStatus: 'installing',
installStartedAt: new Date().toISOString(),
+ installedComponents: [],
});
return await install({ esClient, soClient, logger, definition: entityDefinition });
} catch (e) {
logger.error(`Failed to install entity definition ${definition.id}: ${e}`);
- await stopAndDeleteTransforms(esClient, definition, logger);
- await Promise.all([
- deleteHistoryIngestPipeline(esClient, definition, logger),
- deleteLatestIngestPipeline(esClient, definition, logger),
- ]);
+ await stopLatestTransform(esClient, definition, logger);
+ await deleteLatestTransform(esClient, definition, logger);
- await Promise.all([
- deleteTemplate({
- esClient,
- logger,
- name: generateHistoryIndexTemplateId(definition),
- }),
- deleteTemplate({
- esClient,
- logger,
- name: generateLatestIndexTemplateId(definition),
- }),
- ]);
+ await deleteLatestIngestPipeline(esClient, definition, logger);
+
+ await deleteTemplate({
+ esClient,
+ logger,
+ name: generateLatestIndexTemplateId(definition),
+ });
await deleteEntityDefinition(soClient, definition).catch((err) => {
if (err instanceof EntityDefinitionNotFound) {
@@ -191,36 +160,19 @@ async function install({
);
logger.debug(`Installing index templates for definition ${definition.id}`);
- await Promise.allSettled([
- upsertTemplate({
- esClient,
- logger,
- template: generateEntitiesHistoryIndexTemplateConfig(definition),
- }),
- upsertTemplate({
- esClient,
- logger,
- template: generateEntitiesLatestIndexTemplateConfig(definition),
- }),
- ]).then(throwIfRejected);
+ const templates = await createAndInstallTemplates(esClient, definition, logger);
logger.debug(`Installing ingest pipelines for definition ${definition.id}`);
- await Promise.allSettled([
- createAndInstallHistoryIngestPipeline(esClient, definition, logger),
- createAndInstallLatestIngestPipeline(esClient, definition, logger),
- ]).then(throwIfRejected);
+ const pipelines = await createAndInstallIngestPipelines(esClient, definition, logger);
logger.debug(`Installing transforms for definition ${definition.id}`);
- await Promise.allSettled([
- createAndInstallHistoryTransform(esClient, definition, logger),
- isBackfillEnabled(definition)
- ? createAndInstallHistoryBackfillTransform(esClient, definition, logger)
- : Promise.resolve(),
- createAndInstallLatestTransform(esClient, definition, logger),
- ]).then(throwIfRejected);
-
- await updateEntityDefinition(soClient, definition.id, { installStatus: 'installed' });
- return { ...definition, installStatus: 'installed' };
+ const transforms = await createAndInstallTransforms(esClient, definition, logger);
+
+ const updatedProps = await updateEntityDefinition(soClient, definition.id, {
+ installStatus: 'installed',
+ installedComponents: [...templates, ...pipelines, ...transforms],
+ });
+ return { ...definition, ...updatedProps.attributes };
}
// stop and delete the current transforms and reinstall all the components
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/save_entity_definition.ts b/x-pack/plugins/entity_manager/server/lib/entities/save_entity_definition.ts
index 2dff5178aeeaf..d32edfa146917 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/save_entity_definition.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/save_entity_definition.ts
@@ -41,5 +41,5 @@ export async function updateEntityDefinition(
id: string,
definition: Partial
) {
- await soClient.update(SO_ENTITY_DEFINITION_TYPE, id, definition);
+ return await soClient.update(SO_ENTITY_DEFINITION_TYPE, id, definition);
}
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/start_transforms.ts b/x-pack/plugins/entity_manager/server/lib/entities/start_transforms.ts
index ea2ec7adb5ddc..f4cd8fc89dd11 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/start_transforms.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/start_transforms.ts
@@ -7,13 +7,7 @@
import { ElasticsearchClient, Logger } from '@kbn/core/server';
import { EntityDefinition } from '@kbn/entities-schema';
-import {
- generateHistoryBackfillTransformId,
- generateHistoryTransformId,
- generateLatestTransformId,
-} from './helpers/generate_component_id';
import { retryTransientEsErrors } from './helpers/retry';
-import { isBackfillEnabled } from './helpers/is_backfill_enabled';
export async function startTransforms(
esClient: ElasticsearchClient,
@@ -21,28 +15,15 @@ export async function startTransforms(
logger: Logger
) {
try {
- const historyTransformId = generateHistoryTransformId(definition);
- const latestTransformId = generateLatestTransformId(definition);
- await retryTransientEsErrors(
- () =>
- esClient.transform.startTransform({ transform_id: historyTransformId }, { ignore: [409] }),
- { logger }
- );
- if (isBackfillEnabled(definition)) {
- const historyBackfillTransformId = generateHistoryBackfillTransformId(definition);
- await retryTransientEsErrors(
- () =>
- esClient.transform.startTransform(
- { transform_id: historyBackfillTransformId },
- { ignore: [409] }
- ),
- { logger }
- );
- }
- await retryTransientEsErrors(
- () =>
- esClient.transform.startTransform({ transform_id: latestTransformId }, { ignore: [409] }),
- { logger }
+ await Promise.all(
+ (definition.installedComponents ?? [])
+ .filter(({ type }) => type === 'transform')
+ .map(({ id }) =>
+ retryTransientEsErrors(
+ () => esClient.transform.startTransform({ transform_id: id }, { ignore: [409] }),
+ { logger }
+ )
+ )
);
} catch (err) {
logger.error(`Cannot start entity transforms [${definition.id}]: ${err}`);
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/stop_transforms.ts b/x-pack/plugins/entity_manager/server/lib/entities/stop_transforms.ts
index 98f9ad351e377..9aabad926b239 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/stop_transforms.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/stop_transforms.ts
@@ -8,14 +8,8 @@
import { ElasticsearchClient, Logger } from '@kbn/core/server';
import { EntityDefinition } from '@kbn/entities-schema';
-import {
- generateHistoryTransformId,
- generateHistoryBackfillTransformId,
- generateLatestTransformId,
-} from './helpers/generate_component_id';
import { retryTransientEsErrors } from './helpers/retry';
-
-import { isBackfillEnabled } from './helpers/is_backfill_enabled';
+import { generateLatestTransformId } from './helpers/generate_component_id';
export async function stopTransforms(
esClient: ElasticsearchClient,
@@ -23,43 +17,46 @@ export async function stopTransforms(
logger: Logger
) {
try {
- const historyTransformId = generateHistoryTransformId(definition);
- const latestTransformId = generateLatestTransformId(definition);
-
- await retryTransientEsErrors(
- () =>
- esClient.transform.stopTransform(
- { transform_id: historyTransformId, wait_for_completion: true, force: true },
- { ignore: [409, 404] }
- ),
- { logger }
+ await Promise.all(
+ (definition.installedComponents ?? [])
+ .filter(({ type }) => type === 'transform')
+ .map(({ id }) =>
+ retryTransientEsErrors(
+ () =>
+ esClient.transform.stopTransform(
+ { transform_id: id, wait_for_completion: true, force: true },
+ { ignore: [409, 404] }
+ ),
+ { logger }
+ )
+ )
);
+ } catch (e) {
+ logger.error(`Cannot stop transforms for definition [${definition.id}]: ${e}`);
+ throw e;
+ }
+}
- if (isBackfillEnabled(definition)) {
- const historyBackfillTransformId = generateHistoryBackfillTransformId(definition);
- await retryTransientEsErrors(
- () =>
- esClient.transform.stopTransform(
- {
- transform_id: historyBackfillTransformId,
- wait_for_completion: true,
- force: true,
- },
- { ignore: [409, 404] }
- ),
- { logger }
- );
- }
+export async function stopLatestTransform(
+ esClient: ElasticsearchClient,
+ definition: EntityDefinition,
+ logger: Logger
+) {
+ try {
await retryTransientEsErrors(
() =>
esClient.transform.stopTransform(
- { transform_id: latestTransformId, wait_for_completion: true, force: true },
+ {
+ transform_id: generateLatestTransformId(definition),
+ wait_for_completion: true,
+ force: true,
+ },
{ ignore: [409, 404] }
),
{ logger }
);
} catch (e) {
- logger.error(`Cannot stop entity transforms [${definition.id}]: ${e}`);
+ logger.error(`Cannot stop latest transform for definition [${definition.id}]: ${e}`);
throw e;
}
}
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/templates/__snapshots__/entities_history_template.test.ts.snap b/x-pack/plugins/entity_manager/server/lib/entities/templates/__snapshots__/entities_history_template.test.ts.snap
deleted file mode 100644
index fd4ed11f8cb94..0000000000000
--- a/x-pack/plugins/entity_manager/server/lib/entities/templates/__snapshots__/entities_history_template.test.ts.snap
+++ /dev/null
@@ -1,152 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`generateEntitiesHistoryIndexTemplateConfig(definition) should generate a valid index template for builtin definition 1`] = `
-Object {
- "_meta": Object {
- "description": "Index template for indices managed by the Elastic Entity Model's entity discovery framework for the history dataset",
- "ecs_version": "8.0.0",
- "managed": true,
- "managed_by": "elastic_entity_model",
- },
- "composed_of": Array [
- "entities_v1_history_base",
- "entities_v1_entity",
- "entities_v1_event",
- ],
- "ignore_missing_component_templates": Array [],
- "index_patterns": Array [
- ".entities.v1.history.builtin_mock_entity_definition.*",
- ],
- "name": "entities_v1_history_builtin_mock_entity_definition_index_template",
- "priority": 200,
- "template": Object {
- "aliases": Object {
- "entities-service-history": Object {},
- },
- "mappings": Object {
- "_meta": Object {
- "version": "1.6.0",
- },
- "date_detection": false,
- "dynamic_templates": Array [
- Object {
- "strings_as_keyword": Object {
- "mapping": Object {
- "fields": Object {
- "text": Object {
- "type": "text",
- },
- },
- "ignore_above": 1024,
- "type": "keyword",
- },
- "match_mapping_type": "string",
- },
- },
- Object {
- "entity_metrics": Object {
- "mapping": Object {
- "type": "{dynamic_type}",
- },
- "match_mapping_type": Array [
- "long",
- "double",
- ],
- "path_match": "entity.metrics.*",
- },
- },
- ],
- },
- "settings": Object {
- "index": Object {
- "codec": "best_compression",
- "mapping": Object {
- "total_fields": Object {
- "limit": 2000,
- },
- },
- },
- },
- },
-}
-`;
-
-exports[`generateEntitiesHistoryIndexTemplateConfig(definition) should generate a valid index template for custom definition 1`] = `
-Object {
- "_meta": Object {
- "description": "Index template for indices managed by the Elastic Entity Model's entity discovery framework for the history dataset",
- "ecs_version": "8.0.0",
- "managed": true,
- "managed_by": "elastic_entity_model",
- },
- "composed_of": Array [
- "entities_v1_history_base",
- "entities_v1_entity",
- "entities_v1_event",
- "admin-console-services@platform",
- "admin-console-services-history@platform",
- "admin-console-services@custom",
- "admin-console-services-history@custom",
- ],
- "ignore_missing_component_templates": Array [
- "admin-console-services@platform",
- "admin-console-services-history@platform",
- "admin-console-services@custom",
- "admin-console-services-history@custom",
- ],
- "index_patterns": Array [
- ".entities.v1.history.admin-console-services.*",
- ],
- "name": "entities_v1_history_admin-console-services_index_template",
- "priority": 200,
- "template": Object {
- "aliases": Object {
- "entities-service-history": Object {},
- },
- "mappings": Object {
- "_meta": Object {
- "version": "1.6.0",
- },
- "date_detection": false,
- "dynamic_templates": Array [
- Object {
- "strings_as_keyword": Object {
- "mapping": Object {
- "fields": Object {
- "text": Object {
- "type": "text",
- },
- },
- "ignore_above": 1024,
- "type": "keyword",
- },
- "match_mapping_type": "string",
- },
- },
- Object {
- "entity_metrics": Object {
- "mapping": Object {
- "type": "{dynamic_type}",
- },
- "match_mapping_type": Array [
- "long",
- "double",
- ],
- "path_match": "entity.metrics.*",
- },
- },
- ],
- },
- "settings": Object {
- "index": Object {
- "codec": "best_compression",
- "mapping": Object {
- "total_fields": Object {
- "limit": 2000,
- },
- },
- },
- },
- },
-}
-`;
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/templates/entities_history_template.test.ts b/x-pack/plugins/entity_manager/server/lib/entities/templates/entities_history_template.test.ts
deleted file mode 100644
index 72e8d8591ab2d..0000000000000
--- a/x-pack/plugins/entity_manager/server/lib/entities/templates/entities_history_template.test.ts
+++ /dev/null
@@ -1,21 +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 { entityDefinition, builtInEntityDefinition } from '../helpers/fixtures';
-import { generateEntitiesHistoryIndexTemplateConfig } from './entities_history_template';
-
-describe('generateEntitiesHistoryIndexTemplateConfig(definition)', () => {
- it('should generate a valid index template for custom definition', () => {
- const template = generateEntitiesHistoryIndexTemplateConfig(entityDefinition);
- expect(template).toMatchSnapshot();
- });
-
- it('should generate a valid index template for builtin definition', () => {
- const template = generateEntitiesHistoryIndexTemplateConfig(builtInEntityDefinition);
- expect(template).toMatchSnapshot();
- });
-});
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/templates/entities_history_template.ts b/x-pack/plugins/entity_manager/server/lib/entities/templates/entities_history_template.ts
deleted file mode 100644
index b1539d8108a6d..0000000000000
--- a/x-pack/plugins/entity_manager/server/lib/entities/templates/entities_history_template.ts
+++ /dev/null
@@ -1,96 +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 { IndicesPutIndexTemplateRequest } from '@elastic/elasticsearch/lib/api/types';
-import {
- ENTITY_HISTORY,
- EntityDefinition,
- entitiesIndexPattern,
- entitiesAliasPattern,
- ENTITY_SCHEMA_VERSION_V1,
-} from '@kbn/entities-schema';
-import { generateHistoryIndexTemplateId } from '../helpers/generate_component_id';
-import {
- ENTITY_ENTITY_COMPONENT_TEMPLATE_V1,
- ENTITY_EVENT_COMPONENT_TEMPLATE_V1,
- ENTITY_HISTORY_BASE_COMPONENT_TEMPLATE_V1,
-} from '../../../../common/constants_entities';
-import { getCustomHistoryTemplateComponents } from '../../../templates/components/helpers';
-
-export const generateEntitiesHistoryIndexTemplateConfig = (
- definition: EntityDefinition
-): IndicesPutIndexTemplateRequest => ({
- name: generateHistoryIndexTemplateId(definition),
- _meta: {
- description:
- "Index template for indices managed by the Elastic Entity Model's entity discovery framework for the history dataset",
- ecs_version: '8.0.0',
- managed: true,
- managed_by: 'elastic_entity_model',
- },
- ignore_missing_component_templates: getCustomHistoryTemplateComponents(definition),
- composed_of: [
- ENTITY_HISTORY_BASE_COMPONENT_TEMPLATE_V1,
- ENTITY_ENTITY_COMPONENT_TEMPLATE_V1,
- ENTITY_EVENT_COMPONENT_TEMPLATE_V1,
- ...getCustomHistoryTemplateComponents(definition),
- ],
- index_patterns: [
- `${entitiesIndexPattern({
- schemaVersion: ENTITY_SCHEMA_VERSION_V1,
- dataset: ENTITY_HISTORY,
- definitionId: definition.id,
- })}.*`,
- ],
- priority: 200,
- template: {
- aliases: {
- [entitiesAliasPattern({ type: definition.type, dataset: ENTITY_HISTORY })]: {},
- },
- mappings: {
- _meta: {
- version: '1.6.0',
- },
- date_detection: false,
- dynamic_templates: [
- {
- strings_as_keyword: {
- mapping: {
- ignore_above: 1024,
- type: 'keyword',
- fields: {
- text: {
- type: 'text',
- },
- },
- },
- match_mapping_type: 'string',
- },
- },
- {
- entity_metrics: {
- mapping: {
- type: '{dynamic_type}',
- },
- match_mapping_type: ['long', 'double'],
- path_match: 'entity.metrics.*',
- },
- },
- ],
- },
- settings: {
- index: {
- codec: 'best_compression',
- mapping: {
- total_fields: {
- limit: 2000,
- },
- },
- },
- },
- },
-});
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/templates/entities_latest_template.ts b/x-pack/plugins/entity_manager/server/lib/entities/templates/entities_latest_template.ts
index ea476cf769644..e0c02c7471217 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/templates/entities_latest_template.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/templates/entities_latest_template.ts
@@ -19,7 +19,7 @@ import {
ENTITY_EVENT_COMPONENT_TEMPLATE_V1,
ENTITY_LATEST_BASE_COMPONENT_TEMPLATE_V1,
} from '../../../../common/constants_entities';
-import { getCustomLatestTemplateComponents } from '../../../templates/components/helpers';
+import { isBuiltinDefinition } from '../helpers/is_builtin_definition';
export const generateEntitiesLatestIndexTemplateConfig = (
definition: EntityDefinition
@@ -94,3 +94,16 @@ export const generateEntitiesLatestIndexTemplateConfig = (
},
},
});
+
+function getCustomLatestTemplateComponents(definition: EntityDefinition) {
+ if (isBuiltinDefinition(definition)) {
+ return [];
+ }
+
+ return [
+ `${definition.id}@platform`, // @platform goes before so it can be overwritten by custom
+ `${definition.id}-latest@platform`,
+ `${definition.id}@custom`,
+ `${definition.id}-latest@custom`,
+ ];
+}
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap b/x-pack/plugins/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap
deleted file mode 100644
index b19a805b24b12..0000000000000
--- a/x-pack/plugins/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap
+++ /dev/null
@@ -1,305 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`generateHistoryTransform(definition) should generate a valid history backfill transform 1`] = `
-Object {
- "_meta": Object {
- "definitionVersion": "999.999.999",
- "managed": false,
- },
- "defer_validation": true,
- "dest": Object {
- "index": ".entities.v1.history.noop",
- "pipeline": "entities-v1-history-admin-console-services-backfill",
- },
- "frequency": "5m",
- "pivot": Object {
- "aggs": Object {
- "_errorRate_A": Object {
- "filter": Object {
- "bool": Object {
- "minimum_should_match": 1,
- "should": Array [
- Object {
- "match_phrase": Object {
- "log.level": "ERROR",
- },
- },
- ],
- },
- },
- },
- "_logRate_A": Object {
- "filter": Object {
- "bool": Object {
- "minimum_should_match": 1,
- "should": Array [
- Object {
- "exists": Object {
- "field": "log.level",
- },
- },
- ],
- },
- },
- },
- "entity.lastSeenTimestamp": Object {
- "max": Object {
- "field": "@timestamp",
- },
- },
- "entity.metadata.host.name": Object {
- "terms": Object {
- "field": "host.name",
- "size": 1000,
- },
- },
- "entity.metadata.host.os.name": Object {
- "terms": Object {
- "field": "host.os.name",
- "size": 1000,
- },
- },
- "entity.metadata.sourceIndex": Object {
- "terms": Object {
- "field": "_index",
- "size": 1000,
- },
- },
- "entity.metadata.tags": Object {
- "terms": Object {
- "field": "tags",
- "size": 1000,
- },
- },
- "entity.metrics.errorRate": Object {
- "bucket_script": Object {
- "buckets_path": Object {
- "A": "_errorRate_A>_count",
- },
- "script": Object {
- "lang": "painless",
- "source": "params.A",
- },
- },
- },
- "entity.metrics.logRate": Object {
- "bucket_script": Object {
- "buckets_path": Object {
- "A": "_logRate_A>_count",
- },
- "script": Object {
- "lang": "painless",
- "source": "params.A",
- },
- },
- },
- },
- "group_by": Object {
- "@timestamp": Object {
- "date_histogram": Object {
- "field": "@timestamp",
- "fixed_interval": "1m",
- },
- },
- "entity.identity.event.category": Object {
- "terms": Object {
- "field": "event.category",
- "missing_bucket": true,
- },
- },
- "entity.identity.log.logger": Object {
- "terms": Object {
- "field": "log.logger",
- "missing_bucket": false,
- },
- },
- },
- },
- "settings": Object {
- "deduce_mappings": false,
- "unattended": true,
- },
- "source": Object {
- "index": Array [
- "kbn-data-forge-fake_stack.*",
- ],
- "query": Object {
- "bool": Object {
- "filter": Array [
- Object {
- "range": Object {
- "@timestamp": Object {
- "gte": "now-72h",
- },
- },
- },
- Object {
- "exists": Object {
- "field": "log.logger",
- },
- },
- ],
- },
- },
- },
- "sync": Object {
- "time": Object {
- "delay": "15m",
- "field": "@timestamp",
- },
- },
- "transform_id": "entities-v1-history-backfill-admin-console-services-backfill",
-}
-`;
-
-exports[`generateHistoryTransform(definition) should generate a valid history transform 1`] = `
-Object {
- "_meta": Object {
- "definitionVersion": "1.0.0",
- "managed": false,
- },
- "defer_validation": true,
- "dest": Object {
- "index": ".entities.v1.history.noop",
- "pipeline": "entities-v1-history-admin-console-services",
- },
- "frequency": "2m",
- "pivot": Object {
- "aggs": Object {
- "_errorRate_A": Object {
- "filter": Object {
- "bool": Object {
- "minimum_should_match": 1,
- "should": Array [
- Object {
- "match_phrase": Object {
- "log.level": "ERROR",
- },
- },
- ],
- },
- },
- },
- "_logRate_A": Object {
- "filter": Object {
- "bool": Object {
- "minimum_should_match": 1,
- "should": Array [
- Object {
- "exists": Object {
- "field": "log.level",
- },
- },
- ],
- },
- },
- },
- "entity.lastSeenTimestamp": Object {
- "max": Object {
- "field": "@timestamp",
- },
- },
- "entity.metadata.host.name": Object {
- "terms": Object {
- "field": "host.name",
- "size": 1000,
- },
- },
- "entity.metadata.host.os.name": Object {
- "terms": Object {
- "field": "host.os.name",
- "size": 1000,
- },
- },
- "entity.metadata.sourceIndex": Object {
- "terms": Object {
- "field": "_index",
- "size": 1000,
- },
- },
- "entity.metadata.tags": Object {
- "terms": Object {
- "field": "tags",
- "size": 1000,
- },
- },
- "entity.metrics.errorRate": Object {
- "bucket_script": Object {
- "buckets_path": Object {
- "A": "_errorRate_A>_count",
- },
- "script": Object {
- "lang": "painless",
- "source": "params.A",
- },
- },
- },
- "entity.metrics.logRate": Object {
- "bucket_script": Object {
- "buckets_path": Object {
- "A": "_logRate_A>_count",
- },
- "script": Object {
- "lang": "painless",
- "source": "params.A",
- },
- },
- },
- },
- "group_by": Object {
- "@timestamp": Object {
- "date_histogram": Object {
- "field": "@timestamp",
- "fixed_interval": "1m",
- },
- },
- "entity.identity.event.category": Object {
- "terms": Object {
- "field": "event.category",
- "missing_bucket": true,
- },
- },
- "entity.identity.log.logger": Object {
- "terms": Object {
- "field": "log.logger",
- "missing_bucket": false,
- },
- },
- },
- },
- "settings": Object {
- "deduce_mappings": false,
- "unattended": true,
- },
- "source": Object {
- "index": Array [
- "kbn-data-forge-fake_stack.*",
- ],
- "query": Object {
- "bool": Object {
- "filter": Array [
- Object {
- "exists": Object {
- "field": "log.logger",
- },
- },
- Object {
- "range": Object {
- "@timestamp": Object {
- "gte": "now-10m",
- },
- },
- },
- ],
- },
- },
- },
- "sync": Object {
- "time": Object {
- "delay": "2m",
- "field": "@timestamp",
- },
- },
- "transform_id": "entities-v1-history-admin-console-services",
-}
-`;
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/transform/__snapshots__/generate_latest_transform.test.ts.snap b/x-pack/plugins/entity_manager/server/lib/entities/transform/__snapshots__/generate_latest_transform.test.ts.snap
index ab1224525f4d7..49f8ff4536120 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/transform/__snapshots__/generate_latest_transform.test.ts.snap
+++ b/x-pack/plugins/entity_manager/server/lib/entities/transform/__snapshots__/generate_latest_transform.test.ts.snap
@@ -14,76 +14,37 @@ Object {
"frequency": "30s",
"pivot": Object {
"aggs": Object {
- "_errorRate": Object {
- "top_metrics": Object {
- "metrics": Array [
- Object {
- "field": "entity.metrics.errorRate",
- },
- ],
- "sort": Array [
- Object {
- "@timestamp": "desc",
- },
- ],
- },
- },
- "_logRate": Object {
- "top_metrics": Object {
- "metrics": Array [
- Object {
- "field": "entity.metrics.logRate",
- },
- ],
- "sort": Array [
- Object {
- "@timestamp": "desc",
- },
- ],
- },
- },
- "entity.firstSeenTimestamp": Object {
- "min": Object {
- "field": "@timestamp",
- },
- },
- "entity.identity.event.category": Object {
- "aggs": Object {
- "top_metric": Object {
- "top_metrics": Object {
- "metrics": Object {
- "field": "event.category",
- },
- "sort": "_score",
- },
- },
- },
+ "_errorRate_A": Object {
"filter": Object {
- "exists": Object {
- "field": "event.category",
- },
- },
- },
- "entity.identity.log.logger": Object {
- "aggs": Object {
- "top_metric": Object {
- "top_metrics": Object {
- "metrics": Object {
- "field": "log.logger",
+ "bool": Object {
+ "minimum_should_match": 1,
+ "should": Array [
+ Object {
+ "match_phrase": Object {
+ "log.level": "ERROR",
+ },
},
- "sort": "_score",
- },
+ ],
},
},
+ },
+ "_logRate_A": Object {
"filter": Object {
- "exists": Object {
- "field": "log.logger",
+ "bool": Object {
+ "minimum_should_match": 1,
+ "should": Array [
+ Object {
+ "exists": Object {
+ "field": "log.level",
+ },
+ },
+ ],
},
},
},
"entity.lastSeenTimestamp": Object {
"max": Object {
- "field": "entity.lastSeenTimestamp",
+ "field": "@timestamp",
},
},
"entity.metadata.host.name": Object {
@@ -91,14 +52,14 @@ Object {
"data": Object {
"terms": Object {
"field": "host.name",
- "size": 1000,
+ "size": 10,
},
},
},
"filter": Object {
"range": Object {
"@timestamp": Object {
- "gte": "now-360s",
+ "gte": "now-10m",
},
},
},
@@ -108,14 +69,14 @@ Object {
"data": Object {
"terms": Object {
"field": "host.os.name",
- "size": 1000,
+ "size": 10,
},
},
},
"filter": Object {
"range": Object {
"@timestamp": Object {
- "gte": "now-360s",
+ "gte": "now-10m",
},
},
},
@@ -124,15 +85,15 @@ Object {
"aggs": Object {
"data": Object {
"terms": Object {
- "field": "sourceIndex",
- "size": 1000,
+ "field": "_index",
+ "size": 10,
},
},
},
"filter": Object {
"range": Object {
"@timestamp": Object {
- "gte": "now-360s",
+ "gte": "now-10m",
},
},
},
@@ -142,14 +103,14 @@ Object {
"data": Object {
"terms": Object {
"field": "tags",
- "size": 1000,
+ "size": 10,
},
},
},
"filter": Object {
"range": Object {
"@timestamp": Object {
- "gte": "now-360s",
+ "gte": "now-10m",
},
},
},
@@ -157,24 +118,37 @@ Object {
"entity.metrics.errorRate": Object {
"bucket_script": Object {
"buckets_path": Object {
- "value": "_errorRate[entity.metrics.errorRate]",
+ "A": "_errorRate_A>_count",
+ },
+ "script": Object {
+ "lang": "painless",
+ "source": "params.A",
},
- "script": "params.value",
},
},
"entity.metrics.logRate": Object {
"bucket_script": Object {
"buckets_path": Object {
- "value": "_logRate[entity.metrics.logRate]",
+ "A": "_logRate_A>_count",
+ },
+ "script": Object {
+ "lang": "painless",
+ "source": "params.A",
},
- "script": "params.value",
},
},
},
"group_by": Object {
- "entity.id": Object {
+ "entity.identity.event.category": Object {
+ "terms": Object {
+ "field": "event.category",
+ "missing_bucket": true,
+ },
+ },
+ "entity.identity.log.logger": Object {
"terms": Object {
- "field": "entity.id",
+ "field": "log.logger",
+ "missing_bucket": false,
},
},
},
@@ -184,12 +158,32 @@ Object {
"unattended": true,
},
"source": Object {
- "index": ".entities.v1.history.admin-console-services.*",
+ "index": Array [
+ "kbn-data-forge-fake_stack.*",
+ ],
+ "query": Object {
+ "bool": Object {
+ "filter": Array [
+ Object {
+ "exists": Object {
+ "field": "log.logger",
+ },
+ },
+ Object {
+ "range": Object {
+ "@timestamp": Object {
+ "gte": "now-10m",
+ },
+ },
+ },
+ ],
+ },
+ },
},
"sync": Object {
"time": Object {
- "delay": "1s",
- "field": "event.ingested",
+ "delay": "10s",
+ "field": "@timestamp",
},
},
"transform_id": "entities-v1-latest-admin-console-services",
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_history_transform.test.ts b/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_history_transform.test.ts
deleted file mode 100644
index f49ec0cd88a37..0000000000000
--- a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_history_transform.test.ts
+++ /dev/null
@@ -1,24 +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 { entityDefinition } from '../helpers/fixtures/entity_definition';
-import { entityDefinitionWithBackfill } from '../helpers/fixtures/entity_definition_with_backfill';
-import {
- generateBackfillHistoryTransform,
- generateHistoryTransform,
-} from './generate_history_transform';
-
-describe('generateHistoryTransform(definition)', () => {
- it('should generate a valid history transform', () => {
- const transform = generateHistoryTransform(entityDefinition);
- expect(transform).toMatchSnapshot();
- });
- it('should generate a valid history backfill transform', () => {
- const transform = generateBackfillHistoryTransform(entityDefinitionWithBackfill);
- expect(transform).toMatchSnapshot();
- });
-});
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_history_transform.ts b/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_history_transform.ts
deleted file mode 100644
index 239359738624c..0000000000000
--- a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_history_transform.ts
+++ /dev/null
@@ -1,178 +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 { EntityDefinition } from '@kbn/entities-schema';
-import {
- QueryDslQueryContainer,
- TransformPutTransformRequest,
-} from '@elastic/elasticsearch/lib/api/types';
-import { getElasticsearchQueryOrThrow } from '../helpers/get_elasticsearch_query_or_throw';
-import { generateHistoryMetricAggregations } from './generate_metric_aggregations';
-import {
- ENTITY_DEFAULT_HISTORY_FREQUENCY,
- ENTITY_DEFAULT_HISTORY_SYNC_DELAY,
-} from '../../../../common/constants_entities';
-import { generateHistoryMetadataAggregations } from './generate_metadata_aggregations';
-import {
- generateHistoryTransformId,
- generateHistoryIngestPipelineId,
- generateHistoryIndexName,
- generateHistoryBackfillTransformId,
-} from '../helpers/generate_component_id';
-import { isBackfillEnabled } from '../helpers/is_backfill_enabled';
-
-export function generateHistoryTransform(
- definition: EntityDefinition
-): TransformPutTransformRequest {
- const filter: QueryDslQueryContainer[] = [];
-
- if (definition.filter) {
- filter.push(getElasticsearchQueryOrThrow(definition.filter));
- }
-
- if (definition.identityFields.some(({ optional }) => !optional)) {
- definition.identityFields
- .filter(({ optional }) => !optional)
- .forEach(({ field }) => {
- filter.push({ exists: { field } });
- });
- }
-
- filter.push({
- range: {
- [definition.history.timestampField]: {
- gte: `now-${definition.history.settings.lookbackPeriod}`,
- },
- },
- });
-
- return generateTransformPutRequest({
- definition,
- filter,
- transformId: generateHistoryTransformId(definition),
- frequency: definition.history.settings.frequency,
- syncDelay: definition.history.settings.syncDelay,
- });
-}
-
-export function generateBackfillHistoryTransform(
- definition: EntityDefinition
-): TransformPutTransformRequest {
- if (!isBackfillEnabled(definition)) {
- throw new Error(
- 'generateBackfillHistoryTransform called without history.settings.backfillSyncDelay set'
- );
- }
-
- const filter: QueryDslQueryContainer[] = [];
-
- if (definition.filter) {
- filter.push(getElasticsearchQueryOrThrow(definition.filter));
- }
-
- if (definition.history.settings.backfillLookbackPeriod) {
- filter.push({
- range: {
- [definition.history.timestampField]: {
- gte: `now-${definition.history.settings.backfillLookbackPeriod}`,
- },
- },
- });
- }
-
- if (definition.identityFields.some(({ optional }) => !optional)) {
- definition.identityFields
- .filter(({ optional }) => !optional)
- .forEach(({ field }) => {
- filter.push({ exists: { field } });
- });
- }
-
- return generateTransformPutRequest({
- definition,
- filter,
- transformId: generateHistoryBackfillTransformId(definition),
- frequency: definition.history.settings.backfillFrequency,
- syncDelay: definition.history.settings.backfillSyncDelay,
- });
-}
-
-const generateTransformPutRequest = ({
- definition,
- filter,
- transformId,
- frequency,
- syncDelay,
-}: {
- definition: EntityDefinition;
- transformId: string;
- filter: QueryDslQueryContainer[];
- frequency?: string;
- syncDelay?: string;
-}) => {
- return {
- transform_id: transformId,
- _meta: {
- definitionVersion: definition.version,
- managed: definition.managed,
- },
- defer_validation: true,
- source: {
- index: definition.indexPatterns,
- ...(filter.length > 0 && {
- query: {
- bool: {
- filter,
- },
- },
- }),
- },
- dest: {
- index: `${generateHistoryIndexName({ id: 'noop' } as EntityDefinition)}`,
- pipeline: generateHistoryIngestPipelineId(definition),
- },
- frequency: frequency || ENTITY_DEFAULT_HISTORY_FREQUENCY,
- sync: {
- time: {
- field: definition.history.settings.syncField || definition.history.timestampField,
- delay: syncDelay || ENTITY_DEFAULT_HISTORY_SYNC_DELAY,
- },
- },
- settings: {
- deduce_mappings: false,
- unattended: true,
- },
- pivot: {
- group_by: {
- ...definition.identityFields.reduce(
- (acc, id) => ({
- ...acc,
- [`entity.identity.${id.field}`]: {
- terms: { field: id.field, missing_bucket: id.optional },
- },
- }),
- {}
- ),
- ['@timestamp']: {
- date_histogram: {
- field: definition.history.timestampField,
- fixed_interval: definition.history.interval,
- },
- },
- },
- aggs: {
- ...generateHistoryMetricAggregations(definition),
- ...generateHistoryMetadataAggregations(definition),
- 'entity.lastSeenTimestamp': {
- max: {
- field: definition.history.timestampField,
- },
- },
- },
- },
- };
-};
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_latest_transform.ts b/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_latest_transform.ts
index 85ee57fefea2c..573bb2225f183 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_latest_transform.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_latest_transform.ts
@@ -5,44 +5,97 @@
* 2.0.
*/
-import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
import { EntityDefinition } from '@kbn/entities-schema';
+import {
+ QueryDslQueryContainer,
+ TransformPutTransformRequest,
+} from '@elastic/elasticsearch/lib/api/types';
+import { getElasticsearchQueryOrThrow } from '../helpers/get_elasticsearch_query_or_throw';
+import { generateLatestMetricAggregations } from './generate_metric_aggregations';
import {
ENTITY_DEFAULT_LATEST_FREQUENCY,
ENTITY_DEFAULT_LATEST_SYNC_DELAY,
} from '../../../../common/constants_entities';
import {
- generateHistoryIndexName,
- generateLatestIndexName,
- generateLatestIngestPipelineId,
generateLatestTransformId,
+ generateLatestIngestPipelineId,
+ generateLatestIndexName,
} from '../helpers/generate_component_id';
-import { generateIdentityAggregations } from './generate_identity_aggregations';
import { generateLatestMetadataAggregations } from './generate_metadata_aggregations';
-import { generateLatestMetricAggregations } from './generate_metric_aggregations';
export function generateLatestTransform(
definition: EntityDefinition
): TransformPutTransformRequest {
+ const filter: QueryDslQueryContainer[] = [];
+
+ if (definition.filter) {
+ filter.push(getElasticsearchQueryOrThrow(definition.filter));
+ }
+
+ if (definition.identityFields.some(({ optional }) => !optional)) {
+ definition.identityFields
+ .filter(({ optional }) => !optional)
+ .forEach(({ field }) => {
+ filter.push({ exists: { field } });
+ });
+ }
+
+ filter.push({
+ range: {
+ [definition.latest.timestampField]: {
+ gte: `now-${definition.latest.lookbackPeriod}`,
+ },
+ },
+ });
+
+ return generateTransformPutRequest({
+ definition,
+ filter,
+ transformId: generateLatestTransformId(definition),
+ frequency: definition.latest.settings?.frequency ?? ENTITY_DEFAULT_LATEST_FREQUENCY,
+ syncDelay: definition.latest.settings?.syncDelay ?? ENTITY_DEFAULT_LATEST_SYNC_DELAY,
+ });
+}
+
+const generateTransformPutRequest = ({
+ definition,
+ filter,
+ transformId,
+ frequency,
+ syncDelay,
+}: {
+ definition: EntityDefinition;
+ transformId: string;
+ filter: QueryDslQueryContainer[];
+ frequency: string;
+ syncDelay: string;
+}) => {
return {
- transform_id: generateLatestTransformId(definition),
+ transform_id: transformId,
_meta: {
definitionVersion: definition.version,
managed: definition.managed,
},
defer_validation: true,
source: {
- index: `${generateHistoryIndexName(definition)}.*`,
+ index: definition.indexPatterns,
+ ...(filter.length > 0 && {
+ query: {
+ bool: {
+ filter,
+ },
+ },
+ }),
},
dest: {
index: `${generateLatestIndexName({ id: 'noop' } as EntityDefinition)}`,
pipeline: generateLatestIngestPipelineId(definition),
},
- frequency: definition.latest?.settings?.frequency ?? ENTITY_DEFAULT_LATEST_FREQUENCY,
+ frequency,
sync: {
time: {
- field: definition.latest?.settings?.syncField ?? 'event.ingested',
- delay: definition.latest?.settings?.syncDelay ?? ENTITY_DEFAULT_LATEST_SYNC_DELAY,
+ field: definition.latest.settings?.syncField || definition.latest.timestampField,
+ delay: syncDelay,
},
},
settings: {
@@ -51,25 +104,25 @@ export function generateLatestTransform(
},
pivot: {
group_by: {
- ['entity.id']: {
- terms: { field: 'entity.id' },
- },
+ ...definition.identityFields.reduce(
+ (acc, id) => ({
+ ...acc,
+ [`entity.identity.${id.field}`]: {
+ terms: { field: id.field, missing_bucket: id.optional },
+ },
+ }),
+ {}
+ ),
},
aggs: {
...generateLatestMetricAggregations(definition),
...generateLatestMetadataAggregations(definition),
- ...generateIdentityAggregations(definition),
'entity.lastSeenTimestamp': {
max: {
- field: 'entity.lastSeenTimestamp',
- },
- },
- 'entity.firstSeenTimestamp': {
- min: {
- field: '@timestamp',
+ field: definition.latest.timestampField,
},
},
},
},
};
-}
+};
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metadata_aggregations.test.ts b/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metadata_aggregations.test.ts
index 7746be66f5033..12535d313143b 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metadata_aggregations.test.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metadata_aggregations.test.ts
@@ -7,134 +7,22 @@
import { entityDefinitionSchema } from '@kbn/entities-schema';
import { rawEntityDefinition } from '../helpers/fixtures/entity_definition';
-import {
- generateHistoryMetadataAggregations,
- generateLatestMetadataAggregations,
-} from './generate_metadata_aggregations';
+import { generateLatestMetadataAggregations } from './generate_metadata_aggregations';
describe('Generate Metadata Aggregations for history and latest', () => {
- describe('generateHistoryMetadataAggregations()', () => {
- it('should generate metadata aggregations for string format', () => {
- const definition = entityDefinitionSchema.parse({
- ...rawEntityDefinition,
- metadata: ['host.name'],
- });
- expect(generateHistoryMetadataAggregations(definition)).toEqual({
- 'entity.metadata.host.name': {
- terms: {
- field: 'host.name',
- size: 1000,
- },
- },
- });
- });
-
- it('should generate metadata aggregations for object format with only source', () => {
- const definition = entityDefinitionSchema.parse({
- ...rawEntityDefinition,
- metadata: [{ source: 'host.name' }],
- });
- expect(generateHistoryMetadataAggregations(definition)).toEqual({
- 'entity.metadata.host.name': {
- terms: {
- field: 'host.name',
- size: 1000,
- },
- },
- });
- });
-
- it('should generate metadata aggregations for object format with source and aggregation', () => {
- const definition = entityDefinitionSchema.parse({
- ...rawEntityDefinition,
- metadata: [{ source: 'host.name', aggregation: { type: 'terms', limit: 10 } }],
- });
- expect(generateHistoryMetadataAggregations(definition)).toEqual({
- 'entity.metadata.host.name': {
- terms: {
- field: 'host.name',
- size: 10,
- },
- },
- });
- });
-
- it('should generate metadata aggregations for object format with source, aggregation, and destination', () => {
- const definition = entityDefinitionSchema.parse({
- ...rawEntityDefinition,
- metadata: [
- {
- source: 'host.name',
- aggregation: { type: 'terms', limit: 20 },
- destination: 'hostName',
- },
- ],
- });
- expect(generateHistoryMetadataAggregations(definition)).toEqual({
- 'entity.metadata.hostName': {
- terms: {
- field: 'host.name',
- size: 20,
- },
- },
- });
- });
-
- it('should generate metadata aggregations for terms and top_value', () => {
- const definition = entityDefinitionSchema.parse({
- ...rawEntityDefinition,
- metadata: [
- {
- source: 'host.name',
- aggregation: { type: 'terms', limit: 10 },
- destination: 'hostName',
- },
- {
- source: 'agent.name',
- aggregation: { type: 'top_value', sort: { '@timestamp': 'desc' } },
- destination: 'agentName',
- },
- ],
- });
-
- expect(generateHistoryMetadataAggregations(definition)).toEqual({
- 'entity.metadata.hostName': {
- terms: {
- field: 'host.name',
- size: 10,
- },
- },
- 'entity.metadata.agentName': {
- filter: {
- exists: {
- field: 'agent.name',
- },
- },
- aggs: {
- top_value: {
- top_metrics: {
- metrics: { field: 'agent.name' },
- sort: { '@timestamp': 'desc' },
- },
- },
- },
- },
- });
- });
- });
-
describe('generateLatestMetadataAggregations()', () => {
it('should generate metadata aggregations for string format', () => {
const definition = entityDefinitionSchema.parse({
...rawEntityDefinition,
metadata: ['host.name'],
});
+
expect(generateLatestMetadataAggregations(definition)).toEqual({
'entity.metadata.host.name': {
filter: {
range: {
'@timestamp': {
- gte: 'now-360s',
+ gte: 'now-10m',
},
},
},
@@ -142,7 +30,7 @@ describe('Generate Metadata Aggregations for history and latest', () => {
data: {
terms: {
field: 'host.name',
- size: 1000,
+ size: 10,
},
},
},
@@ -160,7 +48,7 @@ describe('Generate Metadata Aggregations for history and latest', () => {
filter: {
range: {
'@timestamp': {
- gte: 'now-360s',
+ gte: 'now-10m',
},
},
},
@@ -168,7 +56,7 @@ describe('Generate Metadata Aggregations for history and latest', () => {
data: {
terms: {
field: 'host.name',
- size: 1000,
+ size: 10,
},
},
},
@@ -179,14 +67,16 @@ describe('Generate Metadata Aggregations for history and latest', () => {
it('should generate metadata aggregations for object format with source and aggregation', () => {
const definition = entityDefinitionSchema.parse({
...rawEntityDefinition,
- metadata: [{ source: 'host.name', aggregation: { type: 'terms', limit: 10 } }],
+ metadata: [
+ { source: 'host.name', aggregation: { type: 'terms', limit: 10, lookbackPeriod: '1h' } },
+ ],
});
expect(generateLatestMetadataAggregations(definition)).toEqual({
'entity.metadata.host.name': {
filter: {
range: {
'@timestamp': {
- gte: 'now-360s',
+ gte: 'now-1h',
},
},
},
@@ -218,14 +108,14 @@ describe('Generate Metadata Aggregations for history and latest', () => {
filter: {
range: {
'@timestamp': {
- gte: 'now-360s',
+ gte: 'now-10m',
},
},
},
aggs: {
data: {
terms: {
- field: 'hostName',
+ field: 'host.name',
size: 10,
},
},
@@ -255,14 +145,14 @@ describe('Generate Metadata Aggregations for history and latest', () => {
filter: {
range: {
'@timestamp': {
- gte: 'now-360s',
+ gte: 'now-10m',
},
},
},
aggs: {
data: {
terms: {
- field: 'hostName',
+ field: 'host.name',
size: 10,
},
},
@@ -275,13 +165,13 @@ describe('Generate Metadata Aggregations for history and latest', () => {
{
range: {
'@timestamp': {
- gte: 'now-360s',
+ gte: 'now-10m',
},
},
},
{
exists: {
- field: 'agentName',
+ field: 'agent.name',
},
},
],
@@ -291,7 +181,7 @@ describe('Generate Metadata Aggregations for history and latest', () => {
top_value: {
top_metrics: {
metrics: {
- field: 'agentName',
+ field: 'agent.name',
},
sort: {
'@timestamp': 'desc',
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metadata_aggregations.ts b/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metadata_aggregations.ts
index 0fc4464672219..796d1e25b55ec 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metadata_aggregations.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metadata_aggregations.ts
@@ -6,70 +6,28 @@
*/
import { EntityDefinition } from '@kbn/entities-schema';
-import { calculateOffset } from '../helpers/calculate_offset';
-
-export function generateHistoryMetadataAggregations(definition: EntityDefinition) {
- if (!definition.metadata) {
- return {};
- }
- return definition.metadata.reduce((aggs, metadata) => {
- let agg;
- if (metadata.aggregation.type === 'terms') {
- agg = {
- terms: {
- field: metadata.source,
- size: metadata.aggregation.limit,
- },
- };
- } else if (metadata.aggregation.type === 'top_value') {
- agg = {
- filter: {
- exists: {
- field: metadata.source,
- },
- },
- aggs: {
- top_value: {
- top_metrics: {
- metrics: {
- field: metadata.source,
- },
- sort: metadata.aggregation.sort,
- },
- },
- },
- };
- }
-
- return {
- ...aggs,
- [`entity.metadata.${metadata.destination}`]: agg,
- };
- }, {});
-}
export function generateLatestMetadataAggregations(definition: EntityDefinition) {
if (!definition.metadata) {
return {};
}
- const offsetInSeconds = `${calculateOffset(definition)}s`;
-
return definition.metadata.reduce((aggs, metadata) => {
+ const lookbackPeriod = metadata.aggregation.lookbackPeriod || definition.latest.lookbackPeriod;
let agg;
if (metadata.aggregation.type === 'terms') {
agg = {
filter: {
range: {
'@timestamp': {
- gte: `now-${offsetInSeconds}`,
+ gte: `now-${lookbackPeriod}`,
},
},
},
aggs: {
data: {
terms: {
- field: metadata.destination,
+ field: metadata.source,
size: metadata.aggregation.limit,
},
},
@@ -83,13 +41,13 @@ export function generateLatestMetadataAggregations(definition: EntityDefinition)
{
range: {
'@timestamp': {
- gte: `now-${metadata.aggregation.lookbackPeriod ?? offsetInSeconds}`,
+ gte: `now-${lookbackPeriod}`,
},
},
},
{
exists: {
- field: metadata.destination,
+ field: metadata.source,
},
},
],
@@ -99,7 +57,7 @@ export function generateLatestMetadataAggregations(definition: EntityDefinition)
top_value: {
top_metrics: {
metrics: {
- field: metadata.destination,
+ field: metadata.source,
},
sort: metadata.aggregation.sort,
},
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metric_aggregations.ts b/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metric_aggregations.ts
index bd1af365116cb..d42dd69b37eff 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metric_aggregations.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/transform/generate_metric_aggregations.ts
@@ -104,41 +104,15 @@ function buildMetricEquation(keyMetric: KeyMetric) {
};
}
-export function generateHistoryMetricAggregations(definition: EntityDefinition) {
- if (!definition.metrics) {
- return {};
- }
- return definition.metrics.reduce((aggs, keyMetric) => {
- return {
- ...aggs,
- ...buildMetricAggregations(keyMetric, definition.history.timestampField),
- [`entity.metrics.${keyMetric.name}`]: buildMetricEquation(keyMetric),
- };
- }, {});
-}
-
export function generateLatestMetricAggregations(definition: EntityDefinition) {
if (!definition.metrics) {
return {};
}
-
return definition.metrics.reduce((aggs, keyMetric) => {
return {
...aggs,
- [`_${keyMetric.name}`]: {
- top_metrics: {
- metrics: [{ field: `entity.metrics.${keyMetric.name}` }],
- sort: [{ '@timestamp': 'desc' }],
- },
- },
- [`entity.metrics.${keyMetric.name}`]: {
- bucket_script: {
- buckets_path: {
- value: `_${keyMetric.name}[entity.metrics.${keyMetric.name}]`,
- },
- script: 'params.value',
- },
- },
+ ...buildMetricAggregations(keyMetric, definition.latest.timestampField),
+ [`entity.metrics.${keyMetric.name}`]: buildMetricEquation(keyMetric),
};
}, {});
}
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/transform/validate_transform_ids.ts b/x-pack/plugins/entity_manager/server/lib/entities/transform/validate_transform_ids.ts
index c16b7f126dded..c703124bdf082 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/transform/validate_transform_ids.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/transform/validate_transform_ids.ts
@@ -7,26 +7,14 @@
import { EntityDefinition } from '@kbn/entities-schema';
import { EntityDefinitionIdInvalid } from '../errors/entity_definition_id_invalid';
-import {
- generateHistoryBackfillTransformId,
- generateHistoryTransformId,
- generateLatestTransformId,
-} from '../helpers/generate_component_id';
+import { generateLatestTransformId } from '../helpers/generate_component_id';
const TRANSFORM_ID_MAX_LENGTH = 64;
export function validateDefinitionCanCreateValidTransformIds(definition: EntityDefinition) {
- const historyTransformId = generateHistoryTransformId(definition);
const latestTransformId = generateLatestTransformId(definition);
- const historyBackfillTransformId = generateHistoryBackfillTransformId(definition);
- const spareChars =
- TRANSFORM_ID_MAX_LENGTH -
- Math.max(
- historyTransformId.length,
- latestTransformId.length,
- historyBackfillTransformId.length
- );
+ const spareChars = TRANSFORM_ID_MAX_LENGTH - latestTransformId.length;
if (spareChars < 0) {
throw new EntityDefinitionIdInvalid(
diff --git a/x-pack/plugins/entity_manager/server/lib/entities/uninstall_entity_definition.ts b/x-pack/plugins/entity_manager/server/lib/entities/uninstall_entity_definition.ts
index 8bc8efa3870aa..d0e0410b6e422 100644
--- a/x-pack/plugins/entity_manager/server/lib/entities/uninstall_entity_definition.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entities/uninstall_entity_definition.ts
@@ -11,14 +11,10 @@ import { EntityDefinition } from '@kbn/entities-schema';
import { Logger } from '@kbn/logging';
import { deleteEntityDefinition } from './delete_entity_definition';
import { deleteIndices } from './delete_index';
-import { deleteHistoryIngestPipeline, deleteLatestIngestPipeline } from './delete_ingest_pipeline';
+import { deleteIngestPipelines } from './delete_ingest_pipeline';
import { findEntityDefinitions } from './find_entity_definition';
-import {
- generateHistoryIndexTemplateId,
- generateLatestIndexTemplateId,
-} from './helpers/generate_component_id';
-import { deleteTemplate } from '../manage_index_templates';
+import { deleteTemplates } from '../manage_index_templates';
import { stopTransforms } from './stop_transforms';
@@ -40,19 +36,13 @@ export async function uninstallEntityDefinition({
await stopTransforms(esClient, definition, logger);
await deleteTransforms(esClient, definition, logger);
- await Promise.all([
- deleteHistoryIngestPipeline(esClient, definition, logger),
- deleteLatestIngestPipeline(esClient, definition, logger),
- ]);
+ await deleteIngestPipelines(esClient, definition, logger);
if (deleteData) {
await deleteIndices(esClient, definition, logger);
}
- await Promise.all([
- deleteTemplate({ esClient, logger, name: generateHistoryIndexTemplateId(definition) }),
- deleteTemplate({ esClient, logger, name: generateLatestIndexTemplateId(definition) }),
- ]);
+ await deleteTemplates(esClient, definition, logger);
await deleteEntityDefinition(soClient, definition);
}
diff --git a/x-pack/plugins/entity_manager/server/lib/entity_client.ts b/x-pack/plugins/entity_manager/server/lib/entity_client.ts
index ee6b59b0ae0ea..710872c04eda0 100644
--- a/x-pack/plugins/entity_manager/server/lib/entity_client.ts
+++ b/x-pack/plugins/entity_manager/server/lib/entity_client.ts
@@ -41,7 +41,7 @@ export class EntityClient {
});
if (!installOnly) {
- await startTransforms(this.options.esClient, definition, this.options.logger);
+ await startTransforms(this.options.esClient, installedDefinition, this.options.logger);
}
return installedDefinition;
diff --git a/x-pack/plugins/entity_manager/server/lib/manage_index_templates.ts b/x-pack/plugins/entity_manager/server/lib/manage_index_templates.ts
index b0789b6cf2769..ffa58cd9c0145 100644
--- a/x-pack/plugins/entity_manager/server/lib/manage_index_templates.ts
+++ b/x-pack/plugins/entity_manager/server/lib/manage_index_templates.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { EntityDefinition } from '@kbn/entities-schema';
import {
ClusterPutComponentTemplateRequest,
IndicesPutIndexTemplateRequest,
@@ -15,6 +16,7 @@ import { entitiesLatestBaseComponentTemplateConfig } from '../templates/componen
import { entitiesEntityComponentTemplateConfig } from '../templates/components/entity';
import { entitiesEventComponentTemplateConfig } from '../templates/components/event';
import { retryTransientEsErrors } from './entities/helpers/retry';
+import { generateEntitiesLatestIndexTemplateConfig } from './entities/templates/entities_latest_template';
interface TemplateManagementOptions {
esClient: ElasticsearchClient;
@@ -67,14 +69,27 @@ interface DeleteTemplateOptions {
export async function upsertTemplate({ esClient, template, logger }: TemplateManagementOptions) {
try {
- await retryTransientEsErrors(() => esClient.indices.putIndexTemplate(template), { logger });
+ const result = await retryTransientEsErrors(() => esClient.indices.putIndexTemplate(template), {
+ logger,
+ });
logger.debug(() => `Installed entity manager index template: ${JSON.stringify(template)}`);
+ return result;
} catch (error: any) {
logger.error(`Error updating entity manager index template: ${error.message}`);
throw error;
}
}
+export async function createAndInstallTemplates(
+ esClient: ElasticsearchClient,
+ definition: EntityDefinition,
+ logger: Logger
+): Promise> {
+ const template = generateEntitiesLatestIndexTemplateConfig(definition);
+ await upsertTemplate({ esClient, template, logger });
+ return [{ type: 'template', id: template.name }];
+}
+
export async function deleteTemplate({ esClient, name, logger }: DeleteTemplateOptions) {
try {
await retryTransientEsErrors(
@@ -87,6 +102,28 @@ export async function deleteTemplate({ esClient, name, logger }: DeleteTemplateO
}
}
+export async function deleteTemplates(
+ esClient: ElasticsearchClient,
+ definition: EntityDefinition,
+ logger: Logger
+) {
+ try {
+ await Promise.all(
+ (definition.installedComponents ?? [])
+ .filter(({ type }) => type === 'template')
+ .map(({ id }) =>
+ retryTransientEsErrors(
+ () => esClient.indices.deleteIndexTemplate({ name: id }, { ignore: [404] }),
+ { logger }
+ )
+ )
+ );
+ } catch (error: any) {
+ logger.error(`Error deleting entity manager index template: ${error.message}`);
+ throw error;
+ }
+}
+
export async function upsertComponent({ esClient, component, logger }: ComponentManagementOptions) {
try {
await retryTransientEsErrors(() => esClient.cluster.putComponentTemplate(component), {
diff --git a/x-pack/plugins/entity_manager/server/routes/enablement/disable.ts b/x-pack/plugins/entity_manager/server/routes/enablement/disable.ts
index bde68eb85ba9f..9c1c4f403636b 100644
--- a/x-pack/plugins/entity_manager/server/routes/enablement/disable.ts
+++ b/x-pack/plugins/entity_manager/server/routes/enablement/disable.ts
@@ -51,8 +51,8 @@ export const disableEntityDiscoveryRoute = createEntityManagerServerRoute({
}),
handler: async ({ context, response, params, logger, server }) => {
try {
- const esClient = (await context.core).elasticsearch.client.asCurrentUser;
- const canDisable = await canDisableEntityDiscovery(esClient);
+ const esClientAsCurrentUser = (await context.core).elasticsearch.client.asCurrentUser;
+ const canDisable = await canDisableEntityDiscovery(esClientAsCurrentUser);
if (!canDisable) {
return response.forbidden({
body: {
@@ -62,6 +62,7 @@ export const disableEntityDiscoveryRoute = createEntityManagerServerRoute({
});
}
+ const esClient = (await context.core).elasticsearch.client.asSecondaryAuthUser;
const soClient = (await context.core).savedObjects.getClient({
includedHiddenTypes: [EntityDiscoveryApiKeyType.name],
});
diff --git a/x-pack/plugins/entity_manager/server/routes/enablement/enable.ts b/x-pack/plugins/entity_manager/server/routes/enablement/enable.ts
index 9814840d20a0b..1002c1e716df2 100644
--- a/x-pack/plugins/entity_manager/server/routes/enablement/enable.ts
+++ b/x-pack/plugins/entity_manager/server/routes/enablement/enable.ts
@@ -80,8 +80,10 @@ export const enableEntityDiscoveryRoute = createEntityManagerServerRoute({
});
}
- const esClient = (await context.core).elasticsearch.client.asCurrentUser;
- const canEnable = await canEnableEntityDiscovery(esClient);
+ const core = await context.core;
+
+ const esClientAsCurrentUser = core.elasticsearch.client.asCurrentUser;
+ const canEnable = await canEnableEntityDiscovery(esClientAsCurrentUser);
if (!canEnable) {
return response.forbidden({
body: {
@@ -91,7 +93,7 @@ export const enableEntityDiscoveryRoute = createEntityManagerServerRoute({
});
}
- const soClient = (await context.core).savedObjects.getClient({
+ const soClient = core.savedObjects.getClient({
includedHiddenTypes: [EntityDiscoveryApiKeyType.name],
});
const existingApiKey = await readEntityDiscoveryAPIKey(server);
@@ -117,6 +119,7 @@ export const enableEntityDiscoveryRoute = createEntityManagerServerRoute({
await saveEntityDiscoveryAPIKey(soClient, apiKey);
+ const esClient = core.elasticsearch.client.asSecondaryAuthUser;
const installedDefinitions = await installBuiltInEntityDefinitions({
esClient,
soClient,
diff --git a/x-pack/plugins/entity_manager/server/routes/entities/reset.ts b/x-pack/plugins/entity_manager/server/routes/entities/reset.ts
index a59c38b3acf7c..0b6942e335e51 100644
--- a/x-pack/plugins/entity_manager/server/routes/entities/reset.ts
+++ b/x-pack/plugins/entity_manager/server/routes/entities/reset.ts
@@ -12,25 +12,13 @@ import { EntitySecurityException } from '../../lib/entities/errors/entity_securi
import { InvalidTransformError } from '../../lib/entities/errors/invalid_transform_error';
import { readEntityDefinition } from '../../lib/entities/read_entity_definition';
-import {
- deleteHistoryIngestPipeline,
- deleteLatestIngestPipeline,
-} from '../../lib/entities/delete_ingest_pipeline';
+import { deleteIngestPipelines } from '../../lib/entities/delete_ingest_pipeline';
import { deleteIndices } from '../../lib/entities/delete_index';
-import {
- createAndInstallHistoryIngestPipeline,
- createAndInstallLatestIngestPipeline,
-} from '../../lib/entities/create_and_install_ingest_pipeline';
-import {
- createAndInstallHistoryBackfillTransform,
- createAndInstallHistoryTransform,
- createAndInstallLatestTransform,
-} from '../../lib/entities/create_and_install_transform';
+import { createAndInstallIngestPipelines } from '../../lib/entities/create_and_install_ingest_pipeline';
+import { createAndInstallTransforms } from '../../lib/entities/create_and_install_transform';
import { startTransforms } from '../../lib/entities/start_transforms';
import { EntityDefinitionNotFound } from '../../lib/entities/errors/entity_not_found';
-import { isBackfillEnabled } from '../../lib/entities/helpers/is_backfill_enabled';
-
import { createEntityManagerServerRoute } from '../create_entity_manager_server_route';
import { deleteTransforms } from '../../lib/entities/delete_transforms';
import { stopTransforms } from '../../lib/entities/stop_transforms';
@@ -51,18 +39,12 @@ export const resetEntityDefinitionRoute = createEntityManagerServerRoute({
await stopTransforms(esClient, definition, logger);
await deleteTransforms(esClient, definition, logger);
- await deleteHistoryIngestPipeline(esClient, definition, logger);
- await deleteLatestIngestPipeline(esClient, definition, logger);
+ await deleteIngestPipelines(esClient, definition, logger);
await deleteIndices(esClient, definition, logger);
// Recreate everything
- await createAndInstallHistoryIngestPipeline(esClient, definition, logger);
- await createAndInstallLatestIngestPipeline(esClient, definition, logger);
- await createAndInstallHistoryTransform(esClient, definition, logger);
- if (isBackfillEnabled(definition)) {
- await createAndInstallHistoryBackfillTransform(esClient, definition, logger);
- }
- await createAndInstallLatestTransform(esClient, definition, logger);
+ await createAndInstallIngestPipelines(esClient, definition, logger);
+ await createAndInstallTransforms(esClient, definition, logger);
await startTransforms(esClient, definition, logger);
return response.ok({ body: { acknowledged: true } });
diff --git a/x-pack/plugins/entity_manager/server/saved_objects/entity_definition.ts b/x-pack/plugins/entity_manager/server/saved_objects/entity_definition.ts
index fdf2510e8627e..bdea2b71e4141 100644
--- a/x-pack/plugins/entity_manager/server/saved_objects/entity_definition.ts
+++ b/x-pack/plugins/entity_manager/server/saved_objects/entity_definition.ts
@@ -5,11 +5,36 @@
* 2.0.
*/
+import { SavedObjectModelDataBackfillFn } from '@kbn/core-saved-objects-server';
import { SavedObject, SavedObjectsType } from '@kbn/core/server';
import { EntityDefinition } from '@kbn/entities-schema';
+import {
+ generateHistoryIndexTemplateId,
+ generateHistoryIngestPipelineId,
+ generateHistoryTransformId,
+ generateLatestIndexTemplateId,
+ generateLatestIngestPipelineId,
+ generateLatestTransformId,
+} from '../lib/entities/helpers/generate_component_id';
export const SO_ENTITY_DEFINITION_TYPE = 'entity-definition';
+export const backfillInstalledComponents: SavedObjectModelDataBackfillFn<
+ EntityDefinition,
+ EntityDefinition
+> = (savedObject) => {
+ const definition = savedObject.attributes;
+ definition.installedComponents = [
+ { type: 'transform', id: generateHistoryTransformId(definition) },
+ { type: 'transform', id: generateLatestTransformId(definition) },
+ { type: 'ingest_pipeline', id: generateHistoryIngestPipelineId(definition) },
+ { type: 'ingest_pipeline', id: generateLatestIngestPipelineId(definition) },
+ { type: 'template', id: generateHistoryIndexTemplateId(definition) },
+ { type: 'template', id: generateLatestIndexTemplateId(definition) },
+ ];
+ return savedObject;
+};
+
export const entityDefinition: SavedObjectsType = {
name: SO_ENTITY_DEFINITION_TYPE,
hidden: false,
@@ -64,5 +89,13 @@ export const entityDefinition: SavedObjectsType = {
},
],
},
+ '3': {
+ changes: [
+ {
+ type: 'data_backfill',
+ backfillFn: backfillInstalledComponents,
+ },
+ ],
+ },
},
};
diff --git a/x-pack/plugins/entity_manager/server/templates/components/helpers.test.ts b/x-pack/plugins/entity_manager/server/templates/components/helpers.test.ts
deleted file mode 100644
index 90c5e90d43f3a..0000000000000
--- a/x-pack/plugins/entity_manager/server/templates/components/helpers.test.ts
+++ /dev/null
@@ -1,31 +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 { EntityDefinition } from '@kbn/entities-schema';
-import { getCustomHistoryTemplateComponents, getCustomLatestTemplateComponents } from './helpers';
-
-describe('helpers', () => {
- it('getCustomLatestTemplateComponents should return template component in the right sort order', () => {
- const result = getCustomLatestTemplateComponents({ id: 'test' } as EntityDefinition);
- expect(result).toEqual([
- 'test@platform',
- 'test-latest@platform',
- 'test@custom',
- 'test-latest@custom',
- ]);
- });
-
- it('getCustomHistoryTemplateComponents should return template component in the right sort order', () => {
- const result = getCustomHistoryTemplateComponents({ id: 'test' } as EntityDefinition);
- expect(result).toEqual([
- 'test@platform',
- 'test-history@platform',
- 'test@custom',
- 'test-history@custom',
- ]);
- });
-});
diff --git a/x-pack/plugins/entity_manager/server/templates/components/helpers.ts b/x-pack/plugins/entity_manager/server/templates/components/helpers.ts
deleted file mode 100644
index 23cc7cccb6a13..0000000000000
--- a/x-pack/plugins/entity_manager/server/templates/components/helpers.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { EntityDefinition } from '@kbn/entities-schema';
-import { isBuiltinDefinition } from '../../lib/entities/helpers/is_builtin_definition';
-
-export const getCustomLatestTemplateComponents = (definition: EntityDefinition) => {
- if (isBuiltinDefinition(definition)) {
- return [];
- }
-
- return [
- `${definition.id}@platform`, // @platform goes before so it can be overwritten by custom
- `${definition.id}-latest@platform`,
- `${definition.id}@custom`,
- `${definition.id}-latest@custom`,
- ];
-};
-
-export const getCustomHistoryTemplateComponents = (definition: EntityDefinition) => {
- if (isBuiltinDefinition(definition)) {
- return [];
- }
-
- return [
- `${definition.id}@platform`, // @platform goes before so it can be overwritten by custom
- `${definition.id}-history@platform`,
- `${definition.id}@custom`,
- `${definition.id}-history@custom`,
- ];
-};
diff --git a/x-pack/plugins/entity_manager/tsconfig.json b/x-pack/plugins/entity_manager/tsconfig.json
index 29c100ee4c9d2..34c57a27dd829 100644
--- a/x-pack/plugins/entity_manager/tsconfig.json
+++ b/x-pack/plugins/entity_manager/tsconfig.json
@@ -34,5 +34,6 @@
"@kbn/zod-helpers",
"@kbn/encrypted-saved-objects-plugin",
"@kbn/licensing-plugin",
+ "@kbn/core-saved-objects-server",
]
}
diff --git a/x-pack/plugins/observability_solution/investigate_app/server/services/get_entities.ts b/x-pack/plugins/observability_solution/investigate_app/server/services/get_entities.ts
index 8f3a0abb62b67..00151f2029d21 100644
--- a/x-pack/plugins/observability_solution/investigate_app/server/services/get_entities.ts
+++ b/x-pack/plugins/observability_solution/investigate_app/server/services/get_entities.ts
@@ -61,7 +61,6 @@ export async function getEntitiesWithSource({
identityFields: entity?.entity.identityFields,
id: entity?.entity.id,
definitionId: entity?.entity.definitionId,
- firstSeenTimestamp: entity?.entity.firstSeenTimestamp,
lastSeenTimestamp: entity?.entity.lastSeenTimestamp,
displayName: entity?.entity.displayName,
metrics: entity?.entity.metrics,
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
index a72e00bf7aceb..09dea151a050a 100644
--- 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
@@ -27,9 +27,8 @@ export const buildHostEntityDefinition = (space: string): EntityDefinition =>
'host.type',
'host.architecture',
],
- history: {
+ latest: {
timestampField: '@timestamp',
- interval: '1m',
},
version: '1.0.0',
managed: true,
@@ -44,9 +43,8 @@ export const buildUserEntityDefinition = (space: string): EntityDefinition =>
identityFields: ['user.name'],
displayNameTemplate: '{{user.name}}',
metadata: ['user.email', 'user.full_name', 'user.hash', 'user.id', 'user.name', 'user.roles'],
- history: {
+ latest: {
timestampField: '@timestamp',
- interval: '1m',
},
version: '1.0.0',
managed: true,
diff --git a/x-pack/test/api_integration/apis/entity_manager/definitions.ts b/x-pack/test/api_integration/apis/entity_manager/definitions.ts
index 466b5e0232bf0..b51a26ad7b5ad 100644
--- a/x-pack/test/api_integration/apis/entity_manager/definitions.ts
+++ b/x-pack/test/api_integration/apis/entity_manager/definitions.ts
@@ -8,10 +8,7 @@
import semver from 'semver';
import expect from '@kbn/expect';
import { entityLatestSchema } from '@kbn/entities-schema';
-import {
- entityDefinition as mockDefinition,
- entityDefinitionWithBackfill as mockBackfillDefinition,
-} from '@kbn/entityManager-plugin/server/lib/entities/helpers/fixtures';
+import { entityDefinition as mockDefinition } from '@kbn/entityManager-plugin/server/lib/entities/helpers/fixtures';
import { PartialConfig, cleanup, generate } from '@kbn/data-forge';
import { generateLatestIndexName } from '@kbn/entityManager-plugin/server/lib/entities/helpers/generate_component_id';
import { FtrProviderContext } from '../../ftr_provider_context';
@@ -33,8 +30,9 @@ export default function ({ getService }: FtrProviderContext) {
describe('Entity definitions', () => {
describe('definitions installations', () => {
it('can install multiple definitions', async () => {
+ const mockDefinitionDup = { ...mockDefinition, id: 'mock_definition_dup' };
await installDefinition(supertest, { definition: mockDefinition });
- await installDefinition(supertest, { definition: mockBackfillDefinition });
+ await installDefinition(supertest, { definition: mockDefinitionDup });
const { definitions } = await getInstalledDefinitions(supertest);
expect(definitions.length).to.eql(2);
@@ -49,7 +47,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(
definitions.some(
(definition) =>
- definition.id === mockBackfillDefinition.id &&
+ definition.id === mockDefinitionDup.id &&
definition.state.installed === true &&
definition.state.running === true
)
@@ -57,7 +55,7 @@ export default function ({ getService }: FtrProviderContext) {
await Promise.all([
uninstallDefinition(supertest, { id: mockDefinition.id, deleteData: true }),
- uninstallDefinition(supertest, { id: mockBackfillDefinition.id, deleteData: true }),
+ uninstallDefinition(supertest, { id: mockDefinitionDup.id, deleteData: true }),
]);
});
@@ -89,7 +87,7 @@ export default function ({ getService }: FtrProviderContext) {
id: mockDefinition.id,
update: {
version: incVersion!,
- history: {
+ latest: {
timestampField: '@updatedTimestampField',
},
},
@@ -99,7 +97,7 @@ export default function ({ getService }: FtrProviderContext) {
definitions: [updatedDefinition],
} = await getInstalledDefinitions(supertest);
expect(updatedDefinition.version).to.eql(incVersion);
- expect(updatedDefinition.history.timestampField).to.eql('@updatedTimestampField');
+ expect(updatedDefinition.latest.timestampField).to.eql('@updatedTimestampField');
await uninstallDefinition(supertest, { id: mockDefinition.id });
});
@@ -114,7 +112,7 @@ export default function ({ getService }: FtrProviderContext) {
id: mockDefinition.id,
update: {
version: '1.0.0',
- history: {
+ latest: {
timestampField: '@updatedTimestampField',
},
},
diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts
index 99d84fbc5427b..4fb2360a049cf 100644
--- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts
+++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts
@@ -27,10 +27,7 @@ export default ({ getService }: FtrProviderContext) => {
it('should have installed the expected user resources', async () => {
await utils.initEntityEngineForEntityType('user');
- const expectedTransforms = [
- 'entities-v1-history-ea_default_user_entity_store',
- 'entities-v1-latest-ea_default_user_entity_store',
- ];
+ const expectedTransforms = ['entities-v1-latest-ea_default_user_entity_store'];
await utils.expectTransformsExist(expectedTransforms);
});
@@ -38,10 +35,7 @@ export default ({ getService }: FtrProviderContext) => {
it('should have installed the expected host resources', async () => {
await utils.initEntityEngineForEntityType('host');
- const expectedTransforms = [
- 'entities-v1-history-ea_default_host_entity_store',
- 'entities-v1-latest-ea_default_host_entity_store',
- ];
+ const expectedTransforms = ['entities-v1-latest-ea_default_host_entity_store'];
await utils.expectTransformsExist(expectedTransforms);
});
@@ -173,7 +167,6 @@ export default ({ getService }: FtrProviderContext) => {
})
.expect(200);
- await utils.expectTransformNotFound('entities-v1-history-ea_host_entity_store');
await utils.expectTransformNotFound('entities-v1-latest-ea_host_entity_store');
});
@@ -187,7 +180,6 @@ export default ({ getService }: FtrProviderContext) => {
})
.expect(200);
- await utils.expectTransformNotFound('entities-v1-history-ea_user_entity_store');
await utils.expectTransformNotFound('entities-v1-latest-ea_user_entity_store');
});
});
diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts
index 112c8b8b21511..e3ef29d937183 100644
--- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts
+++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts
@@ -38,10 +38,7 @@ export default ({ getService }: FtrProviderContextWithSpaces) => {
it('should have installed the expected user resources', async () => {
await utils.initEntityEngineForEntityType('user');
- const expectedTransforms = [
- `entities-v1-history-ea_${namespace}_user_entity_store`,
- `entities-v1-latest-ea_${namespace}_user_entity_store`,
- ];
+ const expectedTransforms = [`entities-v1-latest-ea_${namespace}_user_entity_store`];
await utils.expectTransformsExist(expectedTransforms);
});
@@ -49,10 +46,7 @@ export default ({ getService }: FtrProviderContextWithSpaces) => {
it('should have installed the expected host resources', async () => {
await utils.initEntityEngineForEntityType('host');
- const expectedTransforms = [
- `entities-v1-history-ea_${namespace}_host_entity_store`,
- `entities-v1-latest-ea_${namespace}_host_entity_store`,
- ];
+ const expectedTransforms = [`entities-v1-latest-ea_${namespace}_host_entity_store`];
await utils.expectTransformsExist(expectedTransforms);
});
From 3fa70e122c6a3c77edea3f2c47980403c1835256 Mon Sep 17 00:00:00 2001
From: Philippe Oberti
Date: Wed, 9 Oct 2024 23:41:02 +0200
Subject: [PATCH 21/87] [Security Solution][Notes] - update notes management
page columns (#194860)
---
.../notes/components/delete_confirm_modal.tsx | 29 +++-
.../notes/components/delete_note_button.tsx | 7 +-
.../public/notes/components/notes_list.tsx | 85 ++++++----
.../components/open_event_in_timeline.tsx | 24 ---
.../components/open_flyout_button.test.tsx | 15 +-
.../notes/components/open_flyout_button.tsx | 71 ++++----
.../notes/components/open_timeline_button.tsx | 13 +-
.../public/notes/components/translations.ts | 58 -------
.../public/notes/components/utility_bar.tsx | 32 +++-
.../public/notes/hooks/use_fetch_notes.ts | 15 +-
.../notes/pages/note_management_page.tsx | 155 +++++++++---------
.../public/notes/pages/translations.ts | 27 +--
.../public/notes/store/notes.slice.test.ts | 8 +-
.../public/notes/store/notes.slice.ts | 4 +-
.../translations/translations/fr-FR.json | 5 -
.../translations/translations/ja-JP.json | 5 -
.../translations/translations/zh-CN.json | 5 -
17 files changed, 263 insertions(+), 295 deletions(-)
delete mode 100644 x-pack/plugins/security_solution/public/notes/components/open_event_in_timeline.tsx
delete mode 100644 x-pack/plugins/security_solution/public/notes/components/translations.ts
diff --git a/x-pack/plugins/security_solution/public/notes/components/delete_confirm_modal.tsx b/x-pack/plugins/security_solution/public/notes/components/delete_confirm_modal.tsx
index cba7e81b0fb2b..3c6d6da08e190 100644
--- a/x-pack/plugins/security_solution/public/notes/components/delete_confirm_modal.tsx
+++ b/x-pack/plugins/security_solution/public/notes/components/delete_confirm_modal.tsx
@@ -7,7 +7,7 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { EuiConfirmModal } from '@elastic/eui';
-import * as i18n from './translations';
+import { i18n } from '@kbn/i18n';
import {
deleteNotes,
userClosedDeleteModal,
@@ -16,6 +16,25 @@ import {
ReqStatus,
} from '..';
+export const DELETE = i18n.translate('xpack.securitySolution.notes.management.deleteAction', {
+ defaultMessage: 'Delete',
+});
+export const DELETE_NOTES_CONFIRM = (selectedNotes: number) =>
+ i18n.translate('xpack.securitySolution.notes.management.deleteNotesConfirm', {
+ values: { selectedNotes },
+ defaultMessage:
+ 'Are you sure you want to delete {selectedNotes} {selectedNotes, plural, one {note} other {notes}}?',
+ });
+export const DELETE_NOTES_CANCEL = i18n.translate(
+ 'xpack.securitySolution.notes.management.deleteNotesCancel',
+ {
+ defaultMessage: 'Cancel',
+ }
+);
+
+/**
+ * Renders a confirmation modal to delete notes in the notes management page
+ */
export const DeleteConfirmModal = React.memo(() => {
const dispatch = useDispatch();
const pendingDeleteIds = useSelector(selectNotesTablePendingDeleteIds);
@@ -33,16 +52,16 @@ export const DeleteConfirmModal = React.memo(() => {
return (
- {i18n.DELETE_NOTES_CONFIRM(pendingDeleteIds.length)}
+ {DELETE_NOTES_CONFIRM(pendingDeleteIds.length)}
);
});
diff --git a/x-pack/plugins/security_solution/public/notes/components/delete_note_button.tsx b/x-pack/plugins/security_solution/public/notes/components/delete_note_button.tsx
index 3f9e757d3f5a5..4744c362e469c 100644
--- a/x-pack/plugins/security_solution/public/notes/components/delete_note_button.tsx
+++ b/x-pack/plugins/security_solution/public/notes/components/delete_note_button.tsx
@@ -13,10 +13,10 @@ import { DELETE_NOTE_BUTTON_TEST_ID } from './test_ids';
import type { State } from '../../common/store';
import type { Note } from '../../../common/api/timeline';
import {
- deleteNotes,
ReqStatus,
selectDeleteNotesError,
selectDeleteNotesStatus,
+ userSelectedNotesForDeletion,
} from '../store/notes.slice';
import { useAppToasts } from '../../common/hooks/use_app_toasts';
@@ -42,7 +42,8 @@ export interface DeleteNoteButtonIconProps {
}
/**
- * Renders a button to delete a note
+ * Renders a button to delete a note.
+ * This button works in combination with the DeleteConfirmModal.
*/
export const DeleteNoteButtonIcon = memo(({ note, index }: DeleteNoteButtonIconProps) => {
const dispatch = useDispatch();
@@ -54,8 +55,8 @@ export const DeleteNoteButtonIcon = memo(({ note, index }: DeleteNoteButtonIconP
const deleteNoteFc = useCallback(
(noteId: string) => {
+ dispatch(userSelectedNotesForDeletion(noteId));
setDeletingNoteId(noteId);
- dispatch(deleteNotes({ ids: [noteId] }));
},
[dispatch]
);
diff --git a/x-pack/plugins/security_solution/public/notes/components/notes_list.tsx b/x-pack/plugins/security_solution/public/notes/components/notes_list.tsx
index 47dcf89b06452..344935413731e 100644
--- a/x-pack/plugins/security_solution/public/notes/components/notes_list.tsx
+++ b/x-pack/plugins/security_solution/public/notes/components/notes_list.tsx
@@ -10,6 +10,7 @@ import { EuiAvatar, EuiComment, EuiCommentList, EuiLoadingElastic } from '@elast
import { useSelector } from 'react-redux';
import { FormattedRelative } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
+import { DeleteConfirmModal } from './delete_confirm_modal';
import { OpenFlyoutButtonIcon } from './open_flyout_button';
import { OpenTimelineButtonIcon } from './open_timeline_button';
import { DeleteNoteButtonIcon } from './delete_note_button';
@@ -17,7 +18,11 @@ import { MarkdownRenderer } from '../../common/components/markdown_editor';
import { ADD_NOTE_LOADING_TEST_ID, NOTE_AVATAR_TEST_ID, NOTES_COMMENT_TEST_ID } from './test_ids';
import type { State } from '../../common/store';
import type { Note } from '../../../common/api/timeline';
-import { ReqStatus, selectCreateNoteStatus } from '../store/notes.slice';
+import {
+ ReqStatus,
+ selectCreateNoteStatus,
+ selectNotesTablePendingDeleteIds,
+} from '../store/notes.slice';
import { useUserPrivileges } from '../../common/components/user_privileges';
export const ADDED_A_NOTE = i18n.translate('xpack.securitySolution.notes.addedANoteLabel', {
@@ -59,41 +64,51 @@ export const NotesList = memo(({ notes, options }: NotesListProps) => {
const createStatus = useSelector((state: State) => selectCreateNoteStatus(state));
+ const pendingDeleteIds = useSelector(selectNotesTablePendingDeleteIds);
+ const isDeleteModalVisible = pendingDeleteIds.length > 0;
+
return (
-
- {notes.map((note, index) => (
- {note.created && }>}
- event={ADDED_A_NOTE}
- actions={
- <>
- {note.eventId && !options?.hideFlyoutIcon && (
-
- )}
- {note.timelineId && note.timelineId.length > 0 && !options?.hideTimelineIcon && (
-
- )}
- {canDeleteNotes && }
- >
- }
- timelineAvatar={
-
- }
- >
- {note.note || ''}
-
- ))}
- {createStatus === ReqStatus.Loading && (
-
- )}
-
+ <>
+
+ {notes.map((note, index) => (
+ {note.created && }>}
+ event={ADDED_A_NOTE}
+ actions={
+ <>
+ {note.eventId && !options?.hideFlyoutIcon && (
+
+ )}
+ {note.timelineId && note.timelineId.length > 0 && !options?.hideTimelineIcon && (
+
+ )}
+ {canDeleteNotes && }
+ >
+ }
+ timelineAvatar={
+
+ }
+ >
+ {note.note || ''}
+
+ ))}
+ {createStatus === ReqStatus.Loading && (
+
+ )}
+
+ {isDeleteModalVisible && }
+ >
);
});
diff --git a/x-pack/plugins/security_solution/public/notes/components/open_event_in_timeline.tsx b/x-pack/plugins/security_solution/public/notes/components/open_event_in_timeline.tsx
deleted file mode 100644
index 43f039836ccad..0000000000000
--- a/x-pack/plugins/security_solution/public/notes/components/open_event_in_timeline.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-import React, { memo } from 'react';
-import { EuiLink } from '@elastic/eui';
-import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
-import { useInvestigateInTimeline } from '../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline';
-import * as i18n from './translations';
-
-export const OpenEventInTimeline: React.FC<{ eventId?: string | null }> = memo(({ eventId }) => {
- const ecsRowData = { event: { id: [eventId] }, _id: eventId } as Ecs;
- const { investigateInTimelineAlertClick } = useInvestigateInTimeline({ ecsRowData });
-
- return (
-
- {i18n.VIEW_EVENT_IN_TIMELINE}
-
- );
-});
-
-OpenEventInTimeline.displayName = 'OpenEventInTimeline';
diff --git a/x-pack/plugins/security_solution/public/notes/components/open_flyout_button.test.tsx b/x-pack/plugins/security_solution/public/notes/components/open_flyout_button.test.tsx
index eed5e5bcbd5da..c22a0ebff3fce 100644
--- a/x-pack/plugins/security_solution/public/notes/components/open_flyout_button.test.tsx
+++ b/x-pack/plugins/security_solution/public/notes/components/open_flyout_button.test.tsx
@@ -13,6 +13,7 @@ import { OpenFlyoutButtonIcon } from './open_flyout_button';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { DocumentDetailsRightPanelKey } from '../../flyout/document_details/shared/constants/panel_keys';
import { useSourcererDataView } from '../../sourcerer/containers';
+import { TableId } from '@kbn/securitysolution-data-table';
jest.mock('@kbn/expandable-flyout');
jest.mock('../../sourcerer/containers');
@@ -27,7 +28,11 @@ describe('OpenFlyoutButtonIcon', () => {
const { getByTestId } = render(
-
+
);
@@ -41,7 +46,11 @@ describe('OpenFlyoutButtonIcon', () => {
const { getByTestId } = render(
-
+
);
@@ -54,7 +63,7 @@ describe('OpenFlyoutButtonIcon', () => {
params: {
id: mockEventId,
indexName: 'test1,test2',
- scopeId: mockTimelineId,
+ scopeId: TableId.alertsOnAlertsPage,
},
},
});
diff --git a/x-pack/plugins/security_solution/public/notes/components/open_flyout_button.tsx b/x-pack/plugins/security_solution/public/notes/components/open_flyout_button.tsx
index 0c541cc95740c..34ae9405fdf86 100644
--- a/x-pack/plugins/security_solution/public/notes/components/open_flyout_button.tsx
+++ b/x-pack/plugins/security_solution/public/notes/components/open_flyout_button.tsx
@@ -6,9 +6,11 @@
*/
import React, { memo, useCallback } from 'react';
+import type { IconType } from '@elastic/eui';
import { EuiButtonIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
+import { TableId } from '@kbn/securitysolution-data-table';
import { OPEN_FLYOUT_BUTTON_TEST_ID } from './test_ids';
import { useSourcererDataView } from '../../sourcerer/containers';
import { SourcererScopeName } from '../../sourcerer/store/model';
@@ -31,44 +33,51 @@ export interface OpenFlyoutButtonIconProps {
* Id of the timeline to pass to the flyout for scope
*/
timelineId: string;
+ /**
+ * Icon type to render in the button
+ */
+ iconType: IconType;
}
/**
- * Renders a button to open the alert and event details flyout
+ * Renders a button to open the alert and event details flyout.
+ * This component is meant to be used in timeline and the notes management page, where the cell actions are more basic (no filter in/out).
*/
-export const OpenFlyoutButtonIcon = memo(({ eventId, timelineId }: OpenFlyoutButtonIconProps) => {
- const { selectedPatterns } = useSourcererDataView(SourcererScopeName.timeline);
+export const OpenFlyoutButtonIcon = memo(
+ ({ eventId, timelineId, iconType }: OpenFlyoutButtonIconProps) => {
+ const { selectedPatterns } = useSourcererDataView(SourcererScopeName.timeline);
- const { telemetry } = useKibana().services;
- const { openFlyout } = useExpandableFlyoutApi();
+ const { telemetry } = useKibana().services;
+ const { openFlyout } = useExpandableFlyoutApi();
- const handleClick = useCallback(() => {
- openFlyout({
- right: {
- id: DocumentDetailsRightPanelKey,
- params: {
- id: eventId,
- indexName: selectedPatterns.join(','),
- scopeId: timelineId,
+ const handleClick = useCallback(() => {
+ openFlyout({
+ right: {
+ id: DocumentDetailsRightPanelKey,
+ params: {
+ id: eventId,
+ indexName: selectedPatterns.join(','),
+ scopeId: TableId.alertsOnAlertsPage, // TODO we should update the flyout's code to separate scopeId and preview
+ },
},
- },
- });
- telemetry.reportDetailsFlyoutOpened({
- location: timelineId,
- panel: 'right',
- });
- }, [eventId, openFlyout, selectedPatterns, telemetry, timelineId]);
+ });
+ telemetry.reportDetailsFlyoutOpened({
+ location: timelineId,
+ panel: 'right',
+ });
+ }, [eventId, openFlyout, selectedPatterns, telemetry, timelineId]);
- return (
-
- );
-});
+ return (
+
+ );
+ }
+);
OpenFlyoutButtonIcon.displayName = 'OpenFlyoutButtonIcon';
diff --git a/x-pack/plugins/security_solution/public/notes/components/open_timeline_button.tsx b/x-pack/plugins/security_solution/public/notes/components/open_timeline_button.tsx
index 531983429acd1..b44ffd55a767a 100644
--- a/x-pack/plugins/security_solution/public/notes/components/open_timeline_button.tsx
+++ b/x-pack/plugins/security_solution/public/notes/components/open_timeline_button.tsx
@@ -7,11 +7,16 @@
import React, { memo, useCallback } from 'react';
import { EuiButtonIcon } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
import { useQueryTimelineById } from '../../timelines/components/open_timeline/helpers';
import { OPEN_TIMELINE_BUTTON_TEST_ID } from './test_ids';
import type { Note } from '../../../common/api/timeline';
+const OPEN_TIMELINE = i18n.translate('xpack.securitySolution.notes.management.openTimelineButton', {
+ defaultMessage: 'Open saved timeline',
+});
+
export interface OpenTimelineButtonIconProps {
/**
* The note that contains the id of the timeline to open
@@ -20,7 +25,7 @@ export interface OpenTimelineButtonIconProps {
/**
* The index of the note in the list of notes (used to have unique data-test-subj)
*/
- index: number;
+ index?: number;
}
/**
@@ -47,10 +52,10 @@ export const OpenTimelineButtonIcon = memo(({ note, index }: OpenTimelineButtonI
return (
openTimeline(note)}
/>
);
diff --git a/x-pack/plugins/security_solution/public/notes/components/translations.ts b/x-pack/plugins/security_solution/public/notes/components/translations.ts
deleted file mode 100644
index 8d7a5b4262815..0000000000000
--- a/x-pack/plugins/security_solution/public/notes/components/translations.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { i18n } from '@kbn/i18n';
-
-export const BATCH_ACTIONS = i18n.translate(
- 'xpack.securitySolution.notes.management.batchActionsTitle',
- {
- defaultMessage: 'Bulk actions',
- }
-);
-
-export const DELETE = i18n.translate('xpack.securitySolution.notes.management.deleteAction', {
- defaultMessage: 'Delete',
-});
-
-export const DELETE_NOTES_MODAL_TITLE = i18n.translate(
- 'xpack.securitySolution.notes.management.deleteNotesModalTitle',
- {
- defaultMessage: 'Delete notes?',
- }
-);
-
-export const DELETE_NOTES_CONFIRM = (selectedNotes: number) =>
- i18n.translate('xpack.securitySolution.notes.management.deleteNotesConfirm', {
- values: { selectedNotes },
- defaultMessage:
- 'Are you sure you want to delete {selectedNotes} {selectedNotes, plural, one {note} other {notes}}?',
- });
-
-export const DELETE_NOTES_CANCEL = i18n.translate(
- 'xpack.securitySolution.notes.management.deleteNotesCancel',
- {
- defaultMessage: 'Cancel',
- }
-);
-
-export const DELETE_SELECTED = i18n.translate(
- 'xpack.securitySolution.notes.management.deleteSelected',
- {
- defaultMessage: 'Delete selected notes',
- }
-);
-
-export const REFRESH = i18n.translate('xpack.securitySolution.notes.management.refresh', {
- defaultMessage: 'Refresh',
-});
-
-export const VIEW_EVENT_IN_TIMELINE = i18n.translate(
- 'xpack.securitySolution.notes.management.viewEventInTimeline',
- {
- defaultMessage: 'View event in timeline',
- }
-);
diff --git a/x-pack/plugins/security_solution/public/notes/components/utility_bar.tsx b/x-pack/plugins/security_solution/public/notes/components/utility_bar.tsx
index 0c09f6393f668..f0a337cb6c217 100644
--- a/x-pack/plugins/security_solution/public/notes/components/utility_bar.tsx
+++ b/x-pack/plugins/security_solution/public/notes/components/utility_bar.tsx
@@ -4,9 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+
import React, { useMemo, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { EuiContextMenuItem } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import {
UtilityBarGroup,
UtilityBarText,
@@ -22,8 +24,28 @@ import {
selectNotesTableSearch,
userSelectedBulkDelete,
} from '..';
-import * as i18n from './translations';
+export const BATCH_ACTIONS = i18n.translate(
+ 'xpack.securitySolution.notes.management.batchActionsTitle',
+ {
+ defaultMessage: 'Bulk actions',
+ }
+);
+
+export const DELETE_SELECTED = i18n.translate(
+ 'xpack.securitySolution.notes.management.deleteSelected',
+ {
+ defaultMessage: 'Delete selected notes',
+ }
+);
+
+export const REFRESH = i18n.translate('xpack.securitySolution.notes.management.refresh', {
+ defaultMessage: 'Refresh',
+});
+
+/**
+ * Renders the utility bar for the notes management page
+ */
export const NotesUtilityBar = React.memo(() => {
const dispatch = useDispatch();
const pagination = useSelector(selectNotesPagination);
@@ -49,7 +71,7 @@ export const NotesUtilityBar = React.memo(() => {
icon="trash"
key="DeleteItemKey"
>
- {i18n.DELETE_SELECTED}
+ {DELETE_SELECTED}
);
}, [deleteSelectedNotes, selectedItems.length]);
@@ -83,9 +105,7 @@ export const NotesUtilityBar = React.memo(() => {
iconType="arrowDown"
popoverContent={BulkActionPopoverContent}
>
-
- {i18n.BATCH_ACTIONS}
-
+ {BATCH_ACTIONS}
{
iconType="refresh"
onClick={refresh}
>
- {i18n.REFRESH}
+ {REFRESH}
diff --git a/x-pack/plugins/security_solution/public/notes/hooks/use_fetch_notes.ts b/x-pack/plugins/security_solution/public/notes/hooks/use_fetch_notes.ts
index c9f64bc382454..2cf599e76bcc9 100644
--- a/x-pack/plugins/security_solution/public/notes/hooks/use_fetch_notes.ts
+++ b/x-pack/plugins/security_solution/public/notes/hooks/use_fetch_notes.ts
@@ -4,12 +4,23 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+
import { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
-import { fetchNotesByDocumentIds } from '..';
+import { fetchNotesByDocumentIds } from '../store/notes.slice';
+
+export interface UseFetchNotesResult {
+ /**
+ * Function to fetch the notes for an array of documents
+ */
+ onLoad: (events: Array>) => void;
+}
-export const useFetchNotes = () => {
+/**
+ * Hook that returns a function to fetch the notes for an array of documents
+ */
+export const useFetchNotes = (): UseFetchNotesResult => {
const dispatch = useDispatch();
const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled(
'securitySolutionNotesEnabled'
diff --git a/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx b/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx
index ddfed3fbb6287..9c2900ca4d599 100644
--- a/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx
+++ b/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx
@@ -6,11 +6,18 @@
*/
import React, { useCallback, useMemo, useEffect } from 'react';
-import type { DefaultItemAction, EuiBasicTableColumn } from '@elastic/eui';
-import { EuiBasicTable, EuiEmptyPrompt, EuiLink, EuiSpacer } from '@elastic/eui';
+import type { EuiBasicTableColumn } from '@elastic/eui';
+import {
+ EuiAvatar,
+ EuiBasicTable,
+ EuiEmptyPrompt,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+} from '@elastic/eui';
import { useDispatch, useSelector } from 'react-redux';
-import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
-import { useQueryTimelineById } from '../../timelines/components/open_timeline/helpers';
+import { css } from '@emotion/react';
+import { DeleteNoteButtonIcon } from '../components/delete_note_button';
import { Title } from '../../common/components/header_page/title';
// TODO unify this type from the api with the one in public/common/lib/note
import type { Note } from '../../../common/api/timeline';
@@ -27,7 +34,6 @@ import {
selectNotesTableSearch,
selectFetchNotesStatus,
selectNotesTablePendingDeleteIds,
- userSelectedRowForDeletion,
selectFetchNotesError,
ReqStatus,
} from '..';
@@ -36,42 +42,67 @@ import { SearchRow } from '../components/search_row';
import { NotesUtilityBar } from '../components/utility_bar';
import { DeleteConfirmModal } from '../components/delete_confirm_modal';
import * as i18n from './translations';
-import { OpenEventInTimeline } from '../components/open_event_in_timeline';
-
-const columns: (
- onOpenTimeline: (timelineId: string) => void
-) => Array> = (onOpenTimeline) => {
- return [
- {
- field: 'created',
- name: i18n.CREATED_COLUMN,
- sortable: true,
- render: (created: Note['created']) => ,
- },
- {
- field: 'createdBy',
- name: i18n.CREATED_BY_COLUMN,
- },
- {
- field: 'eventId',
- name: i18n.EVENT_ID_COLUMN,
- sortable: true,
- render: (eventId: Note['eventId']) => ,
- },
- {
- field: 'timelineId',
- name: i18n.TIMELINE_ID_COLUMN,
- render: (timelineId: Note['timelineId']) =>
- timelineId ? (
- onOpenTimeline(timelineId)}>{i18n.OPEN_TIMELINE}
- ) : null,
- },
- {
- field: 'note',
- name: i18n.NOTE_CONTENT_COLUMN,
- },
- ];
-};
+import { OpenFlyoutButtonIcon } from '../components/open_flyout_button';
+import { OpenTimelineButtonIcon } from '../components/open_timeline_button';
+
+const columns: Array> = [
+ {
+ name: i18n.ACTIONS_COLUMN,
+ render: (note: Note) => (
+
+
+ {note.eventId ? (
+
+ ) : null}
+
+
+ <>{note.timelineId ? : null}>
+
+
+
+
+
+ ),
+ width: '72px',
+ },
+ {
+ field: 'createdBy',
+ name: i18n.CREATED_BY_COLUMN,
+ render: (createdBy: Note['createdBy']) => ,
+ width: '100px',
+ align: 'center',
+ },
+ {
+ field: 'note',
+ name: i18n.NOTE_CONTENT_COLUMN,
+ },
+ {
+ field: 'created',
+ name: i18n.CREATED_COLUMN,
+ sortable: true,
+ render: (created: Note['created']) => ,
+ width: '225px',
+ },
+];
const pageSizeOptions = [10, 25, 50, 100];
@@ -129,13 +160,6 @@ export const NoteManagementPage = () => {
[dispatch]
);
- const selectRowForDeletion = useCallback(
- (id: string) => {
- dispatch(userSelectedRowForDeletion(id));
- },
- [dispatch]
- );
-
const onSelectionChange = useCallback(
(selection: Note[]) => {
const rowIds = selection.map((item) => item.noteId);
@@ -148,39 +172,6 @@ export const NoteManagementPage = () => {
return item.noteId;
}, []);
- const unifiedComponentsInTimelineDisabled = useIsExperimentalFeatureEnabled(
- 'unifiedComponentsInTimelineDisabled'
- );
- const queryTimelineById = useQueryTimelineById();
- const openTimeline = useCallback(
- (timelineId: string) =>
- queryTimelineById({
- timelineId,
- unifiedComponentsInTimelineDisabled,
- }),
- [queryTimelineById, unifiedComponentsInTimelineDisabled]
- );
-
- const columnWithActions = useMemo(() => {
- const actions: Array> = [
- {
- name: i18n.DELETE,
- description: i18n.DELETE_SINGLE_NOTE_DESCRIPTION,
- color: 'primary',
- icon: 'trash',
- type: 'icon',
- onClick: (note: Note) => selectRowForDeletion(note.noteId),
- },
- ];
- return [
- ...columns(openTimeline),
- {
- name: 'actions',
- actions,
- },
- ];
- }, [selectRowForDeletion, openTimeline]);
-
const currentPagination = useMemo(() => {
return {
pageIndex: pagination.page - 1,
@@ -223,7 +214,7 @@ export const NoteManagementPage = () => {
{
});
});
- describe('userSelectedRowForDeletion', () => {
- it('should set correct id when user selects a row', () => {
- const action = { type: userSelectedRowForDeletion.type, payload: '1' };
+ describe('userSelectedNotesForDeletion', () => {
+ it('should set correct id when user selects a note to delete', () => {
+ const action = { type: userSelectedNotesForDeletion.type, payload: '1' };
expect(notesReducer(initalEmptyState, action)).toEqual({
...initalEmptyState,
diff --git a/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts b/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts
index 6732f9491676e..2d24ab838ee06 100644
--- a/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts
+++ b/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts
@@ -193,7 +193,7 @@ const notesSlice = createSlice({
userClosedDeleteModal: (state) => {
state.pendingDeleteIds = [];
},
- userSelectedRowForDeletion: (state, action: { payload: string }) => {
+ userSelectedNotesForDeletion: (state, action: { payload: string }) => {
state.pendingDeleteIds = [action.payload];
},
userSelectedBulkDelete: (state) => {
@@ -391,6 +391,6 @@ export const {
userSearchedNotes,
userSelectedRow,
userClosedDeleteModal,
- userSelectedRowForDeletion,
+ userSelectedNotesForDeletion,
userSelectedBulkDelete,
} = notesSlice.actions;
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 3b078d6bb8a90..c441b91d23e31 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -39618,18 +39618,13 @@
"xpack.securitySolution.notes.management.createdByColumnTitle": "Créé par",
"xpack.securitySolution.notes.management.createdColumnTitle": "Créé",
"xpack.securitySolution.notes.management.deleteAction": "Supprimer",
- "xpack.securitySolution.notes.management.deleteDescription": "Supprimer cette note",
"xpack.securitySolution.notes.management.deleteNotesCancel": "Annuler",
"xpack.securitySolution.notes.management.deleteNotesConfirm": "Voulez-vous vraiment supprimer {selectedNotes} {selectedNotes, plural, one {note} other {notes}} ?",
- "xpack.securitySolution.notes.management.deleteNotesModalTitle": "Supprimer les notes ?",
"xpack.securitySolution.notes.management.deleteSelected": "Supprimer les notes sélectionnées",
- "xpack.securitySolution.notes.management.eventIdColumnTitle": "Afficher le document",
"xpack.securitySolution.notes.management.noteContentColumnTitle": "Contenu de la note",
"xpack.securitySolution.notes.management.openTimeline": "Ouvrir la chronologie",
"xpack.securitySolution.notes.management.refresh": "Actualiser",
"xpack.securitySolution.notes.management.tableError": "Impossible de charger les notes",
- "xpack.securitySolution.notes.management.timelineColumnTitle": "Chronologie",
- "xpack.securitySolution.notes.management.viewEventInTimeline": "Afficher l'événement dans la chronologie",
"xpack.securitySolution.notes.noteLabel": "Note",
"xpack.securitySolution.notes.notesTitle": "Notes",
"xpack.securitySolution.notes.search.FilterByUserOrNotePlaceholder": "Filtre par utilisateur ou note",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index e579f87771b20..93e7ab2270f4c 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -39362,18 +39362,13 @@
"xpack.securitySolution.notes.management.createdByColumnTitle": "作成者",
"xpack.securitySolution.notes.management.createdColumnTitle": "作成済み",
"xpack.securitySolution.notes.management.deleteAction": "削除",
- "xpack.securitySolution.notes.management.deleteDescription": "このメモを削除",
"xpack.securitySolution.notes.management.deleteNotesCancel": "キャンセル",
"xpack.securitySolution.notes.management.deleteNotesConfirm": "{selectedNotes} {selectedNotes, plural, other {件のメモ}}を削除しますか?",
- "xpack.securitySolution.notes.management.deleteNotesModalTitle": "メモを削除しますか?",
"xpack.securitySolution.notes.management.deleteSelected": "選択したメモを削除",
- "xpack.securitySolution.notes.management.eventIdColumnTitle": "ドキュメンテーションを表示",
"xpack.securitySolution.notes.management.noteContentColumnTitle": "メモコンテンツ",
"xpack.securitySolution.notes.management.openTimeline": "タイムラインを開く",
"xpack.securitySolution.notes.management.refresh": "更新",
"xpack.securitySolution.notes.management.tableError": "メモを読み込めません",
- "xpack.securitySolution.notes.management.timelineColumnTitle": "Timeline",
- "xpack.securitySolution.notes.management.viewEventInTimeline": "タイムラインでイベントを表示",
"xpack.securitySolution.notes.noteLabel": "注",
"xpack.securitySolution.notes.notesTitle": "メモ",
"xpack.securitySolution.notes.search.FilterByUserOrNotePlaceholder": "ユーザーまたはメモでフィルター",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 09662465c4833..6f81b20c89d2b 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -39407,18 +39407,13 @@
"xpack.securitySolution.notes.management.createdByColumnTitle": "创建者",
"xpack.securitySolution.notes.management.createdColumnTitle": "创建时间",
"xpack.securitySolution.notes.management.deleteAction": "删除",
- "xpack.securitySolution.notes.management.deleteDescription": "删除此备注",
"xpack.securitySolution.notes.management.deleteNotesCancel": "取消",
"xpack.securitySolution.notes.management.deleteNotesConfirm": "是否确定要删除 {selectedNotes} 个{selectedNotes, plural, other {备注}}?",
- "xpack.securitySolution.notes.management.deleteNotesModalTitle": "删除备注?",
"xpack.securitySolution.notes.management.deleteSelected": "删除所选备注",
- "xpack.securitySolution.notes.management.eventIdColumnTitle": "查看文档",
"xpack.securitySolution.notes.management.noteContentColumnTitle": "备注内容",
"xpack.securitySolution.notes.management.openTimeline": "打开时间线",
"xpack.securitySolution.notes.management.refresh": "刷新",
"xpack.securitySolution.notes.management.tableError": "无法加载备注",
- "xpack.securitySolution.notes.management.timelineColumnTitle": "时间线",
- "xpack.securitySolution.notes.management.viewEventInTimeline": "在时间线中查看事件",
"xpack.securitySolution.notes.noteLabel": "备注",
"xpack.securitySolution.notes.notesTitle": "备注",
"xpack.securitySolution.notes.search.FilterByUserOrNotePlaceholder": "按用户或备注筛选",
From 56e1e68b307bc62445f04ae6f443f4854a308547 Mon Sep 17 00:00:00 2001
From: Tim Sullivan
Date: Wed, 9 Oct 2024 14:44:30 -0700
Subject: [PATCH 22/87] [ES|QL] Present ES|QL as an equal to data views on the
"no data views" screen (#194077)
## Summary
Resolves https://github.com/elastic/kibana/issues/176291
### Screenshots
#### Discover/Dashboard/Visualize
#### Stack Management > Data view
#### If User does not have privilege to create a Data View
### Checklist
Delete any items that are not applicable to this PR.
- [x] Use a new SVG resource for the ES|QL illustration
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Liam Thompson <32779855+leemthompo@users.noreply.github.com>
Co-authored-by: Andrea Del Rio
---
.../src/analytics_no_data_page.component.tsx | 12 +-
.../src/analytics_no_data_page.stories.tsx | 4 +-
.../impl/src/analytics_no_data_page.test.tsx | 95 +-
.../impl/src/analytics_no_data_page.tsx | 4 +-
.../page/analytics_no_data/impl/tsconfig.json | 1 +
.../analytics_no_data/mocks/src/storybook.ts | 18 +-
.../page/analytics_no_data/types/index.d.ts | 2 +
.../impl/src/kibana_no_data_page.tsx | 6 +-
.../page/kibana_no_data/types/index.d.ts | 2 +
.../prompt/no_data_views/impl/src/actions.tsx | 76 -
.../impl/src/data_view_illustration.tsx | 11 +-
.../impl/src/documentation_link.tsx | 5 +-
.../impl/src/esql_illustration.svg | 1600 +++++++++++++++++
.../impl/src/esql_illustration.tsx | 24 +
.../impl/src/no_data_views.component.test.tsx | 50 +-
.../impl/src/no_data_views.component.tsx | 261 ++-
.../no_data_views/impl/src/no_data_views.tsx | 7 +-
.../prompt/no_data_views/impl/tsconfig.json | 2 -
.../no_data_views/mocks/src/storybook.ts | 14 +-
.../prompt/no_data_views/types/index.d.ts | 8 +-
src/plugins/data_view_management/kibana.jsonc | 1 +
.../index_pattern_table.tsx | 11 +-
.../mount_management_section.tsx | 44 +-
.../data_view_management/public/plugin.ts | 2 +
.../data_view_management/public/types.ts | 2 +
.../data_view_management/tsconfig.json | 1 +
.../group6/dashboard_esql_no_data.ts | 33 +
.../functional/apps/dashboard/group6/index.ts | 1 +
.../apps/management/data_views/_try_esql.ts | 34 +
test/functional/apps/management/index.ts | 1 +
test/functional/page_objects/discover_page.ts | 6 +
test/functional/services/esql.ts | 7 +
.../translations/translations/fr-FR.json | 5 -
.../translations/translations/ja-JP.json | 5 -
.../translations/translations/zh-CN.json | 5 -
.../data_views/feature_controls/security.ts | 6 +-
36 files changed, 2132 insertions(+), 234 deletions(-)
delete mode 100644 packages/shared-ux/prompt/no_data_views/impl/src/actions.tsx
create mode 100644 packages/shared-ux/prompt/no_data_views/impl/src/esql_illustration.svg
create mode 100644 packages/shared-ux/prompt/no_data_views/impl/src/esql_illustration.tsx
create mode 100644 test/functional/apps/dashboard/group6/dashboard_esql_no_data.ts
create mode 100644 test/functional/apps/management/data_views/_try_esql.ts
diff --git a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.component.tsx b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.component.tsx
index 41c525c5ca0b0..16d1bebd46548 100644
--- a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.component.tsx
+++ b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.component.tsx
@@ -22,12 +22,14 @@ import { getHasApiKeys$ } from '../lib/get_has_api_keys';
export interface Props {
/** Handler for successfully creating a new data view. */
onDataViewCreated: (dataView: unknown) => void;
- /** Handler for when try ES|QL is clicked and user has been navigated to try ES|QL in discover. */
- onESQLNavigationComplete?: () => void;
/** if set to true allows creation of an ad-hoc dataview from data view editor */
allowAdHocDataView?: boolean;
/** if the kibana instance is customly branded */
showPlainSpinner: boolean;
+ /** If the cluster has data, this handler allows the user to try ES|QL */
+ onTryESQL?: () => void;
+ /** Handler for when try ES|QL is clicked and user has been navigated to try ES|QL in discover. */
+ onESQLNavigationComplete?: () => void;
}
type AnalyticsNoDataPageProps = Props &
@@ -119,9 +121,10 @@ const flavors: {
*/
export const AnalyticsNoDataPage: React.FC = ({
onDataViewCreated,
- onESQLNavigationComplete,
allowAdHocDataView,
showPlainSpinner,
+ onTryESQL,
+ onESQLNavigationComplete,
...services
}) => {
const { prependBasePath, kibanaGuideDocLink, getHttp: get, pageFlavor } = services;
@@ -138,8 +141,9 @@ export const AnalyticsNoDataPage: React.FC = ({
{...{
noDataConfig,
onDataViewCreated,
- onESQLNavigationComplete,
allowAdHocDataView,
+ onTryESQL,
+ onESQLNavigationComplete,
showPlainSpinner,
}}
/>
diff --git a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.stories.tsx b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.stories.tsx
index 3c75cefb38cb2..fa251cb03bdbe 100644
--- a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.stories.tsx
+++ b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.stories.tsx
@@ -29,8 +29,8 @@ export default {
export const Analytics = (params: AnalyticsNoDataPageStorybookParams) => {
return (
-
-
+
+
);
};
diff --git a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.test.tsx b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.test.tsx
index 543c1c4817c5b..6b2d3441ed0d1 100644
--- a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.test.tsx
+++ b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.test.tsx
@@ -14,6 +14,7 @@ import {
getAnalyticsNoDataPageServicesMock,
getAnalyticsNoDataPageServicesMockWithCustomBranding,
} from '@kbn/shared-ux-page-analytics-no-data-mocks';
+import { NoDataViewsPrompt } from '@kbn/shared-ux-prompt-no-data-views';
import { AnalyticsNoDataPageProvider } from './services';
import { AnalyticsNoDataPage as Component } from './analytics_no_data_page.component';
@@ -29,28 +30,86 @@ describe('AnalyticsNoDataPage', () => {
jest.resetAllMocks();
});
- it('renders correctly', async () => {
- const component = mountWithIntl(
-
-
-
- );
+ describe('loading state', () => {
+ it('renders correctly', async () => {
+ const component = mountWithIntl(
+
+
+
+ );
- await act(() => new Promise(setImmediate));
+ await act(() => new Promise(setImmediate));
- expect(component.find(Component).length).toBe(1);
- expect(component.find(Component).props().onDataViewCreated).toBe(onDataViewCreated);
- expect(component.find(Component).props().allowAdHocDataView).toBe(true);
+ expect(component.find(Component).length).toBe(1);
+ expect(component.find(Component).props().onDataViewCreated).toBe(onDataViewCreated);
+ expect(component.find(Component).props().allowAdHocDataView).toBe(true);
+ });
+
+ it('passes correct boolean value to showPlainSpinner', async () => {
+ const component = mountWithIntl(
+
+
+
+ );
+
+ await act(async () => {
+ component.update();
+ });
+
+ expect(component.find(Component).length).toBe(1);
+ expect(component.find(Component).props().showPlainSpinner).toBe(true);
+ });
});
- it('passes correct boolean value to showPlainSpinner', () => {
- const component = mountWithIntl(
-
-
-
- );
+ describe('with ES data', () => {
+ jest.spyOn(services, 'hasESData').mockResolvedValue(true);
+ jest.spyOn(services, 'hasUserDataView').mockResolvedValue(false);
+
+ it('renders the prompt to create a data view', async () => {
+ const onTryESQL = jest.fn();
+
+ await act(async () => {
+ const component = mountWithIntl(
+
+
+
+ );
+
+ await new Promise(setImmediate);
+ component.update();
+
+ expect(component.find(Component).length).toBe(1);
+ expect(component.find(NoDataViewsPrompt).length).toBe(1);
+ });
+ });
+
+ it('renders the prompt to create a data view with a custom onTryESQL action', async () => {
+ const onTryESQL = jest.fn();
+
+ await act(async () => {
+ const component = mountWithIntl(
+
+
+
+ );
+
+ await new Promise(setImmediate);
+ component.update();
+
+ const tryESQLLink = component.find('button[data-test-subj="tryESQLLink"]');
+ expect(tryESQLLink.length).toBe(1);
+ tryESQLLink.simulate('click');
- expect(component.find(Component).length).toBe(1);
- expect(component.find(Component).props().showPlainSpinner).toBe(true);
+ expect(onTryESQL).toHaveBeenCalled();
+ });
+ });
});
});
diff --git a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.tsx b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.tsx
index b64a296bbf74a..f7c80705daa58 100644
--- a/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.tsx
+++ b/packages/shared-ux/page/analytics_no_data/impl/src/analytics_no_data_page.tsx
@@ -20,8 +20,9 @@ import { AnalyticsNoDataPage as Component } from './analytics_no_data_page.compo
*/
export const AnalyticsNoDataPage = ({
onDataViewCreated,
- onESQLNavigationComplete,
allowAdHocDataView,
+ onTryESQL,
+ onESQLNavigationComplete,
}: AnalyticsNoDataPageProps) => {
const { customBranding, ...services } = useServices();
const showPlainSpinner = useObservable(customBranding.hasCustomBranding$) ?? false;
@@ -33,6 +34,7 @@ export const AnalyticsNoDataPage = ({
allowAdHocDataView={allowAdHocDataView}
onDataViewCreated={onDataViewCreated}
onESQLNavigationComplete={onESQLNavigationComplete}
+ onTryESQL={onTryESQL}
/>
);
};
diff --git a/packages/shared-ux/page/analytics_no_data/impl/tsconfig.json b/packages/shared-ux/page/analytics_no_data/impl/tsconfig.json
index 659aacfd3874d..ba872e1ecd761 100644
--- a/packages/shared-ux/page/analytics_no_data/impl/tsconfig.json
+++ b/packages/shared-ux/page/analytics_no_data/impl/tsconfig.json
@@ -23,6 +23,7 @@
"@kbn/i18n-react",
"@kbn/core-http-browser",
"@kbn/core-http-browser-mocks",
+ "@kbn/shared-ux-prompt-no-data-views",
],
"exclude": [
"target/**/*",
diff --git a/packages/shared-ux/page/analytics_no_data/mocks/src/storybook.ts b/packages/shared-ux/page/analytics_no_data/mocks/src/storybook.ts
index c664bb192518c..f8cca693a072c 100644
--- a/packages/shared-ux/page/analytics_no_data/mocks/src/storybook.ts
+++ b/packages/shared-ux/page/analytics_no_data/mocks/src/storybook.ts
@@ -18,9 +18,14 @@ import type {
} from '@kbn/shared-ux-page-analytics-no-data-types';
import { of } from 'rxjs';
+interface PropArguments {
+ useCustomOnTryESQL: boolean;
+}
+
type ServiceArguments = Pick;
-export type Params = ArgumentParams<{}, ServiceArguments> & KibanaNoDataPageStorybookParams;
+export type Params = ArgumentParams &
+ KibanaNoDataPageStorybookParams;
const kibanaNoDataMock = new KibanaNoDataPageStorybookMock();
@@ -30,7 +35,13 @@ export class StorybookMock extends AbstractStorybookMock<
{},
ServiceArguments
> {
- propArguments = {};
+ propArguments = {
+ // requires hasESData to be toggled to true
+ useCustomOnTryESQL: {
+ control: 'boolean',
+ defaultValue: false,
+ },
+ };
serviceArguments = {
kibanaGuideDocLink: {
control: 'text',
@@ -59,9 +70,10 @@ export class StorybookMock extends AbstractStorybookMock<
};
}
- getProps() {
+ getProps(params: Params) {
return {
onDataViewCreated: action('onDataViewCreated'),
+ onTryESQL: params.useCustomOnTryESQL ? action('onTryESQL-from-props') : undefined,
};
}
}
diff --git a/packages/shared-ux/page/analytics_no_data/types/index.d.ts b/packages/shared-ux/page/analytics_no_data/types/index.d.ts
index 9fd6653a48b6a..94bf85500da6b 100644
--- a/packages/shared-ux/page/analytics_no_data/types/index.d.ts
+++ b/packages/shared-ux/page/analytics_no_data/types/index.d.ts
@@ -70,6 +70,8 @@ export interface AnalyticsNoDataPageProps {
onDataViewCreated: (dataView: unknown) => void;
/** if set to true allows creation of an ad-hoc data view from data view editor */
allowAdHocDataView?: boolean;
+ /** If the cluster has data, this handler allows the user to try ES|QL */
+ onTryESQL?: () => void;
/** Handler for when try ES|QL is clicked and user has been navigated to try ES|QL in discover. */
onESQLNavigationComplete?: () => void;
}
diff --git a/packages/shared-ux/page/kibana_no_data/impl/src/kibana_no_data_page.tsx b/packages/shared-ux/page/kibana_no_data/impl/src/kibana_no_data_page.tsx
index 2042d7fa1420d..d74c3aabd5662 100644
--- a/packages/shared-ux/page/kibana_no_data/impl/src/kibana_no_data_page.tsx
+++ b/packages/shared-ux/page/kibana_no_data/impl/src/kibana_no_data_page.tsx
@@ -20,9 +20,10 @@ import { useServices } from './services';
*/
export const KibanaNoDataPage = ({
onDataViewCreated,
- onESQLNavigationComplete,
noDataConfig,
allowAdHocDataView,
+ onTryESQL,
+ onESQLNavigationComplete,
showPlainSpinner,
}: KibanaNoDataPageProps) => {
// These hooks are temporary, until this component is moved to a package.
@@ -58,8 +59,9 @@ export const KibanaNoDataPage = ({
return (
);
}
diff --git a/packages/shared-ux/page/kibana_no_data/types/index.d.ts b/packages/shared-ux/page/kibana_no_data/types/index.d.ts
index 56067e9d555f9..c391149f7efaa 100644
--- a/packages/shared-ux/page/kibana_no_data/types/index.d.ts
+++ b/packages/shared-ux/page/kibana_no_data/types/index.d.ts
@@ -60,6 +60,8 @@ export interface KibanaNoDataPageProps {
allowAdHocDataView?: boolean;
/** Set to true if the kibana is customly branded */
showPlainSpinner: boolean;
+ /** If the cluster has data, this handler allows the user to try ES|QL */
+ onTryESQL?: () => void;
/** Handler for when try ES|QL is clicked and user has been navigated to try ES|QL in discover. */
onESQLNavigationComplete?: () => void;
}
diff --git a/packages/shared-ux/prompt/no_data_views/impl/src/actions.tsx b/packages/shared-ux/prompt/no_data_views/impl/src/actions.tsx
deleted file mode 100644
index 6f2af97df6e04..0000000000000
--- a/packages/shared-ux/prompt/no_data_views/impl/src/actions.tsx
+++ /dev/null
@@ -1,76 +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", 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 { EuiButton, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n-react';
-import React from 'react';
-
-interface NoDataButtonProps {
- onClickCreate: (() => void) | undefined;
- canCreateNewDataView: boolean;
- onTryESQL?: () => void;
- esqlDocLink?: string;
-}
-
-const createDataViewText = i18n.translate('sharedUXPackages.noDataViewsPrompt.addDataViewText', {
- defaultMessage: 'Create data view',
-});
-
-export const NoDataButtonLink = ({
- onClickCreate,
- canCreateNewDataView,
- onTryESQL,
- esqlDocLink,
-}: NoDataButtonProps) => {
- if (!onTryESQL && !canCreateNewDataView) {
- return null;
- }
-
- return (
- <>
- {canCreateNewDataView && (
-
- {createDataViewText}
-
- )}
- {canCreateNewDataView && onTryESQL && }
- {onTryESQL && (
-
-
-
-
- ),
- }}
- />
-
-
-
-
-
- )}
- >
- );
-};
diff --git a/packages/shared-ux/prompt/no_data_views/impl/src/data_view_illustration.tsx b/packages/shared-ux/prompt/no_data_views/impl/src/data_view_illustration.tsx
index cb817225254a9..099cdc87a21eb 100644
--- a/packages/shared-ux/prompt/no_data_views/impl/src/data_view_illustration.tsx
+++ b/packages/shared-ux/prompt/no_data_views/impl/src/data_view_illustration.tsx
@@ -26,5 +26,14 @@ export const DataViewIllustration = () => {
}
`;
- return ;
+ return (
+
+ );
};
diff --git a/packages/shared-ux/prompt/no_data_views/impl/src/documentation_link.tsx b/packages/shared-ux/prompt/no_data_views/impl/src/documentation_link.tsx
index 8e74bead6922e..d190764af947d 100644
--- a/packages/shared-ux/prompt/no_data_views/impl/src/documentation_link.tsx
+++ b/packages/shared-ux/prompt/no_data_views/impl/src/documentation_link.tsx
@@ -13,9 +13,10 @@ import { FormattedMessage } from '@kbn/i18n-react';
interface Props {
href: string;
+ ['data-test-subj']?: string;
}
-export function DocumentationLink({ href }: Props) {
+export function DocumentationLink({ href, ['data-test-subj']: dataTestSubj }: Props) {
return (
@@ -28,7 +29,7 @@ export function DocumentationLink({ href }: Props) {
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/shared-ux/prompt/no_data_views/impl/src/esql_illustration.tsx b/packages/shared-ux/prompt/no_data_views/impl/src/esql_illustration.tsx
new file mode 100644
index 0000000000000..a2da4c416ed55
--- /dev/null
+++ b/packages/shared-ux/prompt/no_data_views/impl/src/esql_illustration.tsx
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", 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 React from 'react';
+
+import png from './esql_illustration.svg';
+
+export const EsqlIllustration = () => {
+ return (
+
+ );
+};
diff --git a/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.component.test.tsx b/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.component.test.tsx
index ad2e176a511f0..75363c80b67b5 100644
--- a/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.component.test.tsx
+++ b/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.component.test.tsx
@@ -9,7 +9,7 @@
import React from 'react';
import { mountWithIntl } from '@kbn/test-jest-helpers';
-import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
+import { EuiButton, EuiCard } from '@elastic/eui';
import { NoDataViewsPrompt } from './no_data_views.component';
import { DocumentationLink } from './documentation_link';
@@ -19,36 +19,64 @@ describe(' ', () => {
);
- expect(component.find(EuiEmptyPrompt).length).toBe(1);
- expect(component.find(EuiButton).length).toBe(1);
- expect(component.find(DocumentationLink).length).toBe(1);
+ expect(component.find(EuiCard).length).toBe(2);
+ expect(component.find(EuiButton).length).toBe(2);
+ expect(component.find(DocumentationLink).length).toBe(2);
+
+ expect(component.find('EuiButton[data-test-subj="createDataViewButton"]').length).toBe(1);
+ expect(component.find('DocumentationLink[data-test-subj="docLinkDataViews"]').length).toBe(1);
+
+ expect(component.find('EuiButton[data-test-subj="tryESQLLink"]').length).toBe(1);
+ expect(component.find('DocumentationLink[data-test-subj="docLinkEsql"]').length).toBe(1);
});
- test('does not render button if canCreateNewDataViews is false', () => {
+ test('does not render "Create data view" button if canCreateNewDataViews is false', () => {
const component = mountWithIntl( );
- expect(component.find(EuiButton).length).toBe(0);
+ expect(component.find('EuiButton[data-test-subj="createDataViewButton"]').length).toBe(0);
});
- test('does not documentation link if linkToDocumentation is not provided', () => {
+ test('does not render documentation links if links to documentation are not provided', () => {
const component = mountWithIntl(
);
- expect(component.find(DocumentationLink).length).toBe(0);
+ expect(component.find('DocumentationLink[data-test-subj="docLinkDataViews"]').length).toBe(0);
+ expect(component.find('DocumentationLink[data-test-subj="docLinkEsql"]').length).toBe(0);
});
test('onClickCreate', () => {
const onClickCreate = jest.fn();
const component = mountWithIntl(
-
+
);
- component.find('button').simulate('click');
+ component.find('button[data-test-subj="createDataViewButton"]').simulate('click');
expect(onClickCreate).toHaveBeenCalledTimes(1);
});
+
+ test('onClickTryEsql', () => {
+ const onClickTryEsql = jest.fn();
+ const component = mountWithIntl(
+
+ );
+
+ component.find('button[data-test-subj="tryESQLLink"]').simulate('click');
+
+ expect(onClickTryEsql).toHaveBeenCalledTimes(1);
+ });
});
diff --git a/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.component.tsx b/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.component.tsx
index d5807891e734d..3bfed37aa0b1a 100644
--- a/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.component.tsx
+++ b/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.component.tsx
@@ -7,95 +7,222 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import React from 'react';
import { css } from '@emotion/react';
+import React from 'react';
-import { EuiEmptyPrompt, EuiPanel } from '@elastic/eui';
+import {
+ EuiButton,
+ EuiCard,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHorizontalRule,
+ EuiSpacer,
+ EuiText,
+ EuiTextAlign,
+ EuiToolTip,
+ useEuiPaddingCSS,
+} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
-import { withSuspense } from '@kbn/shared-ux-utility';
import { NoDataViewsPromptComponentProps } from '@kbn/shared-ux-prompt-no-data-views-types';
import { DocumentationLink } from './documentation_link';
-import { NoDataButtonLink } from './actions';
+import { DataViewIllustration } from './data_view_illustration';
+import { EsqlIllustration } from './esql_illustration';
-// Using raw value because it is content dependent
-const MAX_WIDTH = 830;
+// max width value to use in pixels
+const MAX_WIDTH = 770;
-/**
- * A presentational component that is shown in cases when there are no data views created yet.
- */
-export const NoDataViewsPrompt = ({
+const PromptAddDataViews = ({
onClickCreate,
canCreateNewDataView,
dataViewsDocLink,
+ emptyPromptColor,
+}: Pick<
+ NoDataViewsPromptComponentProps,
+ 'onClickCreate' | 'canCreateNewDataView' | 'dataViewsDocLink' | 'emptyPromptColor'
+>) => {
+ const icon = ;
+
+ const title = (
+
+ );
+
+ const description = (
+ <>
+ {canCreateNewDataView ? (
+
+ ) : (
+
+ )}
+ >
+ );
+
+ const footer = dataViewsDocLink ? (
+ <>
+ {canCreateNewDataView ? (
+
+
+
+ ) : (
+
+ }
+ >
+
+
+
+
+ )}
+
+
+ >
+ ) : undefined;
+
+ return (
+
+ );
+};
+
+const PromptTryEsql = ({
onTryESQL,
esqlDocLink,
- emptyPromptColor = 'plain',
-}: NoDataViewsPromptComponentProps) => {
- const title = canCreateNewDataView ? (
-
-
-
-
-
- ) : (
-
-
-
- );
+ emptyPromptColor,
+}: Pick<
+ NoDataViewsPromptComponentProps,
+ 'onClickCreate' | 'onTryESQL' | 'esqlDocLink' | 'emptyPromptColor'
+>) => {
+ if (!onTryESQL) {
+ // we need to handle the case where the Try ES|QL click handler is not set because
+ // onTryESQL is set via a useEffect that has asynchronous dependencies
+ return null;
+ }
+
+ const icon = ;
- const body = canCreateNewDataView ? (
-
-
-
- ) : (
-
-
-
+ const title = (
+
);
- const footer = dataViewsDocLink ? : undefined;
+ const description = (
+
+ );
- // Load this illustration lazily
- const Illustration = withSuspense(
- React.lazy(() =>
- import('./data_view_illustration').then(({ DataViewIllustration }) => {
- return { default: DataViewIllustration };
- })
- ),
-
+ const footer = (
+ <>
+
+
+
+
+ {esqlDocLink && }
+ >
);
- const icon = ;
- const actions = (
-
+ return (
+
);
+};
+
+/**
+ * A presentational component that is shown in cases when there are no data views created yet.
+ */
+export const NoDataViewsPrompt = ({
+ onClickCreate,
+ canCreateNewDataView,
+ dataViewsDocLink,
+ onTryESQL,
+ esqlDocLink,
+ emptyPromptColor = 'plain',
+}: NoDataViewsPromptComponentProps) => {
+ const cssStyles = [
+ css`
+ max-width: ${MAX_WIDTH}px;
+ `,
+ useEuiPaddingCSS('top').m,
+ useEuiPaddingCSS('right').m,
+ useEuiPaddingCSS('left').m,
+ ];
return (
-
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.tsx b/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.tsx
index 43ae5f267ea90..340147505cb25 100644
--- a/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.tsx
+++ b/packages/shared-ux/prompt/no_data_views/impl/src/no_data_views.tsx
@@ -27,12 +27,15 @@ type CloseDataViewEditorFn = ReturnType {
- const { canCreateNewDataView, openDataViewEditor, dataViewsDocLink, onTryESQL, esqlDocLink } =
+ const { canCreateNewDataView, openDataViewEditor, dataViewsDocLink, esqlDocLink, ...services } =
useServices();
+ const onTryESQL = onTryESQLProp ?? services.onTryESQL;
+
const closeDataViewEditor = useRef();
useEffect(() => {
diff --git a/packages/shared-ux/prompt/no_data_views/impl/tsconfig.json b/packages/shared-ux/prompt/no_data_views/impl/tsconfig.json
index 673823e620474..2af357080c07c 100644
--- a/packages/shared-ux/prompt/no_data_views/impl/tsconfig.json
+++ b/packages/shared-ux/prompt/no_data_views/impl/tsconfig.json
@@ -16,8 +16,6 @@
],
"kbn_references": [
"@kbn/i18n-react",
- "@kbn/i18n",
- "@kbn/shared-ux-utility",
"@kbn/test-jest-helpers",
"@kbn/shared-ux-prompt-no-data-views-types",
"@kbn/shared-ux-prompt-no-data-views-mocks",
diff --git a/packages/shared-ux/prompt/no_data_views/mocks/src/storybook.ts b/packages/shared-ux/prompt/no_data_views/mocks/src/storybook.ts
index 63f46d2008077..973152201587d 100644
--- a/packages/shared-ux/prompt/no_data_views/mocks/src/storybook.ts
+++ b/packages/shared-ux/prompt/no_data_views/mocks/src/storybook.ts
@@ -34,17 +34,19 @@ export class StorybookMock extends AbstractStorybookMock<
defaultValue: true,
},
dataViewsDocLink: {
- options: ['some/link', undefined],
- control: { type: 'radio' },
- },
- esqlDocLink: {
- options: ['some/link', undefined],
+ options: ['dataviews/link', undefined],
control: { type: 'radio' },
+ defaultValue: 'dataviews/link',
},
canTryEsql: {
control: 'boolean',
defaultValue: true,
},
+ esqlDocLink: {
+ options: ['esql/link', undefined],
+ control: { type: 'radio' },
+ defaultValue: 'esql/link',
+ },
};
dependencies = [];
@@ -59,7 +61,7 @@ export class StorybookMock extends AbstractStorybookMock<
let onTryESQL;
if (canTryEsql !== false) {
- onTryESQL = action('onTryESQL');
+ onTryESQL = action('onTryESQL-from-services');
}
return {
diff --git a/packages/shared-ux/prompt/no_data_views/types/index.d.ts b/packages/shared-ux/prompt/no_data_views/types/index.d.ts
index 15f9f53c59fe6..7bca285bee717 100644
--- a/packages/shared-ux/prompt/no_data_views/types/index.d.ts
+++ b/packages/shared-ux/prompt/no_data_views/types/index.d.ts
@@ -42,7 +42,7 @@ export interface NoDataViewsPromptServices {
openDataViewEditor: (options: DataViewEditorOptions) => () => void;
/** A link to information about Data Views in Kibana */
dataViewsDocLink: string;
- /** Get a handler for trying ES|QL */
+ /** If the cluster has data, this handler allows the user to try ES|QL */
onTryESQL: (() => void) | undefined;
/** A link to the documentation for ES|QL */
esqlDocLink: string;
@@ -92,7 +92,7 @@ export interface NoDataViewsPromptComponentProps {
emptyPromptColor?: EuiEmptyPromptProps['color'];
/** Click handler for create button. **/
onClickCreate?: () => void;
- /** Handler for someone wanting to try ES|QL. */
+ /** If the cluster has data, this handler allows the user to try ES|QL */
onTryESQL?: () => void;
/** Link to documentation on ES|QL. */
esqlDocLink?: string;
@@ -104,6 +104,10 @@ export interface NoDataViewsPromptProps {
allowAdHocDataView?: boolean;
/** Handler for successfully creating a new data view. */
onDataViewCreated: (dataView: unknown) => void;
+ /** If the cluster has data, this handler allows the user to try ES|QL */
+ onTryESQL?: () => void;
/** Handler for when try ES|QL is clicked and user has been navigated to try ES|QL in discover. */
onESQLNavigationComplete?: () => void;
+ /** Empty prompt color **/
+ emptyPromptColor?: PanelColor;
}
diff --git a/src/plugins/data_view_management/kibana.jsonc b/src/plugins/data_view_management/kibana.jsonc
index 479e357804140..5b827868ee1e8 100644
--- a/src/plugins/data_view_management/kibana.jsonc
+++ b/src/plugins/data_view_management/kibana.jsonc
@@ -20,6 +20,7 @@
],
"optionalPlugins": [
"noDataPage",
+ "share",
"spaces"
],
"requiredBundles": [
diff --git a/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx
index cb93e01d1cc15..4512cb520c574 100644
--- a/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx
+++ b/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx
@@ -26,7 +26,7 @@ import { RouteComponentProps, useLocation, withRouter } from 'react-router-dom';
import useObservable from 'react-use/lib/useObservable';
import { reactRouterNavigate, useKibana } from '@kbn/kibana-react-plugin/public';
-import { NoDataViewsPromptComponent } from '@kbn/shared-ux-prompt-no-data-views';
+import { NoDataViewsPromptComponent, useOnTryESQL } from '@kbn/shared-ux-prompt-no-data-views';
import type { SpacesContextProps } from '@kbn/spaces-plugin/public';
import { DataViewType } from '@kbn/data-views-plugin/public';
import { RollupDeprecationTooltip } from '@kbn/rollup';
@@ -86,6 +86,7 @@ export const IndexPatternTable = ({
application,
chrome,
dataViews,
+ share,
IndexPatternEditor,
spaces,
overlays,
@@ -116,6 +117,12 @@ export const IndexPatternTable = ({
const hasDataView = useObservable(dataViewController.hasDataView$, defaults.hasDataView);
const hasESData = useObservable(dataViewController.hasESData$, defaults.hasEsData);
+ const useOnTryESQLParams = {
+ locatorClient: share?.url.locators,
+ navigateToApp: application.navigateToApp,
+ };
+ const onTryESQL = useOnTryESQL(useOnTryESQLParams);
+
const handleOnChange = ({ queryText, error }: { queryText: string; error: unknown }) => {
if (!error) {
setQuery(queryText);
@@ -370,6 +377,8 @@ export const IndexPatternTable = ({
onClickCreate={() => setShowCreateDialog(true)}
canCreateNewDataView={application.capabilities.indexPatterns.save as boolean}
dataViewsDocLink={docLinks.links.indexPatterns.introduction}
+ onTryESQL={onTryESQL}
+ esqlDocLink={docLinks.links.query.queryESQL}
emptyPromptColor={'subdued'}
/>
>
diff --git a/src/plugins/data_view_management/public/management_app/mount_management_section.tsx b/src/plugins/data_view_management/public/management_app/mount_management_section.tsx
index 995d5ed977ed3..96e5ae6c96b0c 100644
--- a/src/plugins/data_view_management/public/management_app/mount_management_section.tsx
+++ b/src/plugins/data_view_management/public/management_app/mount_management_section.tsx
@@ -17,6 +17,7 @@ import { StartServicesAccessor } from '@kbn/core/public';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { ManagementAppMountParams } from '@kbn/management-plugin/public';
+import { NoDataViewsPromptKibanaProvider } from '@kbn/shared-ux-prompt-no-data-views';
import {
IndexPatternTableWithRouter,
EditIndexPatternContainer,
@@ -64,11 +65,13 @@ export async function mountManagementSection(
dataViews,
fieldFormats,
unifiedSearch,
+ share,
spaces,
savedObjectsManagement,
},
indexPatternManagementStart,
] = await getStartServices();
+
const canSave = dataViews.getCanSaveSync();
if (!canSave) {
@@ -89,6 +92,7 @@ export async function mountManagementSection(
chrome,
uiSettings,
settings,
+ share,
notifications,
overlays,
unifiedSearch,
@@ -115,23 +119,29 @@ export async function mountManagementSection(
ReactDOM.render(
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
,
params.element
diff --git a/src/plugins/data_view_management/public/plugin.ts b/src/plugins/data_view_management/public/plugin.ts
index 77e8c12a13ad0..0d03dc8896fd1 100644
--- a/src/plugins/data_view_management/public/plugin.ts
+++ b/src/plugins/data_view_management/public/plugin.ts
@@ -21,6 +21,7 @@ import { ManagementSetup } from '@kbn/management-plugin/public';
import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
import { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
+import { SharePluginStart } from '@kbn/share-plugin/public';
export interface IndexPatternManagementSetupDependencies {
management: ManagementSetup;
@@ -34,6 +35,7 @@ export interface IndexPatternManagementStartDependencies {
dataViewEditor: DataViewEditorStart;
dataViews: DataViewsPublicPluginStart;
fieldFormats: FieldFormatsStart;
+ share?: SharePluginStart;
spaces?: SpacesPluginStart;
unifiedSearch: UnifiedSearchPublicPluginStart;
savedObjectsManagement: SavedObjectsManagementPluginStart;
diff --git a/src/plugins/data_view_management/public/types.ts b/src/plugins/data_view_management/public/types.ts
index b7a9279de8001..161ee3b1e21de 100644
--- a/src/plugins/data_view_management/public/types.ts
+++ b/src/plugins/data_view_management/public/types.ts
@@ -29,6 +29,7 @@ import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
import type { SettingsStart } from '@kbn/core-ui-settings-browser';
import type { NoDataPagePluginSetup } from '@kbn/no-data-page-plugin/public';
+import { SharePluginStart } from '@kbn/share-plugin/public';
import type { IndexPatternManagementStart } from '.';
import type { DataViewMgmtService } from './management_app/data_view_management_service';
@@ -53,6 +54,7 @@ export interface IndexPatternManagmentContext extends StartServices {
fieldFormatEditors: IndexPatternFieldEditorStart['fieldFormatEditors'];
IndexPatternEditor: DataViewEditorStart['IndexPatternEditorComponent'];
fieldFormats: FieldFormatsStart;
+ share?: SharePluginStart;
spaces?: SpacesPluginStart;
savedObjectsManagement: SavedObjectsManagementPluginStart;
noDataPage?: NoDataPagePluginSetup;
diff --git a/src/plugins/data_view_management/tsconfig.json b/src/plugins/data_view_management/tsconfig.json
index ea0c96cc66b74..9857dd44829fa 100644
--- a/src/plugins/data_view_management/tsconfig.json
+++ b/src/plugins/data_view_management/tsconfig.json
@@ -45,6 +45,7 @@
"@kbn/code-editor",
"@kbn/react-kibana-mount",
"@kbn/rollup",
+ "@kbn/share-plugin",
],
"exclude": [
"target/**/*",
diff --git a/test/functional/apps/dashboard/group6/dashboard_esql_no_data.ts b/test/functional/apps/dashboard/group6/dashboard_esql_no_data.ts
new file mode 100644
index 0000000000000..148cb95a82b11
--- /dev/null
+++ b/test/functional/apps/dashboard/group6/dashboard_esql_no_data.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", 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 { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const kibanaServer = getService('kibanaServer');
+ const testSubjects = getService('testSubjects');
+ const esql = getService('esql');
+ const PageObjects = getPageObjects(['discover', 'dashboard']);
+
+ describe('No Data Views: Try ES|QL', () => {
+ before(async () => {
+ await kibanaServer.savedObjects.cleanStandardList();
+ });
+
+ it('enables user to create a dashboard with ES|QL from no-data-prompt', async () => {
+ await PageObjects.dashboard.navigateToApp();
+
+ await testSubjects.existOrFail('noDataViewsPrompt');
+ await testSubjects.click('tryESQLLink');
+
+ await PageObjects.discover.expectOnDiscover();
+ await esql.expectEsqlStatement('FROM logs* | LIMIT 10');
+ });
+ });
+}
diff --git a/test/functional/apps/dashboard/group6/index.ts b/test/functional/apps/dashboard/group6/index.ts
index 302ca2e0480a0..340c9b425571b 100644
--- a/test/functional/apps/dashboard/group6/index.ts
+++ b/test/functional/apps/dashboard/group6/index.ts
@@ -37,5 +37,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./dashboard_snapshots'));
loadTestFile(require.resolve('./embeddable_library'));
loadTestFile(require.resolve('./dashboard_esql_chart'));
+ loadTestFile(require.resolve('./dashboard_esql_no_data'));
});
}
diff --git a/test/functional/apps/management/data_views/_try_esql.ts b/test/functional/apps/management/data_views/_try_esql.ts
new file mode 100644
index 0000000000000..276e61c4a721f
--- /dev/null
+++ b/test/functional/apps/management/data_views/_try_esql.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", 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 { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const kibanaServer = getService('kibanaServer');
+ const testSubjects = getService('testSubjects');
+ const esql = getService('esql');
+ const PageObjects = getPageObjects(['settings', 'common', 'discover']);
+
+ describe('No Data Views: Try ES|QL', () => {
+ before(async () => {
+ await kibanaServer.savedObjects.cleanStandardList();
+ });
+
+ it('navigates to Discover and presents an ES|QL query', async () => {
+ await PageObjects.settings.navigateTo();
+ await PageObjects.settings.clickKibanaIndexPatterns();
+
+ await testSubjects.existOrFail('noDataViewsPrompt');
+ await testSubjects.click('tryESQLLink');
+
+ await PageObjects.discover.expectOnDiscover();
+ await esql.expectEsqlStatement('FROM logs* | LIMIT 10');
+ });
+ });
+}
diff --git a/test/functional/apps/management/index.ts b/test/functional/apps/management/index.ts
index f3d26f2e1c6d7..2300543f06d51 100644
--- a/test/functional/apps/management/index.ts
+++ b/test/functional/apps/management/index.ts
@@ -38,6 +38,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./data_views/_legacy_url_redirect'));
loadTestFile(require.resolve('./data_views/_exclude_index_pattern'));
loadTestFile(require.resolve('./data_views/_index_pattern_filter'));
+ loadTestFile(require.resolve('./data_views/_try_esql'));
loadTestFile(require.resolve('./data_views/_scripted_fields_filter'));
loadTestFile(require.resolve('./_import_objects'));
loadTestFile(require.resolve('./data_views/_test_huge_fields'));
diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts
index 1474e9d315538..ab6356075fd81 100644
--- a/test/functional/page_objects/discover_page.ts
+++ b/test/functional/page_objects/discover_page.ts
@@ -32,6 +32,12 @@ export class DiscoverPageObject extends FtrService {
private readonly defaultFindTimeout = this.config.get('timeouts.find');
+ /** Ensures that navigation to discover has completed */
+ public async expectOnDiscover() {
+ await this.testSubjects.existOrFail('discoverNewButton');
+ await this.testSubjects.existOrFail('discoverOpenButton');
+ }
+
public async getChartTimespan() {
return await this.testSubjects.getAttribute('unifiedHistogramChart', 'data-time-range');
}
diff --git a/test/functional/services/esql.ts b/test/functional/services/esql.ts
index 63836d2c5d2f5..c144c6e8993be 100644
--- a/test/functional/services/esql.ts
+++ b/test/functional/services/esql.ts
@@ -7,12 +7,19 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
+import expect from '@kbn/expect';
import { FtrService } from '../ftr_provider_context';
export class ESQLService extends FtrService {
private readonly retry = this.ctx.getService('retry');
private readonly testSubjects = this.ctx.getService('testSubjects');
+ /** Ensures that the ES|QL code editor is loaded with a given statement */
+ public async expectEsqlStatement(statement: string) {
+ const codeEditor = await this.testSubjects.find('ESQLEditor');
+ expect(await codeEditor.getAttribute('innerText')).to.contain(statement);
+ }
+
public async getHistoryItems(): Promise {
const queryHistory = await this.testSubjects.find('ESQLEditor-queryHistory');
const tableBody = await this.retry.try(async () => queryHistory.findByTagName('tbody'));
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index c441b91d23e31..9aa58bd4f5286 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -7268,9 +7268,6 @@
"sharedUXPackages.fileUpload.uploadCompleteButtonLabel": "Chargement terminé",
"sharedUXPackages.fileUpload.uploadDoneToolTipContent": "Votre fichier a bien été chargé !",
"sharedUXPackages.fileUpload.uploadingButtonLabel": "Chargement",
- "sharedUXPackages.no_data_views.esqlButtonLabel": "Langue : ES|QL",
- "sharedUXPackages.no_data_views.esqlDocsLink": "En savoir plus.",
- "sharedUXPackages.no_data_views.esqlMessage": "Vous pouvez aussi rechercher vos données en utilisant directement ES|QL. {docsLink}",
"sharedUXPackages.noDataConfig.addIntegrationsDescription": "Utilisez Elastic Agent pour collecter des données et créer des solutions Analytics.",
"sharedUXPackages.noDataConfig.addIntegrationsTitle": "Ajouter des intégrations",
"sharedUXPackages.noDataConfig.analytics": "Analyse",
@@ -7292,8 +7289,6 @@
"sharedUXPackages.noDataViewsPrompt.dataViewExplanation": "Les vues de données identifient les données Elasticsearch que vous souhaitez explorer. Vous pouvez faire pointer des vues de données vers un ou plusieurs flux de données, index et alias d'index, tels que vos données de log d'hier, ou vers tous les index contenant vos données de log.",
"sharedUXPackages.noDataViewsPrompt.learnMore": "Envie d'en savoir plus ?",
"sharedUXPackages.noDataViewsPrompt.noPermission.dataViewExplanation": "Les vues de données identifient les données Elasticsearch que vous souhaitez explorer. Pour créer des vues de données, demandez les autorisations requises à votre administrateur.",
- "sharedUXPackages.noDataViewsPrompt.noPermission.title": "Vous devez disposer d'une autorisation pour pouvoir créer des vues de données",
- "sharedUXPackages.noDataViewsPrompt.nowCreate": "Créez à présent une vue de données.",
"sharedUXPackages.noDataViewsPrompt.readDocumentation": "Lisez les documents",
"sharedUXPackages.noDataViewsPrompt.youHaveData": "Vous avez des données dans Elasticsearch.",
"sharedUXPackages.prompt.errors.notFound.body": "Désolé, la page que vous recherchez est introuvable. Elle a peut-être été retirée ou renommée, ou peut-être qu'elle n'a jamais existé.",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 93e7ab2270f4c..72afb1947e928 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -7022,9 +7022,6 @@
"sharedUXPackages.fileUpload.uploadCompleteButtonLabel": "アップロード完了",
"sharedUXPackages.fileUpload.uploadDoneToolTipContent": "ファイルは正常にアップロードされました。",
"sharedUXPackages.fileUpload.uploadingButtonLabel": "アップロード中",
- "sharedUXPackages.no_data_views.esqlButtonLabel": "言語:ES|QL",
- "sharedUXPackages.no_data_views.esqlDocsLink": "詳細情報",
- "sharedUXPackages.no_data_views.esqlMessage": "あるいは、直接ES|QLを使用してデータをクエリできます。{docsLink}",
"sharedUXPackages.noDataConfig.addIntegrationsDescription": "Elasticエージェントを使用して、データを収集し、分析ソリューションを構築します。",
"sharedUXPackages.noDataConfig.addIntegrationsTitle": "統合の追加",
"sharedUXPackages.noDataConfig.analytics": "分析",
@@ -7046,8 +7043,6 @@
"sharedUXPackages.noDataViewsPrompt.dataViewExplanation": "データビューは、探索するElasticsearchデータを特定します。昨日からのログデータ、ログデータを含むすべてのインデックスなど、1つ以上のデータストリーム、インデックス、インデックスエイリアスをデータビューで参照できます。",
"sharedUXPackages.noDataViewsPrompt.learnMore": "詳細について",
"sharedUXPackages.noDataViewsPrompt.noPermission.dataViewExplanation": "データビューは、探索するElasticsearchデータを特定します。データビューを作成するには、必要な権限を管理者に依頼してください。",
- "sharedUXPackages.noDataViewsPrompt.noPermission.title": "データビューを作成するための権限が必要です。",
- "sharedUXPackages.noDataViewsPrompt.nowCreate": "ここでデータビューを作成します。",
"sharedUXPackages.noDataViewsPrompt.readDocumentation": "ドキュメント を読む ",
"sharedUXPackages.noDataViewsPrompt.youHaveData": "Elasticsearchにデータがあります。",
"sharedUXPackages.prompt.errors.notFound.body": "申し訳ございません。お探しのページは見つかりませんでした。削除または名前変更されたか、そもそも存在していなかった可能性があります。",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 6f81b20c89d2b..c27a5241e5a33 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -7037,9 +7037,6 @@
"sharedUXPackages.fileUpload.uploadCompleteButtonLabel": "上传完成",
"sharedUXPackages.fileUpload.uploadDoneToolTipContent": "您的文件已成功上传!",
"sharedUXPackages.fileUpload.uploadingButtonLabel": "正在上传",
- "sharedUXPackages.no_data_views.esqlButtonLabel": "语言:ES|QL",
- "sharedUXPackages.no_data_views.esqlDocsLink": "了解详情。",
- "sharedUXPackages.no_data_views.esqlMessage": "或者,您可以直接使用 ES|QL 查询数据。{docsLink}",
"sharedUXPackages.noDataConfig.addIntegrationsDescription": "使用 Elastic 代理收集数据并增建分析解决方案。",
"sharedUXPackages.noDataConfig.addIntegrationsTitle": "添加集成",
"sharedUXPackages.noDataConfig.analytics": "分析",
@@ -7061,8 +7058,6 @@
"sharedUXPackages.noDataViewsPrompt.dataViewExplanation": "数据视图标识您要浏览的 Elasticsearch 数据。您可以将数据视图指向一个或多个数据流、索引和索引别名(例如昨天的日志数据),或包含日志数据的所有索引。",
"sharedUXPackages.noDataViewsPrompt.learnMore": "希望了解详情?",
"sharedUXPackages.noDataViewsPrompt.noPermission.dataViewExplanation": "数据视图标识您要浏览的 Elasticsearch 数据。要创建数据视图,请联系管理员获得所需权限。",
- "sharedUXPackages.noDataViewsPrompt.noPermission.title": "您需要权限以创建数据视图",
- "sharedUXPackages.noDataViewsPrompt.nowCreate": "现在,创建数据视图。",
"sharedUXPackages.noDataViewsPrompt.readDocumentation": "阅读文档",
"sharedUXPackages.noDataViewsPrompt.youHaveData": "您在 Elasticsearch 中有数据。",
"sharedUXPackages.prompt.errors.notFound.body": "抱歉,找不到您要查找的页面。该页面可能已移除、重命名,或可能根本不存在。",
diff --git a/x-pack/test/functional/apps/data_views/feature_controls/security.ts b/x-pack/test/functional/apps/data_views/feature_controls/security.ts
index 1cc62baf0abba..34317932a6b21 100644
--- a/x-pack/test/functional/apps/data_views/feature_controls/security.ts
+++ b/x-pack/test/functional/apps/data_views/feature_controls/security.ts
@@ -131,10 +131,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
expect(navLinks).to.eql(['Stack Management']);
});
- it(`index pattern listing doesn't show create button`, async () => {
+ it(`index pattern listing shows disabled create button`, async () => {
await settings.clickKibanaIndexPatterns();
await testSubjects.existOrFail('noDataViewsPrompt');
- await testSubjects.missingOrFail('createDataViewButton');
+ const createDataViewButton = await testSubjects.find('createDataViewButton');
+ const isDisabled = await createDataViewButton.getAttribute('disabled');
+ expect(isDisabled).to.be('true');
});
it(`shows read-only badge`, async () => {
From 57096d1f4fbcb4fc0135505b6c6100566ff08cc9 Mon Sep 17 00:00:00 2001
From: Patrick Mueller
Date: Wed, 9 Oct 2024 17:52:09 -0400
Subject: [PATCH 23/87] [ResponseOps] add pre-create, pre-update, and
post-delete hooks for connectors (#194081)
Allows connector types to add functions to be called when connectors are created, updated, and deleted.
Extracted from https://github.com/elastic/kibana/pull/189027, commit c97afebbe1462eb3eb2b0fb89d0ce9126ff118db
Co-authored-by: Yuliia Naumenko
---
x-pack/plugins/actions/README.md | 72 +++-
.../actions_client/actions_client.test.ts | 49 +++
.../server/actions_client/actions_client.ts | 110 ++++-
.../actions_client_hooks.test.ts | 385 ++++++++++++++++++
.../connector/methods/update/update.ts | 98 ++++-
x-pack/plugins/actions/server/lib/index.ts | 1 +
.../plugins/actions/server/lib/try_catch.ts | 17 +
.../sub_action_framework/register.test.ts | 13 +-
.../server/sub_action_framework/register.ts | 3 +
.../server/sub_action_framework/types.ts | 33 ++
x-pack/plugins/actions/server/types.ts | 46 +++
.../alerting_api_integration/common/config.ts | 1 +
.../plugins/alerts/server/action_types.ts | 91 +++++
.../group2/tests/actions/create.ts | 81 +++-
.../group2/tests/actions/delete.ts | 85 +++-
.../group2/tests/actions/update.ts | 103 ++++-
16 files changed, 1151 insertions(+), 37 deletions(-)
create mode 100644 x-pack/plugins/actions/server/actions_client/actions_client_hooks.test.ts
create mode 100644 x-pack/plugins/actions/server/lib/try_catch.ts
diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md
index 7cab1ffe0c0b3..4e7f20e47cb7d 100644
--- a/x-pack/plugins/actions/README.md
+++ b/x-pack/plugins/actions/README.md
@@ -89,13 +89,16 @@ The following table describes the properties of the `options` object.
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
| id | Unique identifier for the action type. For convention, ids starting with `.` are reserved for built in action types. We recommend using a convention like `.mySpecialAction` for your action types. | string |
| name | A user-friendly name for the action type. These will be displayed in dropdowns when chosing action types. | string |
-| maxAttempts | The maximum number of times this action will attempt to run when scheduled. | number |
+| maxAttempts | The maximum number of times this action will attempt to run when scheduled. | number |
| minimumLicenseRequired | The license required to use the action type. | string |
| supportedFeatureIds | List of IDs of the features that this action type is available in. Allowed values are `alerting`, `siem`, `uptime`, `cases`. See `x-pack/plugins/actions/common/connector_feature_config.ts` for the most up to date list. | string[] |
| validate.params | When developing an action type, it needs to accept parameters to know what to do with the action. (Example `to`, `from`, `subject`, `body` of an email). See the current built-in email action type for an example of the state-of-the-art validation. Technically, the value of this property should have a property named `validate()` which is a function that takes a params object to validate and returns a sanitized version of that object to pass to the execution function. Validation errors should be thrown from the `validate()` function and will be available as an error message | schema / validation function |
| validate.config | Similar to params, a config may be required when creating an action (for example `host` and `port` for an email server). | schema / validation function |
| validate.secrets | Similar to params, a secrets object may be required when creating an action (for example `user` and `password` for an email server). | schema / validation function |
-| executor | This is where the code of an action type lives. This is a function gets called for generating an action from either alerting or manually by using the exposed function (see firing actions). For full details, see executor section below. | Function |
+| executor | This is where the code of an action type lives. This is a function gets called for generating an action from either alerting or manually by using the exposed function (see firing actions). For full details, see executor section below. | Function |
+| preSaveHook | This optional function is called before the connector saved object is saved. For full details, see hooks section below. | Function |
+| postSaveHook | This optional function is called after the connector saved object is saved. For full details, see hooks section below. | Function |
+| postDeleteHook | This optional function is called after the connector saved object is deleted. For full details, see hooks section below. | Function |
| renderParameterTemplates | Optionally define a function to provide custom rendering for this action type. | Function |
**Important** - The config object is persisted in ElasticSearch and updated via the ElasticSearch update document API. This API allows "partial updates" - and this can cause issues with the encryption used on specified properties. So, a `validate()` function should return values for all configuration properties, so that partial updates do not occur. Setting property values to `null` rather than `undefined`, or not including a property in the config object, is all you need to do to ensure partial updates won't occur.
@@ -116,6 +119,71 @@ This is the primary function for an action type. Whenever the action needs to ru
| services.savedObjectsClient | This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in. The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled). |
| services.log(tags, [data], [timestamp]) | Use this to create server logs. (This is the same function as server.log) |
+### Hooks
+
+Hooks allow a connector implementation to be called during connector creation, update, and delete. When not using hooks, the connector implementation is not involved in creation, update and delete, except for the schema validation that happens for creation and update. Hooks can be used to force a create or update to fail, or run arbitrary code before and after update and create, and after delete. We don't have a need for a hook before delete at the moment, so that hook is currently not available.
+
+Hooks are passed the following parameters:
+
+```ts
+interface PreSaveConnectorHookParams {
+ connectorId: string;
+ config: Config;
+ secrets: Secrets;
+ logger: Logger;
+ request: KibanaRequest;
+ services: HookServices;
+ isUpdate: boolean;
+}
+
+interface PostSaveConnectorHookParams {
+ connectorId: string;
+ config: Config;
+ secrets: Secrets;
+ logger: Logger;
+ request: KibanaRequest;
+ services: HookServices;
+ isUpdate: boolean;
+ wasSuccessful: boolean;
+}
+
+interface PostDeleteConnectorHookParams {
+ connectorId: string;
+ config: Config;
+ // secrets not provided, yet
+ logger: Logger;
+ request: KibanaRequest;
+ services: HookServices;
+}
+```
+
+| parameter | description
+| --------- | -----------
+| `connectorId` | The id of the connector.
+| `config` | The connector's `config` object.
+| `secrets` | The connector's `secrets` object.
+| `logger` | A standard Kibana logger.
+| `request` | The request causing this operation
+| `services` | Common service objects, see below.
+| `isUpdate` | For the `PreSave` and `PostSave` hooks, `isUpdate` is false for create operations, and true for update operations.
+| `wasSuccessful` | For the `PostSave` hook, this indicates if the connector was persisted as a Saved Object successfully.
+
+The `services` object contains the following properties:
+
+| property | description
+| --------- | -----------
+| `scopedClusterClient` | A standard `scopeClusterClient` object.
+
+The hooks are called just before, and just after, the Saved Object operation for the client methods is invoked.
+
+The `PostDelete` hook does not have a `wasSuccessful` property, as the hook is not called if the delete operation fails. The saved object will still exist. Only a successful call to delete the connector will cause the hook to run.
+
+The `PostSave` hook is useful if the `PreSave` hook is creating / modifying other resources. The `PreSave` hook is called just before the connector SO is actually created/updated, and of course that create/update could fail for some reason. In those cases, the `PostSave` hook is passed `wasSuccessful: false` and can "undo" any work it did in the `PreSave` hook.
+
+The `PreSave` hook can be used to cancel a create or update, by throwing an exception. The `PostSave` and `PostDelete` invocations will have thrown exceptions caught and logged to the Kibana log, and will not cancel the operation.
+
+When throwing an error in the `PreSave` hook, the Error's message will be used as the error failing the operation, so should include a human-readable description of what it was doing, along with any message from an underlying API that failed, if available. When an error is thrown from a `PreSave` hook, the `PostSave` hook will **NOT** be run.
+
### Example
The built-in email action type provides a good example of creating an action type with non-trivial configuration and params:
diff --git a/x-pack/plugins/actions/server/actions_client/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client/actions_client.test.ts
index 46e73f7bb3591..7f15dd6287d6b 100644
--- a/x-pack/plugins/actions/server/actions_client/actions_client.test.ts
+++ b/x-pack/plugins/actions/server/actions_client/actions_client.test.ts
@@ -113,6 +113,9 @@ const mockTaskManager = taskManagerMock.createSetup();
const configurationUtilities = actionsConfigMock.create();
const eventLogClient = eventLogClientMock.create();
const getEventLogClient = jest.fn();
+const preSaveHook = jest.fn();
+const postSaveHook = jest.fn();
+const postDeleteHook = jest.fn();
let actionsClient: ActionsClient;
let mockedLicenseState: jest.Mocked;
@@ -392,6 +395,8 @@ describe('create()', () => {
params: { schema: schema.object({}) },
},
executor,
+ preSaveHook,
+ postSaveHook,
});
unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
const result = await actionsClient.create({
@@ -428,6 +433,8 @@ describe('create()', () => {
},
]
`);
+ expect(preSaveHook).toHaveBeenCalledTimes(1);
+ expect(postSaveHook).toHaveBeenCalledTimes(1);
});
test('validates config', async () => {
@@ -1973,6 +1980,33 @@ describe('getOAuthAccessToken()', () => {
});
describe('delete()', () => {
+ beforeEach(() => {
+ actionTypeRegistry.register({
+ id: 'my-action-delete',
+ name: 'My action type',
+ minimumLicenseRequired: 'basic',
+ supportedFeatureIds: ['alerting'],
+ validate: {
+ config: { schema: schema.object({}) },
+ secrets: { schema: schema.object({}) },
+ params: { schema: schema.object({}) },
+ },
+ executor,
+ postDeleteHook: async (options) => postDeleteHook(options),
+ });
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'action',
+ attributes: {
+ actionTypeId: 'my-action-delete',
+ isMissingSecrets: false,
+ config: {},
+ secrets: {},
+ },
+ references: [],
+ });
+ });
+
describe('authorization', () => {
test('ensures user is authorised to delete actions', async () => {
await actionsClient.delete({ id: '1' });
@@ -2052,6 +2086,16 @@ describe('delete()', () => {
`);
});
+ test('calls postDeleteHook', async () => {
+ const expectedResult = Symbol();
+ unsecuredSavedObjectsClient.delete.mockResolvedValueOnce(expectedResult);
+
+ const result = await actionsClient.delete({ id: '1' });
+ expect(result).toEqual(expectedResult);
+ expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledTimes(1);
+ expect(postDeleteHook).toHaveBeenCalledTimes(1);
+ });
+
it('throws when trying to delete a preconfigured connector', async () => {
actionsClient = new ActionsClient({
logger,
@@ -2250,6 +2294,8 @@ describe('update()', () => {
params: { schema: schema.object({}) },
},
executor,
+ preSaveHook,
+ postSaveHook,
});
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
@@ -2315,6 +2361,9 @@ describe('update()', () => {
"my-action",
]
`);
+
+ expect(preSaveHook).toHaveBeenCalledTimes(1);
+ expect(postSaveHook).toHaveBeenCalledTimes(1);
});
test('updates an action with isMissingSecrets "true" (set true as the import result), to isMissingSecrets', async () => {
diff --git a/x-pack/plugins/actions/server/actions_client/actions_client.ts b/x-pack/plugins/actions/server/actions_client/actions_client.ts
index 7e4d72faedaed..f485d82b2f120 100644
--- a/x-pack/plugins/actions/server/actions_client/actions_client.ts
+++ b/x-pack/plugins/actions/server/actions_client/actions_client.ts
@@ -43,6 +43,7 @@ import {
validateConnector,
ActionExecutionSource,
parseDate,
+ tryCatch,
} from '../lib';
import {
ActionResult,
@@ -50,6 +51,7 @@ import {
InMemoryConnector,
ActionTypeExecutorResult,
ConnectorTokenClientContract,
+ HookServices,
} from '../types';
import { PreconfiguredActionDisabledModificationError } from '../lib/errors/preconfigured_action_disabled_modification';
import { ExecuteOptions } from '../lib/action_executor';
@@ -246,6 +248,33 @@ export class ActionsClient {
}
this.context.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
+ const hookServices: HookServices = {
+ scopedClusterClient: this.context.scopedClusterClient,
+ };
+
+ if (actionType.preSaveHook) {
+ try {
+ await actionType.preSaveHook({
+ connectorId: id,
+ config,
+ secrets,
+ logger: this.context.logger,
+ request: this.context.request,
+ services: hookServices,
+ isUpdate: false,
+ });
+ } catch (error) {
+ this.context.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.CREATE,
+ savedObject: { type: 'action', id },
+ error,
+ })
+ );
+ throw error;
+ }
+ }
+
this.context.auditLogger?.log(
connectorAuditEvent({
action: ConnectorAuditAction.CREATE,
@@ -254,18 +283,48 @@ export class ActionsClient {
})
);
- const result = await this.context.unsecuredSavedObjectsClient.create(
- 'action',
- {
- actionTypeId,
- name,
- isMissingSecrets: false,
- config: validatedActionTypeConfig as SavedObjectAttributes,
- secrets: validatedActionTypeSecrets as SavedObjectAttributes,
- },
- { id }
+ const result = await tryCatch(
+ async () =>
+ await this.context.unsecuredSavedObjectsClient.create(
+ 'action',
+ {
+ actionTypeId,
+ name,
+ isMissingSecrets: false,
+ config: validatedActionTypeConfig as SavedObjectAttributes,
+ secrets: validatedActionTypeSecrets as SavedObjectAttributes,
+ },
+ { id }
+ )
);
+ const wasSuccessful = !(result instanceof Error);
+ const label = `connectorId: "${id}"; type: ${actionTypeId}`;
+ const tags = ['post-save-hook', id];
+
+ if (actionType.postSaveHook) {
+ try {
+ await actionType.postSaveHook({
+ connectorId: id,
+ config,
+ secrets,
+ logger: this.context.logger,
+ request: this.context.request,
+ services: hookServices,
+ isUpdate: false,
+ wasSuccessful,
+ });
+ } catch (err) {
+ this.context.logger.error(`postSaveHook create error for ${label}: ${err.message}`, {
+ tags,
+ });
+ }
+ }
+
+ if (!wasSuccessful) {
+ throw result;
+ }
+
return {
id: result.id,
actionTypeId: result.attributes.actionTypeId,
@@ -558,7 +617,36 @@ export class ActionsClient {
);
}
- return await this.context.unsecuredSavedObjectsClient.delete('action', id);
+ const rawAction = await this.context.unsecuredSavedObjectsClient.get('action', id);
+ const {
+ attributes: { actionTypeId, config },
+ } = rawAction;
+
+ const actionType = this.context.actionTypeRegistry.get(actionTypeId);
+ const result = await this.context.unsecuredSavedObjectsClient.delete('action', id);
+
+ const hookServices: HookServices = {
+ scopedClusterClient: this.context.scopedClusterClient,
+ };
+
+ if (actionType.postDeleteHook) {
+ try {
+ await actionType.postDeleteHook({
+ connectorId: id,
+ config,
+ logger: this.context.logger,
+ request: this.context.request,
+ services: hookServices,
+ });
+ } catch (error) {
+ const tags = ['post-delete-hook', id];
+ this.context.logger.error(
+ `The post delete hook failed for for connector "${id}": ${error.message}`,
+ { tags }
+ );
+ }
+ }
+ return result;
}
private getSystemActionKibanaPrivileges(connectorId: string, params?: ExecuteOptions['params']) {
diff --git a/x-pack/plugins/actions/server/actions_client/actions_client_hooks.test.ts b/x-pack/plugins/actions/server/actions_client/actions_client_hooks.test.ts
new file mode 100644
index 0000000000000..7a1a0fb5e3d91
--- /dev/null
+++ b/x-pack/plugins/actions/server/actions_client/actions_client_hooks.test.ts
@@ -0,0 +1,385 @@
+/*
+ * 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 { omit } from 'lodash';
+import { schema } from '@kbn/config-schema';
+import { MockedLogger, loggerMock } from '@kbn/logging-mocks';
+import { ActionTypeRegistry, ActionTypeRegistryOpts } from '../action_type_registry';
+import { ActionsClient } from './actions_client';
+import { ExecutorType } from '../types';
+import { ActionExecutor, TaskRunnerFactory, ILicenseState } from '../lib';
+import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
+import { actionsConfigMock } from '../actions_config.mock';
+import { licenseStateMock } from '../lib/license_state.mock';
+import { licensingMock } from '@kbn/licensing-plugin/server/mocks';
+import {
+ httpServerMock,
+ elasticsearchServiceMock,
+ savedObjectsClientMock,
+} from '@kbn/core/server/mocks';
+import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
+import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock';
+import { actionExecutorMock } from '../lib/action_executor.mock';
+import { ActionsAuthorization } from '../authorization/actions_authorization';
+import { actionsAuthorizationMock } from '../authorization/actions_authorization.mock';
+import { connectorTokenClientMock } from '../lib/connector_token_client.mock';
+import { inMemoryMetricsMock } from '../monitoring/in_memory_metrics.mock';
+
+jest.mock('uuid', () => ({
+ v4: () => ConnectorSavedObject.id,
+}));
+
+const kibanaIndices = ['.kibana'];
+const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
+const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
+const actionExecutor = actionExecutorMock.create();
+const authorization = actionsAuthorizationMock.create();
+const ephemeralExecutionEnqueuer = jest.fn();
+const bulkExecutionEnqueuer = jest.fn();
+const request = httpServerMock.createKibanaRequest();
+const auditLogger = auditLoggerMock.create();
+const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract();
+const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test');
+const mockTaskManager = taskManagerMock.createSetup();
+const getEventLogClient = jest.fn();
+const preSaveHook = jest.fn();
+const postSaveHook = jest.fn();
+const postDeleteHook = jest.fn();
+
+let actionsClient: ActionsClient;
+let mockedLicenseState: jest.Mocked;
+let actionTypeRegistry: ActionTypeRegistry;
+let actionTypeRegistryParams: ActionTypeRegistryOpts;
+const executor: ExecutorType<{}, {}, {}, void> = async (options) => {
+ return { status: 'ok', actionId: options.actionId };
+};
+
+const ConnectorSavedObject = {
+ id: 'connector-id-uuid',
+ type: 'action',
+ attributes: {
+ actionTypeId: 'hooked-action-type',
+ isMissingSecrets: false,
+ name: 'Hooked Action',
+ config: { foo: 42 },
+ secrets: { bar: 2001 },
+ },
+ references: [],
+};
+
+const CreateParms = {
+ action: {
+ name: ConnectorSavedObject.attributes.name,
+ actionTypeId: ConnectorSavedObject.attributes.actionTypeId,
+ config: ConnectorSavedObject.attributes.config,
+ secrets: ConnectorSavedObject.attributes.secrets,
+ },
+};
+
+const UpdateParms = {
+ id: ConnectorSavedObject.id,
+ action: {
+ name: ConnectorSavedObject.attributes.name,
+ config: ConnectorSavedObject.attributes.config,
+ secrets: ConnectorSavedObject.attributes.secrets,
+ },
+};
+
+const CoreHookParams = {
+ connectorId: ConnectorSavedObject.id,
+ config: ConnectorSavedObject.attributes.config,
+ secrets: ConnectorSavedObject.attributes.secrets,
+ request,
+ services: {
+ // this will be checked with a function test
+ scopedClusterClient: expect.any(Object),
+ },
+};
+
+const connectorTokenClient = connectorTokenClientMock.create();
+const inMemoryMetrics = inMemoryMetricsMock.create();
+
+let logger: MockedLogger;
+
+beforeEach(() => {
+ jest.resetAllMocks();
+ logger = loggerMock.create();
+ mockedLicenseState = licenseStateMock.create();
+
+ actionTypeRegistryParams = {
+ licensing: licensingMock.createSetup(),
+ taskManager: mockTaskManager,
+ taskRunnerFactory: new TaskRunnerFactory(
+ new ActionExecutor({ isESOCanEncrypt: true }),
+ inMemoryMetrics
+ ),
+ actionsConfigUtils: actionsConfigMock.create(),
+ licenseState: mockedLicenseState,
+ inMemoryConnectors: [],
+ };
+
+ actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
+ actionsClient = new ActionsClient({
+ logger,
+ actionTypeRegistry,
+ unsecuredSavedObjectsClient,
+ scopedClusterClient,
+ kibanaIndices,
+ inMemoryConnectors: [],
+ actionExecutor,
+ ephemeralExecutionEnqueuer,
+ bulkExecutionEnqueuer,
+ request,
+ authorization: authorization as unknown as ActionsAuthorization,
+ auditLogger,
+ usageCounter: mockUsageCounter,
+ connectorTokenClient,
+ getEventLogClient,
+ });
+
+ actionTypeRegistry.register({
+ id: 'hooked-action-type',
+ name: 'Hooked action type',
+ minimumLicenseRequired: 'gold',
+ supportedFeatureIds: ['alerting'],
+ validate: {
+ config: { schema: schema.object({ foo: schema.number() }) },
+ secrets: { schema: schema.object({ bar: schema.number() }) },
+ params: { schema: schema.object({}) },
+ },
+ executor,
+ preSaveHook,
+ postSaveHook,
+ postDeleteHook,
+ });
+});
+
+describe('connector type hooks', () => {
+ describe('successful operation and successful hook', () => {
+ test('for create', async () => {
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce(ConnectorSavedObject);
+ const result = await actionsClient.create(CreateParms);
+ expect(result.id).toBe(ConnectorSavedObject.id);
+
+ const preParams = { ...CoreHookParams, logger, isUpdate: false };
+ const postParams = { ...preParams, wasSuccessful: true };
+
+ expect(preSaveHook).toHaveBeenCalledTimes(1);
+ expect(preSaveHook.mock.calls[0]).toStrictEqual([preParams]);
+
+ expect(postSaveHook).toHaveBeenCalledTimes(1);
+ expect(postSaveHook.mock.calls[0]).toStrictEqual([postParams]);
+ });
+
+ test('for update', async () => {
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce(ConnectorSavedObject);
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce(ConnectorSavedObject);
+ const result = await actionsClient.update(UpdateParms);
+ expect(result.id).toBe(ConnectorSavedObject.id);
+
+ const preParams = { ...CoreHookParams, logger, isUpdate: true };
+ const postParams = { ...preParams, wasSuccessful: true };
+
+ expect(preSaveHook).toHaveBeenCalledTimes(1);
+ expect(preSaveHook.mock.calls[0]).toStrictEqual([preParams]);
+
+ expect(postSaveHook).toHaveBeenCalledTimes(1);
+ expect(postSaveHook.mock.calls[0]).toStrictEqual([postParams]);
+ });
+
+ test('for delete', async () => {
+ const expectedResult = Symbol();
+ unsecuredSavedObjectsClient.delete.mockResolvedValueOnce(expectedResult);
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce(ConnectorSavedObject);
+
+ const result = await actionsClient.delete({ id: ConnectorSavedObject.id });
+ expect(result).toBe(expectedResult);
+
+ const postParamsWithSecrets = { ...CoreHookParams, logger };
+ const postParams = omit(postParamsWithSecrets, 'secrets');
+
+ expect(postDeleteHook).toHaveBeenCalledTimes(1);
+ expect(postDeleteHook.mock.calls[0]).toEqual([postParams]);
+ });
+ });
+
+ describe('unsuccessful operation and successful hook', () => {
+ test('for create', async () => {
+ unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('OMG create'));
+ await expect(actionsClient.create(CreateParms)).rejects.toMatchInlineSnapshot(
+ `[Error: OMG create]`
+ );
+
+ const preParams = { ...CoreHookParams, logger, isUpdate: false };
+ const postParams = { ...preParams, wasSuccessful: false };
+
+ expect(preSaveHook).toHaveBeenCalledTimes(1);
+ expect(preSaveHook.mock.calls[0]).toStrictEqual([preParams]);
+
+ expect(postSaveHook).toHaveBeenCalledTimes(1);
+ expect(postSaveHook.mock.calls[0]).toStrictEqual([postParams]);
+ });
+
+ test('for update', async () => {
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce(ConnectorSavedObject);
+ unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('OMG update'));
+ await expect(actionsClient.update(UpdateParms)).rejects.toMatchInlineSnapshot(
+ `[Error: OMG update]`
+ );
+
+ const preParams = { ...CoreHookParams, logger, isUpdate: true };
+ const postParams = { ...preParams, wasSuccessful: false };
+
+ expect(preSaveHook).toHaveBeenCalledTimes(1);
+ expect(preSaveHook.mock.calls[0]).toStrictEqual([preParams]);
+
+ expect(postSaveHook).toHaveBeenCalledTimes(1);
+ expect(postSaveHook.mock.calls[0]).toStrictEqual([postParams]);
+ });
+
+ test('for delete', async () => {
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce(ConnectorSavedObject);
+ unsecuredSavedObjectsClient.delete.mockRejectedValueOnce(new Error('OMG delete'));
+
+ await expect(
+ actionsClient.delete({ id: ConnectorSavedObject.id })
+ ).rejects.toMatchInlineSnapshot(`[Error: OMG delete]`);
+
+ expect(postDeleteHook).toHaveBeenCalledTimes(0);
+ });
+ });
+
+ describe('successful operation and unsuccessful hook', () => {
+ test('for create pre hook', async () => {
+ preSaveHook.mockRejectedValueOnce(new Error('OMG create pre save'));
+
+ await expect(actionsClient.create(CreateParms)).rejects.toMatchInlineSnapshot(
+ `[Error: OMG create pre save]`
+ );
+
+ const preParams = { ...CoreHookParams, logger, isUpdate: false };
+
+ expect(preSaveHook).toHaveBeenCalledTimes(1);
+ expect(preSaveHook.mock.calls[0]).toStrictEqual([preParams]);
+
+ expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(0);
+ expect(postSaveHook).toHaveBeenCalledTimes(0);
+ });
+
+ test('for create post hook', async () => {
+ postSaveHook.mockRejectedValueOnce(new Error('OMG create post save'));
+
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce(ConnectorSavedObject);
+ const result = await actionsClient.create(CreateParms);
+ expect(result.id).toBe(ConnectorSavedObject.id);
+
+ const preParams = { ...CoreHookParams, logger, isUpdate: false };
+ const postParams = { ...preParams, wasSuccessful: true };
+
+ expect(preSaveHook).toHaveBeenCalledTimes(1);
+ expect(preSaveHook.mock.calls[0]).toStrictEqual([preParams]);
+
+ expect(postSaveHook).toHaveBeenCalledTimes(1);
+ expect(postSaveHook.mock.calls[0]).toStrictEqual([postParams]);
+ expect(logger.error).toHaveBeenCalledTimes(1);
+ expect(logger.error.mock.calls).toMatchInlineSnapshot(`
+ Array [
+ Array [
+ "postSaveHook create error for connectorId: \\"connector-id-uuid\\"; type: hooked-action-type: OMG create post save",
+ Object {
+ "tags": Array [
+ "post-save-hook",
+ "connector-id-uuid",
+ ],
+ },
+ ],
+ ]
+ `);
+ });
+
+ test('for update pre hook', async () => {
+ preSaveHook.mockRejectedValueOnce(new Error('OMG update pre save'));
+
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce(ConnectorSavedObject);
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce(ConnectorSavedObject);
+ await expect(actionsClient.update(UpdateParms)).rejects.toMatchInlineSnapshot(
+ `[Error: OMG update pre save]`
+ );
+
+ const preParams = { ...CoreHookParams, logger, isUpdate: true };
+
+ expect(preSaveHook).toHaveBeenCalledTimes(1);
+ expect(preSaveHook.mock.calls[0]).toStrictEqual([preParams]);
+
+ expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(0);
+ expect(postSaveHook).toHaveBeenCalledTimes(0);
+ });
+
+ test('for update post hook', async () => {
+ postSaveHook.mockRejectedValueOnce(new Error('OMG update post save'));
+
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce(ConnectorSavedObject);
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce(ConnectorSavedObject);
+ const result = await actionsClient.update(UpdateParms);
+ expect(result.id).toBe(ConnectorSavedObject.id);
+
+ const preParams = { ...CoreHookParams, logger, isUpdate: true };
+ const postParams = { ...preParams, wasSuccessful: true };
+
+ expect(preSaveHook).toHaveBeenCalledTimes(1);
+ expect(preSaveHook.mock.calls[0]).toStrictEqual([preParams]);
+
+ expect(postSaveHook).toHaveBeenCalledTimes(1);
+ expect(postSaveHook.mock.calls[0]).toStrictEqual([postParams]);
+ expect(logger.error).toHaveBeenCalledTimes(1);
+ expect(logger.error.mock.calls).toMatchInlineSnapshot(`
+ Array [
+ Array [
+ "postSaveHook update error for connectorId: \\"connector-id-uuid\\"; type: hooked-action-type: OMG update post save",
+ Object {
+ "tags": Array [
+ "post-save-hook",
+ "connector-id-uuid",
+ ],
+ },
+ ],
+ ]
+ `);
+ });
+
+ test('for delete post hook', async () => {
+ postDeleteHook.mockRejectedValueOnce(new Error('OMG delete post delete'));
+
+ const expectedResult = Symbol();
+ unsecuredSavedObjectsClient.delete.mockResolvedValueOnce(expectedResult);
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce(ConnectorSavedObject);
+
+ const result = await actionsClient.delete({ id: ConnectorSavedObject.id });
+ expect(result).toBe(expectedResult);
+
+ const postParamsWithSecrets = { ...CoreHookParams, logger };
+ const postParams = omit(postParamsWithSecrets, 'secrets');
+
+ expect(postDeleteHook).toHaveBeenCalledTimes(1);
+ expect(postDeleteHook.mock.calls[0]).toEqual([postParams]);
+ expect(logger.error).toHaveBeenCalledTimes(1);
+ expect(logger.error.mock.calls).toMatchInlineSnapshot(`
+ Array [
+ Array [
+ "The post delete hook failed for for connector \\"connector-id-uuid\\": OMG delete post delete",
+ Object {
+ "tags": Array [
+ "post-delete-hook",
+ "connector-id-uuid",
+ ],
+ },
+ ],
+ ]
+ `);
+ });
+ });
+});
diff --git a/x-pack/plugins/actions/server/application/connector/methods/update/update.ts b/x-pack/plugins/actions/server/application/connector/methods/update/update.ts
index 7baa099a29029..e22715c31d149 100644
--- a/x-pack/plugins/actions/server/application/connector/methods/update/update.ts
+++ b/x-pack/plugins/actions/server/application/connector/methods/update/update.ts
@@ -15,7 +15,8 @@ import { PreconfiguredActionDisabledModificationError } from '../../../../lib/er
import { ConnectorAuditAction, connectorAuditEvent } from '../../../../lib/audit_events';
import { validateConfig, validateConnector, validateSecrets } from '../../../../lib';
import { isConnectorDeprecated } from '../../lib';
-import { RawAction } from '../../../../types';
+import { RawAction, HookServices } from '../../../../types';
+import { tryCatch } from '../../../../lib';
export async function update({ context, id, action }: ConnectorUpdateParams): Promise {
try {
@@ -75,6 +76,33 @@ export async function update({ context, id, action }: ConnectorUpdateParams): Pr
context.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
+ const hookServices: HookServices = {
+ scopedClusterClient: context.scopedClusterClient,
+ };
+
+ if (actionType.preSaveHook) {
+ try {
+ await actionType.preSaveHook({
+ connectorId: id,
+ config,
+ secrets,
+ logger: context.logger,
+ request: context.request,
+ services: hookServices,
+ isUpdate: true,
+ });
+ } catch (error) {
+ context.auditLogger?.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.UPDATE,
+ savedObject: { type: 'action', id },
+ error,
+ })
+ );
+ throw error;
+ }
+ }
+
context.auditLogger?.log(
connectorAuditEvent({
action: ConnectorAuditAction.UPDATE,
@@ -83,27 +111,57 @@ export async function update({ context, id, action }: ConnectorUpdateParams): Pr
})
);
- const result = await context.unsecuredSavedObjectsClient.create(
- 'action',
- {
- ...attributes,
- actionTypeId,
- name,
- isMissingSecrets: false,
- config: validatedActionTypeConfig as SavedObjectAttributes,
- secrets: validatedActionTypeSecrets as SavedObjectAttributes,
- },
- omitBy(
- {
- id,
- overwrite: true,
- references,
- version,
- },
- isUndefined
- )
+ const result = await tryCatch(
+ async () =>
+ await context.unsecuredSavedObjectsClient.create(
+ 'action',
+ {
+ ...attributes,
+ actionTypeId,
+ name,
+ isMissingSecrets: false,
+ config: validatedActionTypeConfig as SavedObjectAttributes,
+ secrets: validatedActionTypeSecrets as SavedObjectAttributes,
+ },
+ omitBy(
+ {
+ id,
+ overwrite: true,
+ references,
+ version,
+ },
+ isUndefined
+ )
+ )
);
+ const wasSuccessful = !(result instanceof Error);
+ const label = `connectorId: "${id}"; type: ${actionTypeId}`;
+ const tags = ['post-save-hook', id];
+
+ if (actionType.postSaveHook) {
+ try {
+ await actionType.postSaveHook({
+ connectorId: id,
+ config,
+ secrets,
+ logger: context.logger,
+ request: context.request,
+ services: hookServices,
+ isUpdate: true,
+ wasSuccessful,
+ });
+ } catch (err) {
+ context.logger.error(`postSaveHook update error for ${label}: ${err.message}`, {
+ tags,
+ });
+ }
+ }
+
+ if (!wasSuccessful) {
+ throw result;
+ }
+
try {
await context.connectorTokenClient.deleteConnectorTokens({ connectorId: id });
} catch (e) {
diff --git a/x-pack/plugins/actions/server/lib/index.ts b/x-pack/plugins/actions/server/lib/index.ts
index 9b8d452f446a9..e13fb85008a84 100644
--- a/x-pack/plugins/actions/server/lib/index.ts
+++ b/x-pack/plugins/actions/server/lib/index.ts
@@ -38,3 +38,4 @@ export {
export { parseDate } from './parse_date';
export type { RelatedSavedObjects } from './related_saved_objects';
export { getBasicAuthHeader, combineHeadersWithBasicAuthHeader } from './get_basic_auth_header';
+export { tryCatch } from './try_catch';
diff --git a/x-pack/plugins/actions/server/lib/try_catch.ts b/x-pack/plugins/actions/server/lib/try_catch.ts
new file mode 100644
index 0000000000000..a9932601c8256
--- /dev/null
+++ b/x-pack/plugins/actions/server/lib/try_catch.ts
@@ -0,0 +1,17 @@
+/*
+ * 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.
+ */
+
+// functional version of try/catch, allows you to not have to use
+// `let` vars initialied to `undefined` to capture the result value
+
+export async function tryCatch(fn: () => Promise): Promise {
+ try {
+ return await fn();
+ } catch (err) {
+ return err;
+ }
+}
diff --git a/x-pack/plugins/actions/server/sub_action_framework/register.test.ts b/x-pack/plugins/actions/server/sub_action_framework/register.test.ts
index a0e56c1a39b80..8ae7f3cf3350f 100644
--- a/x-pack/plugins/actions/server/sub_action_framework/register.test.ts
+++ b/x-pack/plugins/actions/server/sub_action_framework/register.test.ts
@@ -21,6 +21,9 @@ import { ServiceParams } from './types';
describe('Registration', () => {
const renderedVariables = { body: '' };
const mockRenderParameterTemplates = jest.fn().mockReturnValue(renderedVariables);
+ const mockPreSaveHook = jest.fn();
+ const mockPostSaveHook = jest.fn();
+ const mockPostDeleteHook = jest.fn();
const connector = {
id: '.test',
@@ -47,7 +50,12 @@ describe('Registration', () => {
it('registers the connector correctly', async () => {
register({
actionTypeRegistry,
- connector,
+ connector: {
+ ...connector,
+ preSaveHook: mockPreSaveHook,
+ postSaveHook: mockPostSaveHook,
+ postDeleteHook: mockPostDeleteHook,
+ },
configurationUtilities: mockedActionsConfig,
logger,
});
@@ -62,6 +70,9 @@ describe('Registration', () => {
executor: expect.any(Function),
getService: expect.any(Function),
renderParameterTemplates: expect.any(Function),
+ preSaveHook: expect.any(Function),
+ postSaveHook: expect.any(Function),
+ postDeleteHook: expect.any(Function),
});
});
diff --git a/x-pack/plugins/actions/server/sub_action_framework/register.ts b/x-pack/plugins/actions/server/sub_action_framework/register.ts
index dd05cc4e99967..04e7f0d9ea417 100644
--- a/x-pack/plugins/actions/server/sub_action_framework/register.ts
+++ b/x-pack/plugins/actions/server/sub_action_framework/register.ts
@@ -43,5 +43,8 @@ export const register = {
/**
@@ -76,6 +77,35 @@ export type Validators = Array<
ConfigValidator | SecretsValidator
>;
+export interface PreSaveConnectorHookParams {
+ connectorId: string;
+ config: Config;
+ secrets: Secrets;
+ logger: Logger;
+ request: KibanaRequest;
+ services: HookServices;
+ isUpdate: boolean;
+}
+
+export interface PostSaveConnectorHookParams {
+ connectorId: string;
+ config: Config;
+ secrets: Secrets;
+ logger: Logger;
+ request: KibanaRequest;
+ services: HookServices;
+ isUpdate: boolean;
+ wasSuccessful: boolean;
+}
+
+export interface PostDeleteConnectorHookParams {
+ connectorId: string;
+ config: Config;
+ logger: Logger;
+ services: HookServices;
+ request: KibanaRequest;
+}
+
export interface SubActionConnectorType {
id: string;
name: string;
@@ -92,6 +122,9 @@ export interface SubActionConnectorType {
getKibanaPrivileges?: (args?: {
params?: { subAction: string; subActionParams: Record };
}) => string[];
+ preSaveHook?: (params: PreSaveConnectorHookParams) => Promise;
+ postSaveHook?: (params: PostSaveConnectorHookParams) => Promise;
+ postDeleteHook?: (params: PostDeleteConnectorHookParams) => Promise;
}
export interface ExecutorParams extends ActionTypeParams {
diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts
index 487e7630d40f9..d7c3497edc376 100644
--- a/x-pack/plugins/actions/server/types.ts
+++ b/x-pack/plugins/actions/server/types.ts
@@ -16,6 +16,7 @@ import {
SavedObjectReference,
Logger,
ISavedObjectsRepository,
+ IScopedClusterClient,
} from '@kbn/core/server';
import { AnySchema } from 'joi';
import { SubActionConnector } from './sub_action_framework/sub_action_connector';
@@ -57,6 +58,10 @@ export interface UnsecuredServices {
connectorTokenClient: ConnectorTokenClient;
}
+export interface HookServices {
+ scopedClusterClient: IScopedClusterClient;
+}
+
export interface ActionsApiRequestHandlerContext {
getActionsClient: () => ActionsClient;
listTypes: ActionTypeRegistry['list'];
@@ -138,6 +143,44 @@ export type RenderParameterTemplates = (
actionId?: string
) => Params;
+export interface PreSaveConnectorHookParams<
+ Config extends ActionTypeConfig = ActionTypeConfig,
+ Secrets extends ActionTypeSecrets = ActionTypeSecrets
+> {
+ connectorId: string;
+ config: Config;
+ secrets: Secrets;
+ logger: Logger;
+ request: KibanaRequest;
+ services: HookServices;
+ isUpdate: boolean;
+}
+
+export interface PostSaveConnectorHookParams<
+ Config extends ActionTypeConfig = ActionTypeConfig,
+ Secrets extends ActionTypeSecrets = ActionTypeSecrets
+> {
+ connectorId: string;
+ config: Config;
+ secrets: Secrets;
+ logger: Logger;
+ request: KibanaRequest;
+ services: HookServices;
+ isUpdate: boolean;
+ wasSuccessful: boolean;
+}
+
+export interface PostDeleteConnectorHookParams<
+ Config extends ActionTypeConfig = ActionTypeConfig,
+ Secrets extends ActionTypeSecrets = ActionTypeSecrets
+> {
+ connectorId: string;
+ config: Config;
+ logger: Logger;
+ request: KibanaRequest;
+ services: HookServices;
+}
+
export interface ActionType<
Config extends ActionTypeConfig = ActionTypeConfig,
Secrets extends ActionTypeSecrets = ActionTypeSecrets,
@@ -171,6 +214,9 @@ export interface ActionType<
renderParameterTemplates?: RenderParameterTemplates;
executor: ExecutorType;
getService?: (params: ServiceParams) => SubActionConnector;
+ preSaveHook?: (params: PreSaveConnectorHookParams) => Promise;
+ postSaveHook?: (params: PostSaveConnectorHookParams) => Promise;
+ postDeleteHook?: (params: PostDeleteConnectorHookParams) => Promise;
}
export interface RawAction extends Record {
diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts
index fb0194b01be99..3ff3def3f4b70 100644
--- a/x-pack/test/alerting_api_integration/common/config.ts
+++ b/x-pack/test/alerting_api_integration/common/config.ts
@@ -77,6 +77,7 @@ const enabledActionTypes = [
'test.system-action',
'test.system-action-kibana-privileges',
'test.system-action-connector-adapter',
+ 'test.connector-with-hooks',
];
export function createTestConfig(name: string, options: CreateTestConfigOptions) {
diff --git a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts
index f6903da3c62bc..8d5caf79a4c89 100644
--- a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts
+++ b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/action_types.ts
@@ -76,6 +76,7 @@ export function defineActionTypes(
actions.registerType(getNoAttemptsRateLimitedActionType());
actions.registerType(getAuthorizationActionType(core));
actions.registerType(getExcludedActionType());
+ actions.registerType(getHookedActionType());
/**
* System actions
@@ -139,6 +140,96 @@ function getIndexRecordActionType() {
return result;
}
+function getHookedActionType() {
+ const paramsSchema = schema.object({});
+ type ParamsType = TypeOf;
+ const configSchema = schema.object({
+ index: schema.string(),
+ source: schema.string(),
+ });
+ type ConfigType = TypeOf;
+ const secretsSchema = schema.object({
+ encrypted: schema.string(),
+ });
+ type SecretsType = TypeOf;
+ const result: ActionType = {
+ id: 'test.connector-with-hooks',
+ name: 'Test: Connector with hooks',
+ minimumLicenseRequired: 'gold',
+ supportedFeatureIds: ['alerting'],
+ validate: {
+ params: { schema: paramsSchema },
+ config: { schema: configSchema },
+ secrets: { schema: secretsSchema },
+ },
+ async executor({ config, secrets, params, services, actionId }) {
+ return { status: 'ok', actionId };
+ },
+ async preSaveHook({ connectorId, config, secrets, services, isUpdate, logger }) {
+ const body = {
+ state: {
+ connectorId,
+ config,
+ secrets,
+ isUpdate,
+ },
+ reference: 'pre-save',
+ source: config.source,
+ };
+ logger.info(`running hook pre-save for ${JSON.stringify(body)}`);
+ await services.scopedClusterClient.asInternalUser.index({
+ index: config.index,
+ refresh: 'wait_for',
+ body,
+ });
+ },
+ async postSaveHook({
+ connectorId,
+ config,
+ secrets,
+ services,
+ logger,
+ isUpdate,
+ wasSuccessful,
+ }) {
+ const body = {
+ state: {
+ connectorId,
+ config,
+ secrets,
+ isUpdate,
+ wasSuccessful,
+ },
+ reference: 'post-save',
+ source: config.source,
+ };
+ logger.info(`running hook post-save for ${JSON.stringify(body)}`);
+ await services.scopedClusterClient.asInternalUser.index({
+ index: config.index,
+ refresh: 'wait_for',
+ body,
+ });
+ },
+ async postDeleteHook({ connectorId, config, services, logger }) {
+ const body = {
+ state: {
+ connectorId,
+ config,
+ },
+ reference: 'post-delete',
+ source: config.source,
+ };
+ logger.info(`running hook post-delete for ${JSON.stringify(body)}`);
+ await services.scopedClusterClient.asInternalUser.index({
+ index: config.index,
+ refresh: 'wait_for',
+ body,
+ });
+ },
+ };
+ return result;
+}
+
function getDelayedActionType() {
const paramsSchema = schema.object({
delayInMs: schema.number({ defaultValue: 1000 }),
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/create.ts
index 017fd3e45999b..e05a1ea9e0350 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/create.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/create.ts
@@ -7,6 +7,7 @@
import { v4 as uuidv4 } from 'uuid';
import expect from '@kbn/expect';
+import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers';
import { UserAtSpaceScenarios } from '../../../scenarios';
import { checkAAD, getUrlPrefix, ObjectRemover } from '../../../../common/lib';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
@@ -15,11 +16,21 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context';
export default function createActionTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
+ const es = getService('es');
+ const retry = getService('retry');
+ const esTestIndexTool = new ESTestIndexTool(es, retry);
describe('create', () => {
const objectRemover = new ObjectRemover(supertest);
- after(() => objectRemover.removeAll());
+ before(async () => {
+ await esTestIndexTool.destroy();
+ await esTestIndexTool.setup();
+ });
+ after(async () => {
+ await esTestIndexTool.destroy();
+ await objectRemover.removeAll();
+ });
for (const scenario of UserAtSpaceScenarios) {
const { user, space } = scenario;
@@ -396,6 +407,74 @@ export default function createActionTests({ getService }: FtrProviderContext) {
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
+
+ it('should handle save hooks appropriately', async () => {
+ const source = uuidv4();
+ const encryptedValue = 'This value should be encrypted';
+ const response = await supertestWithoutAuth
+ .post(`${getUrlPrefix(space.id)}/api/actions/connector`)
+ .auth(user.username, user.password)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'Hooked action',
+ connector_type_id: 'test.connector-with-hooks',
+ config: {
+ index: ES_TEST_INDEX_NAME,
+ source,
+ },
+ secrets: {
+ encrypted: encryptedValue,
+ },
+ });
+
+ const searchResult = await esTestIndexTool.search(source);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'global_read at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(searchResult.body.hits.hits.length).to.eql(0);
+ break;
+ case 'superuser at space1':
+ case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(200);
+ objectRemover.add(space.id, response.body.id, 'action', 'actions');
+
+ const refs: string[] = [];
+ for (const hit of searchResult.body.hits.hits) {
+ const doc = hit._source as any;
+
+ const reference = doc.reference;
+ delete doc.reference;
+ refs.push(reference);
+
+ if (reference === 'post-save') {
+ expect(doc.state.wasSuccessful).to.be(true);
+ delete doc.state.wasSuccessful;
+ }
+
+ const expected = {
+ state: {
+ connectorId: response.body.id,
+ config: { index: ES_TEST_INDEX_NAME, source },
+ secrets: { encrypted: encryptedValue },
+ isUpdate: false,
+ },
+ source,
+ };
+ expect(doc).to.eql(expected);
+ }
+
+ refs.sort();
+ expect(refs).to.eql(['post-save', 'pre-save']);
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
});
}
});
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/delete.ts
index b5b11036a3dfd..edb9821418f8d 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/delete.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/delete.ts
@@ -5,7 +5,9 @@
* 2.0.
*/
+import { v4 as uuidv4 } from 'uuid';
import expect from '@kbn/expect';
+import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers';
import { UserAtSpaceScenarios } from '../../../scenarios';
import { getUrlPrefix, ObjectRemover } from '../../../../common/lib';
@@ -15,11 +17,21 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context';
export default function deleteActionTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
+ const es = getService('es');
+ const retry = getService('retry');
+ const esTestIndexTool = new ESTestIndexTool(es, retry);
describe('delete', () => {
const objectRemover = new ObjectRemover(supertest);
- after(() => objectRemover.removeAll());
+ before(async () => {
+ await esTestIndexTool.destroy();
+ await esTestIndexTool.setup();
+ });
+ after(async () => {
+ await esTestIndexTool.destroy();
+ await objectRemover.removeAll();
+ });
for (const scenario of UserAtSpaceScenarios) {
const { user, space } = scenario;
@@ -212,6 +224,77 @@ export default function deleteActionTests({ getService }: FtrProviderContext) {
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
+
+ it('should handle delete hooks appropriately', async () => {
+ const source = uuidv4();
+ const encryptedValue = 'This value should be encrypted';
+ const { body: createdAction } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/actions/connector`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'Hooked action',
+ connector_type_id: 'test.connector-with-hooks',
+ config: {
+ index: ES_TEST_INDEX_NAME,
+ source,
+ },
+ secrets: {
+ encrypted: encryptedValue,
+ },
+ })
+ .expect(200);
+
+ // clear out docs from create
+ await esTestIndexTool.destroy();
+ await esTestIndexTool.setup();
+
+ const response = await supertestWithoutAuth
+ .delete(`${getUrlPrefix(space.id)}/api/actions/connector/${createdAction.id}`)
+ .auth(user.username, user.password)
+ .set('kbn-xsrf', 'foo');
+
+ const searchResult = await esTestIndexTool.search(source);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'global_read at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(searchResult.body.hits.hits.length).to.eql(0);
+ objectRemover.add(space.id, createdAction.id, 'action', 'actions');
+ break;
+ case 'superuser at space1':
+ case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(204);
+
+ const refs: string[] = [];
+ for (const hit of searchResult.body.hits.hits) {
+ const doc = hit._source as any;
+
+ const reference = doc.reference;
+ delete doc.reference;
+ refs.push(reference);
+
+ const expected = {
+ state: {
+ connectorId: createdAction.id,
+ config: { index: ES_TEST_INDEX_NAME, source },
+ },
+ source,
+ };
+ expect(doc).to.eql(expected);
+ }
+
+ refs.sort();
+ expect(refs).to.eql(['post-delete']);
+ break;
+ default:
+ objectRemover.add(space.id, createdAction.id, 'action', 'actions');
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
});
}
});
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/update.ts
index 7c3c00534f11d..cb9fe8a94c8c0 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/update.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/update.ts
@@ -5,7 +5,10 @@
* 2.0.
*/
+import { v4 as uuidv4 } from 'uuid';
import expect from '@kbn/expect';
+import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers';
+
import { UserAtSpaceScenarios } from '../../../scenarios';
import { checkAAD, getUrlPrefix, ObjectRemover } from '../../../../common/lib';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
@@ -14,11 +17,21 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context';
export default function updateActionTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
+ const es = getService('es');
+ const retry = getService('retry');
+ const esTestIndexTool = new ESTestIndexTool(es, retry);
describe('update', () => {
const objectRemover = new ObjectRemover(supertest);
- after(() => objectRemover.removeAll());
+ before(async () => {
+ await esTestIndexTool.destroy();
+ await esTestIndexTool.setup();
+ });
+ after(async () => {
+ await esTestIndexTool.destroy();
+ await objectRemover.removeAll();
+ });
for (const scenario of UserAtSpaceScenarios) {
const { user, space } = scenario;
@@ -430,6 +443,94 @@ export default function updateActionTests({ getService }: FtrProviderContext) {
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
+
+ it('should handle save hooks appropriately', async () => {
+ const source = uuidv4();
+ const encryptedValue = 'This value should be encrypted';
+
+ const { body: createdAction } = await supertest
+ .post(`${getUrlPrefix(space.id)}/api/actions/connector`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'Hooked action',
+ connector_type_id: 'test.connector-with-hooks',
+ config: {
+ index: ES_TEST_INDEX_NAME,
+ source,
+ },
+ secrets: {
+ encrypted: encryptedValue,
+ },
+ })
+ .expect(200);
+ objectRemover.add(space.id, createdAction.id, 'action', 'actions');
+
+ // clear out docs from create
+ await esTestIndexTool.destroy();
+ await esTestIndexTool.setup();
+
+ const response = await supertestWithoutAuth
+ .put(`${getUrlPrefix(space.id)}/api/actions/connector/${createdAction.id}`)
+ .auth(user.username, user.password)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'Hooked action',
+ config: {
+ index: ES_TEST_INDEX_NAME,
+ source,
+ },
+ secrets: {
+ encrypted: encryptedValue,
+ },
+ });
+
+ const searchResult = await esTestIndexTool.search(source);
+
+ switch (scenario.id) {
+ case 'no_kibana_privileges at space1':
+ case 'global_read at space1':
+ case 'space_1_all_alerts_none_actions at space1':
+ case 'space_1_all at space2':
+ expect(response.statusCode).to.eql(403);
+ expect(searchResult.body.hits.hits.length).to.eql(0);
+ break;
+ case 'superuser at space1':
+ case 'space_1_all at space1':
+ case 'space_1_all_with_restricted_fixture at space1':
+ expect(response.statusCode).to.eql(200);
+
+ const refs: string[] = [];
+ for (const hit of searchResult.body.hits.hits) {
+ const doc = hit._source as any;
+
+ const reference = doc.reference;
+ delete doc.reference;
+ refs.push(reference);
+
+ if (reference === 'post-save') {
+ expect(doc.state.wasSuccessful).to.be(true);
+ delete doc.state.wasSuccessful;
+ }
+
+ const expected = {
+ state: {
+ connectorId: response.body.id,
+ config: { index: ES_TEST_INDEX_NAME, source },
+ secrets: { encrypted: encryptedValue },
+ isUpdate: true,
+ },
+ source,
+ };
+ expect(doc).to.eql(expected);
+ }
+
+ refs.sort();
+ expect(refs).to.eql(['post-save', 'pre-save']);
+ break;
+ default:
+ throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
+ }
+ });
});
}
});
From 83a701e837a7a84a86dcc8d359154f900f69676a Mon Sep 17 00:00:00 2001
From: Ievgen Sorokopud
Date: Thu, 10 Oct 2024 00:07:31 +0200
Subject: [PATCH 24/87] [Epic] AI Insights + Assistant - Add "Other" option to
the existing OpenAI Connector dropdown list (#8936) (#194831)
---
.../output/kibana.serverless.staging.yaml | 1 +
oas_docs/output/kibana.serverless.yaml | 1 +
oas_docs/output/kibana.staging.yaml | 1 +
oas_docs/output/kibana.yaml | 1 +
...sistant_api_2023_10_31.bundled.schema.yaml | 1 +
...sistant_api_2023_10_31.bundled.schema.yaml | 1 +
.../conversations/common_attributes.gen.ts | 2 +-
.../common_attributes.schema.yaml | 1 +
.../impl/connectorland/helpers.tsx | 1 +
.../server/usage/actions_telemetry.test.ts | 4 +-
x-pack/plugins/actions/server/usage/types.ts | 1 +
.../server/routes/utils.test.ts | 12 +
.../elastic_assistant/server/routes/utils.ts | 26 +-
.../plugins/search_playground/common/types.ts | 1 +
.../public/hooks/use_llms_models.test.ts | 35 +-
.../public/hooks/use_llms_models.ts | 17 +-
.../public/hooks/use_load_connectors.test.ts | 16 +
.../public/hooks/use_load_connectors.ts | 14 +
.../server/lib/get_chat_params.test.ts | 37 ++
.../server/lib/get_chat_params.ts | 2 +-
.../use_attack_discovery/helpers.ts | 1 +
.../common/openai/constants.ts | 1 +
.../stack_connectors/common/openai/schema.ts | 6 +
.../lib/gen_ai/use_get_dashboard.test.ts | 1 +
.../connector_types/openai/connector.test.tsx | 29 ++
.../connector_types/openai/connector.tsx | 10 +
.../connector_types/openai/constants.tsx | 65 +++
.../connector_types/openai/params.test.tsx | 5 +-
.../connector_types/openai/translations.ts | 4 +
.../server/connector_types/openai/index.ts | 6 +-
.../openai/lib/other_openai_utils.test.ts | 116 +++++
.../openai/lib/other_openai_utils.ts | 39 ++
.../connector_types/openai/lib/utils.test.ts | 43 ++
.../connector_types/openai/lib/utils.ts | 11 +-
.../connector_types/openai/openai.test.ts | 428 ++++++++++++++++++
.../schema/xpack_plugins.json | 3 +
.../tests/actions/connector_types/openai.ts | 4 +-
37 files changed, 915 insertions(+), 32 deletions(-)
create mode 100644 x-pack/plugins/stack_connectors/server/connector_types/openai/lib/other_openai_utils.test.ts
create mode 100644 x-pack/plugins/stack_connectors/server/connector_types/openai/lib/other_openai_utils.ts
diff --git a/oas_docs/output/kibana.serverless.staging.yaml b/oas_docs/output/kibana.serverless.staging.yaml
index 69b783c6ccc44..20ab121c161bd 100644
--- a/oas_docs/output/kibana.serverless.staging.yaml
+++ b/oas_docs/output/kibana.serverless.staging.yaml
@@ -40891,6 +40891,7 @@ components:
enum:
- OpenAI
- Azure OpenAI
+ - Other
type: string
Security_AI_Assistant_API_Reader:
additionalProperties: true
diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml
index 69b783c6ccc44..20ab121c161bd 100644
--- a/oas_docs/output/kibana.serverless.yaml
+++ b/oas_docs/output/kibana.serverless.yaml
@@ -40891,6 +40891,7 @@ components:
enum:
- OpenAI
- Azure OpenAI
+ - Other
type: string
Security_AI_Assistant_API_Reader:
additionalProperties: true
diff --git a/oas_docs/output/kibana.staging.yaml b/oas_docs/output/kibana.staging.yaml
index bc0828a44b619..6aa75efa5bd70 100644
--- a/oas_docs/output/kibana.staging.yaml
+++ b/oas_docs/output/kibana.staging.yaml
@@ -49452,6 +49452,7 @@ components:
enum:
- OpenAI
- Azure OpenAI
+ - Other
type: string
Security_AI_Assistant_API_Reader:
additionalProperties: true
diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml
index bc0828a44b619..6aa75efa5bd70 100644
--- a/oas_docs/output/kibana.yaml
+++ b/oas_docs/output/kibana.yaml
@@ -49452,6 +49452,7 @@ components:
enum:
- OpenAI
- Azure OpenAI
+ - Other
type: string
Security_AI_Assistant_API_Reader:
additionalProperties: true
diff --git a/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml
index 1e070b75322d4..8f80e61c07040 100644
--- a/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml
+++ b/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml
@@ -1194,6 +1194,7 @@ components:
enum:
- OpenAI
- Azure OpenAI
+ - Other
type: string
Reader:
additionalProperties: true
diff --git a/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml
index e13d7a05af41f..97c18a2f77b6e 100644
--- a/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml
+++ b/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml
@@ -1194,6 +1194,7 @@ components:
enum:
- OpenAI
- Azure OpenAI
+ - Other
type: string
Reader:
additionalProperties: true
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.gen.ts
index 1ba701474b1f8..1dad26e1628db 100644
--- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.gen.ts
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.gen.ts
@@ -46,7 +46,7 @@ export const Reader = z.object({}).catchall(z.unknown());
* Provider
*/
export type Provider = z.infer;
-export const Provider = z.enum(['OpenAI', 'Azure OpenAI']);
+export const Provider = z.enum(['OpenAI', 'Azure OpenAI', 'Other']);
export type ProviderEnum = typeof Provider.enum;
export const ProviderEnum = Provider.enum;
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.schema.yaml
index f6a8189182474..20423236f7423 100644
--- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.schema.yaml
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.schema.yaml
@@ -34,6 +34,7 @@ components:
enum:
- OpenAI
- Azure OpenAI
+ - Other
MessageRole:
type: string
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/helpers.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/helpers.tsx
index 2bbc74af5a45a..99550f1cafe75 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/helpers.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/helpers.tsx
@@ -18,6 +18,7 @@ import { PRECONFIGURED_CONNECTOR } from './translations';
enum OpenAiProviderType {
OpenAi = 'OpenAI',
AzureAi = 'Azure OpenAI',
+ Other = 'Other',
}
interface GenAiConfig {
diff --git a/x-pack/plugins/actions/server/usage/actions_telemetry.test.ts b/x-pack/plugins/actions/server/usage/actions_telemetry.test.ts
index b4f6d785584a4..26c37b36566e4 100644
--- a/x-pack/plugins/actions/server/usage/actions_telemetry.test.ts
+++ b/x-pack/plugins/actions/server/usage/actions_telemetry.test.ts
@@ -1025,15 +1025,17 @@ describe('actions telemetry', () => {
'.d3security': 2,
'.gen-ai__Azure OpenAI': 3,
'.gen-ai__OpenAI': 1,
+ '.gen-ai__Other': 1,
};
const { countByType, countGenAiProviderTypes } = getCounts(aggs);
expect(countByType).toEqual({
__d3security: 2,
- '__gen-ai': 4,
+ '__gen-ai': 5,
});
expect(countGenAiProviderTypes).toEqual({
'Azure OpenAI': 3,
OpenAI: 1,
+ Other: 1,
});
});
});
diff --git a/x-pack/plugins/actions/server/usage/types.ts b/x-pack/plugins/actions/server/usage/types.ts
index d9fe796c2b4e0..6bdfe316c76e2 100644
--- a/x-pack/plugins/actions/server/usage/types.ts
+++ b/x-pack/plugins/actions/server/usage/types.ts
@@ -51,6 +51,7 @@ export const byGenAiProviderTypeSchema: MakeSchemaFrom['count_by_t
// Known providers:
['Azure OpenAI']: { type: 'long' },
['OpenAI']: { type: 'long' },
+ ['Other']: { type: 'long' },
};
export const byServiceProviderTypeSchema: MakeSchemaFrom['count_active_email_connectors_by_service_type'] =
diff --git a/x-pack/plugins/elastic_assistant/server/routes/utils.test.ts b/x-pack/plugins/elastic_assistant/server/routes/utils.test.ts
index 3ca1b8edb5036..9a77e645686dd 100644
--- a/x-pack/plugins/elastic_assistant/server/routes/utils.test.ts
+++ b/x-pack/plugins/elastic_assistant/server/routes/utils.test.ts
@@ -65,5 +65,17 @@ describe('Utils', () => {
const isOpenModel = isOpenSourceModel(connector);
expect(isOpenModel).toEqual(true);
});
+
+ it('should return `true` when apiProvider of OpenAiProviderType.Other is specified', async () => {
+ const connector = {
+ actionTypeId: '.gen-ai',
+ config: {
+ apiUrl: OPENAI_CHAT_URL,
+ apiProvider: OpenAiProviderType.Other,
+ },
+ } as unknown as Connector;
+ const isOpenModel = isOpenSourceModel(connector);
+ expect(isOpenModel).toEqual(true);
+ });
});
});
diff --git a/x-pack/plugins/elastic_assistant/server/routes/utils.ts b/x-pack/plugins/elastic_assistant/server/routes/utils.ts
index ea05fc814ec69..0fb51c7364809 100644
--- a/x-pack/plugins/elastic_assistant/server/routes/utils.ts
+++ b/x-pack/plugins/elastic_assistant/server/routes/utils.ts
@@ -203,19 +203,25 @@ export const isOpenSourceModel = (connector?: Connector): boolean => {
}
const llmType = getLlmType(connector.actionTypeId);
- const connectorApiUrl = connector.config?.apiUrl
- ? (connector.config.apiUrl as string)
- : undefined;
+ const isOpenAiType = llmType === 'openai';
+
+ if (!isOpenAiType) {
+ return false;
+ }
const connectorApiProvider = connector.config?.apiProvider
? (connector.config?.apiProvider as OpenAiProviderType)
: undefined;
+ if (connectorApiProvider === OpenAiProviderType.Other) {
+ return true;
+ }
- const isOpenAiType = llmType === 'openai';
- const isOpenAI =
- isOpenAiType &&
- (!connectorApiUrl ||
- connectorApiUrl === OPENAI_CHAT_URL ||
- connectorApiProvider === OpenAiProviderType.AzureAi);
+ const connectorApiUrl = connector.config?.apiUrl
+ ? (connector.config.apiUrl as string)
+ : undefined;
- return isOpenAiType && !isOpenAI;
+ return (
+ !!connectorApiUrl &&
+ connectorApiUrl !== OPENAI_CHAT_URL &&
+ connectorApiProvider !== OpenAiProviderType.AzureAi
+ );
};
diff --git a/x-pack/plugins/search_playground/common/types.ts b/x-pack/plugins/search_playground/common/types.ts
index c239858b5b459..e2a0ae34c2ef3 100644
--- a/x-pack/plugins/search_playground/common/types.ts
+++ b/x-pack/plugins/search_playground/common/types.ts
@@ -57,6 +57,7 @@ export enum APIRoutes {
export enum LLMs {
openai = 'openai',
openai_azure = 'openai_azure',
+ openai_other = 'openai_other',
bedrock = 'bedrock',
gemini = 'gemini',
}
diff --git a/x-pack/plugins/search_playground/public/hooks/use_llms_models.test.ts b/x-pack/plugins/search_playground/public/hooks/use_llms_models.test.ts
index d661084306583..ebce3883a471b 100644
--- a/x-pack/plugins/search_playground/public/hooks/use_llms_models.test.ts
+++ b/x-pack/plugins/search_playground/public/hooks/use_llms_models.test.ts
@@ -15,9 +15,10 @@ jest.mock('./use_load_connectors', () => ({
}));
const mockConnectors = [
- { id: 'connectorId1', title: 'OpenAI Connector', type: LLMs.openai },
- { id: 'connectorId2', title: 'OpenAI Azure Connector', type: LLMs.openai_azure },
- { id: 'connectorId2', title: 'Bedrock Connector', type: LLMs.bedrock },
+ { id: 'connectorId1', name: 'OpenAI Connector', type: LLMs.openai },
+ { id: 'connectorId2', name: 'OpenAI Azure Connector', type: LLMs.openai_azure },
+ { id: 'connectorId2', name: 'Bedrock Connector', type: LLMs.bedrock },
+ { id: 'connectorId3', name: 'OpenAI OSS Model Connector', type: LLMs.openai_other },
];
const mockUseLoadConnectors = (data: any) => {
(useLoadConnectors as jest.Mock).mockReturnValue({ data });
@@ -36,7 +37,7 @@ describe('useLLMsModels Hook', () => {
expect(result.current).toEqual([
{
connectorId: 'connectorId1',
- connectorName: undefined,
+ connectorName: 'OpenAI Connector',
connectorType: LLMs.openai,
disabled: false,
icon: expect.any(Function),
@@ -48,7 +49,7 @@ describe('useLLMsModels Hook', () => {
},
{
connectorId: 'connectorId1',
- connectorName: undefined,
+ connectorName: 'OpenAI Connector',
connectorType: LLMs.openai,
disabled: false,
icon: expect.any(Function),
@@ -60,7 +61,7 @@ describe('useLLMsModels Hook', () => {
},
{
connectorId: 'connectorId1',
- connectorName: undefined,
+ connectorName: 'OpenAI Connector',
connectorType: LLMs.openai,
disabled: false,
icon: expect.any(Function),
@@ -72,19 +73,19 @@ describe('useLLMsModels Hook', () => {
},
{
connectorId: 'connectorId2',
- connectorName: undefined,
+ connectorName: 'OpenAI Azure Connector',
connectorType: LLMs.openai_azure,
disabled: false,
icon: expect.any(Function),
- id: 'connectorId2Azure OpenAI ',
- name: 'Azure OpenAI ',
+ id: 'connectorId2OpenAI Azure Connector (Azure OpenAI)',
+ name: 'OpenAI Azure Connector (Azure OpenAI)',
showConnectorName: false,
value: undefined,
promptTokenLimit: undefined,
},
{
connectorId: 'connectorId2',
- connectorName: undefined,
+ connectorName: 'Bedrock Connector',
connectorType: LLMs.bedrock,
disabled: false,
icon: expect.any(Function),
@@ -96,7 +97,7 @@ describe('useLLMsModels Hook', () => {
},
{
connectorId: 'connectorId2',
- connectorName: undefined,
+ connectorName: 'Bedrock Connector',
connectorType: LLMs.bedrock,
disabled: false,
icon: expect.any(Function),
@@ -106,6 +107,18 @@ describe('useLLMsModels Hook', () => {
value: 'anthropic.claude-3-5-sonnet-20240620-v1:0',
promptTokenLimit: 200000,
},
+ {
+ connectorId: 'connectorId3',
+ connectorName: 'OpenAI OSS Model Connector',
+ connectorType: LLMs.openai_other,
+ disabled: false,
+ icon: expect.any(Function),
+ id: 'connectorId3OpenAI OSS Model Connector (OpenAI Compatible Service)',
+ name: 'OpenAI OSS Model Connector (OpenAI Compatible Service)',
+ showConnectorName: false,
+ value: undefined,
+ promptTokenLimit: undefined,
+ },
]);
});
diff --git a/x-pack/plugins/search_playground/public/hooks/use_llms_models.ts b/x-pack/plugins/search_playground/public/hooks/use_llms_models.ts
index 7a9b01e085a6d..3d5cee7719f10 100644
--- a/x-pack/plugins/search_playground/public/hooks/use_llms_models.ts
+++ b/x-pack/plugins/search_playground/public/hooks/use_llms_models.ts
@@ -34,11 +34,22 @@ const mapLlmToModels: Record<
},
[LLMs.openai_azure]: {
icon: OpenAILogo,
- getModels: (connectorName, includeName) => [
+ getModels: (connectorName) => [
{
label: i18n.translate('xpack.searchPlayground.openAIAzureModel', {
- defaultMessage: 'Azure OpenAI {name}',
- values: { name: includeName ? `(${connectorName})` : '' },
+ defaultMessage: '{name} (Azure OpenAI)',
+ values: { name: connectorName },
+ }),
+ },
+ ],
+ },
+ [LLMs.openai_other]: {
+ icon: OpenAILogo,
+ getModels: (connectorName) => [
+ {
+ label: i18n.translate('xpack.searchPlayground.otherOpenAIModel', {
+ defaultMessage: '{name} (OpenAI Compatible Service)',
+ values: { name: connectorName },
}),
},
],
diff --git a/x-pack/plugins/search_playground/public/hooks/use_load_connectors.test.ts b/x-pack/plugins/search_playground/public/hooks/use_load_connectors.test.ts
index 3a68d91fd0246..eb2f36eb62e5f 100644
--- a/x-pack/plugins/search_playground/public/hooks/use_load_connectors.test.ts
+++ b/x-pack/plugins/search_playground/public/hooks/use_load_connectors.test.ts
@@ -71,6 +71,12 @@ describe('useLoadConnectors', () => {
actionTypeId: '.bedrock',
isMissingSecrets: false,
},
+ {
+ id: '5',
+ actionTypeId: '.gen-ai',
+ isMissingSecrets: false,
+ config: { apiProvider: OpenAiProviderType.Other },
+ },
];
mockedLoadConnectors.mockResolvedValue(connectors);
@@ -106,6 +112,16 @@ describe('useLoadConnectors', () => {
title: 'Bedrock',
type: 'bedrock',
},
+ {
+ actionTypeId: '.gen-ai',
+ config: {
+ apiProvider: 'Other',
+ },
+ id: '5',
+ isMissingSecrets: false,
+ title: 'OpenAI Other',
+ type: 'openai_other',
+ },
]);
});
});
diff --git a/x-pack/plugins/search_playground/public/hooks/use_load_connectors.ts b/x-pack/plugins/search_playground/public/hooks/use_load_connectors.ts
index 94bb2da37b1ed..3d2a3e8c90b86 100644
--- a/x-pack/plugins/search_playground/public/hooks/use_load_connectors.ts
+++ b/x-pack/plugins/search_playground/public/hooks/use_load_connectors.ts
@@ -63,6 +63,20 @@ const connectorTypeToLLM: Array<{
type: LLMs.openai,
}),
},
+ {
+ actionId: OPENAI_CONNECTOR_ID,
+ actionProvider: OpenAiProviderType.Other,
+ match: (connector) =>
+ connector.actionTypeId === OPENAI_CONNECTOR_ID &&
+ (connector as OpenAIConnector)?.config?.apiProvider === OpenAiProviderType.Other,
+ transform: (connector) => ({
+ ...connector,
+ title: i18n.translate('xpack.searchPlayground.openAIOtherConnectorTitle', {
+ defaultMessage: 'OpenAI Other',
+ }),
+ type: LLMs.openai_other,
+ }),
+ },
{
actionId: BEDROCK_CONNECTOR_ID,
match: (connector) => connector.actionTypeId === BEDROCK_CONNECTOR_ID,
diff --git a/x-pack/plugins/search_playground/server/lib/get_chat_params.test.ts b/x-pack/plugins/search_playground/server/lib/get_chat_params.test.ts
index cbc696a50085e..614d00dc16e66 100644
--- a/x-pack/plugins/search_playground/server/lib/get_chat_params.test.ts
+++ b/x-pack/plugins/search_playground/server/lib/get_chat_params.test.ts
@@ -152,4 +152,41 @@ describe('getChatParams', () => {
)
).rejects.toThrow('Invalid connector id');
});
+
+ it('returns the correct chat model and uses the default model when not specified in the params', async () => {
+ mockActionsClient.get.mockResolvedValue({
+ id: '2',
+ actionTypeId: OPENAI_CONNECTOR_ID,
+ config: { defaultModel: 'local' },
+ });
+
+ const result = await getChatParams(
+ {
+ connectorId: '2',
+ prompt: 'How does it work?',
+ citations: false,
+ },
+ { actions, request, logger }
+ );
+
+ expect(Prompt).toHaveBeenCalledWith('How does it work?', {
+ citations: false,
+ context: true,
+ type: 'openai',
+ });
+ expect(QuestionRewritePrompt).toHaveBeenCalledWith({
+ type: 'openai',
+ });
+ expect(ActionsClientChatOpenAI).toHaveBeenCalledWith({
+ logger: expect.anything(),
+ model: 'local',
+ connectorId: '2',
+ actionsClient: expect.anything(),
+ signal: expect.anything(),
+ traceId: 'test-uuid',
+ temperature: 0.2,
+ maxRetries: 0,
+ });
+ expect(result.chatPrompt).toContain('How does it work?');
+ });
});
diff --git a/x-pack/plugins/search_playground/server/lib/get_chat_params.ts b/x-pack/plugins/search_playground/server/lib/get_chat_params.ts
index d2c4bb1afaa9d..34f902e0d1ca2 100644
--- a/x-pack/plugins/search_playground/server/lib/get_chat_params.ts
+++ b/x-pack/plugins/search_playground/server/lib/get_chat_params.ts
@@ -57,7 +57,7 @@ export const getChatParams = async (
actionsClient,
logger,
connectorId,
- model,
+ model: model || connector?.config?.defaultModel,
traceId: uuidv4(),
signal: abortSignal,
temperature: getDefaultArguments().temperature,
diff --git a/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/helpers.ts b/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/helpers.ts
index f800651985217..97eb132bdaaeb 100644
--- a/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/helpers.ts
+++ b/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/helpers.ts
@@ -18,6 +18,7 @@ import { isEmpty } from 'lodash/fp';
enum OpenAiProviderType {
OpenAi = 'OpenAI',
AzureAi = 'Azure OpenAI',
+ Other = 'Other',
}
interface GenAiConfig {
diff --git a/x-pack/plugins/stack_connectors/common/openai/constants.ts b/x-pack/plugins/stack_connectors/common/openai/constants.ts
index c57720d9847af..3d629360d03f3 100644
--- a/x-pack/plugins/stack_connectors/common/openai/constants.ts
+++ b/x-pack/plugins/stack_connectors/common/openai/constants.ts
@@ -27,6 +27,7 @@ export enum SUB_ACTION {
export enum OpenAiProviderType {
OpenAi = 'OpenAI',
AzureAi = 'Azure OpenAI',
+ Other = 'Other',
}
export const DEFAULT_TIMEOUT_MS = 120000;
diff --git a/x-pack/plugins/stack_connectors/common/openai/schema.ts b/x-pack/plugins/stack_connectors/common/openai/schema.ts
index f62ee1f35174c..8a08da157b163 100644
--- a/x-pack/plugins/stack_connectors/common/openai/schema.ts
+++ b/x-pack/plugins/stack_connectors/common/openai/schema.ts
@@ -21,6 +21,12 @@ export const ConfigSchema = schema.oneOf([
defaultModel: schema.string({ defaultValue: DEFAULT_OPENAI_MODEL }),
headers: schema.maybe(schema.recordOf(schema.string(), schema.string())),
}),
+ schema.object({
+ apiProvider: schema.oneOf([schema.literal(OpenAiProviderType.Other)]),
+ apiUrl: schema.string(),
+ defaultModel: schema.string(),
+ headers: schema.maybe(schema.recordOf(schema.string(), schema.string())),
+ }),
]);
export const SecretsSchema = schema.object({ apiKey: schema.string() });
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/lib/gen_ai/use_get_dashboard.test.ts b/x-pack/plugins/stack_connectors/public/connector_types/lib/gen_ai/use_get_dashboard.test.ts
index 8ca9b97292fa3..18bcdc6232792 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/lib/gen_ai/use_get_dashboard.test.ts
+++ b/x-pack/plugins/stack_connectors/public/connector_types/lib/gen_ai/use_get_dashboard.test.ts
@@ -53,6 +53,7 @@ describe('useGetDashboard', () => {
it.each([
['Azure OpenAI', 'openai'],
['OpenAI', 'openai'],
+ ['Other', 'openai'],
['Bedrock', 'bedrock'],
])(
'fetches the %p dashboard and sets the dashboard URL with %p',
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/openai/connector.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/openai/connector.test.tsx
index 03d41dd01caa9..2c8eaf8a76257 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/openai/connector.test.tsx
+++ b/x-pack/plugins/stack_connectors/public/connector_types/openai/connector.test.tsx
@@ -50,6 +50,17 @@ const azureConnector = {
apiKey: 'thats-a-nice-looking-key',
},
};
+const otherOpenAiConnector = {
+ ...openAiConnector,
+ config: {
+ apiUrl: 'https://localhost/oss-llm',
+ apiProvider: OpenAiProviderType.Other,
+ defaultModel: 'local-model',
+ },
+ secrets: {
+ apiKey: 'thats-a-nice-looking-key',
+ },
+};
const navigateToUrl = jest.fn();
@@ -93,6 +104,24 @@ describe('ConnectorFields renders', () => {
expect(getAllByTestId('azure-ai-api-keys-doc')[0]).toBeInTheDocument();
});
+ test('other open ai connector fields are rendered', async () => {
+ const { getAllByTestId } = render(
+
+ {}} />
+
+ );
+ expect(getAllByTestId('config.apiUrl-input')[0]).toBeInTheDocument();
+ expect(getAllByTestId('config.apiUrl-input')[0]).toHaveValue(
+ otherOpenAiConnector.config.apiUrl
+ );
+ expect(getAllByTestId('config.apiProvider-select')[0]).toBeInTheDocument();
+ expect(getAllByTestId('config.apiProvider-select')[0]).toHaveValue(
+ otherOpenAiConnector.config.apiProvider
+ );
+ expect(getAllByTestId('other-ai-api-doc')[0]).toBeInTheDocument();
+ expect(getAllByTestId('other-ai-api-keys-doc')[0]).toBeInTheDocument();
+ });
+
describe('Dashboard link', () => {
it('Does not render if isEdit is false and dashboardUrl is defined', async () => {
const { queryByTestId } = render(
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/openai/connector.tsx b/x-pack/plugins/stack_connectors/public/connector_types/openai/connector.tsx
index c940ad76e3643..27cbb9a4dac08 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/openai/connector.tsx
+++ b/x-pack/plugins/stack_connectors/public/connector_types/openai/connector.tsx
@@ -24,6 +24,8 @@ import * as i18n from './translations';
import {
azureAiConfig,
azureAiSecrets,
+ otherOpenAiConfig,
+ otherOpenAiSecrets,
openAiConfig,
openAiSecrets,
providerOptions,
@@ -85,6 +87,14 @@ const ConnectorFields: React.FC = ({ readOnly, isEdi
secretsFormSchema={azureAiSecrets}
/>
)}
+ {config != null && config.apiProvider === OpenAiProviderType.Other && (
+
+ )}
{isEdit && (
+ {`${i18n.OTHER_OPENAI} ${i18n.DOCUMENTATION}`}
+
+ ),
+ }}
+ />
+ ),
+ },
+ {
+ id: 'defaultModel',
+ label: i18n.DEFAULT_MODEL_LABEL,
+ helpText: (
+
+ ),
+ },
+];
+
export const openAiSecrets: SecretsFieldSchema[] = [
{
id: 'apiKey',
@@ -142,6 +177,31 @@ export const azureAiSecrets: SecretsFieldSchema[] = [
},
];
+export const otherOpenAiSecrets: SecretsFieldSchema[] = [
+ {
+ id: 'apiKey',
+ label: i18n.API_KEY_LABEL,
+ isPasswordField: true,
+ helpText: (
+
+ {`${i18n.OTHER_OPENAI} ${i18n.DOCUMENTATION}`}
+
+ ),
+ }}
+ />
+ ),
+ },
+];
+
export const providerOptions = [
{
value: OpenAiProviderType.OpenAi,
@@ -153,4 +213,9 @@ export const providerOptions = [
text: i18n.AZURE_AI,
label: i18n.AZURE_AI,
},
+ {
+ value: OpenAiProviderType.Other,
+ text: i18n.OTHER_OPENAI,
+ label: i18n.OTHER_OPENAI,
+ },
];
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/openai/params.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/openai/params.test.tsx
index 09a2652ad8f1d..7539cc6bf6373 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/openai/params.test.tsx
+++ b/x-pack/plugins/stack_connectors/public/connector_types/openai/params.test.tsx
@@ -37,7 +37,7 @@ describe('Gen AI Params Fields renders', () => {
expect(getByTestId('bodyJsonEditor')).toHaveProperty('value', '{"message": "test"}');
expect(getByTestId('bodyAddVariableButton')).toBeInTheDocument();
});
- test.each([OpenAiProviderType.OpenAi, OpenAiProviderType.AzureAi])(
+ test.each([OpenAiProviderType.OpenAi, OpenAiProviderType.AzureAi, OpenAiProviderType.Other])(
'useEffect handles the case when subAction and subActionParams are undefined and apiProvider is %p',
(apiProvider) => {
const actionParams = {
@@ -79,6 +79,9 @@ describe('Gen AI Params Fields renders', () => {
if (apiProvider === OpenAiProviderType.AzureAi) {
expect(editAction).toHaveBeenCalledWith('subActionParams', { body: DEFAULT_BODY_AZURE }, 0);
}
+ if (apiProvider === OpenAiProviderType.Other) {
+ expect(editAction).toHaveBeenCalledWith('subActionParams', { body: DEFAULT_BODY }, 0);
+ }
}
);
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/openai/translations.ts b/x-pack/plugins/stack_connectors/public/connector_types/openai/translations.ts
index 4c72866c6ece4..55815faac1c8e 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/openai/translations.ts
+++ b/x-pack/plugins/stack_connectors/public/connector_types/openai/translations.ts
@@ -47,6 +47,10 @@ export const AZURE_AI = i18n.translate('xpack.stackConnectors.components.genAi.a
defaultMessage: 'Azure OpenAI',
});
+export const OTHER_OPENAI = i18n.translate('xpack.stackConnectors.components.genAi.otherAi', {
+ defaultMessage: 'Other (OpenAI Compatible Service)',
+});
+
export const DOCUMENTATION = i18n.translate(
'xpack.stackConnectors.components.genAi.documentation',
{
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/openai/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/openai/index.ts
index f8a3a3d32ddb2..5bf0ba6c3a562 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/openai/index.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/openai/index.ts
@@ -53,7 +53,11 @@ export const configValidator = (configObject: Config, validatorServices: Validat
const { apiProvider } = configObject;
- if (apiProvider !== OpenAiProviderType.OpenAi && apiProvider !== OpenAiProviderType.AzureAi) {
+ if (
+ apiProvider !== OpenAiProviderType.OpenAi &&
+ apiProvider !== OpenAiProviderType.AzureAi &&
+ apiProvider !== OpenAiProviderType.Other
+ ) {
throw new Error(
`API Provider is not supported${
apiProvider && (apiProvider as OpenAiProviderType).length ? `: ${apiProvider}` : ``
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/other_openai_utils.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/other_openai_utils.test.ts
new file mode 100644
index 0000000000000..33722314f5422
--- /dev/null
+++ b/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/other_openai_utils.test.ts
@@ -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 { sanitizeRequest, getRequestWithStreamOption } from './other_openai_utils';
+
+describe('Other (OpenAI Compatible Service) Utils', () => {
+ describe('sanitizeRequest', () => {
+ it('sets stream to false when stream is set to true in the body', () => {
+ const body = {
+ model: 'mistral',
+ stream: true,
+ messages: [
+ {
+ role: 'user',
+ content: 'This is a test',
+ },
+ ],
+ };
+
+ const sanitizedBodyString = sanitizeRequest(JSON.stringify(body));
+ expect(sanitizedBodyString).toEqual(
+ `{\"model\":\"mistral\",\"stream\":false,\"messages\":[{\"role\":\"user\",\"content\":\"This is a test\"}]}`
+ );
+ });
+
+ it('sets stream to false when stream is not defined in the body', () => {
+ const body = {
+ model: 'mistral',
+ messages: [
+ {
+ role: 'user',
+ content: 'This is a test',
+ },
+ ],
+ };
+
+ const sanitizedBodyString = sanitizeRequest(JSON.stringify(body));
+ expect(sanitizedBodyString).toEqual(
+ `{\"model\":\"mistral\",\"messages\":[{\"role\":\"user\",\"content\":\"This is a test\"}],\"stream\":false}`
+ );
+ });
+
+ it('sets stream to false when stream is set to false in the body', () => {
+ const body = {
+ model: 'mistral',
+ stream: false,
+ messages: [
+ {
+ role: 'user',
+ content: 'This is a test',
+ },
+ ],
+ };
+
+ const sanitizedBodyString = sanitizeRequest(JSON.stringify(body));
+ expect(sanitizedBodyString).toEqual(
+ `{\"model\":\"mistral\",\"stream\":false,\"messages\":[{\"role\":\"user\",\"content\":\"This is a test\"}]}`
+ );
+ });
+
+ it('does nothing when body is malformed JSON', () => {
+ const bodyString = `{\"model\":\"mistral\",\"messages\":[{\"role\":\"user\",\"content\":\"This is a test\"}],,}`;
+
+ const sanitizedBodyString = sanitizeRequest(bodyString);
+ expect(sanitizedBodyString).toEqual(bodyString);
+ });
+ });
+
+ describe('getRequestWithStreamOption', () => {
+ it('sets stream parameter when stream is not defined in the body', () => {
+ const body = {
+ model: 'mistral',
+ messages: [
+ {
+ role: 'user',
+ content: 'This is a test',
+ },
+ ],
+ };
+
+ const sanitizedBodyString = getRequestWithStreamOption(JSON.stringify(body), true);
+ expect(sanitizedBodyString).toEqual(
+ `{\"model\":\"mistral\",\"messages\":[{\"role\":\"user\",\"content\":\"This is a test\"}],\"stream\":true}`
+ );
+ });
+
+ it('overrides stream parameter if defined in body', () => {
+ const body = {
+ model: 'mistral',
+ stream: true,
+ messages: [
+ {
+ role: 'user',
+ content: 'This is a test',
+ },
+ ],
+ };
+
+ const sanitizedBodyString = getRequestWithStreamOption(JSON.stringify(body), false);
+ expect(sanitizedBodyString).toEqual(
+ `{\"model\":\"mistral\",\"stream\":false,\"messages\":[{\"role\":\"user\",\"content\":\"This is a test\"}]}`
+ );
+ });
+
+ it('does nothing when body is malformed JSON', () => {
+ const bodyString = `{\"model\":\"mistral\",\"messages\":[{\"role\":\"user\",\"content\":\"This is a test\"}],,}`;
+
+ const sanitizedBodyString = getRequestWithStreamOption(bodyString, false);
+ expect(sanitizedBodyString).toEqual(bodyString);
+ });
+ });
+});
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/other_openai_utils.ts b/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/other_openai_utils.ts
new file mode 100644
index 0000000000000..8288e0dba9ad1
--- /dev/null
+++ b/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/other_openai_utils.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.
+ */
+
+/**
+ * Sanitizes the Other (OpenAI Compatible Service) request body to set stream to false
+ * so users cannot specify a streaming response when the framework
+ * is not prepared to handle streaming
+ *
+ * The stream parameter is accepted in the ChatCompletion
+ * API and the Completion API only
+ */
+export const sanitizeRequest = (body: string): string => {
+ return getRequestWithStreamOption(body, false);
+};
+
+/**
+ * Intercepts the Other (OpenAI Compatible Service) request body to set the stream parameter
+ *
+ * The stream parameter is accepted in the ChatCompletion
+ * API and the Completion API only
+ */
+export const getRequestWithStreamOption = (body: string, stream: boolean): string => {
+ try {
+ const jsonBody = JSON.parse(body);
+ if (jsonBody) {
+ jsonBody.stream = stream;
+ }
+
+ return JSON.stringify(jsonBody);
+ } catch (err) {
+ // swallow the error
+ }
+
+ return body;
+};
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/utils.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/utils.test.ts
index 9dffaab3e5e00..142f3a319eeb6 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/utils.test.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/utils.test.ts
@@ -19,8 +19,14 @@ import {
sanitizeRequest as azureAiSanitizeRequest,
getRequestWithStreamOption as azureAiGetRequestWithStreamOption,
} from './azure_openai_utils';
+import {
+ sanitizeRequest as otherOpenAiSanitizeRequest,
+ getRequestWithStreamOption as otherOpenAiGetRequestWithStreamOption,
+} from './other_openai_utils';
+
jest.mock('./openai_utils');
jest.mock('./azure_openai_utils');
+jest.mock('./other_openai_utils');
describe('Utils', () => {
const azureAiUrl =
@@ -38,6 +44,7 @@ describe('Utils', () => {
describe('sanitizeRequest', () => {
const mockOpenAiSanitizeRequest = openAiSanitizeRequest as jest.Mock;
const mockAzureAiSanitizeRequest = azureAiSanitizeRequest as jest.Mock;
+ const mockOtherOpenAiSanitizeRequest = otherOpenAiSanitizeRequest as jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
});
@@ -50,24 +57,36 @@ describe('Utils', () => {
DEFAULT_OPENAI_MODEL
);
expect(mockAzureAiSanitizeRequest).not.toHaveBeenCalled();
+ expect(mockOtherOpenAiSanitizeRequest).not.toHaveBeenCalled();
+ });
+
+ it('calls other_openai_utils sanitizeRequest when provider is Other OpenAi', () => {
+ sanitizeRequest(OpenAiProviderType.Other, OPENAI_CHAT_URL, bodyString, DEFAULT_OPENAI_MODEL);
+ expect(mockOtherOpenAiSanitizeRequest).toHaveBeenCalledWith(bodyString);
+ expect(mockOpenAiSanitizeRequest).not.toHaveBeenCalled();
+ expect(mockAzureAiSanitizeRequest).not.toHaveBeenCalled();
});
it('calls azure_openai_utils sanitizeRequest when provider is AzureAi', () => {
sanitizeRequest(OpenAiProviderType.AzureAi, azureAiUrl, bodyString);
expect(mockAzureAiSanitizeRequest).toHaveBeenCalledWith(azureAiUrl, bodyString);
expect(mockOpenAiSanitizeRequest).not.toHaveBeenCalled();
+ expect(mockOtherOpenAiSanitizeRequest).not.toHaveBeenCalled();
});
it('does not call any helper fns when provider is unrecognized', () => {
sanitizeRequest('foo', OPENAI_CHAT_URL, bodyString);
expect(mockOpenAiSanitizeRequest).not.toHaveBeenCalled();
expect(mockAzureAiSanitizeRequest).not.toHaveBeenCalled();
+ expect(mockOtherOpenAiSanitizeRequest).not.toHaveBeenCalled();
});
});
describe('getRequestWithStreamOption', () => {
const mockOpenAiGetRequestWithStreamOption = openAiGetRequestWithStreamOption as jest.Mock;
const mockAzureAiGetRequestWithStreamOption = azureAiGetRequestWithStreamOption as jest.Mock;
+ const mockOtherOpenAiGetRequestWithStreamOption =
+ otherOpenAiGetRequestWithStreamOption as jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
});
@@ -88,6 +107,15 @@ describe('Utils', () => {
DEFAULT_OPENAI_MODEL
);
expect(mockAzureAiGetRequestWithStreamOption).not.toHaveBeenCalled();
+ expect(mockOtherOpenAiGetRequestWithStreamOption).not.toHaveBeenCalled();
+ });
+
+ it('calls other_openai_utils getRequestWithStreamOption when provider is Other OpenAi', () => {
+ getRequestWithStreamOption(OpenAiProviderType.Other, OPENAI_CHAT_URL, bodyString, true);
+
+ expect(mockOtherOpenAiGetRequestWithStreamOption).toHaveBeenCalledWith(bodyString, true);
+ expect(mockOpenAiGetRequestWithStreamOption).not.toHaveBeenCalled();
+ expect(mockAzureAiGetRequestWithStreamOption).not.toHaveBeenCalled();
});
it('calls azure_openai_utils getRequestWithStreamOption when provider is AzureAi', () => {
@@ -99,6 +127,7 @@ describe('Utils', () => {
true
);
expect(mockOpenAiGetRequestWithStreamOption).not.toHaveBeenCalled();
+ expect(mockOtherOpenAiGetRequestWithStreamOption).not.toHaveBeenCalled();
});
it('does not call any helper fns when provider is unrecognized', () => {
@@ -110,6 +139,7 @@ describe('Utils', () => {
);
expect(mockOpenAiGetRequestWithStreamOption).not.toHaveBeenCalled();
expect(mockAzureAiGetRequestWithStreamOption).not.toHaveBeenCalled();
+ expect(mockOtherOpenAiGetRequestWithStreamOption).not.toHaveBeenCalled();
});
});
@@ -127,6 +157,19 @@ describe('Utils', () => {
});
});
+ it('returns correct axios options when provider is other openai and stream is false', () => {
+ expect(getAxiosOptions(OpenAiProviderType.Other, 'api-abc', false)).toEqual({
+ headers: { Authorization: `Bearer api-abc`, ['content-type']: 'application/json' },
+ });
+ });
+
+ it('returns correct axios options when provider is other openai and stream is true', () => {
+ expect(getAxiosOptions(OpenAiProviderType.Other, 'api-abc', true)).toEqual({
+ headers: { Authorization: `Bearer api-abc`, ['content-type']: 'application/json' },
+ responseType: 'stream',
+ });
+ });
+
it('returns correct axios options when provider is azure openai and stream is false', () => {
expect(getAxiosOptions(OpenAiProviderType.AzureAi, 'api-abc', false)).toEqual({
headers: { ['api-key']: `api-abc`, ['content-type']: 'application/json' },
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/utils.ts b/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/utils.ts
index 811dfd4ce63b4..3028433656503 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/utils.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/openai/lib/utils.ts
@@ -16,6 +16,10 @@ import {
sanitizeRequest as azureAiSanitizeRequest,
getRequestWithStreamOption as azureAiGetRequestWithStreamOption,
} from './azure_openai_utils';
+import {
+ sanitizeRequest as otherOpenAiSanitizeRequest,
+ getRequestWithStreamOption as otherOpenAiGetRequestWithStreamOption,
+} from './other_openai_utils';
export const sanitizeRequest = (
provider: string,
@@ -28,6 +32,8 @@ export const sanitizeRequest = (
return openAiSanitizeRequest(url, body, defaultModel!);
case OpenAiProviderType.AzureAi:
return azureAiSanitizeRequest(url, body);
+ case OpenAiProviderType.Other:
+ return otherOpenAiSanitizeRequest(body);
default:
return body;
}
@@ -42,7 +48,7 @@ export function getRequestWithStreamOption(
): string;
export function getRequestWithStreamOption(
- provider: OpenAiProviderType.AzureAi,
+ provider: OpenAiProviderType.AzureAi | OpenAiProviderType.Other,
url: string,
body: string,
stream: boolean
@@ -68,6 +74,8 @@ export function getRequestWithStreamOption(
return openAiGetRequestWithStreamOption(url, body, stream, defaultModel!);
case OpenAiProviderType.AzureAi:
return azureAiGetRequestWithStreamOption(url, body, stream);
+ case OpenAiProviderType.Other:
+ return otherOpenAiGetRequestWithStreamOption(body, stream);
default:
return body;
}
@@ -81,6 +89,7 @@ export const getAxiosOptions = (
const responseType = stream ? { responseType: 'stream' as ResponseType } : {};
switch (provider) {
case OpenAiProviderType.OpenAi:
+ case OpenAiProviderType.Other:
return {
headers: { Authorization: `Bearer ${apiKey}`, ['content-type']: 'application/json' },
...responseType,
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/openai/openai.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/openai/openai.test.ts
index 87dacaf4e6f17..1362b7610e2cd 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/openai/openai.test.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/openai/openai.test.ts
@@ -20,6 +20,9 @@ import { RunActionResponseSchema, StreamingResponseSchema } from '../../../commo
import { initDashboard } from '../lib/gen_ai/create_gen_ai_dashboard';
import { PassThrough, Transform } from 'stream';
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
+
+const DEFAULT_OTHER_OPENAI_MODEL = 'local-model';
+
jest.mock('../lib/gen_ai/create_gen_ai_dashboard');
const mockTee = jest.fn();
@@ -713,6 +716,431 @@ describe('OpenAIConnector', () => {
});
});
+ describe('Other OpenAI', () => {
+ const connector = new OpenAIConnector({
+ configurationUtilities: actionsConfigMock.create(),
+ connector: { id: '1', type: OPENAI_CONNECTOR_ID },
+ config: {
+ apiUrl: 'http://localhost:1234/v1/chat/completions',
+ apiProvider: OpenAiProviderType.Other,
+ defaultModel: DEFAULT_OTHER_OPENAI_MODEL,
+ headers: {
+ 'X-My-Custom-Header': 'foo',
+ Authorization: 'override',
+ },
+ },
+ secrets: { apiKey: '123' },
+ logger,
+ services: actionsMock.createServices(),
+ });
+
+ const sampleOpenAiBody = {
+ model: DEFAULT_OTHER_OPENAI_MODEL,
+ messages: [
+ {
+ role: 'user',
+ content: 'Hello world',
+ },
+ ],
+ };
+
+ beforeEach(() => {
+ // @ts-ignore
+ connector.request = mockRequest;
+ jest.clearAllMocks();
+ });
+
+ describe('runApi', () => {
+ it('the Other OpenAI API call is successful with correct parameters', async () => {
+ const response = await connector.runApi(
+ { body: JSON.stringify(sampleOpenAiBody) },
+ connectorUsageCollector
+ );
+ expect(mockRequest).toBeCalledTimes(1);
+ expect(mockRequest).toHaveBeenCalledWith(
+ {
+ ...mockDefaults,
+ url: 'http://localhost:1234/v1/chat/completions',
+ data: JSON.stringify({
+ ...sampleOpenAiBody,
+ stream: false,
+ model: DEFAULT_OTHER_OPENAI_MODEL,
+ }),
+ headers: {
+ Authorization: 'Bearer 123',
+ 'X-My-Custom-Header': 'foo',
+ 'content-type': 'application/json',
+ },
+ },
+ connectorUsageCollector
+ );
+ expect(response).toEqual(mockResponse.data);
+ });
+
+ it('overrides stream parameter if set in the body', async () => {
+ const body = {
+ model: 'llama-3.1',
+ messages: [
+ {
+ role: 'user',
+ content: 'Hello world',
+ },
+ ],
+ };
+ const response = await connector.runApi(
+ {
+ body: JSON.stringify({
+ ...body,
+ stream: true,
+ }),
+ },
+ connectorUsageCollector
+ );
+ expect(mockRequest).toBeCalledTimes(1);
+ expect(mockRequest).toHaveBeenCalledWith(
+ {
+ ...mockDefaults,
+ url: 'http://localhost:1234/v1/chat/completions',
+ data: JSON.stringify({
+ ...body,
+ stream: false,
+ }),
+ headers: {
+ Authorization: 'Bearer 123',
+ 'X-My-Custom-Header': 'foo',
+ 'content-type': 'application/json',
+ },
+ },
+ connectorUsageCollector
+ );
+ expect(response).toEqual(mockResponse.data);
+ });
+
+ it('errors during API calls are properly handled', async () => {
+ // @ts-ignore
+ connector.request = mockError;
+
+ await expect(
+ connector.runApi({ body: JSON.stringify(sampleOpenAiBody) }, connectorUsageCollector)
+ ).rejects.toThrow('API Error');
+ });
+ });
+
+ describe('streamApi', () => {
+ it('the Other OpenAI API call is successful with correct parameters when stream = false', async () => {
+ const response = await connector.streamApi(
+ {
+ body: JSON.stringify(sampleOpenAiBody),
+ stream: false,
+ },
+ connectorUsageCollector
+ );
+ expect(mockRequest).toBeCalledTimes(1);
+ expect(mockRequest).toHaveBeenCalledWith(
+ {
+ url: 'http://localhost:1234/v1/chat/completions',
+ method: 'post',
+ responseSchema: RunActionResponseSchema,
+ data: JSON.stringify({
+ ...sampleOpenAiBody,
+ stream: false,
+ }),
+ headers: {
+ Authorization: 'Bearer 123',
+ 'X-My-Custom-Header': 'foo',
+ 'content-type': 'application/json',
+ },
+ },
+ connectorUsageCollector
+ );
+ expect(response).toEqual(mockResponse.data);
+ });
+
+ it('the Other OpenAI API call is successful with correct parameters when stream = true', async () => {
+ const response = await connector.streamApi(
+ {
+ body: JSON.stringify(sampleOpenAiBody),
+ stream: true,
+ },
+ connectorUsageCollector
+ );
+ expect(mockRequest).toBeCalledTimes(1);
+ expect(mockRequest).toHaveBeenCalledWith(
+ {
+ responseType: 'stream',
+ url: 'http://localhost:1234/v1/chat/completions',
+ method: 'post',
+ responseSchema: StreamingResponseSchema,
+ data: JSON.stringify({
+ ...sampleOpenAiBody,
+ stream: true,
+ model: DEFAULT_OTHER_OPENAI_MODEL,
+ }),
+ headers: {
+ Authorization: 'Bearer 123',
+ 'X-My-Custom-Header': 'foo',
+ 'content-type': 'application/json',
+ },
+ },
+ connectorUsageCollector
+ );
+ expect(response).toEqual({
+ headers: { 'Content-Type': 'dont-compress-this' },
+ ...mockResponse.data,
+ });
+ });
+
+ it('overrides stream parameter if set in the body with explicit stream parameter', async () => {
+ const body = {
+ model: 'llama-3.1',
+ messages: [
+ {
+ role: 'user',
+ content: 'Hello world',
+ },
+ ],
+ };
+ const response = await connector.streamApi(
+ {
+ body: JSON.stringify({
+ ...body,
+ stream: false,
+ }),
+ stream: true,
+ },
+ connectorUsageCollector
+ );
+ expect(mockRequest).toBeCalledTimes(1);
+ expect(mockRequest).toHaveBeenCalledWith(
+ {
+ responseType: 'stream',
+ url: 'http://localhost:1234/v1/chat/completions',
+ method: 'post',
+ responseSchema: StreamingResponseSchema,
+ data: JSON.stringify({
+ ...body,
+ stream: true,
+ }),
+ headers: {
+ Authorization: 'Bearer 123',
+ 'X-My-Custom-Header': 'foo',
+ 'content-type': 'application/json',
+ },
+ },
+ connectorUsageCollector
+ );
+ expect(response).toEqual({
+ headers: { 'Content-Type': 'dont-compress-this' },
+ ...mockResponse.data,
+ });
+ });
+
+ it('errors during API calls are properly handled', async () => {
+ // @ts-ignore
+ connector.request = mockError;
+
+ await expect(
+ connector.streamApi(
+ { body: JSON.stringify(sampleOpenAiBody), stream: true },
+ connectorUsageCollector
+ )
+ ).rejects.toThrow('API Error');
+ });
+ });
+
+ describe('invokeStream', () => {
+ const mockStream = (
+ dataToStream: string[] = [
+ 'data: {"object":"chat.completion.chunk","choices":[{"delta":{"content":"My"}}]}\ndata: {"object":"chat.completion.chunk","choices":[{"delta":{"content":" new"}}]}',
+ ]
+ ) => {
+ const streamMock = createStreamMock();
+ dataToStream.forEach((chunk) => {
+ streamMock.write(chunk);
+ });
+ streamMock.complete();
+ mockRequest = jest.fn().mockResolvedValue({ ...mockResponse, data: streamMock.transform });
+ return mockRequest;
+ };
+ beforeEach(() => {
+ // @ts-ignore
+ connector.request = mockStream();
+ });
+
+ it('the API call is successful with correct request parameters', async () => {
+ await connector.invokeStream(sampleOpenAiBody, connectorUsageCollector);
+ expect(mockRequest).toBeCalledTimes(1);
+ expect(mockRequest).toHaveBeenCalledWith(
+ {
+ url: 'http://localhost:1234/v1/chat/completions',
+ method: 'post',
+ responseSchema: StreamingResponseSchema,
+ responseType: 'stream',
+ data: JSON.stringify({
+ ...sampleOpenAiBody,
+ stream: true,
+ }),
+ headers: {
+ Authorization: 'Bearer 123',
+ 'X-My-Custom-Header': 'foo',
+ 'content-type': 'application/json',
+ },
+ },
+ connectorUsageCollector
+ );
+ });
+
+ it('signal is properly passed to streamApi', async () => {
+ const signal = jest.fn();
+ await connector.invokeStream({ ...sampleOpenAiBody, signal }, connectorUsageCollector);
+
+ expect(mockRequest).toHaveBeenCalledWith(
+ {
+ url: 'http://localhost:1234/v1/chat/completions',
+ method: 'post',
+ responseSchema: StreamingResponseSchema,
+ responseType: 'stream',
+ data: JSON.stringify({
+ ...sampleOpenAiBody,
+ stream: true,
+ }),
+ headers: {
+ Authorization: 'Bearer 123',
+ 'X-My-Custom-Header': 'foo',
+ 'content-type': 'application/json',
+ },
+ signal,
+ },
+ connectorUsageCollector
+ );
+ });
+
+ it('timeout is properly passed to streamApi', async () => {
+ const timeout = 180000;
+ await connector.invokeStream({ ...sampleOpenAiBody, timeout }, connectorUsageCollector);
+
+ expect(mockRequest).toHaveBeenCalledWith(
+ {
+ url: 'http://localhost:1234/v1/chat/completions',
+ method: 'post',
+ responseSchema: StreamingResponseSchema,
+ responseType: 'stream',
+ data: JSON.stringify({
+ ...sampleOpenAiBody,
+ stream: true,
+ }),
+ headers: {
+ Authorization: 'Bearer 123',
+ 'X-My-Custom-Header': 'foo',
+ 'content-type': 'application/json',
+ },
+ timeout,
+ },
+ connectorUsageCollector
+ );
+ });
+
+ it('errors during API calls are properly handled', async () => {
+ // @ts-ignore
+ connector.request = mockError;
+
+ await expect(
+ connector.invokeStream(sampleOpenAiBody, connectorUsageCollector)
+ ).rejects.toThrow('API Error');
+ });
+
+ it('responds with a readable stream', async () => {
+ // @ts-ignore
+ connector.request = mockStream();
+ const response = await connector.invokeStream(sampleOpenAiBody, connectorUsageCollector);
+ expect(response instanceof PassThrough).toEqual(true);
+ });
+ });
+
+ describe('invokeAI', () => {
+ it('the API call is successful with correct parameters', async () => {
+ const response = await connector.invokeAI(sampleOpenAiBody, connectorUsageCollector);
+ expect(mockRequest).toBeCalledTimes(1);
+ expect(mockRequest).toHaveBeenCalledWith(
+ {
+ ...mockDefaults,
+ url: 'http://localhost:1234/v1/chat/completions',
+ data: JSON.stringify({
+ ...sampleOpenAiBody,
+ stream: false,
+ model: DEFAULT_OTHER_OPENAI_MODEL,
+ }),
+ headers: {
+ Authorization: 'Bearer 123',
+ 'X-My-Custom-Header': 'foo',
+ 'content-type': 'application/json',
+ },
+ },
+ connectorUsageCollector
+ );
+ expect(response.message).toEqual(mockResponseString);
+ expect(response.usage.total_tokens).toEqual(9);
+ });
+
+ it('signal is properly passed to runApi', async () => {
+ const signal = jest.fn();
+ await connector.invokeAI({ ...sampleOpenAiBody, signal }, connectorUsageCollector);
+
+ expect(mockRequest).toHaveBeenCalledWith(
+ {
+ ...mockDefaults,
+ url: 'http://localhost:1234/v1/chat/completions',
+ data: JSON.stringify({
+ ...sampleOpenAiBody,
+ stream: false,
+ model: DEFAULT_OTHER_OPENAI_MODEL,
+ }),
+ headers: {
+ Authorization: 'Bearer 123',
+ 'X-My-Custom-Header': 'foo',
+ 'content-type': 'application/json',
+ },
+ signal,
+ },
+ connectorUsageCollector
+ );
+ });
+
+ it('timeout is properly passed to runApi', async () => {
+ const timeout = 180000;
+ await connector.invokeAI({ ...sampleOpenAiBody, timeout }, connectorUsageCollector);
+
+ expect(mockRequest).toHaveBeenCalledWith(
+ {
+ ...mockDefaults,
+ url: 'http://localhost:1234/v1/chat/completions',
+ data: JSON.stringify({
+ ...sampleOpenAiBody,
+ stream: false,
+ model: DEFAULT_OTHER_OPENAI_MODEL,
+ }),
+ headers: {
+ Authorization: 'Bearer 123',
+ 'X-My-Custom-Header': 'foo',
+ 'content-type': 'application/json',
+ },
+ timeout,
+ },
+ connectorUsageCollector
+ );
+ });
+
+ it('errors during API calls are properly handled', async () => {
+ // @ts-ignore
+ connector.request = mockError;
+
+ await expect(connector.invokeAI(sampleOpenAiBody, connectorUsageCollector)).rejects.toThrow(
+ 'API Error'
+ );
+ });
+ });
+ });
+
describe('AzureAI', () => {
const connector = new OpenAIConnector({
configurationUtilities: actionsConfigMock.create(),
diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
index 0de2cbd77db7b..0e5d4156d9760 100644
--- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
+++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
@@ -73,6 +73,9 @@
},
"[OpenAI]": {
"type": "long"
+ },
+ "[Other]": {
+ "type": "long"
}
}
},
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/openai.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/openai.ts
index 05dfc61dd59e3..8a47b6a882456 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/openai.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/openai.ts
@@ -147,7 +147,7 @@ export default function genAiTest({ getService }: FtrProviderContext) {
statusCode: 400,
error: 'Bad Request',
message:
- 'error validating action type config: types that failed validation:\n- [0.apiProvider]: expected at least one defined value but got [undefined]\n- [1.apiProvider]: expected at least one defined value but got [undefined]',
+ 'error validating action type config: types that failed validation:\n- [0.apiProvider]: expected at least one defined value but got [undefined]\n- [1.apiProvider]: expected at least one defined value but got [undefined]\n- [2.apiProvider]: expected at least one defined value but got [undefined]',
});
});
});
@@ -168,7 +168,7 @@ export default function genAiTest({ getService }: FtrProviderContext) {
statusCode: 400,
error: 'Bad Request',
message:
- 'error validating action type config: types that failed validation:\n- [0.apiProvider]: expected value to equal [Azure OpenAI]\n- [1.apiUrl]: expected value of type [string] but got [undefined]',
+ 'error validating action type config: types that failed validation:\n- [0.apiProvider]: expected value to equal [Azure OpenAI]\n- [1.apiUrl]: expected value of type [string] but got [undefined]\n- [2.apiProvider]: expected value to equal [Other]',
});
});
});
From d051743e6b4102323d4031113e35e90cdf9da512 Mon Sep 17 00:00:00 2001
From: Jon
Date: Wed, 9 Oct 2024 18:29:09 -0500
Subject: [PATCH 25/87] [ci] Rebuild image after elasticsearch promotion
(#195671)
1) After an elasticsearch image is promoted, this triggers a VM rebuild
to update the snapshot cache
1) Moves elasticsearch builds to later in the day, when there's less
activity.
---
.../pipeline-resource-definitions/kibana-es-snapshots.yml | 8 ++++----
.buildkite/scripts/steps/es_snapshots/promote.sh | 8 ++++++++
2 files changed, 12 insertions(+), 4 deletions(-)
diff --git a/.buildkite/pipeline-resource-definitions/kibana-es-snapshots.yml b/.buildkite/pipeline-resource-definitions/kibana-es-snapshots.yml
index 851862a613111..d386542fbdf0c 100644
--- a/.buildkite/pipeline-resource-definitions/kibana-es-snapshots.yml
+++ b/.buildkite/pipeline-resource-definitions/kibana-es-snapshots.yml
@@ -46,19 +46,19 @@ spec:
access_level: MANAGE_BUILD_AND_READ
schedules:
Daily build (main):
- cronline: 0 9 * * * America/New_York
+ cronline: 0 22 * * * America/New_York
message: Daily build
branch: main
Daily build (8.x):
- cronline: 0 9 * * * America/New_York
+ cronline: 0 22 * * * America/New_York
message: Daily build
branch: '8.x'
Daily build (8.15):
- cronline: 0 9 * * * America/New_York
+ cronline: 0 22 * * * America/New_York
message: Daily build
branch: '8.15'
Daily build (7.17):
- cronline: 0 9 * * * America/New_York
+ cronline: 0 22 * * * America/New_York
message: Daily build
branch: '7.17'
tags:
diff --git a/.buildkite/scripts/steps/es_snapshots/promote.sh b/.buildkite/scripts/steps/es_snapshots/promote.sh
index cf52f5e9ff650..5654d7bd3b8d3 100755
--- a/.buildkite/scripts/steps/es_snapshots/promote.sh
+++ b/.buildkite/scripts/steps/es_snapshots/promote.sh
@@ -16,4 +16,12 @@ ts-node "$(dirname "${0}")/promote_manifest.ts" "$ES_SNAPSHOT_MANIFEST"
if [[ "$BUILDKITE_BRANCH" == "main" ]]; then
echo "--- Trigger agent packer cache pipeline"
ts-node .buildkite/scripts/steps/trigger_pipeline.ts kibana-agent-packer-cache main
+ cat << EOF | buildkite-agent pipeline upload
+steps:
+ - label: "Builds Kibana VM images for cache update"
+ trigger: ci-vm-images
+ build:
+ env:
+ IMAGES_CONFIG="kibana/images.yml"
+EOF
fi
From 69ff471983a543c3052923e6b05385460079e45e Mon Sep 17 00:00:00 2001
From: Philippe Oberti
Date: Thu, 10 Oct 2024 01:49:36 +0200
Subject: [PATCH 26/87] [Security Solution][Notes] - limit visible text from
note content on notes management page (#195296)
---
.../notes/components/note_content.test.tsx | 28 +++++++
.../public/notes/components/note_content.tsx | 73 +++++++++++++++++++
.../public/notes/components/test_ids.ts | 2 +
.../notes/pages/note_management_page.tsx | 2 +
4 files changed, 105 insertions(+)
create mode 100644 x-pack/plugins/security_solution/public/notes/components/note_content.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/notes/components/note_content.tsx
diff --git a/x-pack/plugins/security_solution/public/notes/components/note_content.test.tsx b/x-pack/plugins/security_solution/public/notes/components/note_content.test.tsx
new file mode 100644
index 0000000000000..6cc9d33d886b7
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/notes/components/note_content.test.tsx
@@ -0,0 +1,28 @@
+/*
+ * 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 { render } from '@testing-library/react';
+import React from 'react';
+import { NoteContent } from './note_content';
+import { NOTE_CONTENT_BUTTON_TEST_ID, NOTE_CONTENT_POPOVER_TEST_ID } from './test_ids';
+
+const note = 'note-text';
+
+describe('NoteContent', () => {
+ it('should render a note and the popover', () => {
+ const { getByTestId, getByText } = render( );
+
+ const button = getByTestId(NOTE_CONTENT_BUTTON_TEST_ID);
+
+ expect(button).toBeInTheDocument();
+ expect(getByText(note)).toBeInTheDocument();
+
+ button.click();
+
+ expect(getByTestId(NOTE_CONTENT_POPOVER_TEST_ID)).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/notes/components/note_content.tsx b/x-pack/plugins/security_solution/public/notes/components/note_content.tsx
new file mode 100644
index 0000000000000..ba8710e85c215
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/notes/components/note_content.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, { memo, useCallback, useMemo, useState } from 'react';
+import { EuiButtonEmpty, EuiMarkdownFormat, EuiPopover, useEuiTheme } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { css } from '@emotion/react';
+import { NOTE_CONTENT_BUTTON_TEST_ID, NOTE_CONTENT_POPOVER_TEST_ID } from './test_ids';
+
+const OPEN_POPOVER = i18n.translate('xpack.securitySolution.notes.expandRow.buttonLabel', {
+ defaultMessage: 'Expand',
+});
+
+export interface NoteContentProps {
+ /**
+ * The note content to display
+ */
+ note: string;
+}
+
+/**
+ * Renders the note content to be displayed in the notes management table.
+ * The content is truncated with an expand button to show the full content within the row.
+ */
+export const NoteContent = memo(({ note }: NoteContentProps) => {
+ const { euiTheme } = useEuiTheme();
+
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+ const togglePopover = useCallback(() => setIsPopoverOpen((value) => !value), []);
+ const closePopover = useCallback(() => setIsPopoverOpen(false), []);
+
+ const button = useMemo(
+ () => (
+
+ {note}
+
+ ),
+ [euiTheme.size.l, note, togglePopover]
+ );
+
+ return (
+
+
+ {note}
+
+
+ );
+});
+
+NoteContent.displayName = 'NoteContent';
diff --git a/x-pack/plugins/security_solution/public/notes/components/test_ids.ts b/x-pack/plugins/security_solution/public/notes/components/test_ids.ts
index 6c63a43f365ac..ac4eeb1948748 100644
--- a/x-pack/plugins/security_solution/public/notes/components/test_ids.ts
+++ b/x-pack/plugins/security_solution/public/notes/components/test_ids.ts
@@ -17,3 +17,5 @@ export const DELETE_NOTE_BUTTON_TEST_ID = `${PREFIX}DeleteNotesButton` as const;
export const OPEN_TIMELINE_BUTTON_TEST_ID = `${PREFIX}OpenTimelineButton` as const;
export const OPEN_FLYOUT_BUTTON_TEST_ID = `${PREFIX}OpenFlyoutButton` as const;
export const TIMELINE_DESCRIPTION_COMMENT_TEST_ID = `${PREFIX}TimelineDescriptionComment` as const;
+export const NOTE_CONTENT_BUTTON_TEST_ID = `${PREFIX}NoteContentButton` as const;
+export const NOTE_CONTENT_POPOVER_TEST_ID = `${PREFIX}NoteContentPopover` as const;
diff --git a/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx b/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx
index 9c2900ca4d599..2b7f0f690532c 100644
--- a/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx
+++ b/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx
@@ -44,6 +44,7 @@ import { DeleteConfirmModal } from '../components/delete_confirm_modal';
import * as i18n from './translations';
import { OpenFlyoutButtonIcon } from '../components/open_flyout_button';
import { OpenTimelineButtonIcon } from '../components/open_timeline_button';
+import { NoteContent } from '../components/note_content';
const columns: Array> = [
{
@@ -94,6 +95,7 @@ const columns: Array> = [
{
field: 'note',
name: i18n.NOTE_CONTENT_COLUMN,
+ render: (note: Note['note']) => <>{note && }>,
},
{
field: 'created',
From 65ed9899de2733ec7017ef7277bd24723131684a Mon Sep 17 00:00:00 2001
From: Yara Tercero
Date: Wed, 9 Oct 2024 17:14:03 -0700
Subject: [PATCH 27/87] [Detection Engine] Remove technical preview for certain
rule types of alert suppression (#195425)
## Summary
GA-ing alert suppression for IM rule, ML rule, Threshold rule, ES|QL
rule and New Terms rule. Thanks to @vitaliidm for setting up the
groundwork to easily update which rules GA.
Rules that remain in technical preview are: EQL.
---
.../common/detection_engine/constants.ts | 10 +++++++++-
.../common/detection_engine/utils.test.ts | 10 +++++-----
.../components/step_define_rule/translations.tsx | 8 ++++----
x-pack/plugins/translations/translations/fr-FR.json | 2 --
x-pack/plugins/translations/translations/ja-JP.json | 2 --
x-pack/plugins/translations/translations/zh-CN.json | 2 --
.../indicator_match_rule_suppression.cy.ts | 4 ----
.../indicator_match_rule_suppression_ess_basic.cy.ts | 4 ----
.../machine_learning_rule_suppression.cy.ts | 7 -------
.../detection_engine/rule_edit/esql_rule.cy.ts | 4 ----
.../rule_edit/indicator_match_rule.cy.ts | 4 ----
.../rule_edit/machine_learning_rule.cy.ts | 4 ----
.../detection_engine/rule_edit/threshold_rule.cy.ts | 3 ---
13 files changed, 18 insertions(+), 46 deletions(-)
diff --git a/x-pack/plugins/security_solution/common/detection_engine/constants.ts b/x-pack/plugins/security_solution/common/detection_engine/constants.ts
index 7057e3c8b3091..270af1a91cf46 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/constants.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/constants.ts
@@ -51,4 +51,12 @@ export const SUPPRESSIBLE_ALERT_RULES: Type[] = [
'machine_learning',
];
-export const SUPPRESSIBLE_ALERT_RULES_GA: Type[] = ['saved_query', 'query'];
+export const SUPPRESSIBLE_ALERT_RULES_GA: Type[] = [
+ 'threshold',
+ 'esql',
+ 'saved_query',
+ 'query',
+ 'new_terms',
+ 'threat_match',
+ 'machine_learning',
+];
diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts
index a4db006a67463..be0b6ce9c2927 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts
@@ -250,14 +250,14 @@ describe('Alert Suppression Rules', () => {
test('should return true for rule type suppression in global availability', () => {
expect(isSuppressionRuleInGA('saved_query')).toBe(true);
expect(isSuppressionRuleInGA('query')).toBe(true);
+ expect(isSuppressionRuleInGA('esql')).toBe(true);
+ expect(isSuppressionRuleInGA('threshold')).toBe(true);
+ expect(isSuppressionRuleInGA('threat_match')).toBe(true);
+ expect(isSuppressionRuleInGA('new_terms')).toBe(true);
+ expect(isSuppressionRuleInGA('machine_learning')).toBe(true);
});
test('should return false for rule type suppression in tech preview', () => {
- expect(isSuppressionRuleInGA('machine_learning')).toBe(false);
- expect(isSuppressionRuleInGA('esql')).toBe(false);
- expect(isSuppressionRuleInGA('threshold')).toBe(false);
- expect(isSuppressionRuleInGA('threat_match')).toBe(false);
- expect(isSuppressionRuleInGA('new_terms')).toBe(false);
expect(isSuppressionRuleInGA('eql')).toBe(false);
});
});
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx
index 7d7bb9c4a9253..b212aa7c67dd4 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx
@@ -205,15 +205,15 @@ export const THRESHOLD_SUPPRESSION_PER_RULE_EXECUTION_WARNING = i18n.translate(
export const getEnableThresholdSuppressionLabel = (fields: string[] | undefined) =>
fields?.length ? (
{fields.join(', ')} }}
/>
) : (
i18n.translate(
- 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.enableThresholdSuppressionLabel',
+ 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ga.enableThresholdSuppressionLabel',
{
- defaultMessage: 'Suppress alerts (Technical Preview)',
+ defaultMessage: 'Suppress alerts',
}
)
);
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 9aa58bd4f5286..c6f8753f75b9e 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -36129,8 +36129,6 @@
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredEmptyError": "Toutes les correspondances requièrent un champ et un champ d'index des menaces.",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredError": "Au moins une correspondance d'indicateur est requise.",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.dataViewSelectorFieldRequired": "Veuillez sélectionner une vue des données ou un modèle d'index disponible.",
- "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.enableThresholdSuppressionForFieldsLabel": "Supprimer les alertes par champs sélectionnés : {fieldsString} (version d'évaluation technique)",
- "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.enableThresholdSuppressionLabel": "Supprimer les alertes (version d'évaluation technique)",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.EqlQueryBarLabel": "Requête EQL",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.eqlQueryFieldRequiredError": "Une requête EQL est requise.",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.eqlSequenceSuppressionDisableText": "La suppression n'est pas prise en charge pour les requêtes de séquence EQL.",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 72afb1947e928..19a01d7325113 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -35873,8 +35873,6 @@
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredEmptyError": "すべての一致には、フィールドと脅威インデックスフィールドの両方が必要です。",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredError": "1 つ以上のインジケーター一致が必要です。",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.dataViewSelectorFieldRequired": "使用可能なデータビューまたはインデックスパターンを選択してください。",
- "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.enableThresholdSuppressionForFieldsLabel": "選択したフィールドでアラートを非表示:{fieldsString}(テクニカルプレビュー)",
- "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.enableThresholdSuppressionLabel": "アラートを抑制(テクニカルプレビュー)",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.EqlQueryBarLabel": "EQL クエリ",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.eqlQueryFieldRequiredError": "EQLクエリは必須です。",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.eqlSequenceSuppressionDisableText": "EQLシーケンスクエリでは抑制はサポートされていません。",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index c27a5241e5a33..30ac0196e8993 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -35917,8 +35917,6 @@
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredEmptyError": "所有匹配项都需要字段和威胁索引字段。",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customThreatQueryFieldRequiredError": "至少需要一个指标匹配。",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.dataViewSelectorFieldRequired": "请选择可用的数据视图或索引模式。",
- "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.enableThresholdSuppressionForFieldsLabel": "选定字段阻止告警:{fieldsString}(技术预览)",
- "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.enableThresholdSuppressionLabel": "阻止告警(技术预览)",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.EqlQueryBarLabel": "EQL 查询",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.eqlQueryFieldRequiredError": "EQL 查询必填。",
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.eqlSequenceSuppressionDisableText": "EQL 序列查询不支持阻止。",
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/indicator_match_rule_suppression.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/indicator_match_rule_suppression.cy.ts
index 42fb37184da1c..d0539683e5a64 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/indicator_match_rule_suppression.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/indicator_match_rule_suppression.cy.ts
@@ -12,7 +12,6 @@ import {
SUPPRESS_FOR_DETAILS,
SUPPRESS_BY_DETAILS,
SUPPRESS_MISSING_FIELD,
- DETAILS_TITLE,
} from '../../../../screens/rule_details';
import {
@@ -67,9 +66,6 @@ describe(
'have.text',
'Suppress and group alerts for events with missing fields'
);
-
- // suppression functionality should be under Tech Preview
- cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).contains('Technical Preview');
});
fillAboutRuleMinimumAndContinue(rule);
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/indicator_match_rule_suppression_ess_basic.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/indicator_match_rule_suppression_ess_basic.cy.ts
index dd3c086224e49..6223ac017281d 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/indicator_match_rule_suppression_ess_basic.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/indicator_match_rule_suppression_ess_basic.cy.ts
@@ -9,7 +9,6 @@ import { getNewThreatIndicatorRule } from '../../../../objects/rule';
import {
SUPPRESS_FOR_DETAILS,
- DETAILS_TITLE,
SUPPRESS_BY_DETAILS,
SUPPRESS_MISSING_FIELD,
DEFINITION_DETAILS,
@@ -62,9 +61,6 @@ describe(
'have.text',
'Do not suppress alerts for events with missing fields'
);
-
- // suppression functionality should be under Tech Preview
- cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).contains('Technical Preview');
});
// Platinum license is required for configuration to apply
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule_suppression.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule_suppression.cy.ts
index c38a6ef43150a..45ccc2c5aba8d 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule_suppression.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule_suppression.cy.ts
@@ -13,7 +13,6 @@ import {
} from '../../../../screens/create_new_rule';
import {
DEFINITION_DETAILS,
- DETAILS_TITLE,
SUPPRESS_BY_DETAILS,
SUPPRESS_FOR_DETAILS,
SUPPRESS_MISSING_FIELD,
@@ -129,9 +128,6 @@ describe(
'have.text',
'Suppress and group alerts for events with missing fields'
);
-
- // suppression functionality should be under Tech Preview
- cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).contains('Technical Preview');
});
fillAboutRuleMinimumAndContinue(mlRule);
@@ -163,9 +159,6 @@ describe(
'have.text',
'Do not suppress alerts for events with missing fields'
);
-
- // suppression functionality should be under Tech Preview
- cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).contains('Technical Preview');
});
fillAboutRuleMinimumAndContinue(mlRule);
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/esql_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/esql_rule.cy.ts
index 511ea42c06767..9fa45987407f0 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/esql_rule.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/esql_rule.cy.ts
@@ -14,7 +14,6 @@ import {
DEFINITION_DETAILS,
SUPPRESS_MISSING_FIELD,
SUPPRESS_BY_DETAILS,
- DETAILS_TITLE,
} from '../../../../screens/rule_details';
import {
@@ -191,9 +190,6 @@ describe(
'have.text',
'Suppress and group alerts for events with missing fields'
);
-
- // suppression functionality should be under Tech Preview
- cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).contains('Technical Preview');
});
});
});
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/indicator_match_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/indicator_match_rule.cy.ts
index 62d9a95398797..fe616f6ba1969 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/indicator_match_rule.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/indicator_match_rule.cy.ts
@@ -9,7 +9,6 @@ import { getNewThreatIndicatorRule } from '../../../../objects/rule';
import {
SUPPRESS_FOR_DETAILS,
- DETAILS_TITLE,
SUPPRESS_BY_DETAILS,
SUPPRESS_MISSING_FIELD,
DEFINITION_DETAILS,
@@ -81,9 +80,6 @@ describe(
'have.text',
'Suppress and group alerts for events with missing fields'
);
-
- // suppression functionality should be under Tech Preview
- cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).contains('Technical Preview');
});
});
});
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/machine_learning_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/machine_learning_rule.cy.ts
index e89e4b6afb817..7410d9fefae6d 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/machine_learning_rule.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/machine_learning_rule.cy.ts
@@ -13,7 +13,6 @@ import {
} from '../../../../screens/create_new_rule';
import {
DEFINITION_DETAILS,
- DETAILS_TITLE,
SUPPRESS_BY_DETAILS,
SUPPRESS_FOR_DETAILS,
SUPPRESS_MISSING_FIELD,
@@ -88,9 +87,6 @@ describe(
'have.text',
'Suppress and group alerts for events with missing fields'
);
-
- // suppression functionality should be under Tech Preview
- cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).contains('Technical Preview');
});
});
});
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/threshold_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/threshold_rule.cy.ts
index 8d4bdf2d34976..dcc35a9e00080 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/threshold_rule.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/threshold_rule.cy.ts
@@ -9,7 +9,6 @@ import { getNewThresholdRule } from '../../../../objects/rule';
import {
SUPPRESS_FOR_DETAILS,
- DETAILS_TITLE,
SUPPRESS_BY_DETAILS,
SUPPRESS_MISSING_FIELD,
} from '../../../../screens/rule_details';
@@ -63,8 +62,6 @@ describe(
// ensure typed interval is displayed on details page
getDetails(SUPPRESS_FOR_DETAILS).should('have.text', '60m');
- // suppression functionality should be under Tech Preview
- cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).contains('Technical Preview');
// the rest of suppress properties do not exist for threshold rule
assertDetailsNotExist(SUPPRESS_BY_DETAILS);
From b51ba0a27c852f967b922130d01ac7cf2ec11d64 Mon Sep 17 00:00:00 2001
From: Paulo Silva
Date: Wed, 9 Oct 2024 17:36:33 -0700
Subject: [PATCH 28/87] fix flaky test with timestamp (#195681)
## Summary
It fixes the flaky test raised on #195634 by adding the possibility to
pass the timestamp to the function. That helps to eliminate flakiness,
by passing the same `currentTimestamp` to both the test and the
function. Also, it's a simpler approach that doesn't require mocking
global objects or using Jest's fake timers, keeping your test
straightforward and easy to understand.
---
.../create_detection_rule_from_vulnerability.test.ts | 2 +-
.../utils/create_detection_rule_from_vulnerability.ts | 9 +++++----
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.test.ts b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.test.ts
index 209ec81168271..7dd0982cc58b5 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.test.ts
+++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.test.ts
@@ -89,7 +89,7 @@ describe('CreateDetectionRuleFromVulnerability', () => {
} as Vulnerability;
const currentTimestamp = new Date().toISOString();
- const query = generateVulnerabilitiesRuleQuery(mockVulnerability);
+ const query = generateVulnerabilitiesRuleQuery(mockVulnerability, currentTimestamp);
expect(query).toEqual(
`vulnerability.id: "CVE-2024-00005" AND event.ingested >= "${currentTimestamp}"`
);
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.ts b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.ts
index b723c60f9ee3d..804e89fad61d8 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.ts
+++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.ts
@@ -53,10 +53,11 @@ export const getVulnerabilityRuleName = (vulnerability: Vulnerability) => {
});
};
-export const generateVulnerabilitiesRuleQuery = (vulnerability: Vulnerability) => {
- const currentTimestamp = new Date().toISOString();
-
- return `vulnerability.id: "${vulnerability.id}" AND event.ingested >= "${currentTimestamp}"`;
+export const generateVulnerabilitiesRuleQuery = (
+ vulnerability: Vulnerability,
+ startTimestamp = new Date().toISOString()
+) => {
+ return `vulnerability.id: "${vulnerability.id}" AND event.ingested >= "${startTimestamp}"`;
};
const CSP_RULE_TAG = 'Cloud Security';
From 447617e2be18cbf8fdd495cb4b9570921b7fd467 Mon Sep 17 00:00:00 2001
From: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com>
Date: Wed, 9 Oct 2024 19:01:58 -0700
Subject: [PATCH 29/87] [ResponseOps][Flapping] Add Rule Specific Flapping Form
to New Rule Form Page (#194516)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Depends on: https://github.com/elastic/kibana/pull/194086
Designs:
https://www.figma.com/design/eTr6WsKzhSLcQ24AlgrY8R/Flapping-per-Rule--%3E-%23294?node-id=5265-29867&node-type=frame&t=1VfgdlcjkSHmpbje-0
Adds the rule specific flapping form to the new rule form page.
## To test:
1. change `IS_RULE_SPECIFIC_FLAPPING_ENABLED` to true
2. run `yarn start --run-examples`
3. assert the new flapping UI exists by going to developer examples ->
create/edit rule
### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine
---
packages/kbn-alerting-types/index.ts | 1 +
packages/kbn-alerting-types/rule_settings.ts | 46 ++
.../fetch_flapping_settings.test.ts | 44 ++
.../fetch_flapping_settings.ts | 21 +
.../apis/fetch_flapping_settings/index.ts | 10 +
...ansform_flapping_settings_response.test.ts | 36 ++
.../transform_flapping_settings_response.ts | 29 ++
.../src/common/constants/rule_flapping.ts | 11 +
.../use_fetch_flapping_settings.test.tsx | 106 +++++
.../hooks/use_fetch_flapping_settings.ts | 43 ++
.../src/rule_form/create_rule_form.tsx | 3 +
.../src/rule_form/edit_rule_form.tsx | 3 +
.../hooks/use_load_dependencies.test.tsx | 20 +
.../rule_form/hooks/use_load_dependencies.ts | 18 +
.../rule_definition/rule_definition.test.tsx | 133 ++++++
.../rule_definition/rule_definition.tsx | 62 ++-
.../src/rule_form/translations.ts | 15 +
.../src/rule_form/types.ts | 4 +-
.../rule_settings_flapping_form.tsx | 318 +++++++++++++
.../rule_settings_flapping_message.tsx | 31 +-
.../rule_settings_flapping_title_tooltip.tsx | 140 ++++++
.../plugins/alerting/common/rules_settings.ts | 50 +--
.../rules_settings_flapping_form_section.tsx | 1 +
.../rules_settings_link.test.tsx | 12 +-
.../rules_settings_modal.test.tsx | 18 +-
.../rules_setting/rules_settings_modal.tsx | 6 +-
.../hooks/use_get_flapping_settings.ts | 41 --
.../lib/rule_api/get_flapping_settings.ts | 28 --
.../sections/rule_form/rule_add.test.tsx | 4 +-
.../sections/rule_form/rule_add.tsx | 2 +-
.../sections/rule_form/rule_edit.test.tsx | 4 +-
.../sections/rule_form/rule_edit.tsx | 2 +-
.../sections/rule_form/rule_form.test.tsx | 4 +-
.../sections/rule_form/rule_form.tsx | 9 +-
.../rule_form_advanced_options.test.tsx | 8 +-
.../rule_form/rule_form_advanced_options.tsx | 416 +-----------------
.../public/common/constants/index.ts | 3 -
37 files changed, 1162 insertions(+), 540 deletions(-)
create mode 100644 packages/kbn-alerting-types/rule_settings.ts
create mode 100644 packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/fetch_flapping_settings.test.ts
create mode 100644 packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/fetch_flapping_settings.ts
create mode 100644 packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/index.ts
create mode 100644 packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/transform_flapping_settings_response.test.ts
create mode 100644 packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/transform_flapping_settings_response.ts
create mode 100644 packages/kbn-alerts-ui-shared/src/common/constants/rule_flapping.ts
create mode 100644 packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.test.tsx
create mode 100644 packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.ts
create mode 100644 packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_form.tsx
create mode 100644 packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx
delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_flapping_settings.ts
delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_flapping_settings.ts
diff --git a/packages/kbn-alerting-types/index.ts b/packages/kbn-alerting-types/index.ts
index 0a930e6a9319c..b2288900a1248 100644
--- a/packages/kbn-alerting-types/index.ts
+++ b/packages/kbn-alerting-types/index.ts
@@ -18,4 +18,5 @@ export * from './r_rule_types';
export * from './rule_notify_when_type';
export * from './rule_type_types';
export * from './rule_types';
+export * from './rule_settings';
export * from './search_strategy_types';
diff --git a/packages/kbn-alerting-types/rule_settings.ts b/packages/kbn-alerting-types/rule_settings.ts
new file mode 100644
index 0000000000000..b25ad201c2dc0
--- /dev/null
+++ b/packages/kbn-alerting-types/rule_settings.ts
@@ -0,0 +1,46 @@
+/*
+ * 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".
+ */
+
+export interface RulesSettingsModificationMetadata {
+ createdBy: string | null;
+ updatedBy: string | null;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface RulesSettingsFlappingProperties {
+ enabled: boolean;
+ lookBackWindow: number;
+ statusChangeThreshold: number;
+}
+
+export interface RuleSpecificFlappingProperties {
+ lookBackWindow: number;
+ statusChangeThreshold: number;
+}
+
+export type RulesSettingsFlapping = RulesSettingsFlappingProperties &
+ RulesSettingsModificationMetadata;
+
+export interface RulesSettingsQueryDelayProperties {
+ delay: number;
+}
+
+export type RulesSettingsQueryDelay = RulesSettingsQueryDelayProperties &
+ RulesSettingsModificationMetadata;
+
+export interface RulesSettingsProperties {
+ flapping?: RulesSettingsFlappingProperties;
+ queryDelay?: RulesSettingsQueryDelayProperties;
+}
+
+export interface RulesSettings {
+ flapping?: RulesSettingsFlapping;
+ queryDelay?: RulesSettingsQueryDelay;
+}
diff --git a/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/fetch_flapping_settings.test.ts b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/fetch_flapping_settings.test.ts
new file mode 100644
index 0000000000000..d5feaa731335a
--- /dev/null
+++ b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/fetch_flapping_settings.test.ts
@@ -0,0 +1,44 @@
+/*
+ * 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 { httpServiceMock } from '@kbn/core/public/mocks';
+import { fetchFlappingSettings } from './fetch_flapping_settings';
+
+const http = httpServiceMock.createStartContract();
+
+describe('fetchFlappingSettings', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ test('should call fetch rule flapping API', async () => {
+ const now = new Date().toISOString();
+ http.get.mockResolvedValue({
+ created_by: 'test',
+ updated_by: 'test',
+ created_at: now,
+ updated_at: now,
+ enabled: true,
+ look_back_window: 20,
+ status_change_threshold: 20,
+ });
+
+ const result = await fetchFlappingSettings({ http });
+
+ expect(result).toEqual({
+ createdBy: 'test',
+ updatedBy: 'test',
+ createdAt: now,
+ updatedAt: now,
+ enabled: true,
+ lookBackWindow: 20,
+ statusChangeThreshold: 20,
+ });
+ });
+});
diff --git a/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/fetch_flapping_settings.ts b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/fetch_flapping_settings.ts
new file mode 100644
index 0000000000000..6ad702ebc945e
--- /dev/null
+++ b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/fetch_flapping_settings.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 { HttpSetup } from '@kbn/core/public';
+import { AsApiContract } from '@kbn/actions-types';
+import { RulesSettingsFlapping } from '@kbn/alerting-types';
+import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';
+import { transformFlappingSettingsResponse } from './transform_flapping_settings_response';
+
+export const fetchFlappingSettings = async ({ http }: { http: HttpSetup }) => {
+ const res = await http.get>(
+ `${INTERNAL_BASE_ALERTING_API_PATH}/rules/settings/_flapping`
+ );
+ return transformFlappingSettingsResponse(res);
+};
diff --git a/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/index.ts b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/index.ts
new file mode 100644
index 0000000000000..68ff193255403
--- /dev/null
+++ b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/index.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", 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".
+ */
+
+export * from './fetch_flapping_settings';
diff --git a/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/transform_flapping_settings_response.test.ts b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/transform_flapping_settings_response.test.ts
new file mode 100644
index 0000000000000..e53d133f6838b
--- /dev/null
+++ b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/transform_flapping_settings_response.test.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", 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 { transformFlappingSettingsResponse } from './transform_flapping_settings_response';
+
+describe('transformFlappingSettingsResponse', () => {
+ test('should transform flapping settings response', () => {
+ const now = new Date().toISOString();
+
+ const result = transformFlappingSettingsResponse({
+ created_by: 'test',
+ updated_by: 'test',
+ created_at: now,
+ updated_at: now,
+ enabled: true,
+ look_back_window: 20,
+ status_change_threshold: 20,
+ });
+
+ expect(result).toEqual({
+ createdBy: 'test',
+ updatedBy: 'test',
+ createdAt: now,
+ updatedAt: now,
+ enabled: true,
+ lookBackWindow: 20,
+ statusChangeThreshold: 20,
+ });
+ });
+});
diff --git a/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/transform_flapping_settings_response.ts b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/transform_flapping_settings_response.ts
new file mode 100644
index 0000000000000..a628829927a3b
--- /dev/null
+++ b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/transform_flapping_settings_response.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", 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 { AsApiContract } from '@kbn/actions-types';
+import { RulesSettingsFlapping } from '@kbn/alerting-types';
+
+export const transformFlappingSettingsResponse = ({
+ look_back_window: lookBackWindow,
+ status_change_threshold: statusChangeThreshold,
+ created_at: createdAt,
+ created_by: createdBy,
+ updated_at: updatedAt,
+ updated_by: updatedBy,
+ ...rest
+}: AsApiContract): RulesSettingsFlapping => ({
+ ...rest,
+ lookBackWindow,
+ statusChangeThreshold,
+ createdAt,
+ createdBy,
+ updatedAt,
+ updatedBy,
+});
diff --git a/packages/kbn-alerts-ui-shared/src/common/constants/rule_flapping.ts b/packages/kbn-alerts-ui-shared/src/common/constants/rule_flapping.ts
new file mode 100644
index 0000000000000..49ea5a63b3fca
--- /dev/null
+++ b/packages/kbn-alerts-ui-shared/src/common/constants/rule_flapping.ts
@@ -0,0 +1,11 @@
+/*
+ * 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".
+ */
+
+// Feature flag for frontend rule specific flapping in rule flyout
+export const IS_RULE_SPECIFIC_FLAPPING_ENABLED = false;
diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.test.tsx b/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.test.tsx
new file mode 100644
index 0000000000000..10e1869b9e64c
--- /dev/null
+++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.test.tsx
@@ -0,0 +1,106 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", 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 React, { FunctionComponent } from 'react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { renderHook } from '@testing-library/react-hooks';
+import { testQueryClientConfig } from '../test_utils/test_query_client_config';
+import { useFetchFlappingSettings } from './use_fetch_flapping_settings';
+import { httpServiceMock } from '@kbn/core-http-browser-mocks';
+
+const queryClient = new QueryClient(testQueryClientConfig);
+
+const wrapper: FunctionComponent> = ({ children }) => (
+ {children}
+);
+
+const http = httpServiceMock.createStartContract();
+
+const now = new Date().toISOString();
+
+describe('useFetchFlappingSettings', () => {
+ beforeEach(() => {
+ http.get.mockResolvedValue({
+ created_by: 'test',
+ updated_by: 'test',
+ created_at: now,
+ updated_at: now,
+ enabled: true,
+ look_back_window: 20,
+ status_change_threshold: 20,
+ });
+ });
+
+ afterEach(() => {
+ jest.resetAllMocks();
+ queryClient.clear();
+ });
+
+ test('should call fetchFlappingSettings with the correct parameters', async () => {
+ const { result, waitFor } = renderHook(
+ () => useFetchFlappingSettings({ http, enabled: true }),
+ {
+ wrapper,
+ }
+ );
+
+ await waitFor(() => {
+ return expect(result.current.isInitialLoading).toEqual(false);
+ });
+
+ expect(result.current.data).toEqual({
+ createdAt: now,
+ createdBy: 'test',
+ updatedAt: now,
+ updatedBy: 'test',
+ enabled: true,
+ lookBackWindow: 20,
+ statusChangeThreshold: 20,
+ });
+ });
+
+ test('should not call fetchFlappingSettings if enabled is false', async () => {
+ const { result, waitFor } = renderHook(
+ () => useFetchFlappingSettings({ http, enabled: false }),
+ {
+ wrapper,
+ }
+ );
+
+ await waitFor(() => {
+ return expect(result.current.isInitialLoading).toEqual(false);
+ });
+
+ expect(http.get).not.toHaveBeenCalled();
+ });
+
+ test('should call onSuccess when the fetching was successful', async () => {
+ const onSuccessMock = jest.fn();
+ const { result, waitFor } = renderHook(
+ () => useFetchFlappingSettings({ http, enabled: true, onSuccess: onSuccessMock }),
+ {
+ wrapper,
+ }
+ );
+
+ await waitFor(() => {
+ return expect(result.current.isInitialLoading).toEqual(false);
+ });
+
+ expect(onSuccessMock).toHaveBeenCalledWith({
+ createdAt: now,
+ createdBy: 'test',
+ updatedAt: now,
+ updatedBy: 'test',
+ enabled: true,
+ lookBackWindow: 20,
+ statusChangeThreshold: 20,
+ });
+ });
+});
diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.ts b/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.ts
new file mode 100644
index 0000000000000..6b72c2fea734b
--- /dev/null
+++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.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", 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 { useQuery } from '@tanstack/react-query';
+import { HttpStart } from '@kbn/core-http-browser';
+import { RulesSettingsFlapping } from '@kbn/alerting-types/rule_settings';
+import { fetchFlappingSettings } from '../apis/fetch_flapping_settings';
+
+interface UseFetchFlappingSettingsProps {
+ http: HttpStart;
+ enabled: boolean;
+ onSuccess?: (settings: RulesSettingsFlapping) => void;
+}
+
+export const useFetchFlappingSettings = (props: UseFetchFlappingSettingsProps) => {
+ const { http, enabled, onSuccess } = props;
+
+ const queryFn = () => {
+ return fetchFlappingSettings({ http });
+ };
+
+ const { data, isFetching, isError, isLoadingError, isLoading, isInitialLoading } = useQuery({
+ queryKey: ['fetchFlappingSettings'],
+ queryFn,
+ onSuccess,
+ enabled,
+ refetchOnWindowFocus: false,
+ retry: false,
+ });
+
+ return {
+ isInitialLoading,
+ isLoading: isLoading || isFetching,
+ isError: isError || isLoadingError,
+ data,
+ };
+};
diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx
index 71aeb2bcaab77..fc96ae214a7a8 100644
--- a/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx
+++ b/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx
@@ -92,6 +92,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
connectors,
connectorTypes,
aadTemplateFields,
+ flappingSettings,
} = useLoadDependencies({
http,
toasts: notifications.toasts,
@@ -117,6 +118,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
actions: newFormData.actions,
notifyWhen: newFormData.notifyWhen,
alertDelay: newFormData.alertDelay,
+ flapping: newFormData.flapping,
},
});
},
@@ -173,6 +175,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
selectedRuleTypeModel: ruleTypeModel,
selectedRuleType: ruleType,
validConsumers,
+ flappingSettings,
canShowConsumerSelection,
showMustacheAutocompleteSwitch,
multiConsumerSelection: getInitialMultiConsumer({
diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx
index 5091444276873..6e92b94cc2e0d 100644
--- a/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx
+++ b/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx
@@ -69,6 +69,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => {
connectors,
connectorTypes,
aadTemplateFields,
+ flappingSettings,
} = useLoadDependencies({
http,
toasts: notifications.toasts,
@@ -89,6 +90,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => {
actions: newFormData.actions,
notifyWhen: newFormData.notifyWhen,
alertDelay: newFormData.alertDelay,
+ flapping: newFormData.flapping,
},
});
},
@@ -160,6 +162,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => {
minimumScheduleInterval: uiConfig?.minimumScheduleInterval,
selectedRuleType: ruleType,
selectedRuleTypeModel: ruleTypeModel,
+ flappingSettings,
showMustacheAutocompleteSwitch,
}}
>
diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx
index 263c9e2118056..9d2ce3b6f1211 100644
--- a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx
+++ b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx
@@ -50,6 +50,10 @@ jest.mock('../utils/get_authorized_rule_types', () => ({
getAvailableRuleTypes: jest.fn(),
}));
+jest.mock('../../common/hooks/use_fetch_flapping_settings', () => ({
+ useFetchFlappingSettings: jest.fn(),
+}));
+
const { useLoadUiConfig } = jest.requireMock('../../common/hooks/use_load_ui_config');
const { useHealthCheck } = jest.requireMock('../../common/hooks/use_health_check');
const { useResolveRule } = jest.requireMock('../../common/hooks/use_resolve_rule');
@@ -60,6 +64,9 @@ const { useLoadRuleTypeAadTemplateField } = jest.requireMock(
);
const { useLoadRuleTypesQuery } = jest.requireMock('../../common/hooks/use_load_rule_types_query');
const { getAvailableRuleTypes } = jest.requireMock('../utils/get_authorized_rule_types');
+const { useFetchFlappingSettings } = jest.requireMock(
+ '../../common/hooks/use_fetch_flapping_settings'
+);
const uiConfigMock = {
isUsingSecurity: true,
@@ -103,6 +110,15 @@ useResolveRule.mockReturnValue({
data: ruleMock,
});
+useFetchFlappingSettings.mockReturnValue({
+ isLoading: false,
+ isInitialLoading: false,
+ data: {
+ lookBackWindow: 20,
+ statusChangeThreshold: 20,
+ },
+});
+
const indexThresholdRuleType = {
enabledInLicense: true,
recoveryActionGroup: {
@@ -260,6 +276,10 @@ describe('useLoadDependencies', () => {
uiConfig: uiConfigMock,
healthCheckError: null,
fetchedFormData: ruleMock,
+ flappingSettings: {
+ lookBackWindow: 20,
+ statusChangeThreshold: 20,
+ },
connectors: [mockConnector],
connectorTypes: [mockConnectorType],
aadTemplateFields: [mockAadTemplateField],
diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts
index da59e85a933a1..5e0c52b1089ba 100644
--- a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts
+++ b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts
@@ -22,6 +22,8 @@ import {
} from '../../common/hooks';
import { getAvailableRuleTypes } from '../utils';
import { RuleTypeRegistryContract } from '../../common';
+import { useFetchFlappingSettings } from '../../common/hooks/use_fetch_flapping_settings';
+import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '../../common/constants/rule_flapping';
import { useLoadRuleTypeAadTemplateField } from '../../common/hooks/use_load_rule_type_aad_template_fields';
export interface UseLoadDependencies {
@@ -81,6 +83,15 @@ export const useLoadDependencies = (props: UseLoadDependencies) => {
filteredRuleTypes,
});
+ const {
+ data: flappingSettings,
+ isLoading: isLoadingFlappingSettings,
+ isInitialLoading: isInitialLoadingFlappingSettings,
+ } = useFetchFlappingSettings({
+ http,
+ enabled: IS_RULE_SPECIFIC_FLAPPING_ENABLED,
+ });
+
const {
data: connectors = [],
isLoading: isLoadingConnectors,
@@ -144,6 +155,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => {
isLoadingUiConfig ||
isLoadingHealthCheck ||
isLoadingRuleTypes ||
+ isLoadingFlappingSettings ||
isLoadingConnectors ||
isLoadingConnectorTypes ||
isLoadingAadtemplateFields
@@ -156,6 +168,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => {
isLoadingHealthCheck ||
isLoadingRule ||
isLoadingRuleTypes ||
+ isLoadingFlappingSettings ||
isLoadingConnectors ||
isLoadingConnectorTypes ||
isLoadingAadtemplateFields
@@ -166,6 +179,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => {
isLoadingHealthCheck,
isLoadingRule,
isLoadingRuleTypes,
+ isLoadingFlappingSettings,
isLoadingConnectors,
isLoadingConnectorTypes,
isLoadingAadtemplateFields,
@@ -178,6 +192,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => {
isInitialLoadingUiConfig ||
isInitialLoadingHealthCheck ||
isInitialLoadingRuleTypes ||
+ isInitialLoadingFlappingSettings ||
isInitialLoadingConnectors ||
isInitialLoadingConnectorTypes ||
isInitialLoadingAadTemplateField
@@ -190,6 +205,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => {
isInitialLoadingHealthCheck ||
isInitialLoadingRule ||
isInitialLoadingRuleTypes ||
+ isInitialLoadingFlappingSettings ||
isInitialLoadingConnectors ||
isInitialLoadingConnectorTypes ||
isInitialLoadingAadTemplateField
@@ -200,6 +216,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => {
isInitialLoadingHealthCheck,
isInitialLoadingRule,
isInitialLoadingRuleTypes,
+ isInitialLoadingFlappingSettings,
isInitialLoadingConnectors,
isInitialLoadingConnectorTypes,
isInitialLoadingAadTemplateField,
@@ -213,6 +230,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => {
uiConfig,
healthCheckError,
fetchedFormData,
+ flappingSettings,
connectors,
connectorTypes,
aadTemplateFields,
diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.test.tsx
index 01f9f39e9d086..b91148c220844 100644
--- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.test.tsx
+++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.test.tsx
@@ -19,12 +19,37 @@ import type { DocLinksStart } from '@kbn/core-doc-links-browser';
import { RuleDefinition } from './rule_definition';
import { RuleType } from '@kbn/alerting-types';
import { RuleTypeModel } from '../../common/types';
+import { RuleSettingsFlappingFormProps } from '../../rule_settings/rule_settings_flapping_form';
+import { ALERT_FLAPPING_DETECTION_TITLE } from '../translations';
+import userEvent from '@testing-library/user-event';
+import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
jest.mock('../hooks', () => ({
useRuleFormState: jest.fn(),
useRuleFormDispatch: jest.fn(),
}));
+jest.mock('../../common/constants/rule_flapping', () => ({
+ IS_RULE_SPECIFIC_FLAPPING_ENABLED: true,
+}));
+
+jest.mock('../../rule_settings/rule_settings_flapping_form', () => ({
+ RuleSettingsFlappingForm: (props: RuleSettingsFlappingFormProps) => (
+
+
+ props.onFlappingChange({
+ lookBackWindow: 15,
+ statusChangeThreshold: 15,
+ })
+ }
+ >
+ onFlappingChange
+
+
+ ),
+}));
+
const ruleType = {
id: '.es-query',
name: 'Test',
@@ -73,6 +98,13 @@ const plugins = {
dataViews: {} as DataViewsPublicPluginStart,
unifiedSearch: {} as UnifiedSearchPublicPluginStart,
docLinks: {} as DocLinksStart,
+ application: {
+ capabilities: {
+ rulesSettings: {
+ writeFlappingSettingsUI: true,
+ },
+ },
+ },
};
const { useRuleFormState, useRuleFormDispatch } = jest.requireMock('../hooks');
@@ -279,4 +311,105 @@ describe('Rule Definition', () => {
},
});
});
+
+ test('should render rule flapping settings correctly', () => {
+ useRuleFormState.mockReturnValue({
+ plugins,
+ formData: {
+ id: 'test-id',
+ params: {},
+ schedule: {
+ interval: '1m',
+ },
+ alertDelay: {
+ active: 5,
+ },
+ notifyWhen: null,
+ consumer: 'stackAlerts',
+ },
+ selectedRuleType: ruleType,
+ selectedRuleTypeModel: ruleModel,
+ canShowConsumerSelection: true,
+ validConsumers: ['logs', 'stackAlerts'],
+ });
+
+ render( );
+
+ expect(screen.getByText(ALERT_FLAPPING_DETECTION_TITLE)).toBeInTheDocument();
+ expect(screen.getByTestId('ruleSettingsFlappingForm')).toBeInTheDocument();
+ });
+
+ test('should allow flapping to be changed', async () => {
+ useRuleFormState.mockReturnValue({
+ plugins,
+ formData: {
+ id: 'test-id',
+ params: {},
+ schedule: {
+ interval: '1m',
+ },
+ alertDelay: {
+ active: 5,
+ },
+ notifyWhen: null,
+ consumer: 'stackAlerts',
+ },
+ selectedRuleType: ruleType,
+ selectedRuleTypeModel: ruleModel,
+ canShowConsumerSelection: true,
+ validConsumers: ['logs', 'stackAlerts'],
+ });
+
+ render( );
+
+ await userEvent.click(screen.getByText('onFlappingChange'));
+ expect(mockOnChange).toHaveBeenCalledWith({
+ payload: {
+ property: 'flapping',
+ value: {
+ lookBackWindow: 15,
+ statusChangeThreshold: 15,
+ },
+ },
+ type: 'setRuleProperty',
+ });
+ });
+
+ test('should open and close flapping popover when button icon is clicked', async () => {
+ useRuleFormState.mockReturnValue({
+ plugins,
+ formData: {
+ id: 'test-id',
+ params: {},
+ schedule: {
+ interval: '1m',
+ },
+ alertDelay: {
+ active: 5,
+ },
+ notifyWhen: null,
+ consumer: 'stackAlerts',
+ },
+ selectedRuleType: ruleType,
+ selectedRuleTypeModel: ruleModel,
+ canShowConsumerSelection: true,
+ validConsumers: ['logs', 'stackAlerts'],
+ });
+
+ render(
+
+
+
+ );
+
+ expect(screen.queryByTestId('ruleSettingsFlappingTooltipTitle')).not.toBeInTheDocument();
+
+ await userEvent.click(screen.getByTestId('ruleSettingsFlappingTitleTooltipButton'));
+
+ expect(screen.queryByTestId('ruleSettingsFlappingTooltipTitle')).toBeInTheDocument();
+
+ await userEvent.click(screen.getByTestId('ruleSettingsFlappingTitleTooltipButton'));
+
+ expect(screen.queryByTestId('ruleSettingsFlappingTooltipTitle')).not.toBeVisible();
+ });
});
diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx
index fe4812436144a..3b404edc5d029 100644
--- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx
+++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx
@@ -25,6 +25,7 @@ import {
useEuiTheme,
COLOR_MODES_STANDARD,
} from '@elastic/eui';
+import { RuleSpecificFlappingProperties } from '@kbn/alerting-types';
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
import { AlertConsumers } from '@kbn/rule-data-utils';
import {
@@ -39,6 +40,8 @@ import {
ADVANCED_OPTIONS_TITLE,
ALERT_DELAY_DESCRIPTION_TEXT,
ALERT_DELAY_HELP_TEXT,
+ ALERT_FLAPPING_DETECTION_TITLE,
+ ALERT_FLAPPING_DETECTION_DESCRIPTION,
} from '../translations';
import { RuleAlertDelay } from './rule_alert_delay';
import { RuleConsumerSelection } from './rule_consumer_selection';
@@ -46,6 +49,9 @@ import { RuleSchedule } from './rule_schedule';
import { useRuleFormState, useRuleFormDispatch } from '../hooks';
import { MULTI_CONSUMER_RULE_TYPE_IDS } from '../constants';
import { getAuthorizedConsumers } from '../utils';
+import { RuleSettingsFlappingTitleTooltip } from '../../rule_settings/rule_settings_flapping_title_tooltip';
+import { RuleSettingsFlappingForm } from '../../rule_settings/rule_settings_flapping_form';
+import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '../../common/constants/rule_flapping';
export const RuleDefinition = () => {
const {
@@ -58,17 +64,26 @@ export const RuleDefinition = () => {
selectedRuleTypeModel,
validConsumers,
canShowConsumerSelection = false,
+ flappingSettings,
} = useRuleFormState();
const { colorMode } = useEuiTheme();
const dispatch = useRuleFormDispatch();
- const { charts, data, dataViews, unifiedSearch, docLinks } = plugins;
+ const { charts, data, dataViews, unifiedSearch, docLinks, application } = plugins;
- const { params, schedule, notifyWhen } = formData;
+ const {
+ capabilities: { rulesSettings },
+ } = application;
+
+ const { writeFlappingSettingsUI } = rulesSettings || {};
+
+ const { params, schedule, notifyWhen, flapping } = formData;
const [isAdvancedOptionsVisible, setIsAdvancedOptionsVisible] = useState(false);
+ const [isFlappingPopoverOpen, setIsFlappingPopoverOpen] = useState(false);
+
const authorizedConsumers = useMemo(() => {
if (!validConsumers?.length) {
return [];
@@ -143,6 +158,19 @@ export const RuleDefinition = () => {
[dispatch]
);
+ const onSetFlapping = useCallback(
+ (value: RuleSpecificFlappingProperties | null) => {
+ dispatch({
+ type: 'setRuleProperty',
+ payload: {
+ property: 'flapping',
+ value,
+ },
+ });
+ },
+ [dispatch]
+ );
+
return (
@@ -243,7 +271,10 @@ export const RuleDefinition = () => {
{
+ setIsAdvancedOptionsVisible(isOpen);
+ setIsFlappingPopoverOpen(false);
+ }}
initialIsOpen={isAdvancedOptionsVisible}
buttonProps={{
'data-test-subj': 'advancedOptionsAccordionButton',
@@ -274,6 +305,31 @@ export const RuleDefinition = () => {
>
+ {IS_RULE_SPECIFIC_FLAPPING_ENABLED && (
+ {ALERT_FLAPPING_DETECTION_TITLE}}
+ description={
+
+
+ {ALERT_FLAPPING_DETECTION_DESCRIPTION}
+
+
+
+ }
+ >
+
+
+ )}
diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts b/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts
index e7b060dce9831..20e87c66f10f4 100644
--- a/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts
+++ b/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts
@@ -85,6 +85,21 @@ export const ALERT_DELAY_TITLE_PREFIX = i18n.translate(
}
);
+export const ALERT_FLAPPING_DETECTION_TITLE = i18n.translate(
+ 'alertsUIShared.ruleForm.ruleDefinition.alertFlappingDetectionTitle',
+ {
+ defaultMessage: 'Alert flapping detection',
+ }
+);
+
+export const ALERT_FLAPPING_DETECTION_DESCRIPTION = i18n.translate(
+ 'alertsUIShared.ruleForm.ruleDefinition.alertFlappingDetectionDescription',
+ {
+ defaultMessage:
+ 'Detect alerts that switch quickly between active and recovered states and reduce unwanted noise for these flapping alerts',
+ }
+);
+
export const SCHEDULE_TITLE_PREFIX = i18n.translate(
'alertsUIShared.ruleForm.ruleSchedule.scheduleTitlePrefix',
{
diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/types.ts b/packages/kbn-alerts-ui-shared/src/rule_form/types.ts
index ac81f45de19e6..d33c74da528db 100644
--- a/packages/kbn-alerts-ui-shared/src/rule_form/types.ts
+++ b/packages/kbn-alerts-ui-shared/src/rule_form/types.ts
@@ -20,7 +20,7 @@ import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/
import type { SettingsStart } from '@kbn/core-ui-settings-browser';
import { RuleCreationValidConsumer } from '@kbn/rule-data-utils';
import { ActionType } from '@kbn/actions-types';
-import { ActionVariable } from '@kbn/alerting-types';
+import { ActionVariable, RulesSettingsFlapping } from '@kbn/alerting-types';
import {
ActionConnector,
ActionTypeRegistryContract,
@@ -46,6 +46,7 @@ export interface RuleFormData {
alertDelay?: Rule['alertDelay'];
notifyWhen?: Rule['notifyWhen'];
ruleTypeId?: Rule['ruleTypeId'];
+ flapping?: Rule['flapping'];
}
export interface RuleFormPlugins {
@@ -83,6 +84,7 @@ export interface RuleFormState {
minimumScheduleInterval?: MinimumScheduleInterval;
canShowConsumerSelection?: boolean;
validConsumers?: RuleCreationValidConsumer[];
+ flappingSettings?: RulesSettingsFlapping;
}
export type InitialRule = Partial &
diff --git a/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_form.tsx b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_form.tsx
new file mode 100644
index 0000000000000..99f64f0a3977f
--- /dev/null
+++ b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_form.tsx
@@ -0,0 +1,318 @@
+/*
+ * 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 React, { useCallback, useMemo, useRef, useState } from 'react';
+import {
+ EuiBadge,
+ EuiButtonIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHorizontalRule,
+ EuiLink,
+ EuiPopover,
+ EuiSpacer,
+ EuiSplitPanel,
+ EuiSwitch,
+ EuiText,
+ EuiOutsideClickDetector,
+ useEuiTheme,
+ useIsWithinMinBreakpoint,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { RuleSpecificFlappingProperties, RulesSettingsFlapping } from '@kbn/alerting-types';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { RuleSettingsFlappingMessage } from './rule_settings_flapping_message';
+import { RuleSettingsFlappingInputs } from './rule_settings_flapping_inputs';
+
+const flappingLabel = i18n.translate('alertsUIShared.ruleSettingsFlappingForm.flappingLabel', {
+ defaultMessage: 'Flapping Detection',
+});
+
+const flappingOnLabel = i18n.translate('alertsUIShared.ruleSettingsFlappingForm.onLabel', {
+ defaultMessage: 'ON',
+});
+
+const flappingOffLabel = i18n.translate('alertsUIShared.ruleSettingsFlappingForm.offLabel', {
+ defaultMessage: 'OFF',
+});
+
+const flappingOverrideLabel = i18n.translate(
+ 'alertsUIShared.ruleSettingsFlappingForm.overrideLabel',
+ {
+ defaultMessage: 'Custom',
+ }
+);
+
+const flappingOffContentRules = i18n.translate(
+ 'alertsUIShared.ruleSettingsFlappingForm.flappingOffContentRules',
+ {
+ defaultMessage: 'Rules',
+ }
+);
+
+const flappingOffContentSettings = i18n.translate(
+ 'alertsUIShared.ruleSettingsFlappingForm.flappingOffContentSettings',
+ {
+ defaultMessage: 'Settings',
+ }
+);
+
+const flappingExternalLinkLabel = i18n.translate(
+ 'alertsUIShared.ruleSettingsFlappingForm.flappingExternalLinkLabel',
+ {
+ defaultMessage: "What's this?",
+ }
+);
+
+const flappingOverrideConfiguration = i18n.translate(
+ 'alertsUIShared.ruleSettingsFlappingForm.flappingOverrideConfiguration',
+ {
+ defaultMessage: 'Customize Configuration',
+ }
+);
+
+const clampFlappingValues = (flapping: RuleSpecificFlappingProperties) => {
+ return {
+ ...flapping,
+ statusChangeThreshold: Math.min(flapping.lookBackWindow, flapping.statusChangeThreshold),
+ };
+};
+
+export interface RuleSettingsFlappingFormProps {
+ flappingSettings?: RuleSpecificFlappingProperties | null;
+ spaceFlappingSettings?: RulesSettingsFlapping;
+ canWriteFlappingSettingsUI: boolean;
+ onFlappingChange: (value: RuleSpecificFlappingProperties | null) => void;
+}
+
+export const RuleSettingsFlappingForm = (props: RuleSettingsFlappingFormProps) => {
+ const { flappingSettings, spaceFlappingSettings, canWriteFlappingSettingsUI, onFlappingChange } =
+ props;
+
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+ const cachedFlappingSettings = useRef();
+
+ const isDesktop = useIsWithinMinBreakpoint('xl');
+
+ const { euiTheme } = useEuiTheme();
+
+ const onFlappingToggle = useCallback(() => {
+ if (!spaceFlappingSettings) {
+ return;
+ }
+ if (flappingSettings) {
+ cachedFlappingSettings.current = flappingSettings;
+ return onFlappingChange(null);
+ }
+ const initialFlappingSettings = cachedFlappingSettings.current || spaceFlappingSettings;
+ onFlappingChange({
+ lookBackWindow: initialFlappingSettings.lookBackWindow,
+ statusChangeThreshold: initialFlappingSettings.statusChangeThreshold,
+ });
+ }, [spaceFlappingSettings, flappingSettings, onFlappingChange]);
+
+ const internalOnFlappingChange = useCallback(
+ (flapping: RuleSpecificFlappingProperties) => {
+ const clampedValue = clampFlappingValues(flapping);
+ onFlappingChange(clampedValue);
+ cachedFlappingSettings.current = clampedValue;
+ },
+ [onFlappingChange]
+ );
+
+ const onLookBackWindowChange = useCallback(
+ (value: number) => {
+ if (!flappingSettings) {
+ return;
+ }
+ internalOnFlappingChange({
+ ...flappingSettings,
+ lookBackWindow: value,
+ });
+ },
+ [flappingSettings, internalOnFlappingChange]
+ );
+
+ const onStatusChangeThresholdChange = useCallback(
+ (value: number) => {
+ if (!flappingSettings) {
+ return;
+ }
+ internalOnFlappingChange({
+ ...flappingSettings,
+ statusChangeThreshold: value,
+ });
+ },
+ [flappingSettings, internalOnFlappingChange]
+ );
+
+ const flappingOffTooltip = useMemo(() => {
+ if (!spaceFlappingSettings) {
+ return null;
+ }
+ const { enabled } = spaceFlappingSettings;
+ if (enabled) {
+ return null;
+ }
+
+ if (canWriteFlappingSettingsUI) {
+ return (
+ setIsPopoverOpen(false)}>
+ setIsPopoverOpen(!isPopoverOpen)}
+ />
+ }
+ >
+
+ {flappingOffContentRules},
+ settings: {flappingOffContentSettings} ,
+ }}
+ />
+
+
+
+ );
+ }
+ // TODO: Add the external doc link here!
+ return (
+
+ {flappingExternalLinkLabel}
+
+ );
+ }, [canWriteFlappingSettingsUI, isPopoverOpen, spaceFlappingSettings]);
+
+ const flappingFormHeader = useMemo(() => {
+ if (!spaceFlappingSettings) {
+ return null;
+ }
+ const { enabled } = spaceFlappingSettings;
+
+ return (
+
+
+
+
+ {flappingLabel}
+
+
+ {enabled ? flappingOnLabel : flappingOffLabel}
+
+ {flappingSettings && enabled && (
+ {flappingOverrideLabel}
+ )}
+
+
+ {enabled && (
+
+ )}
+ {flappingOffTooltip}
+
+
+ {flappingSettings && enabled && (
+ <>
+
+
+ >
+ )}
+
+ );
+ }, [
+ isDesktop,
+ euiTheme,
+ spaceFlappingSettings,
+ flappingSettings,
+ flappingOffTooltip,
+ onFlappingToggle,
+ ]);
+
+ const flappingFormBody = useMemo(() => {
+ if (!flappingSettings) {
+ return null;
+ }
+ if (!spaceFlappingSettings?.enabled) {
+ return null;
+ }
+ return (
+
+
+
+ );
+ }, [
+ flappingSettings,
+ spaceFlappingSettings,
+ onLookBackWindowChange,
+ onStatusChangeThresholdChange,
+ ]);
+
+ const flappingFormMessage = useMemo(() => {
+ if (!spaceFlappingSettings || !spaceFlappingSettings.enabled) {
+ return null;
+ }
+ const settingsToUse = flappingSettings || spaceFlappingSettings;
+ return (
+
+
+
+ );
+ }, [spaceFlappingSettings, flappingSettings, euiTheme]);
+
+ return (
+
+
+
+ {flappingFormHeader}
+ {flappingFormBody}
+
+
+ {flappingFormMessage}
+
+ );
+};
diff --git a/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_message.tsx b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_message.tsx
index b7c8681ef221b..d6d488e08f0c1 100644
--- a/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_message.tsx
+++ b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_message.tsx
@@ -37,21 +37,34 @@ export const flappingOffMessage = i18n.translate(
export interface RuleSettingsFlappingMessageProps {
lookBackWindow: number;
statusChangeThreshold: number;
+ isUsingRuleSpecificFlapping: boolean;
}
export const RuleSettingsFlappingMessage = (props: RuleSettingsFlappingMessageProps) => {
- const { lookBackWindow, statusChangeThreshold } = props;
+ const { lookBackWindow, statusChangeThreshold, isUsingRuleSpecificFlapping } = props;
return (
- {getLookBackWindowLabelRuleRuns(lookBackWindow)},
- statusChangeThreshold: {getStatusChangeThresholdRuleRuns(statusChangeThreshold)} ,
- }}
- />
+ {!isUsingRuleSpecificFlapping && (
+ {getLookBackWindowLabelRuleRuns(lookBackWindow)},
+ statusChangeThreshold: {getStatusChangeThresholdRuleRuns(statusChangeThreshold)} ,
+ }}
+ />
+ )}
+ {isUsingRuleSpecificFlapping && (
+ {getLookBackWindowLabelRuleRuns(lookBackWindow)},
+ statusChangeThreshold: {getStatusChangeThresholdRuleRuns(statusChangeThreshold)} ,
+ }}
+ />
+ )}
);
};
diff --git a/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx
new file mode 100644
index 0000000000000..2a5cc4186013d
--- /dev/null
+++ b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx
@@ -0,0 +1,140 @@
+/*
+ * 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 {
+ EuiButtonIcon,
+ EuiPopover,
+ EuiPopoverProps,
+ EuiPopoverTitle,
+ EuiSpacer,
+ EuiText,
+ EuiOutsideClickDetector,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n-react';
+
+const tooltipTitle = i18n.translate(
+ 'alertsUIShared.ruleSettingsFlappingTitleTooltip.tooltipTitle',
+ {
+ defaultMessage: 'Alert flapping detection',
+ }
+);
+
+const flappingTitlePopoverFlappingDetection = i18n.translate(
+ 'alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingTitlePopoverFlappingDetection',
+ {
+ defaultMessage: 'flapping detection',
+ }
+);
+
+const flappingTitlePopoverAlertStatus = i18n.translate(
+ 'alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingTitlePopoverAlertStatus',
+ {
+ defaultMessage: 'alert status change threshold',
+ }
+);
+
+const flappingTitlePopoverLookBack = i18n.translate(
+ 'alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingTitlePopoverLookBack',
+ {
+ defaultMessage: 'rule run look back window',
+ }
+);
+
+const flappingOffContentRules = i18n.translate(
+ 'alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingOffContentRules',
+ {
+ defaultMessage: 'Rules',
+ }
+);
+
+const flappingOffContentSettings = i18n.translate(
+ 'alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingOffContentSettings',
+ {
+ defaultMessage: 'Settings',
+ }
+);
+
+interface RuleSettingsFlappingTitleTooltipProps {
+ isOpen: boolean;
+ setIsPopoverOpen: (isOpen: boolean) => void;
+ anchorPosition?: EuiPopoverProps['anchorPosition'];
+}
+
+export const RuleSettingsFlappingTitleTooltip = (props: RuleSettingsFlappingTitleTooltipProps) => {
+ const { isOpen, setIsPopoverOpen, anchorPosition = 'leftCenter' } = props;
+
+ return (
+ setIsPopoverOpen(false)}>
+ setIsPopoverOpen(!isOpen)}
+ />
+ }
+ >
+
+ {tooltipTitle}
+
+
+ {flappingTitlePopoverFlappingDetection},
+ }}
+ />
+
+
+
+ {flappingTitlePopoverAlertStatus},
+ }}
+ />
+
+
+
+ {flappingTitlePopoverLookBack},
+ }}
+ />
+
+
+
+ {flappingOffContentRules},
+ settings: {flappingOffContentSettings} ,
+ }}
+ />
+
+
+
+ );
+};
diff --git a/x-pack/plugins/alerting/common/rules_settings.ts b/x-pack/plugins/alerting/common/rules_settings.ts
index 2a4162ca2c5d3..6dcfd377eeb7c 100644
--- a/x-pack/plugins/alerting/common/rules_settings.ts
+++ b/x-pack/plugins/alerting/common/rules_settings.ts
@@ -5,38 +5,28 @@
* 2.0.
*/
-export interface RulesSettingsModificationMetadata {
- createdBy: string | null;
- updatedBy: string | null;
- createdAt: string;
- updatedAt: string;
-}
+import type {
+ RulesSettingsFlappingProperties,
+ RulesSettingsQueryDelayProperties,
+} from '@kbn/alerting-types';
-export interface RulesSettingsFlappingProperties {
- enabled: boolean;
- lookBackWindow: number;
- statusChangeThreshold: number;
-}
+export {
+ MIN_LOOK_BACK_WINDOW,
+ MAX_LOOK_BACK_WINDOW,
+ MIN_STATUS_CHANGE_THRESHOLD,
+ MAX_STATUS_CHANGE_THRESHOLD,
+} from '@kbn/alerting-types/flapping/latest';
-export type RulesSettingsFlapping = RulesSettingsFlappingProperties &
- RulesSettingsModificationMetadata;
-
-export interface RulesSettingsQueryDelayProperties {
- delay: number;
-}
-
-export type RulesSettingsQueryDelay = RulesSettingsQueryDelayProperties &
- RulesSettingsModificationMetadata;
-
-export interface RulesSettingsProperties {
- flapping?: RulesSettingsFlappingProperties;
- queryDelay?: RulesSettingsQueryDelayProperties;
-}
-
-export interface RulesSettings {
- flapping?: RulesSettingsFlapping;
- queryDelay?: RulesSettingsQueryDelay;
-}
+export type {
+ RulesSettingsModificationMetadata,
+ RulesSettingsFlappingProperties,
+ RulesSettingsQueryDelayProperties,
+ RuleSpecificFlappingProperties,
+ RulesSettingsFlapping,
+ RulesSettingsQueryDelay,
+ RulesSettingsProperties,
+ RulesSettings,
+} from '@kbn/alerting-types';
export const MIN_QUERY_DELAY = 0;
export const MAX_QUERY_DELAY = 60;
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/flapping/rules_settings_flapping_form_section.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/flapping/rules_settings_flapping_form_section.tsx
index a78658044a192..1b38eede40e68 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/flapping/rules_settings_flapping_form_section.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/flapping/rules_settings_flapping_form_section.tsx
@@ -82,6 +82,7 @@ export const RulesSettingsFlappingFormSection = memo(
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.test.tsx
index 8d32eb2c9940c..e1cdf5a8ee150 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.test.tsx
@@ -14,12 +14,12 @@ import { coreMock } from '@kbn/core/public/mocks';
import { RulesSettingsFlapping, RulesSettingsQueryDelay } from '@kbn/alerting-plugin/common';
import { RulesSettingsLink } from './rules_settings_link';
import { useKibana } from '../../../common/lib/kibana';
-import { getFlappingSettings } from '../../lib/rule_api/get_flapping_settings';
+import { fetchFlappingSettings } from '@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings';
import { getQueryDelaySettings } from '../../lib/rule_api/get_query_delay_settings';
jest.mock('../../../common/lib/kibana');
-jest.mock('../../lib/rule_api/get_flapping_settings', () => ({
- getFlappingSettings: jest.fn(),
+jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings', () => ({
+ fetchFlappingSettings: jest.fn(),
}));
jest.mock('../../lib/rule_api/get_query_delay_settings', () => ({
getQueryDelaySettings: jest.fn(),
@@ -38,8 +38,8 @@ const useKibanaMock = useKibana as jest.Mocked;
const mocks = coreMock.createSetup();
-const getFlappingSettingsMock = getFlappingSettings as unknown as jest.MockedFunction<
- typeof getFlappingSettings
+const fetchFlappingSettingsMock = fetchFlappingSettings as unknown as jest.MockedFunction<
+ typeof fetchFlappingSettings
>;
const getQueryDelaySettingsMock = getQueryDelaySettings as unknown as jest.MockedFunction<
typeof getQueryDelaySettings
@@ -88,7 +88,7 @@ describe('rules_settings_link', () => {
readQueryDelaySettingsUI: true,
},
};
- getFlappingSettingsMock.mockResolvedValue(mockFlappingSetting);
+ fetchFlappingSettingsMock.mockResolvedValue(mockFlappingSetting);
getQueryDelaySettingsMock.mockResolvedValue(mockQueryDelaySetting);
});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.test.tsx
index 592705b56984d..1dea8bdf88a6e 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.test.tsx
@@ -15,14 +15,14 @@ import { IToasts } from '@kbn/core/public';
import { RulesSettingsFlapping, RulesSettingsQueryDelay } from '@kbn/alerting-plugin/common';
import { RulesSettingsModal, RulesSettingsModalProps } from './rules_settings_modal';
import { useKibana } from '../../../common/lib/kibana';
-import { getFlappingSettings } from '../../lib/rule_api/get_flapping_settings';
+import { fetchFlappingSettings } from '@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings';
import { updateFlappingSettings } from '../../lib/rule_api/update_flapping_settings';
import { getQueryDelaySettings } from '../../lib/rule_api/get_query_delay_settings';
import { updateQueryDelaySettings } from '../../lib/rule_api/update_query_delay_settings';
jest.mock('../../../common/lib/kibana');
-jest.mock('../../lib/rule_api/get_flapping_settings', () => ({
- getFlappingSettings: jest.fn(),
+jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings', () => ({
+ fetchFlappingSettings: jest.fn(),
}));
jest.mock('../../lib/rule_api/update_flapping_settings', () => ({
updateFlappingSettings: jest.fn(),
@@ -47,8 +47,8 @@ const useKibanaMock = useKibana as jest.Mocked;
const mocks = coreMock.createSetup();
-const getFlappingSettingsMock = getFlappingSettings as unknown as jest.MockedFunction<
- typeof getFlappingSettings
+const fetchFlappingSettingsMock = fetchFlappingSettings as unknown as jest.MockedFunction<
+ typeof fetchFlappingSettings
>;
const updateFlappingSettingsMock = updateFlappingSettings as unknown as jest.MockedFunction<
typeof updateFlappingSettings
@@ -142,7 +142,7 @@ describe('rules_settings_modal', () => {
useKibanaMock().services.isServerless = true;
- getFlappingSettingsMock.mockResolvedValue(mockFlappingSetting);
+ fetchFlappingSettingsMock.mockResolvedValue(mockFlappingSetting);
updateFlappingSettingsMock.mockResolvedValue(mockFlappingSetting);
getQueryDelaySettingsMock.mockResolvedValue(mockQueryDelaySetting);
updateQueryDelaySettingsMock.mockResolvedValue(mockQueryDelaySetting);
@@ -156,7 +156,7 @@ describe('rules_settings_modal', () => {
test('renders flapping settings correctly', async () => {
const result = render( );
- expect(getFlappingSettingsMock).toHaveBeenCalledTimes(1);
+ expect(fetchFlappingSettingsMock).toHaveBeenCalledTimes(1);
await waitForModalLoad();
expect(
result.getByTestId('rulesSettingsFlappingEnableSwitch').getAttribute('aria-checked')
@@ -204,7 +204,7 @@ describe('rules_settings_modal', () => {
test('reset flapping settings to initial state on cancel without triggering another server reload', async () => {
const result = render( );
- expect(getFlappingSettingsMock).toHaveBeenCalledTimes(1);
+ expect(fetchFlappingSettingsMock).toHaveBeenCalledTimes(1);
expect(getQueryDelaySettingsMock).toHaveBeenCalledTimes(1);
await waitForModalLoad();
@@ -228,7 +228,7 @@ describe('rules_settings_modal', () => {
expect(lookBackWindowInput.getAttribute('value')).toBe('10');
expect(statusChangeThresholdInput.getAttribute('value')).toBe('10');
- expect(getFlappingSettingsMock).toHaveBeenCalledTimes(1);
+ expect(fetchFlappingSettingsMock).toHaveBeenCalledTimes(1);
expect(getQueryDelaySettingsMock).toHaveBeenCalledTimes(1);
});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.tsx
index 4431f05975906..09828e067369b 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.tsx
@@ -26,8 +26,8 @@ import {
EuiSpacer,
EuiEmptyPrompt,
} from '@elastic/eui';
+import { useFetchFlappingSettings } from '@kbn/alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings';
import { useKibana } from '../../../common/lib/kibana';
-import { useGetFlappingSettings } from '../../hooks/use_get_flapping_settings';
import { RulesSettingsFlappingSection } from './flapping/rules_settings_flapping_section';
import { RulesSettingsQueryDelaySection } from './query_delay/rules_settings_query_delay_section';
import { useGetQueryDelaySettings } from '../../hooks/use_get_query_delay_settings';
@@ -93,6 +93,7 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => {
const {
application: { capabilities },
isServerless,
+ http,
} = useKibana().services;
const {
rulesSettings: {
@@ -109,7 +110,8 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => {
const [queryDelaySettings, hasQueryDelayChanged, setQueryDelaySettings, resetQueryDelaySettings] =
useResettableState();
- const { isLoading: isFlappingLoading, isError: hasFlappingError } = useGetFlappingSettings({
+ const { isLoading: isFlappingLoading, isError: hasFlappingError } = useFetchFlappingSettings({
+ http,
enabled: isVisible,
onSuccess: (fetchedSettings) => {
if (!flappingSettings) {
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_flapping_settings.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_flapping_settings.ts
deleted file mode 100644
index 26b9fdcaeb1c2..0000000000000
--- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_flapping_settings.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { useQuery } from '@tanstack/react-query';
-import { RulesSettingsFlapping } from '@kbn/alerting-plugin/common';
-import { useKibana } from '../../common/lib/kibana';
-import { getFlappingSettings } from '../lib/rule_api/get_flapping_settings';
-
-interface UseGetFlappingSettingsProps {
- enabled: boolean;
- onSuccess?: (settings: RulesSettingsFlapping) => void;
-}
-
-export const useGetFlappingSettings = (props: UseGetFlappingSettingsProps) => {
- const { enabled, onSuccess } = props;
- const { http } = useKibana().services;
-
- const queryFn = () => {
- return getFlappingSettings({ http });
- };
-
- const { data, isFetching, isError, isLoadingError, isLoading, isInitialLoading } = useQuery({
- queryKey: ['getFlappingSettings'],
- queryFn,
- onSuccess,
- enabled,
- refetchOnWindowFocus: false,
- retry: false,
- });
-
- return {
- isInitialLoading,
- isLoading: isLoading || isFetching,
- isError: isError || isLoadingError,
- data,
- };
-};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_flapping_settings.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_flapping_settings.ts
deleted file mode 100644
index 931b1037ef729..0000000000000
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_flapping_settings.ts
+++ /dev/null
@@ -1,28 +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 { HttpSetup } from '@kbn/core/public';
-import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common';
-import { RulesSettingsFlapping } from '@kbn/alerting-plugin/common';
-import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';
-
-const rewriteBodyRes: RewriteRequestCase = ({
- look_back_window: lookBackWindow,
- status_change_threshold: statusChangeThreshold,
- ...rest
-}: any) => ({
- ...rest,
- lookBackWindow,
- statusChangeThreshold,
-});
-
-export const getFlappingSettings = async ({ http }: { http: HttpSetup }) => {
- const res = await http.get>(
- `${INTERNAL_BASE_ALERTING_API_PATH}/rules/settings/_flapping`
- );
- return rewriteBodyRes(res);
-};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx
index af8bda5704b0f..c7b2876d83d84 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx
@@ -67,8 +67,8 @@ jest.mock('../../lib/action_connector_api', () => ({
loadAllActions: jest.fn(),
}));
-jest.mock('../../lib/rule_api/get_flapping_settings', () => ({
- getFlappingSettings: jest.fn().mockResolvedValue({
+jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings', () => ({
+ fetchFlappingSettings: jest.fn().mockResolvedValue({
lookBackWindow: 20,
statusChangeThreshold: 20,
}),
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx
index 8657248a29df3..ccdca1bd1250d 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx
@@ -14,6 +14,7 @@ import { toMountPoint } from '@kbn/react-kibana-mount';
import { parseRuleCircuitBreakerErrorMessage } from '@kbn/alerting-plugin/common';
import { createRule, CreateRuleBody } from '@kbn/alerts-ui-shared/src/common/apis/create_rule';
import { fetchUiConfig as triggersActionsUiConfig } from '@kbn/alerts-ui-shared/src/common/apis/fetch_ui_config';
+import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '@kbn/alerts-ui-shared/src/common/constants/rule_flapping';
import {
Rule,
RuleTypeParams,
@@ -37,7 +38,6 @@ import { hasShowActionsCapability } from '../../lib/capabilities';
import RuleAddFooter from './rule_add_footer';
import { HealthContextProvider } from '../../context/health_context';
import { useKibana } from '../../../common/lib/kibana';
-import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '../../../common/constants';
import { hasRuleChanged, haveRuleParamsChanged } from './has_rule_changed';
import { getRuleWithInvalidatedFields } from '../../lib/value_validators';
import { DEFAULT_RULE_INTERVAL, MULTI_CONSUMER_RULE_TYPE_IDS } from '../../constants';
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx
index 331b10505a5d7..243236d7f6b93 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx
@@ -63,8 +63,8 @@ jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_ui_health_status', () =>
fetchUiHealthStatus: jest.fn(() => ({ isRulesAvailable: true })),
}));
-jest.mock('../../lib/rule_api/get_flapping_settings', () => ({
- getFlappingSettings: jest.fn().mockResolvedValue({
+jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings', () => ({
+ fetchFlappingSettings: jest.fn().mockResolvedValue({
lookBackWindow: 20,
statusChangeThreshold: 20,
}),
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx
index 72eab243ad0c8..a24fd0eec2eb1 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx
@@ -30,7 +30,7 @@ import { toMountPoint } from '@kbn/react-kibana-mount';
import { parseRuleCircuitBreakerErrorMessage } from '@kbn/alerting-plugin/common';
import { updateRule } from '@kbn/alerts-ui-shared/src/common/apis/update_rule';
import { fetchUiConfig as triggersActionsUiConfig } from '@kbn/alerts-ui-shared/src/common/apis/fetch_ui_config';
-import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '../../../common/constants';
+import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '@kbn/alerts-ui-shared/src/common/constants/rule_flapping';
import {
Rule,
RuleFlyoutCloseReason,
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx
index 38ee1c73ac40b..17bdcc92997ca 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx
@@ -71,8 +71,8 @@ jest.mock('../../lib/capabilities', () => ({
hasShowActionsCapability: jest.fn(() => true),
hasExecuteActionsCapability: jest.fn(() => true),
}));
-jest.mock('../../lib/rule_api/get_flapping_settings', () => ({
- getFlappingSettings: jest.fn().mockResolvedValue({
+jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings', () => ({
+ fetchFlappingSettings: jest.fn().mockResolvedValue({
lookBackWindow: 20,
statusChangeThreshold: 20,
}),
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx
index c3f79c3458374..665dd93325c2b 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx
@@ -62,9 +62,11 @@ import {
isActionGroupDisabledForActionTypeId,
RuleActionAlertsFilterProperty,
RuleActionKey,
+ Flapping,
} from '@kbn/alerting-plugin/common';
import { AlertingConnectorFeatureId } from '@kbn/actions-plugin/common';
import { AlertConsumers } from '@kbn/rule-data-utils';
+import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '@kbn/alerts-ui-shared/src/common/constants/rule_flapping';
import { RuleReducerAction, InitialRule } from './rule_reducer';
import {
RuleTypeModel,
@@ -91,10 +93,7 @@ import {
ruleTypeGroupCompare,
ruleTypeUngroupedCompare,
} from '../../lib/rule_type_compare';
-import {
- IS_RULE_SPECIFIC_FLAPPING_ENABLED,
- VIEW_LICENSE_OPTIONS_LINK,
-} from '../../../common/constants';
+import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants';
import { MULTI_CONSUMER_RULE_TYPE_IDS } from '../../constants';
import { SectionLoading } from '../../components/section_loading';
import { RuleFormConsumerSelection, VALID_CONSUMERS } from './rule_form_consumer_selection';
@@ -882,7 +881,7 @@ export const RuleForm = ({
alertDelay={alertDelay}
flappingSettings={rule.flapping}
onAlertDelayChange={onAlertDelayChange}
- onFlappingChange={(flapping) => setRuleProperty('flapping', flapping)}
+ onFlappingChange={(flapping) => setRuleProperty('flapping', flapping as Flapping)}
enabledFlapping={IS_RULE_SPECIFIC_FLAPPING_ENABLED}
/>
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.test.tsx
index f6534f7451405..25c6de0225edb 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.test.tsx
@@ -88,7 +88,7 @@ describe('ruleFormAdvancedOptions', () => {
expect(screen.getByTestId('ruleFormAdvancedOptionsOverrideSwitch')).not.toBeChecked();
expect(screen.queryByText('Custom')).not.toBeInTheDocument();
expect(screen.getByTestId('ruleSettingsFlappingMessage')).toHaveTextContent(
- 'An alert is flapping if it changes status at least 3 times in the last 10 rule runs.'
+ 'All rules (in this space) detect an alert is flapping when it changes status at least 3 times in the last 10 rule runs.'
);
await userEvent.click(screen.getByTestId('ruleFormAdvancedOptionsOverrideSwitch'));
@@ -121,7 +121,7 @@ describe('ruleFormAdvancedOptions', () => {
expect(screen.getByTestId('lookBackWindowRangeInput')).toHaveValue('6');
expect(screen.getByTestId('statusChangeThresholdRangeInput')).toHaveValue('4');
expect(screen.getByTestId('ruleSettingsFlappingMessage')).toHaveTextContent(
- 'An alert is flapping if it changes status at least 4 times in the last 6 rule runs.'
+ 'This rule detects an alert is flapping if it changes status at least 4 times in the last 6 rule runs.'
);
await userEvent.click(screen.getByTestId('ruleFormAdvancedOptionsOverrideSwitch'));
@@ -157,6 +157,10 @@ describe('ruleFormAdvancedOptions', () => {
expect(screen.queryByText('Custom')).not.toBeInTheDocument();
expect(screen.queryByTestId('ruleFormAdvancedOptionsOverrideSwitch')).not.toBeInTheDocument();
expect(screen.queryByTestId('ruleSettingsFlappingMessage')).not.toBeInTheDocument();
+
+ await userEvent.click(screen.getByTestId('ruleSettingsFlappingFormTooltipButton'));
+
+ expect(screen.getByTestId('ruleSettingsFlappingFormTooltipContent')).toBeInTheDocument();
});
test('should allow for flapping inputs to be modified', async () => {
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.tsx
index ca6e17451c1aa..00ad6186d58e8 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.tsx
@@ -5,36 +5,21 @@
* 2.0.
*/
-import React, { useCallback, useMemo, useRef, useState } from 'react';
+import React, { useCallback, useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
- EuiBadge,
EuiFieldNumber,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiIconTip,
EuiPanel,
- EuiSwitch,
- EuiText,
- useIsWithinMinBreakpoint,
- useEuiTheme,
- EuiHorizontalRule,
- EuiSpacer,
- EuiSplitPanel,
EuiLoadingSpinner,
- EuiLink,
- EuiButtonIcon,
- EuiPopover,
- EuiPopoverTitle,
- EuiOutsideClickDetector,
} from '@elastic/eui';
-import { RuleSettingsFlappingInputs } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_flapping_inputs';
-import { RuleSettingsFlappingMessage } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_flapping_message';
-import { Rule } from '@kbn/alerts-ui-shared';
-import { FormattedMessage } from '@kbn/i18n-react';
-import { Flapping } from '@kbn/alerting-plugin/common';
-import { useGetFlappingSettings } from '../../hooks/use_get_flapping_settings';
+import { RuleSpecificFlappingProperties } from '@kbn/alerting-types/rule_settings';
+import { RuleSettingsFlappingForm } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_flapping_form';
+import { RuleSettingsFlappingTitleTooltip } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip';
+import { useFetchFlappingSettings } from '@kbn/alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings';
import { useKibana } from '../../../common/lib/kibana';
const alertDelayFormRowLabel = i18n.translate(
@@ -66,45 +51,6 @@ const alertDelayAppendLabel = i18n.translate(
}
);
-const flappingLabel = i18n.translate(
- 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingLabel',
- {
- defaultMessage: 'Flapping Detection',
- }
-);
-
-const flappingOnLabel = i18n.translate('xpack.triggersActionsUI.ruleFormAdvancedOptions.onLabel', {
- defaultMessage: 'ON',
-});
-
-const flappingOffLabel = i18n.translate(
- 'xpack.triggersActionsUI.ruleFormAdvancedOptions.offLabel',
- {
- defaultMessage: 'OFF',
- }
-);
-
-const flappingOverrideLabel = i18n.translate(
- 'xpack.triggersActionsUI.ruleFormAdvancedOptions.overrideLabel',
- {
- defaultMessage: 'Custom',
- }
-);
-
-const flappingOverrideConfiguration = i18n.translate(
- 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingOverrideConfiguration',
- {
- defaultMessage: 'Override Configuration',
- }
-);
-
-const flappingExternalLinkLabel = i18n.translate(
- 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingExternalLinkLabel',
- {
- defaultMessage: "What's this?",
- }
-);
-
const flappingFormRowLabel = i18n.translate(
'xpack.triggersActionsUI.sections.ruleForm.flappingLabel',
{
@@ -112,58 +58,13 @@ const flappingFormRowLabel = i18n.translate(
}
);
-const flappingOffContentRules = i18n.translate(
- 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingOffContentRules',
- {
- defaultMessage: 'Rules',
- }
-);
-
-const flappingOffContentSettings = i18n.translate(
- 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingOffContentSettings',
- {
- defaultMessage: 'Settings',
- }
-);
-
-const flappingTitlePopoverFlappingDetection = i18n.translate(
- 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingTitlePopoverFlappingDetection',
- {
- defaultMessage: 'flapping detection',
- }
-);
-
-const flappingTitlePopoverAlertStatus = i18n.translate(
- 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingTitlePopoverAlertStatus',
- {
- defaultMessage: 'alert status change threshold',
- }
-);
-
-const flappingTitlePopoverLookBack = i18n.translate(
- 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingTitlePopoverLookBack',
- {
- defaultMessage: 'rule run look back window',
- }
-);
-
-const clampFlappingValues = (flapping: Rule['flapping']) => {
- if (!flapping) {
- return;
- }
- return {
- ...flapping,
- statusChangeThreshold: Math.min(flapping.lookBackWindow, flapping.statusChangeThreshold),
- };
-};
-
const INTEGER_REGEX = /^[1-9][0-9]*$/;
export interface RuleFormAdvancedOptionsProps {
alertDelay?: number;
- flappingSettings?: Flapping | null;
+ flappingSettings?: RuleSpecificFlappingProperties | null;
onAlertDelayChange: (value: string) => void;
- onFlappingChange: (value: Flapping | null) => void;
+ onFlappingChange: (value: RuleSpecificFlappingProperties | null) => void;
enabledFlapping?: boolean;
}
@@ -180,20 +81,15 @@ export const RuleFormAdvancedOptions = (props: RuleFormAdvancedOptionsProps) =>
application: {
capabilities: { rulesSettings },
},
+ http,
} = useKibana().services;
- const { writeFlappingSettingsUI = false } = rulesSettings || {};
+ const { writeFlappingSettingsUI } = rulesSettings || {};
- const [isFlappingOffPopoverOpen, setIsFlappingOffPopoverOpen] = useState(false);
const [isFlappingTitlePopoverOpen, setIsFlappingTitlePopoverOpen] = useState(false);
- const cachedFlappingSettings = useRef();
-
- const isDesktop = useIsWithinMinBreakpoint('xl');
-
- const { euiTheme } = useEuiTheme();
-
- const { data: spaceFlappingSettings, isInitialLoading } = useGetFlappingSettings({
+ const { data: spaceFlappingSettings, isInitialLoading } = useFetchFlappingSettings({
+ http,
enabled: enabledFlapping,
});
@@ -207,274 +103,6 @@ export const RuleFormAdvancedOptions = (props: RuleFormAdvancedOptionsProps) =>
[onAlertDelayChange]
);
- const internalOnFlappingChange = useCallback(
- (flapping: Flapping) => {
- const clampedValue = clampFlappingValues(flapping);
- if (!clampedValue) {
- return;
- }
- onFlappingChange(clampedValue);
- cachedFlappingSettings.current = clampedValue;
- },
- [onFlappingChange]
- );
-
- const onLookBackWindowChange = useCallback(
- (value: number) => {
- if (!flappingSettings) {
- return;
- }
- internalOnFlappingChange({
- ...flappingSettings,
- lookBackWindow: value,
- });
- },
- [flappingSettings, internalOnFlappingChange]
- );
-
- const onStatusChangeThresholdChange = useCallback(
- (value: number) => {
- if (!flappingSettings) {
- return;
- }
- internalOnFlappingChange({
- ...flappingSettings,
- statusChangeThreshold: value,
- });
- },
- [flappingSettings, internalOnFlappingChange]
- );
-
- const onFlappingToggle = useCallback(() => {
- if (!spaceFlappingSettings) {
- return;
- }
- if (flappingSettings) {
- cachedFlappingSettings.current = flappingSettings;
- return onFlappingChange(null);
- }
- const initialFlappingSettings = cachedFlappingSettings.current || spaceFlappingSettings;
- onFlappingChange({
- lookBackWindow: initialFlappingSettings.lookBackWindow,
- statusChangeThreshold: initialFlappingSettings.statusChangeThreshold,
- });
- }, [spaceFlappingSettings, flappingSettings, onFlappingChange]);
-
- const flappingTitleTooltip = useMemo(() => {
- return (
- setIsFlappingTitlePopoverOpen(false)}>
- setIsFlappingTitlePopoverOpen(!isFlappingTitlePopoverOpen)}
- />
- }
- >
- Alert flapping detection
-
- {flappingTitlePopoverFlappingDetection},
- }}
- />
-
-
-
- {flappingTitlePopoverAlertStatus},
- }}
- />
-
-
-
- {flappingTitlePopoverLookBack},
- }}
- />
-
-
-
- {flappingOffContentRules},
- settings: {flappingOffContentSettings} ,
- }}
- />
-
-
-
- );
- }, [isFlappingTitlePopoverOpen]);
-
- const flappingOffTooltip = useMemo(() => {
- if (!spaceFlappingSettings) {
- return null;
- }
- const { enabled } = spaceFlappingSettings;
- if (enabled) {
- return null;
- }
-
- if (writeFlappingSettingsUI) {
- return (
- setIsFlappingOffPopoverOpen(false)}>
- setIsFlappingOffPopoverOpen(!isFlappingOffPopoverOpen)}
- />
- }
- >
-
- {flappingOffContentRules},
- settings: {flappingOffContentSettings} ,
- }}
- />
-
-
-
- );
- }
- // TODO: Add the external doc link here!
- return (
-
- {flappingExternalLinkLabel}
-
- );
- }, [writeFlappingSettingsUI, isFlappingOffPopoverOpen, spaceFlappingSettings]);
-
- const flappingFormHeader = useMemo(() => {
- if (!spaceFlappingSettings) {
- return null;
- }
- const { enabled } = spaceFlappingSettings;
-
- return (
-
-
-
-
- {flappingLabel}
-
-
- {enabled ? flappingOnLabel : flappingOffLabel}
-
- {flappingSettings && enabled && (
- {flappingOverrideLabel}
- )}
-
-
- {enabled && (
-
- )}
- {flappingOffTooltip}
-
-
- {flappingSettings && enabled && (
- <>
-
-
- >
- )}
-
- );
- }, [
- isDesktop,
- euiTheme,
- spaceFlappingSettings,
- flappingSettings,
- flappingOffTooltip,
- onFlappingToggle,
- ]);
-
- const flappingFormBody = useMemo(() => {
- if (!spaceFlappingSettings || !spaceFlappingSettings.enabled) {
- return null;
- }
- if (!flappingSettings) {
- return null;
- }
- return (
-
-
-
- );
- }, [
- flappingSettings,
- spaceFlappingSettings,
- onLookBackWindowChange,
- onStatusChangeThresholdChange,
- ]);
-
- const flappingFormMessage = useMemo(() => {
- if (!spaceFlappingSettings || !spaceFlappingSettings.enabled) {
- return null;
- }
- const settingsToUse = flappingSettings || spaceFlappingSettings;
- return (
-
-
-
- );
- }, [spaceFlappingSettings, flappingSettings, euiTheme]);
-
return (
@@ -512,21 +140,23 @@ export const RuleFormAdvancedOptions = (props: RuleFormAdvancedOptionsProps) =>
label={
{flappingFormRowLabel}
- {flappingTitleTooltip}
+
+
+
}
data-test-subj="alertFlappingFormRow"
display="rowCompressed"
>
-
-
-
- {flappingFormHeader}
- {flappingFormBody}
-
-
- {flappingFormMessage}
-
+
)}
diff --git a/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts
index 2d6548062eed9..ca87ba3522042 100644
--- a/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts
@@ -25,9 +25,6 @@ export {
I18N_WEEKDAY_OPTIONS_DDD,
} from '@kbn/alerts-ui-shared/src/common/constants/i18n_weekdays';
-// Feature flag for frontend rule specific flapping in rule flyout
-export const IS_RULE_SPECIFIC_FLAPPING_ENABLED = false;
-
export const builtInComparators: { [key: string]: Comparator } = {
[COMPARATORS.GREATER_THAN]: {
text: i18n.translate('xpack.triggersActionsUI.common.constants.comparators.isAboveLabel', {
From 8ebd79326634417c1d4f469747ca6c2ddb3f5999 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?=
Date: Thu, 10 Oct 2024 09:15:46 +0200
Subject: [PATCH 30/87] [Usage counters] Use `refresh=false` (#195619)
---
.../server/usage_counters/saved_objects.test.ts | 1 +
.../usage_collection/server/usage_counters/saved_objects.ts | 1 +
.../server/usage_counters/usage_counters_service.test.ts | 2 ++
3 files changed, 4 insertions(+)
diff --git a/src/plugins/usage_collection/server/usage_counters/saved_objects.test.ts b/src/plugins/usage_collection/server/usage_counters/saved_objects.test.ts
index ebced92622779..927869b6d0f89 100644
--- a/src/plugins/usage_collection/server/usage_counters/saved_objects.test.ts
+++ b/src/plugins/usage_collection/server/usage_counters/saved_objects.test.ts
@@ -80,6 +80,7 @@ describe('storeCounter', () => {
],
Object {
"namespace": "default",
+ "refresh": false,
"upsertAttributes": Object {
"counterName": "b",
"counterType": "c",
diff --git a/src/plugins/usage_collection/server/usage_counters/saved_objects.ts b/src/plugins/usage_collection/server/usage_counters/saved_objects.ts
index 9c4e2832946e6..d5f49016e5296 100644
--- a/src/plugins/usage_collection/server/usage_counters/saved_objects.ts
+++ b/src/plugins/usage_collection/server/usage_counters/saved_objects.ts
@@ -122,6 +122,7 @@ export const storeCounter = async ({ metric, soRepository }: StoreCounterParams)
counterType,
source,
},
+ refresh: false,
}
);
};
diff --git a/src/plugins/usage_collection/server/usage_counters/usage_counters_service.test.ts b/src/plugins/usage_collection/server/usage_counters/usage_counters_service.test.ts
index 6128b643918a1..1041cfb5ce36f 100644
--- a/src/plugins/usage_collection/server/usage_counters/usage_counters_service.test.ts
+++ b/src/plugins/usage_collection/server/usage_counters/usage_counters_service.test.ts
@@ -157,6 +157,7 @@ describe('UsageCountersService', () => {
},
],
Object {
+ "refresh": false,
"upsertAttributes": Object {
"counterName": "counterA",
"counterType": "count",
@@ -175,6 +176,7 @@ describe('UsageCountersService', () => {
},
],
Object {
+ "refresh": false,
"upsertAttributes": Object {
"counterName": "counterB",
"counterType": "count",
From 72c76f9ac9c43365bcfb70903c9d848012260291 Mon Sep 17 00:00:00 2001
From: Artem Shelkovnikov
Date: Thu, 10 Oct 2024 09:34:17 +0200
Subject: [PATCH 31/87] Update configuration on changes in category/advanced
configurations in configView (#195567)
## Closes https://github.com/elastic/search-team/issues/6557
## Summary
Fixes a known bug for Network Drive connector (as this feature is only
used in it). The problem happens when there are Rich Configurable Fields
that are marked as "advanced" and depend on certain fields - in some
cases this field will not be shown until the page is fully reloaded.
Criteria that makes the bug happen:
1. Have some RCFs that are marked as "advanced":
https://github.com/elastic/connectors/blob/main/connectors/sources/network_drive.py#L405-L414.
(`"ui_restrictions": ["advanced"]`)
2. Make it so that this RCF depends on another field, and by default is
hidden - for example this field depends on a field "OS" that has
"Windows" and "Linux" as available options and Windows is default, but
this RCF depends on it being "Linux"
3. Try satisfying the dependency and see if the RCF is displayed - it
won't be, unless you save the form and reload it
The problem happens because for changes in "advanced" section the
configuration is not updated, so the view that's rendered still thinks
that the dependency is not satisfied and the field should not be
rendered
Before:
https://github.com/user-attachments/assets/51f9f8b0-a57a-4d96-a183-6dbbd36a919e
After:
https://github.com/user-attachments/assets/be32f434-0810-4345-bc4e-dc82f617705c
### Checklist
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
### For maintainers
- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---
.../connector_configuration_form.tsx | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/packages/kbn-search-connectors/components/configuration/connector_configuration_form.tsx b/packages/kbn-search-connectors/components/configuration/connector_configuration_form.tsx
index e70754d5e09e8..f7e619f407f12 100644
--- a/packages/kbn-search-connectors/components/configuration/connector_configuration_form.tsx
+++ b/packages/kbn-search-connectors/components/configuration/connector_configuration_form.tsx
@@ -109,6 +109,15 @@ export const ConnectorConfigurationForm: React.FC =
items={category.configEntries}
hasDocumentLevelSecurityEnabled={hasDocumentLevelSecurity}
setConfigEntry={(key, value) => {
+ const entry = localConfig[key];
+ if (entry && !isCategoryEntry(entry)) {
+ const newConfiguration: ConnectorConfiguration = {
+ ...localConfig,
+ [key]: { ...entry, value },
+ };
+ setLocalConfig(newConfiguration);
+ }
+
const categories = configView.categories;
categories[index] = { ...categories[index], [key]: value };
setConfigView({
@@ -136,6 +145,15 @@ export const ConnectorConfigurationForm: React.FC =
items={configView.advancedConfigurations}
hasDocumentLevelSecurityEnabled={hasDocumentLevelSecurity}
setConfigEntry={(key, value) => {
+ const entry = localConfig[key];
+ if (entry && !isCategoryEntry(entry)) {
+ const newConfiguration: ConnectorConfiguration = {
+ ...localConfig,
+ [key]: { ...entry, value },
+ };
+ setLocalConfig(newConfiguration);
+ }
+
setConfigView({
...configView,
advancedConfigurations: configView.advancedConfigurations.map((config) =>
From f687ce2ba34a500522907b76add4327c16ad1bec Mon Sep 17 00:00:00 2001
From: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com>
Date: Thu, 10 Oct 2024 09:06:33 +0100
Subject: [PATCH 32/87] [Security Solution][Detection Engine] adds EBT
telemetry for rule preview (#194326)
## Summary
- adds basic EBT telemetry for rule preview
### To test
Use Discover Data View in staging to see reported events:
https://telemetry-v2-staging.elastic.dev/s/securitysolution/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:now-28h,to:now))&_a=(columns:!(properties.ruleType,properties.loggedRequestsEnabled),filters:!(),index:security-solution-ebt-kibana-browser,interval:auto,query:(language:kuery,query:'event_type%20:%20%22Preview%20rule%22'),sort:!(!(timestamp,desc)))
Note, there is a few hours delay from event reported locally to be
stored on staging host
---
.../public/common/lib/telemetry/constants.ts | 1 +
.../telemetry/events/preview_rule/index.ts | 29 +++++++++++++++++++
.../telemetry/events/preview_rule/types.ts | 20 +++++++++++++
.../lib/telemetry/events/telemetry_events.ts | 2 ++
.../lib/telemetry/telemetry_client.mock.ts | 1 +
.../common/lib/telemetry/telemetry_client.ts | 5 ++++
.../public/common/lib/telemetry/types.ts | 7 +++++
.../rule_preview/use_preview_rule.ts | 18 ++++++++++--
...ecurity_solution_ebt_kibana_browser.ndjson | 4 +--
9 files changed, 83 insertions(+), 4 deletions(-)
create mode 100644 x-pack/plugins/security_solution/public/common/lib/telemetry/events/preview_rule/index.ts
create mode 100644 x-pack/plugins/security_solution/public/common/lib/telemetry/events/preview_rule/types.ts
diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts
index f42f77f19a0f9..5126d75178f5f 100644
--- a/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts
@@ -86,6 +86,7 @@ export enum TelemetryEventTypes {
EventLogShowSourceEventDateRange = 'Event Log -> Show Source -> Event Date Range',
OpenNoteInExpandableFlyoutClicked = 'Open Note In Expandable Flyout Clicked',
AddNoteFromExpandableFlyoutClicked = 'Add Note From Expandable Flyout Clicked',
+ PreviewRule = 'Preview rule',
}
export enum ML_JOB_TELEMETRY_STATUS {
diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/preview_rule/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/preview_rule/index.ts
new file mode 100644
index 0000000000000..12d721c45e2c0
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/preview_rule/index.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { TelemetryEvent } from '../../types';
+import { TelemetryEventTypes } from '../../constants';
+
+export const previewRuleEvent: TelemetryEvent = {
+ eventType: TelemetryEventTypes.PreviewRule,
+ schema: {
+ ruleType: {
+ type: 'keyword',
+ _meta: {
+ description: 'Rule type',
+ optional: false,
+ },
+ },
+ loggedRequestsEnabled: {
+ type: 'boolean',
+ _meta: {
+ description: 'shows if preview executed with enabled logged requests',
+ optional: false,
+ },
+ },
+ },
+};
diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/preview_rule/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/preview_rule/types.ts
new file mode 100644
index 0000000000000..e5523080088fc
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/preview_rule/types.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 { Type } from '@kbn/securitysolution-io-ts-alerting-types';
+
+import type { RootSchema } from '@kbn/core/public';
+import type { TelemetryEventTypes } from '../../constants';
+
+export interface PreviewRuleParams {
+ ruleType: Type;
+ loggedRequestsEnabled: boolean;
+}
+
+export interface PreviewRuleTelemetryEvent {
+ eventType: TelemetryEventTypes.PreviewRule;
+ schema: RootSchema;
+}
diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts
index d1f9502346a04..a0328099b9ff7 100644
--- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts
@@ -48,6 +48,7 @@ import {
addNoteFromExpandableFlyoutClickedEvent,
openNoteInExpandableFlyoutClickedEvent,
} from './notes';
+import { previewRuleEvent } from './preview_rule';
const mlJobUpdateEvent: TelemetryEvent = {
eventType: TelemetryEventTypes.MLJobUpdate,
@@ -192,4 +193,5 @@ export const telemetryEvents = [
eventLogShowSourceEventDateRangeEvent,
openNoteInExpandableFlyoutClickedEvent,
addNoteFromExpandableFlyoutClickedEvent,
+ previewRuleEvent,
];
diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts
index 02342cb4257be..98d6aa64bb9cb 100644
--- a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts
@@ -42,4 +42,5 @@ export const createTelemetryClientMock = (): jest.Mocked =
reportManualRuleRunOpenModal: jest.fn(),
reportOpenNoteInExpandableFlyoutClicked: jest.fn(),
reportAddNoteFromExpandableFlyoutClicked: jest.fn(),
+ reportPreviewRule: jest.fn(),
});
diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts
index 0023064adac69..e09f0a3c2eb66 100644
--- a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts
@@ -44,6 +44,7 @@ import type {
ReportManualRuleRunOpenModalParams,
ReportEventLogShowSourceEventDateRangeParams,
ReportEventLogFilterByRunTypeParams,
+ PreviewRuleParams,
} from './types';
import { TelemetryEventTypes } from './constants';
@@ -211,4 +212,8 @@ export class TelemetryClient implements TelemetryClientStart {
) => {
this.analytics.reportEvent(TelemetryEventTypes.AddNoteFromExpandableFlyoutClicked, params);
};
+
+ public reportPreviewRule = (params: PreviewRuleParams) => {
+ this.analytics.reportEvent(TelemetryEventTypes.PreviewRule, params);
+ };
}
diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts
index 49c78dc50feeb..55b91837a2585 100644
--- a/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts
@@ -72,6 +72,7 @@ import type {
NotesTelemetryEvents,
OpenNoteInExpandableFlyoutClickedParams,
} from './events/notes/types';
+import type { PreviewRuleParams, PreviewRuleTelemetryEvent } from './events/preview_rule/types';
export * from './events/ai_assistant/types';
export * from './events/alerts_grouping/types';
@@ -91,6 +92,7 @@ export type {
export * from './events/document_details/types';
export * from './events/manual_rule_run/types';
export * from './events/event_log/types';
+export * from './events/preview_rule/types';
export interface TelemetryServiceSetupParams {
analytics: AnalyticsServiceSetup;
@@ -136,6 +138,7 @@ export type TelemetryEventParams =
| OnboardingHubStepLinkClickedParams
| ReportManualRuleRunTelemetryEventParams
| ReportEventLogTelemetryEventParams
+ | PreviewRuleParams
| NotesTelemetryEventParams;
export interface TelemetryClientStart {
@@ -194,6 +197,9 @@ export interface TelemetryClientStart {
// new notes
reportOpenNoteInExpandableFlyoutClicked(params: OpenNoteInExpandableFlyoutClickedParams): void;
reportAddNoteFromExpandableFlyoutClicked(params: AddNoteFromExpandableFlyoutClickedParams): void;
+
+ // preview rule
+ reportPreviewRule(params: PreviewRuleParams): void;
}
export type TelemetryEvent =
@@ -221,4 +227,5 @@ export type TelemetryEvent =
| OnboardingHubTelemetryEvent
| ManualRuleRunTelemetryEvent
| EventLogTelemetryEvent
+ | PreviewRuleTelemetryEvent
| NotesTelemetryEvents;
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts
index 05c3b9fe10299..018e2602aa170 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts
@@ -12,7 +12,7 @@ import type {
RuleCreateProps,
RulePreviewResponse,
} from '../../../../../common/api/detection_engine';
-
+import { useKibana } from '../../../../common/lib/kibana';
import { previewRule } from '../../../rule_management/api/api';
import { transformOutput } from '../../../../detections/containers/detection_engine/rules/transforms';
import type { TimeframePreviewOptions } from '../../../../detections/pages/detection_engine/rules/types';
@@ -37,6 +37,7 @@ export const usePreviewRule = ({
const [isLoading, setIsLoading] = useState(false);
const { addError } = useAppToasts();
const { invocationCount, interval, from } = usePreviewInvocationCount({ timeframeOptions });
+ const { telemetry } = useKibana().services;
const timeframeEnd = useMemo(
() => timeframeOptions.timeframeEnd.toISOString(),
@@ -57,6 +58,10 @@ export const usePreviewRule = ({
const createPreviewId = async () => {
if (rule != null) {
try {
+ telemetry.reportPreviewRule({
+ loggedRequestsEnabled: enableLoggedRequests ?? false,
+ ruleType: rule.type,
+ });
setIsLoading(true);
const previewRuleResponse = await previewRule({
rule: {
@@ -90,7 +95,16 @@ export const usePreviewRule = ({
isSubscribed = false;
abortCtrl.abort();
};
- }, [rule, addError, invocationCount, from, interval, timeframeEnd, enableLoggedRequests]);
+ }, [
+ rule,
+ addError,
+ invocationCount,
+ from,
+ interval,
+ timeframeEnd,
+ enableLoggedRequests,
+ telemetry,
+ ]);
return { isLoading, response, rule, setRule };
};
diff --git a/x-pack/plugins/security_solution/scripts/telemetry/saved_objects/security_solution_ebt_kibana_browser.ndjson b/x-pack/plugins/security_solution/scripts/telemetry/saved_objects/security_solution_ebt_kibana_browser.ndjson
index be4eb8f1e7785..f0df277ff5223 100644
--- a/x-pack/plugins/security_solution/scripts/telemetry/saved_objects/security_solution_ebt_kibana_browser.ndjson
+++ b/x-pack/plugins/security_solution/scripts/telemetry/saved_objects/security_solution_ebt_kibana_browser.ndjson
@@ -1,2 +1,2 @@
-{"attributes":{"allowHidden":false,"fieldAttrs":"{\"properties.groupingId\":{\"count\":1},\"properties.target\":{\"count\":1},\"properties.groupName\":{\"count\":2},\"properties.metadata.telemetry.component\":{\"count\":2},\"properties.unallowedMappingFields\":{\"count\":2},\"properties.unallowedValueFields\":{\"count\":1},\"context.labels.serverless\":{\"count\":4},\"properties.tableId\":{\"count\":1},\"properties.groupNumber\":{\"count\":1},\"properties.groupByField\":{\"count\":4},\"properties.status\":{\"count\":1},\"properties.conversationId\":{\"count\":17},\"properties.invokedBy\":{\"count\":7},\"properties.role\":{\"count\":3},\"properties.isEnabledKnowledgeBase\":{\"count\":1},\"properties.isEnabledRAGAlerts\":{\"count\":1},\"properties.promptTitle\":{\"count\":3},\"properties.fieldName\":{\"count\":1},\"properties.actionId\":{\"count\":1},\"properties.displayName\":{\"count\":1},\"properties.batchId\":{\"count\":8},\"properties.indexId\":{\"count\":1},\"properties.indexName\":{\"count\":2},\"properties.numberOfIndices\":{\"count\":1},\"properties.timeConsumedMs\":{\"count\":1},\"properties.ecsVersion\":{\"count\":1},\"properties.errorCount\":{\"count\":1},\"properties.numberOfIncompatibleFields\":{\"count\":1},\"properties.numberOfDocuments\":{\"count\":1},\"properties.sizeInBytes\":{\"count\":4},\"properties.isCheckAll\":{\"count\":5},\"properties.ilmPhase\":{\"count\":2},\"properties.title\":{\"count\":1},\"properties.location\":{\"count\":1},\"context.applicationId\":{\"count\":6},\"context.cloudId\":{\"count\":6},\"context.cluster_name\":{\"count\":13},\"context.cluster_uuid\":{\"count\":28},\"context.cluster_version\":{\"count\":2},\"context.license_type\":{\"count\":1},\"context.page\":{\"count\":8},\"context.pageName\":{\"count\":6},\"context.page_title\":{\"count\":1},\"context.page_url\":{\"count\":1},\"context.session_id\":{\"count\":2},\"event_type\":{\"count\":36},\"properties\":{\"count\":8},\"properties.pattern\":{\"count\":2},\"peoperties.indexName\":{\"count\":1},\"properties.stepId\":{},\"properties.trigger\":{},\"properties.stepLinkId\":{},\"properties.originStepId\":{},\"properties.durationMs\":{},\"properties.isOpen\":{},\"properties.actionTypeId\":{},\"properties.model\":{},\"properties.provider\":{},\"properties.assistantStreamingEnabled\":{},\"properties.alertsContextCount\":{},\"properties.alertsCount\":{},\"properties.configuredAlertsCount\":{},\"properties.entity\":{},\"properties.selectedSeverity\":{},\"properties.file.size\":{},\"properties.processing.startTime\":{},\"properties.processing.endTime\":{},\"properties.processing.tookMs\":{},\"properties.stats.validLines\":{},\"properties.stats.invalidLines\":{},\"properties.stats.totalLines\":{},\"properties.valid\":{},\"properties.errorCode\":{},\"properties.action\":{},\"properties.quantity\":{},\"properties.jobId\":{},\"properties.isElasticJob\":{},\"properties.moduleId\":{},\"properties.errorMessage\":{},\"properties.count\":{},\"properties.numberOfIndicesChecked\":{},\"properties.numberOfSameFamily\":{},\"properties.numberOfFields\":{},\"properties.numberOfEcsFields\":{},\"properties.numberOfCustomFields\":{},\"properties.panel\":{},\"properties.tabId\":{}}","fieldFormatMap":"{}","fields":"[]","name":"security-solution-ebt-kibana-browser","runtimeFieldMap":"{\"properties.groupingId\":{\"type\":\"keyword\"},\"properties.target\":{\"type\":\"keyword\"},\"property.stackByField\":{\"type\":\"keyword\"},\"properties.groupName\":{\"type\":\"keyword\"},\"context.prebuiltRulesPackageVersion\":{\"type\":\"keyword\"},\"properties.metadata.telemetry.component\":{\"type\":\"keyword\"},\"properties.unallowedMappingFields\":{\"type\":\"keyword\"},\"properties.unallowedValueFields\":{\"type\":\"keyword\"},\"context.labels.serverless\":{\"type\":\"keyword\"},\"properties.resourceAccessed\":{\"type\":\"keyword\"},\"properties.resultCount\":{\"type\":\"long\"},\"properties.responseTime\":{\"type\":\"long\"},\"day_of_week\":{\"type\":\"keyword\",\"script\":{\"source\":\"emit(doc['timestamp'].value.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.getDefault()))\"}},\"properties.isOpen\":{\"type\":\"boolean\"},\"properties.tableId\":{\"type\":\"keyword\"},\"properties.groupNumber\":{\"type\":\"long\"},\"properties.groupByField\":{\"type\":\"keyword\"},\"properties.status\":{\"type\":\"keyword\"},\"properties.conversationId\":{\"type\":\"keyword\"},\"properties.invokedBy\":{\"type\":\"keyword\"},\"properties.role\":{\"type\":\"keyword\"},\"properties.isEnabledKnowledgeBase\":{\"type\":\"boolean\"},\"properties.isEnabledRAGAlerts\":{\"type\":\"boolean\"},\"properties.actionTypeId\":{\"type\":\"keyword\"},\"properties.model\":{\"type\":\"keyword\"},\"properties.provider\":{\"type\":\"keyword\"},\"properties.promptTitle\":{\"type\":\"keyword\"},\"properties.assistantStreamingEnabled\":{\"type\":\"boolean\"},\"properties.durationMs\":{\"type\":\"long\"},\"properties.alertsContextCount\":{\"type\":\"long\"},\"properties.alertsCount\":{\"type\":\"long\"},\"properties.configuredAlertsCount\":{\"type\":\"long\"},\"properties.entity\":{\"type\":\"keyword\"},\"properties.selectedSeverity\":{\"type\":\"keyword\"},\"properties.file.size\":{\"type\":\"long\"},\"properties.processing.startTime\":{\"type\":\"date\"},\"properties.processing.endTime\":{\"type\":\"date\"},\"properties.processing.tookMs\":{\"type\":\"long\"},\"properties.stats.validLines\":{\"type\":\"long\"},\"properties.stats.invalidLines\":{\"type\":\"long\"},\"properties.stats.totalLines\":{\"type\":\"long\"},\"properties.valid\":{\"type\":\"boolean\"},\"properties.errorCode\":{\"type\":\"keyword\"},\"properties.action\":{\"type\":\"keyword\"},\"properties.quantity\":{\"type\":\"long\"},\"properties.jobId\":{\"type\":\"keyword\"},\"properties.isElasticJob\":{\"type\":\"boolean\"},\"properties.moduleId\":{\"type\":\"keyword\"},\"properties.errorMessage\":{\"type\":\"keyword\"},\"properties.fieldName\":{\"type\":\"keyword\"},\"properties.actionId\":{\"type\":\"keyword\"},\"properties.displayName\":{\"type\":\"keyword\"},\"properties.count\":{\"type\":\"long\"},\"properties.batchId\":{\"type\":\"keyword\"},\"properties.indexId\":{\"type\":\"keyword\"},\"properties.indexName\":{\"type\":\"keyword\"},\"properties.numberOfIndices\":{\"type\":\"long\"},\"properties.numberOfIndicesChecked\":{\"type\":\"long\"},\"properties.numberOfSameFamily\":{\"type\":\"long\"},\"properties.timeConsumedMs\":{\"type\":\"long\"},\"properties.ecsVersion\":{\"type\":\"keyword\"},\"properties.errorCount\":{\"type\":\"long\"},\"properties.numberOfFields\":{\"type\":\"long\"},\"properties.numberOfIncompatibleFields\":{\"type\":\"long\"},\"properties.numberOfEcsFields\":{\"type\":\"long\"},\"properties.numberOfCustomFields\":{\"type\":\"long\"},\"properties.numberOfDocuments\":{\"type\":\"long\"},\"properties.sizeInBytes\":{\"type\":\"long\"},\"properties.isCheckAll\":{\"type\":\"boolean\"},\"properties.ilmPhase\":{\"type\":\"keyword\"},\"properties.title\":{\"type\":\"keyword\"},\"properties.location\":{\"type\":\"keyword\"},\"properties.panel\":{\"type\":\"keyword\"},\"properties.tabId\":{\"type\":\"keyword\"},\"properties.stepId\":{\"type\":\"keyword\"},\"properties.trigger\":{\"type\":\"keyword\"},\"properties.originStepId\":{\"type\":\"keyword\"},\"properties.stepLinkId\":{\"type\":\"keyword\"}}","sourceFilters":"[]","timeFieldName":"timestamp","title":"ebt-kibana-browser","typeMeta":"{}"},"coreMigrationVersion":"8.8.0","created_at":"2024-05-30T16:12:33.003Z","id":"security-solution-ebt-kibana-browser","managed":false,"references":[],"type":"index-pattern","typeMigrationVersion":"8.0.0","updated_at":"2024-05-30T16:52:03.990Z","version":"WzMwNTU0LDVd"}
-{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":1,"missingRefCount":0,"missingReferences":[]}
+{"attributes":{"allowHidden":false,"fieldAttrs":"{\"properties.groupingId\":{\"count\":1},\"properties.target\":{\"count\":1},\"properties.groupName\":{\"count\":2},\"properties.metadata.telemetry.component\":{\"count\":2},\"properties.unallowedMappingFields\":{\"count\":2},\"properties.unallowedValueFields\":{\"count\":1},\"context.labels.serverless\":{\"count\":4},\"properties.isEnabledRAGAlerts\":{\"count\":1},\"properties.tableId\":{\"count\":1},\"properties.groupNumber\":{\"count\":1},\"properties.groupByField\":{\"count\":4},\"properties.status\":{\"count\":1},\"properties.conversationId\":{\"count\":17},\"properties.invokedBy\":{\"count\":7},\"properties.role\":{\"count\":3},\"properties.isEnabledKnowledgeBase\":{\"count\":1},\"properties.promptTitle\":{\"count\":3},\"properties.fieldName\":{\"count\":1},\"properties.actionId\":{\"count\":1},\"properties.displayName\":{\"count\":1},\"properties.batchId\":{\"count\":8},\"properties.indexId\":{\"count\":1},\"properties.indexName\":{\"count\":2},\"properties.numberOfIndices\":{\"count\":1},\"properties.timeConsumedMs\":{\"count\":1},\"properties.ecsVersion\":{\"count\":1},\"properties.errorCount\":{\"count\":1},\"properties.numberOfIncompatibleFields\":{\"count\":1},\"properties.numberOfDocuments\":{\"count\":1},\"properties.sizeInBytes\":{\"count\":4},\"properties.isCheckAll\":{\"count\":5},\"properties.ilmPhase\":{\"count\":2},\"properties.title\":{\"count\":1},\"properties.location\":{\"count\":1},\"context.applicationId\":{\"count\":6},\"context.cloudId\":{\"count\":6},\"context.cluster_name\":{\"count\":13},\"context.cluster_uuid\":{\"count\":28},\"context.cluster_version\":{\"count\":2},\"context.license_type\":{\"count\":1},\"context.page\":{\"count\":8},\"context.pageName\":{\"count\":6},\"context.page_title\":{\"count\":1},\"context.page_url\":{\"count\":1},\"context.session_id\":{\"count\":2},\"event_type\":{\"count\":36},\"properties\":{\"count\":8},\"properties.pattern\":{\"count\":2},\"peoperties.indexName\":{\"count\":1},\"properties.stepId\":{},\"properties.trigger\":{},\"properties.stepLinkId\":{},\"properties.originStepId\":{},\"properties.durationMs\":{},\"properties.isOpen\":{},\"properties.actionTypeId\":{},\"properties.model\":{},\"properties.provider\":{},\"properties.assistantStreamingEnabled\":{},\"properties.alertsContextCount\":{},\"properties.alertsCount\":{},\"properties.configuredAlertsCount\":{},\"properties.entity\":{},\"properties.selectedSeverity\":{},\"properties.file.size\":{},\"properties.processing.startTime\":{},\"properties.processing.endTime\":{},\"properties.processing.tookMs\":{},\"properties.stats.validLines\":{},\"properties.stats.invalidLines\":{},\"properties.stats.totalLines\":{},\"properties.valid\":{},\"properties.errorCode\":{},\"properties.action\":{},\"properties.quantity\":{},\"properties.jobId\":{},\"properties.isElasticJob\":{},\"properties.moduleId\":{},\"properties.errorMessage\":{},\"properties.count\":{},\"properties.numberOfIndicesChecked\":{},\"properties.numberOfSameFamily\":{},\"properties.numberOfFields\":{},\"properties.numberOfEcsFields\":{},\"properties.numberOfCustomFields\":{},\"properties.panel\":{},\"properties.tabId\":{},\"properties.totalTasks\":{},\"properties.completedTasks\":{},\"properties.errorTasks\":{},\"properties.rangeInMs\":{},\"properties.type\":{},\"properties.runType\":{},\"properties.isVisible\":{},\"properties.alertsCountUpdated\":{},\"properties.rulesCount\":{},\"properties.isRelatedToATimeline\":{},\"propeties.loggedRequestsEnabled\":{},\"properties.ruleType\":{},\"properties.loggedRequestsEnabled\":{}}","fieldFormatMap":"{}","fields":"[]","name":"security-solution-ebt-kibana-browser","runtimeFieldMap":"{\"properties.groupingId\":{\"type\":\"keyword\"},\"properties.target\":{\"type\":\"keyword\"},\"property.stackByField\":{\"type\":\"keyword\"},\"properties.groupName\":{\"type\":\"keyword\"},\"context.prebuiltRulesPackageVersion\":{\"type\":\"keyword\"},\"properties.metadata.telemetry.component\":{\"type\":\"keyword\"},\"properties.unallowedMappingFields\":{\"type\":\"keyword\"},\"properties.unallowedValueFields\":{\"type\":\"keyword\"},\"context.labels.serverless\":{\"type\":\"keyword\"},\"properties.resourceAccessed\":{\"type\":\"keyword\"},\"properties.resultCount\":{\"type\":\"long\"},\"properties.responseTime\":{\"type\":\"long\"},\"day_of_week\":{\"type\":\"keyword\",\"script\":{\"source\":\"emit(doc['timestamp'].value.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.getDefault()))\"}},\"properties.isEnabledRAGAlerts\":{\"type\":\"boolean\"},\"properties.durationMs\":{\"type\":\"long\"},\"properties.alertsContextCount\":{\"type\":\"long\"},\"properties.alertsCount\":{\"type\":\"long\"},\"properties.configuredAlertsCount\":{\"type\":\"long\"},\"properties.runType\":{\"type\":\"keyword\"},\"properties.isOpen\":{\"type\":\"boolean\"},\"properties.tableId\":{\"type\":\"keyword\"},\"properties.groupNumber\":{\"type\":\"long\"},\"properties.groupByField\":{\"type\":\"keyword\"},\"properties.status\":{\"type\":\"keyword\"},\"properties.conversationId\":{\"type\":\"keyword\"},\"properties.invokedBy\":{\"type\":\"keyword\"},\"properties.role\":{\"type\":\"keyword\"},\"properties.actionTypeId\":{\"type\":\"keyword\"},\"properties.model\":{\"type\":\"keyword\"},\"properties.provider\":{\"type\":\"keyword\"},\"properties.isEnabledKnowledgeBase\":{\"type\":\"boolean\"},\"properties.promptTitle\":{\"type\":\"keyword\"},\"properties.alertsCountUpdated\":{\"type\":\"boolean\"},\"properties.assistantStreamingEnabled\":{\"type\":\"boolean\"},\"properties.entity\":{\"type\":\"keyword\"},\"properties.selectedSeverity\":{\"type\":\"keyword\"},\"properties.file.size\":{\"type\":\"long\"},\"properties.processing.startTime\":{\"type\":\"date\"},\"properties.processing.endTime\":{\"type\":\"date\"},\"properties.processing.tookMs\":{\"type\":\"long\"},\"properties.stats.validLines\":{\"type\":\"long\"},\"properties.stats.invalidLines\":{\"type\":\"long\"},\"properties.stats.totalLines\":{\"type\":\"long\"},\"properties.valid\":{\"type\":\"boolean\"},\"properties.errorCode\":{\"type\":\"keyword\"},\"properties.action\":{\"type\":\"keyword\"},\"properties.quantity\":{\"type\":\"long\"},\"properties.jobId\":{\"type\":\"keyword\"},\"properties.isElasticJob\":{\"type\":\"boolean\"},\"properties.moduleId\":{\"type\":\"keyword\"},\"properties.errorMessage\":{\"type\":\"keyword\"},\"properties.fieldName\":{\"type\":\"keyword\"},\"properties.actionId\":{\"type\":\"keyword\"},\"properties.displayName\":{\"type\":\"keyword\"},\"properties.count\":{\"type\":\"long\"},\"properties.batchId\":{\"type\":\"keyword\"},\"properties.indexId\":{\"type\":\"keyword\"},\"properties.indexName\":{\"type\":\"keyword\"},\"properties.numberOfIndices\":{\"type\":\"long\"},\"properties.numberOfIndicesChecked\":{\"type\":\"long\"},\"properties.numberOfSameFamily\":{\"type\":\"long\"},\"properties.timeConsumedMs\":{\"type\":\"long\"},\"properties.ecsVersion\":{\"type\":\"keyword\"},\"properties.errorCount\":{\"type\":\"long\"},\"properties.numberOfFields\":{\"type\":\"long\"},\"properties.numberOfIncompatibleFields\":{\"type\":\"long\"},\"properties.numberOfEcsFields\":{\"type\":\"long\"},\"properties.numberOfCustomFields\":{\"type\":\"long\"},\"properties.numberOfDocuments\":{\"type\":\"long\"},\"properties.sizeInBytes\":{\"type\":\"long\"},\"properties.isCheckAll\":{\"type\":\"boolean\"},\"properties.ilmPhase\":{\"type\":\"keyword\"},\"properties.title\":{\"type\":\"keyword\"},\"properties.location\":{\"type\":\"keyword\"},\"properties.panel\":{\"type\":\"keyword\"},\"properties.tabId\":{\"type\":\"keyword\"},\"properties.stepId\":{\"type\":\"keyword\"},\"properties.trigger\":{\"type\":\"keyword\"},\"properties.originStepId\":{\"type\":\"keyword\"},\"properties.stepLinkId\":{\"type\":\"keyword\"},\"properties.totalTasks\":{\"type\":\"long\"},\"properties.completedTasks\":{\"type\":\"long\"},\"properties.errorTasks\":{\"type\":\"long\"},\"properties.rangeInMs\":{\"type\":\"long\"},\"properties.rulesCount\":{\"type\":\"long\"},\"properties.type\":{\"type\":\"keyword\"},\"properties.isVisible\":{\"type\":\"boolean\"},\"properties.isRelatedToATimeline\":{\"type\":\"boolean\"},\"properties.ruleType\":{\"type\":\"keyword\"},\"properties.loggedRequestsEnabled\":{\"type\":\"boolean\"}}","sourceFilters":"[]","timeFieldName":"timestamp","title":"ebt-kibana-browser","typeMeta":"{}"},"coreMigrationVersion":"8.8.0","created_at":"2024-05-30T16:12:33.003Z","id":"security-solution-ebt-kibana-browser","managed":false,"references":[],"type":"index-pattern","typeMigrationVersion":"8.0.0","updated_at":"2024-10-09T14:55:41.854Z","version":"WzUyMTQ4LDld"}
+{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":1,"missingRefCount":0,"missingReferences":[]}
\ No newline at end of file
From a481da68e58cb20d6407c9866c1511717addfdb0 Mon Sep 17 00:00:00 2001
From: Jean-Louis Leysens
Date: Thu, 10 Oct 2024 10:53:59 +0200
Subject: [PATCH 33/87] [HTTP] Copy array returned by `getRoutes` (#195647)
## Summary
Small follow up based on
https://github.com/elastic/kibana/pull/192675#discussion_r1793601519
---
.../core/http/core-http-router-server-internal/src/router.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/core/http/core-http-router-server-internal/src/router.ts b/packages/core/http/core-http-router-server-internal/src/router.ts
index 1a74e27910c1a..bb99de64581be 100644
--- a/packages/core/http/core-http-router-server-internal/src/router.ts
+++ b/packages/core/http/core-http-router-server-internal/src/router.ts
@@ -240,7 +240,7 @@ export class Router !route.isVersioned);
}
- return this.routes;
+ return [...this.routes];
}
public handleLegacyErrors = wrapErrors;
From dbc0e6f085d73206a9c4efd38a29bfa4bf045029 Mon Sep 17 00:00:00 2001
From: Elena Shostak <165678770+elena-shostak@users.noreply.github.com>
Date: Thu, 10 Oct 2024 11:05:01 +0200
Subject: [PATCH 34/87] [CodeQL] Added env vars for code scanning data
ingestion (#195712)
## Summary
Added env vars for code scanning data ingestion.
---
.github/workflows/codeql.yml | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index e16dbcb261807..e80b3b2c73463 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -73,7 +73,9 @@ jobs:
env:
GITHUB_TOKEN: ${{secrets.KIBANAMACHINE_TOKEN}}
SLACK_TOKEN: ${{secrets.CODE_SCANNING_SLACK_TOKEN}}
- CODEQL_BRANCHES: 7.17,8.x,main
+ CODE_SCANNING_ES_HOST: ${{secrets.CODE_SCANNING_ES_HOST}}
+ CODE_SCANNING_ES_API_KEY: ${{secrets.CODE_SCANNING_ES_API_KEY}}
+ CODE_SCANNING_BRANCHES: 7.17,8.x,main
run: |
npm ci --omit=dev
node codeql-alert
From d44d3543fb71858de5b09e04f3a538bd8cb0bf5b Mon Sep 17 00:00:00 2001
From: Robert Jaszczurek <92210485+rbrtj@users.noreply.github.com>
Date: Thu, 10 Oct 2024 11:19:28 +0200
Subject: [PATCH 35/87] [ML] Fix Anomaly Swim Lane Embeddable not updating
properly on query change (#195090)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Fix for: [#194579](https://github.com/elastic/kibana/issues/194579)
In Anomaly Explorer, we do not limit the query size, as it is based on a
constant value of `1000`.
However, we did limit the query for the embeddable by setting the size
to the value of the previous query cardinality.
After discussing with @darnautov, we couldn't identify any potential
regressions from removing this check.
Includes fix for issue mentioned in:
[#2397303538](https://github.com/elastic/kibana/pull/195090#issuecomment-2397303538)
When querying from a pagination page other than page 1, we didn’t reset
the `fromPage` value, which prevented the query from returning results.
Before:
https://github.com/user-attachments/assets/80476a0c-8fcc-40f7-8cac-04ecfb01d614
After:
https://github.com/user-attachments/assets/f55e20fd-b1a4-446e-b16a-b1a6069bf63c
https://github.com/user-attachments/assets/d31cb47d-cd13-4b3c-b6f9-c0ee60d3a370
---
.../anomaly_swimlane_embeddable_factory.tsx | 19 ++++++++++++++++++-
.../initialize_swim_lane_data_fetcher.ts | 10 ++--------
2 files changed, 20 insertions(+), 9 deletions(-)
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.tsx
index 34390075f927b..464b5bd196675 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.tsx
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.tsx
@@ -18,6 +18,7 @@ import {
apiHasExecutionContext,
apiHasParentApi,
apiPublishesTimeRange,
+ fetch$,
initializeTimeRange,
initializeTitles,
useBatchedPublishingSubjects,
@@ -26,7 +27,8 @@ import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import React, { useCallback, useState } from 'react';
import useUnmount from 'react-use/lib/useUnmount';
import type { Observable } from 'rxjs';
-import { BehaviorSubject, combineLatest, map, of, Subscription } from 'rxjs';
+import { BehaviorSubject, combineLatest, distinctUntilChanged, map, of, Subscription } from 'rxjs';
+import fastIsEqual from 'fast-deep-equal';
import type { AnomalySwimlaneEmbeddableServices } from '..';
import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '..';
import type { MlDependencies } from '../../application/app';
@@ -235,6 +237,21 @@ export const getAnomalySwimLaneEmbeddableFactory = (
anomalySwimLaneServices
);
+ subscriptions.add(
+ fetch$(api)
+ .pipe(
+ map((fetchContext) => ({
+ query: fetchContext.query,
+ filters: fetchContext.filters,
+ timeRange: fetchContext.timeRange,
+ })),
+ distinctUntilChanged(fastIsEqual)
+ )
+ .subscribe(() => {
+ api.updatePagination({ fromPage: 1 });
+ })
+ );
+
const onRenderComplete = () => {};
return {
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/initialize_swim_lane_data_fetcher.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/initialize_swim_lane_data_fetcher.ts
index 268a17fca4a81..be678af02a65b 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/initialize_swim_lane_data_fetcher.ts
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/initialize_swim_lane_data_fetcher.ts
@@ -6,7 +6,7 @@
*/
import type { estypes } from '@elastic/elasticsearch';
-import type { TimeRange } from '@kbn/es-query';
+import { type TimeRange } from '@kbn/es-query';
import type { PublishesUnifiedSearch } from '@kbn/presentation-publishing';
import {
BehaviorSubject,
@@ -29,7 +29,6 @@ import {
SWIMLANE_TYPE,
} from '../../application/explorer/explorer_constants';
import type { OverallSwimlaneData } from '../../application/explorer/explorer_utils';
-import { isViewBySwimLaneData } from '../../application/explorer/swimlane_container';
import { CONTROLLED_BY_SWIM_LANE_FILTER } from '../../ui_actions/constants';
import { getJobsObservable } from '../common/get_jobs_observable';
import { processFilters } from '../common/process_filters';
@@ -114,12 +113,7 @@ export const initializeSwimLaneDataFetcher = (
const { earliest, latest } = overallSwimlaneData;
if (overallSwimlaneData && swimlaneType === SWIMLANE_TYPE.VIEW_BY) {
- const swimlaneData = swimLaneData$.value;
-
- let swimLaneLimit = ANOMALY_SWIM_LANE_HARD_LIMIT;
- if (isViewBySwimLaneData(swimlaneData) && viewBy === swimlaneData.fieldName) {
- swimLaneLimit = swimlaneData.cardinality;
- }
+ const swimLaneLimit = ANOMALY_SWIM_LANE_HARD_LIMIT;
return from(
anomalyTimelineService.loadViewBySwimlane(
From 7a30154fdfc109a87b69d429bb2252cf5499d5b9 Mon Sep 17 00:00:00 2001
From: kosabogi <105062005+kosabogi@users.noreply.github.com>
Date: Thu, 10 Oct 2024 11:27:03 +0200
Subject: [PATCH 36/87] [Search landing page] Update search landing page list
with new links (#194656)
### Overview
This PR updates the search landing page by refreshing the existing list
with new links.
### Related issue
https://github.com/elastic/search-docs-team/issues/200
---------
Co-authored-by: Liam Thompson <32779855+leemthompo@users.noreply.github.com>
---
docs/search/index.asciidoc | 78 +++++++++++++++++++-------------------
1 file changed, 39 insertions(+), 39 deletions(-)
diff --git a/docs/search/index.asciidoc b/docs/search/index.asciidoc
index f046330ac13e9..ab4b007800da4 100644
--- a/docs/search/index.asciidoc
+++ b/docs/search/index.asciidoc
@@ -9,8 +9,8 @@ The *Search* space in {kib} comprises the following features:
* <>
* https://www.elastic.co/guide/en/elasticsearch/reference/current/search-application-overview.html[Search Applications]
* https://www.elastic.co/guide/en/elasticsearch/reference/current/behavioral-analytics-overview.html[Behavioral Analytics]
-* Inference Endpoints UI
-* AI Assistant for Search
+* <>
+* <>
* Persistent Dev Tools <>
[float]
@@ -19,53 +19,53 @@ The *Search* space in {kib} comprises the following features:
The Search solution and use case is made up of many tools and features across the {stack}.
As a result, the release notes for your features of interest might live in different Elastic docs.
-// Use the following table to find links to the appropriate documentation, API references (if applicable), and release notes.
+Use the following table to find links to the appropriate documentation, API references (if applicable), and release notes.
-// [options="header"]
-// |===
-// | Name | API reference | Documentation | Release notes
+[options="header"]
+|===
+| Name | API reference | Documentation | Release notes
-// | Connectors
-// | link:https://example.com/connectors/api[API reference]
-// | link:https://example.com/connectors/docs[Documentation]
-// | link:https://example.com/connectors/notes[Release notes]
+| Connectors
+| {ref}/connector-apis.html[API reference]
+| {ref}/es-connectors.html[Elastic Connectors]
+| {ref}/es-connectors-release-notes.html[Elasticsearch guide]
-// | Web crawler
-// | link:https://example.com/web_crawlers/api[API reference]
-// | link:https://example.com/web_crawlers/docs[Documentation]
-// | link:https://example.com/web_crawlers/notes[Release notes]
+| Web crawler
+| N/A
+| {enterprise-search-ref}/crawler.html[Documentation]
+| {enterprise-search-ref}/changelog.html[Enterprise Search Guide]
-// | Playground
-// | link:https://example.com/playground/api[API reference]
-// | link:https://example.com/playground/docs[Documentation]
-// | link:https://example.com/playground/notes[Release notes]
+| Playground
+| N/A
+| {kibana-ref}/playground.html[Documentation]
+| {kibana-ref}/release-notes.html[Kibana guide]
-// | Search Applications
-// | link:https://example.com/search_apps/api[API reference]
-// | link:https://example.com/search_apps/docs[Documentation]
-// | link:https://example.com/search_apps/notes[Release notes]
+| Search Applications
+| {ref}/search-application-apis.html[API reference]
+| {enterprise-search-ref}/app-search-workplace-search.html[Documentation]
+| {ref}/es-release-notes.html[Elasticsearch guide]
-// | Behavioral Analytics
-// | link:https://example.com/behavioral_analytics/api[API reference]
-// | link:https://example.com/behavioral_analytics/docs[Documentation]
-// | link:https://example.com/behavioral_analytics/notes[Release notes]
+| Behavioral Analytics
+| {ref}/behavioral-analytics-apis.html[API reference]
+| {ref}/behavioral-analytics-start.html[Documentation]
+| {ref}/es-release-notes.html[Elasticsearch guide]
-// | Inference Endpoints
-// | link:https://example.com/inference_endpoints/api[API reference]
-// | link:https://example.com/inference_endpoints/docs[Documentation]
-// | link:https://example.com/inference_endpoints/notes[Release notes]
+| Inference Endpoints
+| {ref}/inference-apis.html[API reference]
+| {kibana-ref}/inference-endpoints.html[Documentation]
+| {ref}/es-release-notes.html[Elasticsearch guide]
-// | Console
-// | link:https://example.com/console/api[API reference]
-// | link:https://example.com/console/docs[Documentation]
-// | link:https://example.com/console/notes[Release notes]
+| Console
+| N/A
+| {kibana-ref}/console-kibana.html[Documentation]
+| {kibana-ref}/release-notes.html[Kibana guide]
-// | Search UI
-// | link:https://www.elastic.co/docs/current/search-ui/api/architecture[API reference]
-// | link:https://www.elastic.co/docs/current/search-ui/overview[Documentation]
-// | link:https://example.com/search_ui/notes[Release notes]
+| Search UI
+| https://www.elastic.co/docs/current/search-ui/api/architecture[API reference]
+| https://www.elastic.co/docs/current/search-ui[Documentation]
+| https://www.elastic.co/docs/current/search-ui[Search UI]
-// |===
+|===
include::search-connection-details.asciidoc[]
include::playground/index.asciidoc[]
From c9200332ffe13e1df7225f023fa493f415ab429f Mon Sep 17 00:00:00 2001
From: Bharat Pasupula <123897612+bhapas@users.noreply.github.com>
Date: Thu, 10 Oct 2024 11:31:30 +0200
Subject: [PATCH 37/87] [Automatic Import] Add Cypress tests for Automatic
Import UI flow (#194948)
## Summary
Adds Cypress functional UI tests for different flows in Automatic
Import.
- Relates [#192684](https://github.com/elastic/kibana/issues/192684)
### RBAC tests
#### Create Integration Landing Page
- Fleet `read` Integrations `all` -- No access
- Fleet `read` Integrations `read` -- No access
- Fleet `read` Integrations `read` -- No access
- Fleet `all` Integrations `all` -- Access
#### Create Integration Assistant Page
- Fleet/integrations `all` Actions `read` [ `show` `execute` ] --
Execute with existing connectors
- Fleet/integrations `all` Actions `all` [ `show` `execute` `save`
`delete` ] -- Create new connector / execute existing ones.
### Create Integration UI Flow - NDJSON example
- Create an integration using Automatic Import with NDJSON samples
https://github.com/user-attachments/assets/9ab4cfc2-f058-4491-a280-6b86bcc5c9ce
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../e2e/integrations_automatic_import.cy.ts | 115 ++++
...ileges_integrations_automatic_import.cy.ts | 159 ++++++
.../fleet/cypress/fixtures/teleport.ndjson | 1 +
.../screens/integrations_automatic_import.ts | 35 ++
.../cypress/tasks/api_calls/connectors.ts | 88 +++
.../cypress/tasks/api_calls/graph_results.ts | 531 ++++++++++++++++++
.../plugins/fleet/cypress/tasks/privileges.ts | 113 +++-
x-pack/plugins/fleet/cypress/tsconfig.json | 1 +
.../missing_privileges_description.tsx | 2 +-
.../success_section/success_section.tsx | 14 +-
.../steps/connector_step/connector_setup.tsx | 5 +-
.../create_integration_landing.tsx | 5 +-
.../integration_assistant_card.tsx | 5 +-
.../integration_builder/readme_files.ts | 15 +-
.../server/templates/build_readme.md.njk | 2 +-
.../{readme.njk => description_readme.njk} | 0
.../server/templates/package_readme.md.njk | 2 +-
17 files changed, 1081 insertions(+), 12 deletions(-)
create mode 100644 x-pack/plugins/fleet/cypress/e2e/integrations_automatic_import.cy.ts
create mode 100644 x-pack/plugins/fleet/cypress/e2e/privileges_integrations_automatic_import.cy.ts
create mode 100644 x-pack/plugins/fleet/cypress/fixtures/teleport.ndjson
create mode 100644 x-pack/plugins/fleet/cypress/screens/integrations_automatic_import.ts
create mode 100644 x-pack/plugins/fleet/cypress/tasks/api_calls/connectors.ts
create mode 100644 x-pack/plugins/fleet/cypress/tasks/api_calls/graph_results.ts
rename x-pack/plugins/integration_assistant/server/templates/{readme.njk => description_readme.njk} (100%)
diff --git a/x-pack/plugins/fleet/cypress/e2e/integrations_automatic_import.cy.ts b/x-pack/plugins/fleet/cypress/e2e/integrations_automatic_import.cy.ts
new file mode 100644
index 0000000000000..e2454cb1dcf77
--- /dev/null
+++ b/x-pack/plugins/fleet/cypress/e2e/integrations_automatic_import.cy.ts
@@ -0,0 +1,115 @@
+/*
+ * 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 { deleteIntegrations } from '../tasks/integrations';
+import {
+ UPLOAD_PACKAGE_LINK,
+ ASSISTANT_BUTTON,
+ TECH_PREVIEW_BADGE,
+ CREATE_INTEGRATION_LANDING_PAGE,
+ BUTTON_FOOTER_NEXT,
+ INTEGRATION_TITLE_INPUT,
+ INTEGRATION_DESCRIPTION_INPUT,
+ DATASTREAM_TITLE_INPUT,
+ DATASTREAM_DESCRIPTION_INPUT,
+ DATASTREAM_NAME_INPUT,
+ DATA_COLLECTION_METHOD_INPUT,
+ LOGS_SAMPLE_FILE_PICKER,
+ EDIT_PIPELINE_BUTTON,
+ SAVE_PIPELINE_BUTTON,
+ VIEW_INTEGRATION_BUTTON,
+ INTEGRATION_SUCCESS_SECTION,
+ SAVE_ZIP_BUTTON,
+} from '../screens/integrations_automatic_import';
+import { cleanupAgentPolicies } from '../tasks/cleanup';
+import { login, logout } from '../tasks/login';
+import { createBedrockConnector, deleteConnectors } from '../tasks/api_calls/connectors';
+import {
+ ecsResultsForJson,
+ categorizationResultsForJson,
+ relatedResultsForJson,
+} from '../tasks/api_calls/graph_results';
+
+describe('Add Integration - Automatic Import', () => {
+ beforeEach(() => {
+ login();
+
+ cleanupAgentPolicies();
+ deleteIntegrations();
+
+ // Create a mock connector
+ deleteConnectors();
+ createBedrockConnector();
+ // Mock API Responses
+ cy.intercept('POST', '/api/integration_assistant/ecs', {
+ statusCode: 200,
+ body: {
+ results: ecsResultsForJson,
+ },
+ });
+ cy.intercept('POST', '/api/integration_assistant/categorization', {
+ statusCode: 200,
+ body: {
+ results: categorizationResultsForJson,
+ },
+ });
+ cy.intercept('POST', '/api/integration_assistant/related', {
+ statusCode: 200,
+ body: {
+ results: relatedResultsForJson,
+ },
+ });
+ });
+
+ afterEach(() => {
+ deleteConnectors();
+ cleanupAgentPolicies();
+ deleteIntegrations();
+ logout();
+ });
+
+ it('should create an integration', () => {
+ cy.visit(CREATE_INTEGRATION_LANDING_PAGE);
+
+ cy.getBySel(ASSISTANT_BUTTON).should('exist');
+ cy.getBySel(UPLOAD_PACKAGE_LINK).should('exist');
+ cy.getBySel(TECH_PREVIEW_BADGE).should('exist');
+
+ // Create Integration Assistant Page
+ cy.getBySel(ASSISTANT_BUTTON).click();
+ cy.getBySel(BUTTON_FOOTER_NEXT).click();
+
+ // Integration details Page
+ cy.getBySel(INTEGRATION_TITLE_INPUT).type('Test Integration');
+ cy.getBySel(INTEGRATION_DESCRIPTION_INPUT).type('Test Integration Description');
+ cy.getBySel(BUTTON_FOOTER_NEXT).click();
+
+ // Datastream details page
+ cy.getBySel(DATASTREAM_TITLE_INPUT).type('Audit');
+ cy.getBySel(DATASTREAM_DESCRIPTION_INPUT).type('Test Datastream Description');
+ cy.getBySel(DATASTREAM_NAME_INPUT).type('audit');
+ cy.getBySel(DATA_COLLECTION_METHOD_INPUT).type('file stream');
+ cy.get('body').click(0, 0);
+
+ // Select sample logs file and Analyze logs
+ cy.fixture('teleport.ndjson', null).as('myFixture');
+ cy.getBySel(LOGS_SAMPLE_FILE_PICKER).selectFile('@myFixture');
+ cy.getBySel(BUTTON_FOOTER_NEXT).click();
+
+ // Edit Pipeline
+ cy.getBySel(EDIT_PIPELINE_BUTTON).click();
+ cy.getBySel(SAVE_PIPELINE_BUTTON).click();
+
+ // Deploy
+ cy.getBySel(BUTTON_FOOTER_NEXT).click();
+ cy.getBySel(INTEGRATION_SUCCESS_SECTION).should('exist');
+ cy.getBySel(SAVE_ZIP_BUTTON).should('exist');
+
+ // View Integration
+ cy.getBySel(VIEW_INTEGRATION_BUTTON).click();
+ });
+});
diff --git a/x-pack/plugins/fleet/cypress/e2e/privileges_integrations_automatic_import.cy.ts b/x-pack/plugins/fleet/cypress/e2e/privileges_integrations_automatic_import.cy.ts
new file mode 100644
index 0000000000000..29eaab7eaca0a
--- /dev/null
+++ b/x-pack/plugins/fleet/cypress/e2e/privileges_integrations_automatic_import.cy.ts
@@ -0,0 +1,159 @@
+/*
+ * 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 { User } from '../tasks/privileges';
+import {
+ deleteUsersAndRoles,
+ getIntegrationsAutoImportRole,
+ createUsersAndRoles,
+ AutomaticImportConnectorNoneUser,
+ AutomaticImportConnectorNoneRole,
+ AutomaticImportConnectorAllUser,
+ AutomaticImportConnectorAllRole,
+ AutomaticImportConnectorReadUser,
+ AutomaticImportConnectorReadRole,
+} from '../tasks/privileges';
+import { login, loginWithUserAndWaitForPage, logout } from '../tasks/login';
+import {
+ ASSISTANT_BUTTON,
+ CONNECTOR_BEDROCK,
+ CONNECTOR_GEMINI,
+ CONNECTOR_OPENAI,
+ CREATE_INTEGRATION_ASSISTANT,
+ CREATE_INTEGRATION_LANDING_PAGE,
+ CREATE_INTEGRATION_UPLOAD,
+ MISSING_PRIVILEGES,
+ UPLOAD_PACKAGE_LINK,
+} from '../screens/integrations_automatic_import';
+
+describe('When the user does not have enough previleges for Integrations', () => {
+ const runs = [
+ { fleetRole: 'read', integrationsRole: 'read' },
+ { fleetRole: 'read', integrationsRole: 'all' },
+ { fleetRole: 'all', integrationsRole: 'read' },
+ ];
+
+ runs.forEach(function (run) {
+ describe(`When the user has '${run.fleetRole}' role for fleet and '${run.integrationsRole}' role for Integrations`, () => {
+ const automaticImportIntegrRole = getIntegrationsAutoImportRole({
+ fleetv2: [run.fleetRole], // fleet
+ fleet: [run.integrationsRole], // integrations
+ });
+ const AutomaticImportIntegrUser: User = {
+ username: 'automatic_import_integrations_read_user',
+ password: 'password',
+ roles: [automaticImportIntegrRole.name],
+ };
+
+ before(() => {
+ createUsersAndRoles([AutomaticImportIntegrUser], [automaticImportIntegrRole]);
+ });
+
+ beforeEach(() => {
+ login();
+ });
+
+ afterEach(() => {
+ logout();
+ });
+
+ after(() => {
+ deleteUsersAndRoles([AutomaticImportIntegrUser], [automaticImportIntegrRole]);
+ });
+
+ it('Create Assistant is not accessible if user has read role in integrations', () => {
+ loginWithUserAndWaitForPage(CREATE_INTEGRATION_ASSISTANT, AutomaticImportIntegrUser);
+ cy.getBySel(MISSING_PRIVILEGES).should('exist');
+ });
+
+ it('Create upload is not accessible if user has read role in integrations', () => {
+ loginWithUserAndWaitForPage(CREATE_INTEGRATION_UPLOAD, AutomaticImportIntegrUser);
+ cy.getBySel(MISSING_PRIVILEGES).should('exist');
+ });
+ });
+ });
+});
+
+describe('When the user has All permissions for Integrations and No permissions for actions', () => {
+ before(() => {
+ createUsersAndRoles([AutomaticImportConnectorNoneUser], [AutomaticImportConnectorNoneRole]);
+ });
+
+ beforeEach(() => {
+ login();
+ });
+
+ afterEach(() => {
+ logout();
+ });
+
+ after(() => {
+ deleteUsersAndRoles([AutomaticImportConnectorNoneUser], [AutomaticImportConnectorNoneRole]);
+ });
+
+ it('Create Assistant is not accessible but upload is accessible', () => {
+ loginWithUserAndWaitForPage(CREATE_INTEGRATION_LANDING_PAGE, AutomaticImportConnectorNoneUser);
+ cy.getBySel(ASSISTANT_BUTTON).should('not.exist');
+ cy.getBySel(UPLOAD_PACKAGE_LINK).should('exist');
+ });
+});
+
+describe('When the user has All permissions for Integrations and read permissions for actions', () => {
+ before(() => {
+ createUsersAndRoles([AutomaticImportConnectorReadUser], [AutomaticImportConnectorReadRole]);
+ });
+
+ beforeEach(() => {
+ login();
+ });
+
+ afterEach(() => {
+ logout();
+ });
+
+ after(() => {
+ deleteUsersAndRoles([AutomaticImportConnectorReadUser], [AutomaticImportConnectorReadRole]);
+ });
+
+ it('Create Assistant is not accessible but upload is accessible', () => {
+ loginWithUserAndWaitForPage(CREATE_INTEGRATION_LANDING_PAGE, AutomaticImportConnectorReadUser);
+ cy.getBySel(ASSISTANT_BUTTON).should('exist');
+ cy.getBySel(UPLOAD_PACKAGE_LINK).should('exist');
+ });
+
+ it('Create Assistant is accessible but execute connector is not accessible', () => {
+ loginWithUserAndWaitForPage(CREATE_INTEGRATION_ASSISTANT, AutomaticImportConnectorReadUser);
+ cy.getBySel(CONNECTOR_BEDROCK).should('not.exist');
+ cy.getBySel(CONNECTOR_OPENAI).should('not.exist');
+ cy.getBySel(CONNECTOR_GEMINI).should('not.exist');
+ });
+});
+
+describe('When the user has All permissions for Integrations and All permissions for actions', () => {
+ before(() => {
+ createUsersAndRoles([AutomaticImportConnectorAllUser], [AutomaticImportConnectorAllRole]);
+ });
+
+ beforeEach(() => {
+ login();
+ });
+
+ afterEach(() => {
+ logout();
+ });
+
+ after(() => {
+ deleteUsersAndRoles([AutomaticImportConnectorAllUser], [AutomaticImportConnectorAllRole]);
+ });
+
+ it('Create Assistant is not accessible but upload is accessible', () => {
+ loginWithUserAndWaitForPage(CREATE_INTEGRATION_ASSISTANT, AutomaticImportConnectorAllUser);
+ cy.getBySel(CONNECTOR_BEDROCK).should('exist');
+ cy.getBySel(CONNECTOR_OPENAI).should('exist');
+ cy.getBySel(CONNECTOR_GEMINI).should('exist');
+ });
+});
diff --git a/x-pack/plugins/fleet/cypress/fixtures/teleport.ndjson b/x-pack/plugins/fleet/cypress/fixtures/teleport.ndjson
new file mode 100644
index 0000000000000..82774ac2297d6
--- /dev/null
+++ b/x-pack/plugins/fleet/cypress/fixtures/teleport.ndjson
@@ -0,0 +1 @@
+{"ei":0,"event":"cert.create","uid":"efd326fc-dd13-4df8-acef-3102c2d717d3","code":"TC000I","time":"2024-02-24T06:56:50.648137154Z"}
\ No newline at end of file
diff --git a/x-pack/plugins/fleet/cypress/screens/integrations_automatic_import.ts b/x-pack/plugins/fleet/cypress/screens/integrations_automatic_import.ts
new file mode 100644
index 0000000000000..e549f88294a3b
--- /dev/null
+++ b/x-pack/plugins/fleet/cypress/screens/integrations_automatic_import.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const UPLOAD_PACKAGE_LINK = 'uploadPackageLink';
+export const ASSISTANT_BUTTON = 'assistantButton';
+export const TECH_PREVIEW_BADGE = 'techPreviewBadge';
+export const MISSING_PRIVILEGES = 'missingPrivilegesCallOut';
+
+export const CONNECTOR_BEDROCK = 'actionType-.bedrock';
+export const CONNECTOR_OPENAI = 'actionType-.gen-ai';
+export const CONNECTOR_GEMINI = 'actionType-.gemini';
+
+export const BUTTON_FOOTER_NEXT = 'buttonsFooter-nextButton';
+
+export const INTEGRATION_TITLE_INPUT = 'integrationTitleInput';
+export const INTEGRATION_DESCRIPTION_INPUT = 'integrationDescriptionInput';
+export const DATASTREAM_TITLE_INPUT = 'dataStreamTitleInput';
+export const DATASTREAM_DESCRIPTION_INPUT = 'dataStreamDescriptionInput';
+export const DATASTREAM_NAME_INPUT = 'dataStreamNameInput';
+export const DATA_COLLECTION_METHOD_INPUT = 'dataCollectionMethodInput';
+export const LOGS_SAMPLE_FILE_PICKER = 'logsSampleFilePicker';
+
+export const EDIT_PIPELINE_BUTTON = 'editPipelineButton';
+export const SAVE_PIPELINE_BUTTON = 'savePipelineButton';
+export const VIEW_INTEGRATION_BUTTON = 'viewIntegrationButton';
+export const INTEGRATION_SUCCESS_SECTION = 'integrationSuccessSection';
+export const SAVE_ZIP_BUTTON = 'saveZipButton';
+
+export const CREATE_INTEGRATION_LANDING_PAGE = '/app/integrations/create';
+export const CREATE_INTEGRATION_ASSISTANT = '/app/integrations/create/assistant';
+export const CREATE_INTEGRATION_UPLOAD = '/app/integrations/create/upload';
diff --git a/x-pack/plugins/fleet/cypress/tasks/api_calls/connectors.ts b/x-pack/plugins/fleet/cypress/tasks/api_calls/connectors.ts
new file mode 100644
index 0000000000000..230fdcd124562
--- /dev/null
+++ b/x-pack/plugins/fleet/cypress/tasks/api_calls/connectors.ts
@@ -0,0 +1,88 @@
+/*
+ * 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 { AllConnectorsResponse } from '@kbn/actions-plugin/common/routes/connector/response';
+
+import { v4 as uuidv4 } from 'uuid';
+
+import { API_AUTH, COMMON_API_HEADERS } from '../common';
+
+export const bedrockId = uuidv4();
+export const azureId = uuidv4();
+
+// Replaces request - adds baseline authentication + global headers
+export const request = ({
+ headers,
+ ...options
+}: Partial): Cypress.Chainable> => {
+ return cy.request({
+ auth: API_AUTH,
+ headers: { ...COMMON_API_HEADERS, ...headers },
+ ...options,
+ });
+};
+export const INTERNAL_CLOUD_CONNECTORS = ['Elastic-Cloud-SMTP'];
+
+export const getConnectors = () =>
+ request({
+ method: 'GET',
+ url: 'api/actions/connectors',
+ });
+
+export const createConnector = (connector: Record, id: string) =>
+ cy.request({
+ method: 'POST',
+ url: `/api/actions/connector/${id}`,
+ body: connector,
+ headers: { 'kbn-xsrf': 'cypress-creds', 'x-elastic-internal-origin': 'security-solution' },
+ });
+
+export const deleteConnectors = () => {
+ getConnectors().then(($response) => {
+ if ($response.body.length > 0) {
+ const ids = $response.body.map((connector) => {
+ return connector.id;
+ });
+ ids.forEach((id) => {
+ if (!INTERNAL_CLOUD_CONNECTORS.includes(id)) {
+ request({
+ method: 'DELETE',
+ url: `api/actions/connector/${id}`,
+ });
+ }
+ });
+ }
+ });
+};
+
+export const azureConnectorAPIPayload = {
+ connector_type_id: '.gen-ai',
+ secrets: {
+ apiKey: '123',
+ },
+ config: {
+ apiUrl:
+ 'https://goodurl.com/openai/deployments/good-gpt4o/chat/completions?api-version=2024-02-15-preview',
+ apiProvider: 'Azure OpenAI',
+ },
+ name: 'Azure OpenAI cypress test e2e connector',
+};
+
+export const bedrockConnectorAPIPayload = {
+ connector_type_id: '.bedrock',
+ secrets: {
+ accessKey: '123',
+ secret: '123',
+ },
+ config: {
+ apiUrl: 'https://bedrock.com',
+ },
+ name: 'Bedrock cypress test e2e connector',
+};
+
+export const createAzureConnector = () => createConnector(azureConnectorAPIPayload, azureId);
+export const createBedrockConnector = () => createConnector(bedrockConnectorAPIPayload, bedrockId);
diff --git a/x-pack/plugins/fleet/cypress/tasks/api_calls/graph_results.ts b/x-pack/plugins/fleet/cypress/tasks/api_calls/graph_results.ts
new file mode 100644
index 0000000000000..3276b6ecf055f
--- /dev/null
+++ b/x-pack/plugins/fleet/cypress/tasks/api_calls/graph_results.ts
@@ -0,0 +1,531 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const ecsResultsForJson = {
+ mapping: {
+ teleport2: {
+ audit: {
+ ei: null,
+ event: {
+ target: 'event.action',
+ confidence: 0.9,
+ type: 'string',
+ date_formats: [],
+ },
+ uid: {
+ target: 'event.id',
+ confidence: 0.95,
+ type: 'string',
+ date_formats: [],
+ },
+ code: {
+ target: 'event.code',
+ confidence: 0.9,
+ type: 'string',
+ date_formats: [],
+ },
+ },
+ },
+ },
+ pipeline: {
+ description: 'Pipeline to process teleport2 audit logs',
+ processors: [
+ {
+ set: {
+ field: 'ecs.version',
+ tag: 'set_ecs_version',
+ value: '8.11.0',
+ },
+ },
+ {
+ remove: {
+ field: 'message',
+ ignore_missing: true,
+ tag: 'remove_message',
+ },
+ },
+ {
+ json: {
+ field: 'event.original',
+ tag: 'json_original',
+ target_field: 'teleport2.audit',
+ },
+ },
+ {
+ rename: {
+ field: 'teleport2.audit.event',
+ target_field: 'event.action',
+ ignore_missing: true,
+ },
+ },
+ {
+ script: {
+ description: 'Ensures the date processor does not receive an array value.',
+ tag: 'script_convert_array_to_string',
+ lang: 'painless',
+ source:
+ 'if (ctx.teleport2?.audit?.time != null &&\n ctx.teleport2.audit.time instanceof ArrayList){\n ctx.teleport2.audit.time = ctx.teleport2.audit.time[0];\n}\n',
+ },
+ },
+ {
+ date: {
+ field: 'teleport2.audit.time',
+ target_field: 'event.start',
+ formats: ["yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'", 'ISO8601'],
+ tag: 'date_processor_teleport2.audit.time',
+ if: 'ctx.teleport2?.audit?.time != null',
+ },
+ },
+ {
+ set: {
+ field: 'event.kind',
+ value: 'pipeline_error',
+ },
+ },
+ ],
+ },
+};
+
+export const categorizationResultsForJson = {
+ docs: [
+ {
+ ecs: {
+ version: '8.11.0',
+ },
+ teleport2: {
+ audit: {
+ cert_type: 'user',
+ time: '2024-02-24T06:56:50.648137154Z',
+ ei: 0,
+ identity: {
+ expires: '2024-02-24T06:56:50.648137154Z',
+ traits: {
+ logins: ['root', 'ubuntu', 'ec2-user'],
+ },
+ private_key_policy: 'none',
+ teleport_cluster: 'teleport.com',
+ prev_identity_expires: '0001-01-01T00:00:00Z',
+ route_to_cluster: 'teleport.com',
+ logins: ['root', 'ubuntu', 'ec2-user', '-teleport-internal-join'],
+ },
+ },
+ },
+ organization: {
+ name: 'teleport.com',
+ },
+ source: {
+ ip: '1.2.3.4',
+ },
+ event: {
+ code: 'TC000I',
+ start: '2024-02-24T06:56:50.648Z',
+ action: 'cert.create',
+ end: '0001-01-01T00:00:00.000Z',
+ id: 'efd326fc-dd13-4df8-acef-3102c2d717d3',
+ category: ['iam', 'authentication'],
+ type: ['creation', 'start'],
+ },
+ user: {
+ name: 'teleport-admin',
+ changes: {
+ name: '2024-02-24T06:56:50.648Z',
+ },
+ roles: ['access', 'editor'],
+ },
+ tags: [
+ '_geoip_database_unavailable_GeoLite2-City.mmdb',
+ '_geoip_database_unavailable_GeoLite2-ASN.mmdb',
+ '_geoip_database_unavailable_GeoLite2-City.mmdb',
+ '_geoip_database_unavailable_GeoLite2-ASN.mmdb',
+ ],
+ },
+ ],
+ pipeline: {
+ description: 'Pipeline to process teleport2 audit logs',
+ processors: [
+ {
+ set: {
+ field: 'ecs.version',
+ tag: 'set_ecs_version',
+ value: '8.11.0',
+ },
+ },
+ {
+ remove: {
+ field: 'message',
+ ignore_missing: true,
+ tag: 'remove_message',
+ },
+ },
+ {
+ json: {
+ field: 'event.original',
+ tag: 'json_original',
+ target_field: 'teleport2.audit',
+ },
+ },
+ {
+ rename: {
+ field: 'teleport2.audit.event',
+ target_field: 'event.action',
+ ignore_missing: true,
+ },
+ },
+ {
+ script: {
+ description: 'Ensures the date processor does not receive an array value.',
+ tag: 'script_convert_array_to_string',
+ lang: 'painless',
+ source:
+ 'if (ctx.teleport2?.audit?.time != null &&\n ctx.teleport2.audit.time instanceof ArrayList){\n ctx.teleport2.audit.time = ctx.teleport2.audit.time[0];\n}\n',
+ },
+ },
+ {
+ date: {
+ field: 'teleport2.audit.time',
+ target_field: 'event.start',
+ formats: ["yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'", 'ISO8601'],
+ tag: 'date_processor_teleport2.audit.time',
+ if: 'ctx.teleport2?.audit?.time != null',
+ },
+ },
+ {
+ set: {
+ field: 'event.kind',
+ value: 'pipeline_error',
+ },
+ },
+ ],
+ },
+};
+
+export const relatedResultsForJson = {
+ docs: [
+ {
+ ecs: {
+ version: '8.11.0',
+ },
+ related: {
+ user: ['teleport-admin'],
+ ip: ['1.2.3.4'],
+ },
+ teleport2: {
+ audit: {
+ cert_type: 'user',
+ time: '2024-02-24T06:56:50.648137154Z',
+ ei: 0,
+ identity: {
+ expires: '2024-02-24T06:56:50.648137154Z',
+ traits: {
+ logins: ['root', 'ubuntu', 'ec2-user'],
+ },
+ private_key_policy: 'none',
+ teleport_cluster: 'teleport.com',
+ prev_identity_expires: '0001-01-01T00:00:00Z',
+ route_to_cluster: 'teleport.com',
+ logins: ['root', 'ubuntu', 'ec2-user', '-teleport-internal-join'],
+ },
+ },
+ },
+ organization: {
+ name: 'teleport.com',
+ },
+ source: {
+ ip: '1.2.3.4',
+ },
+ event: {
+ code: 'TC000I',
+ start: '2024-02-24T06:56:50.648Z',
+ action: 'cert.create',
+ end: '0001-01-01T00:00:00.000Z',
+ id: 'efd326fc-dd13-4df8-acef-3102c2d717d3',
+ category: ['iam', 'authentication'],
+ type: ['creation', 'start'],
+ },
+ user: {
+ name: 'teleport-admin',
+ changes: {
+ name: '2024-02-24T06:56:50.648Z',
+ },
+ roles: ['access', 'editor'],
+ },
+ tags: [
+ '_geoip_database_unavailable_GeoLite2-City.mmdb',
+ '_geoip_database_unavailable_GeoLite2-ASN.mmdb',
+ '_geoip_database_unavailable_GeoLite2-City.mmdb',
+ '_geoip_database_unavailable_GeoLite2-ASN.mmdb',
+ ],
+ },
+ ],
+ pipeline: {
+ description: 'Pipeline to process teleport2 audit logs',
+ processors: [
+ {
+ set: {
+ tag: 'set_ecs_version',
+ field: 'ecs.version',
+ value: '8.11.0',
+ },
+ },
+ {
+ set: {
+ tag: 'copy_original_message',
+ field: 'originalMessage',
+ copy_from: 'message',
+ },
+ },
+ {
+ rename: {
+ ignore_missing: true,
+ if: 'ctx.event?.original == null',
+ tag: 'rename_message',
+ field: 'originalMessage',
+ target_field: 'event.original',
+ },
+ },
+ {
+ rename: {
+ ignore_missing: true,
+ field: 'teleport2.audit.user',
+ target_field: 'user.name',
+ },
+ },
+ {
+ rename: {
+ ignore_missing: true,
+ field: 'teleport2.audit.login',
+ target_field: 'user.id',
+ },
+ },
+ {
+ rename: {
+ ignore_missing: true,
+ field: 'teleport2.audit.server_hostname',
+ target_field: 'destination.domain',
+ },
+ },
+ {
+ rename: {
+ ignore_missing: true,
+ field: 'teleport2.audit.addr.remote',
+ target_field: 'source.address',
+ },
+ },
+ {
+ rename: {
+ ignore_missing: true,
+ field: 'teleport2.audit.proto',
+ target_field: 'network.protocol',
+ },
+ },
+ {
+ script: {
+ tag: 'script_drop_null_empty_values',
+ description: 'Drops null/empty values recursively.',
+ lang: 'painless',
+ source:
+ 'boolean dropEmptyFields(Object object) {\n if (object == null || object == "") {\n return true;\n } else if (object instanceof Map) {\n ((Map) object).values().removeIf(value -> dropEmptyFields(value));\n return (((Map) object).size() == 0);\n } else if (object instanceof List) {\n ((List) object).removeIf(value -> dropEmptyFields(value));\n return (((List) object).length == 0);\n }\n return false;\n}\ndropEmptyFields(ctx);\n',
+ },
+ },
+ {
+ geoip: {
+ ignore_missing: true,
+ tag: 'geoip_source_ip',
+ field: 'source.ip',
+ target_field: 'source.geo',
+ },
+ },
+ {
+ geoip: {
+ ignore_missing: true,
+ tag: 'geoip_source_asn',
+ database_file: 'GeoLite2-ASN.mmdb',
+ field: 'source.ip',
+ target_field: 'source.as',
+ properties: ['asn', 'organization_name'],
+ },
+ },
+ {
+ rename: {
+ ignore_missing: true,
+ tag: 'rename_source_as_asn',
+ field: 'source.as.asn',
+ target_field: 'source.as.number',
+ },
+ },
+ {
+ rename: {
+ ignore_missing: true,
+ tag: 'rename_source_as_organization_name',
+ field: 'source.as.organization_name',
+ target_field: 'source.as.organization.name',
+ },
+ },
+ {
+ geoip: {
+ ignore_missing: true,
+ tag: 'geoip_destination_ip',
+ field: 'destination.ip',
+ target_field: 'destination.geo',
+ },
+ },
+ {
+ geoip: {
+ ignore_missing: true,
+ tag: 'geoip_destination_asn',
+ database_file: 'GeoLite2-ASN.mmdb',
+ field: 'destination.ip',
+ target_field: 'destination.as',
+ properties: ['asn', 'organization_name'],
+ },
+ },
+ {
+ rename: {
+ ignore_missing: true,
+ tag: 'rename_destination_as_asn',
+ field: 'destination.as.asn',
+ target_field: 'destination.as.number',
+ },
+ },
+ {
+ rename: {
+ ignore_missing: true,
+ tag: 'rename_destination_as_organization_name',
+ field: 'destination.as.organization_name',
+ target_field: 'destination.as.organization.name',
+ },
+ },
+ {
+ append: {
+ if: "ctx.event?.action == 'cert.create'",
+ field: 'event.category',
+ value: ['iam'],
+ allow_duplicates: false,
+ },
+ },
+ {
+ append: {
+ if: "ctx.event?.action == 'cert.create'",
+ field: 'event.type',
+ value: ['creation'],
+ allow_duplicates: false,
+ },
+ },
+ {
+ append: {
+ if: "ctx.event?.action == 'cert.create'",
+ field: 'event.category',
+ value: ['authentication'],
+ allow_duplicates: false,
+ },
+ },
+ {
+ append: {
+ if: "ctx.event?.action == 'cert.create'",
+ field: 'event.type',
+ value: ['start'],
+ allow_duplicates: false,
+ },
+ },
+ {
+ append: {
+ if: "ctx.event?.action == 'session.start'",
+ field: 'event.category',
+ value: ['session'],
+ allow_duplicates: false,
+ },
+ },
+ {
+ append: {
+ if: "ctx.event?.action == 'session.start'",
+ field: 'event.type',
+ value: ['start'],
+ allow_duplicates: false,
+ },
+ },
+ {
+ append: {
+ if: "ctx.network?.protocol == 'ssh'",
+ field: 'event.category',
+ value: ['network'],
+ allow_duplicates: false,
+ },
+ },
+ {
+ append: {
+ if: "ctx.network?.protocol == 'ssh'",
+ field: 'event.type',
+ value: ['connection', 'start'],
+ allow_duplicates: false,
+ },
+ },
+ {
+ append: {
+ field: 'related.ip',
+ value: '{{{source.ip}}}',
+ if: 'ctx.source?.ip != null',
+ allow_duplicates: false,
+ },
+ },
+ {
+ append: {
+ field: 'related.user',
+ value: '{{{user.name}}}',
+ if: 'ctx.user?.name != null',
+ allow_duplicates: false,
+ },
+ },
+ {
+ append: {
+ field: 'related.hosts',
+ value: '{{{destination.domain}}}',
+ if: 'ctx.destination?.domain != null',
+ allow_duplicates: false,
+ },
+ },
+ {
+ append: {
+ field: 'related.user',
+ value: '{{{user.id}}}',
+ if: 'ctx.user?.id != null',
+ allow_duplicates: false,
+ },
+ },
+ {
+ remove: {
+ ignore_missing: true,
+ tag: 'remove_fields',
+ field: ['teleport2.audit.identity.client_ip'],
+ },
+ },
+ {
+ remove: {
+ ignore_failure: true,
+ ignore_missing: true,
+ if: 'ctx?.tags == null || !(ctx.tags.contains("preserve_original_event"))',
+ tag: 'remove_original_event',
+ field: 'event.original',
+ },
+ },
+ ],
+ on_failure: [
+ {
+ append: {
+ field: 'error.message',
+ value:
+ 'Processor {{{_ingest.on_failure_processor_type}}} with tag {{{_ingest.on_failure_processor_tag}}} in pipeline {{{_ingest.on_failure_pipeline}}} failed with message: {{{_ingest.on_failure_message}}}',
+ },
+ },
+ {
+ set: {
+ field: 'event.kind',
+ value: 'pipeline_error',
+ },
+ },
+ ],
+ },
+};
diff --git a/x-pack/plugins/fleet/cypress/tasks/privileges.ts b/x-pack/plugins/fleet/cypress/tasks/privileges.ts
index 214bd0f14e6e6..876b88ac9d5b5 100644
--- a/x-pack/plugins/fleet/cypress/tasks/privileges.ts
+++ b/x-pack/plugins/fleet/cypress/tasks/privileges.ts
@@ -8,7 +8,7 @@
import { request } from './common';
import { constructUrlWithUser, getEnvAuth } from './login';
-interface User {
+export interface User {
username: string;
password: string;
description?: string;
@@ -193,6 +193,117 @@ export const FleetNoneIntegrAllUser: User = {
roles: [FleetNoneIntegrAllRole.name],
};
+export const getIntegrationsAutoImportRole = (feature: FeaturesPrivileges): Role => ({
+ name: 'automatic_import_integrations_read_role',
+ privileges: {
+ elasticsearch: {
+ indices: [
+ {
+ names: ['*'],
+ privileges: ['all'],
+ },
+ ],
+ cluster: ['manage_service_account'],
+ },
+ kibana: [
+ {
+ feature,
+ spaces: ['*'],
+ },
+ ],
+ },
+});
+
+export const AutomaticImportConnectorNoneRole: Role = {
+ name: 'automatic_import_connectors_none_role',
+ privileges: {
+ elasticsearch: {
+ indices: [
+ {
+ names: ['*'],
+ privileges: ['all'],
+ },
+ ],
+ cluster: ['manage_service_account'],
+ },
+ kibana: [
+ {
+ feature: {
+ fleetv2: ['all'],
+ fleet: ['all'],
+ actions: ['none'],
+ },
+ spaces: ['*'],
+ },
+ ],
+ },
+};
+export const AutomaticImportConnectorNoneUser: User = {
+ username: 'automatic_import_connectors_none_user',
+ password: 'password',
+ roles: [AutomaticImportConnectorNoneRole.name],
+};
+
+export const AutomaticImportConnectorReadRole: Role = {
+ name: 'automatic_import_connectors_read_role',
+ privileges: {
+ elasticsearch: {
+ indices: [
+ {
+ names: ['*'],
+ privileges: ['all'],
+ },
+ ],
+ cluster: ['manage_service_account'],
+ },
+ kibana: [
+ {
+ feature: {
+ fleetv2: ['all'],
+ fleet: ['all'],
+ actions: ['read'],
+ },
+ spaces: ['*'],
+ },
+ ],
+ },
+};
+export const AutomaticImportConnectorReadUser: User = {
+ username: 'automatic_import_connectors_read_user',
+ password: 'password',
+ roles: [AutomaticImportConnectorReadRole.name],
+};
+
+export const AutomaticImportConnectorAllRole: Role = {
+ name: 'automatic_import_connectors_all_role',
+ privileges: {
+ elasticsearch: {
+ indices: [
+ {
+ names: ['*'],
+ privileges: ['all'],
+ },
+ ],
+ cluster: ['manage_service_account'],
+ },
+ kibana: [
+ {
+ feature: {
+ fleetv2: ['all'],
+ fleet: ['all'],
+ actions: ['all'],
+ },
+ spaces: ['*'],
+ },
+ ],
+ },
+};
+export const AutomaticImportConnectorAllUser: User = {
+ username: 'automatic_import_connectors_all_user',
+ password: 'password',
+ roles: [AutomaticImportConnectorAllRole.name],
+};
+
export const BuiltInEditorUser: User = {
username: 'editor_user',
password: 'password',
diff --git a/x-pack/plugins/fleet/cypress/tsconfig.json b/x-pack/plugins/fleet/cypress/tsconfig.json
index ee3dd7cd1e246..6d1433482b1c2 100644
--- a/x-pack/plugins/fleet/cypress/tsconfig.json
+++ b/x-pack/plugins/fleet/cypress/tsconfig.json
@@ -29,5 +29,6 @@
"force": true
},
"@kbn/rison",
+ "@kbn/actions-plugin",
]
}
diff --git a/x-pack/plugins/integration_assistant/public/common/components/authorization/missing_privileges_description.tsx b/x-pack/plugins/integration_assistant/public/common/components/authorization/missing_privileges_description.tsx
index 15365aeb3a08e..ccc65a2e49f0e 100644
--- a/x-pack/plugins/integration_assistant/public/common/components/authorization/missing_privileges_description.tsx
+++ b/x-pack/plugins/integration_assistant/public/common/components/authorization/missing_privileges_description.tsx
@@ -13,7 +13,7 @@ type MissingPrivilegesDescriptionProps = Partial;
export const MissingPrivilegesDescription = React.memo(
({ canCreateIntegrations, canCreateConnectors, canExecuteConnectors }) => {
return (
-
+
{i18n.PRIVILEGES_REQUIRED_TITLE}
diff --git a/x-pack/plugins/integration_assistant/public/common/components/success_section/success_section.tsx b/x-pack/plugins/integration_assistant/public/common/components/success_section/success_section.tsx
index 62df4a8f98660..08da1329770cd 100644
--- a/x-pack/plugins/integration_assistant/public/common/components/success_section/success_section.tsx
+++ b/x-pack/plugins/integration_assistant/public/common/components/success_section/success_section.tsx
@@ -35,7 +35,13 @@ export const SuccessSection = React.memo(({ integrationName
return (
-
+
(({ integrationName
icon={ }
title={i18n.VIEW_INTEGRATION_TITLE}
description={i18n.VIEW_INTEGRATION_DESCRIPTION}
- footer={{i18n.VIEW_INTEGRATION_BUTTON} }
+ footer={
+
+ {i18n.VIEW_INTEGRATION_BUTTON}
+
+ }
/>
diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/connector_step/connector_setup.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/connector_step/connector_setup.tsx
index 8715f42eb8f58..e85481378f4dd 100644
--- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/connector_step/connector_setup.tsx
+++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/connector_step/connector_setup.tsx
@@ -104,10 +104,13 @@ export const ConnectorSetup = React.memo(
size="xl"
color="text"
type={actionTypeRegistry.get(actionType.id).iconClass}
+ data-test-subj="connectorActionId"
/>
- {actionType.name}
+
+ {actionType.name}
+
diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_landing/create_integration_landing.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_landing/create_integration_landing.tsx
index 39cbd2cea1026..71706625f636f 100644
--- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_landing/create_integration_landing.tsx
+++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_landing/create_integration_landing.tsx
@@ -54,7 +54,10 @@ export const CreateIntegrationLanding = React.memo(() => {
defaultMessage="If you have an existing integration package, {link}"
values={{
link: (
- navigate(Page.upload)}>
+ navigate(Page.upload)}
+ data-test-subj="uploadPackageLink"
+ >
{
tooltipContent={i18n.TECH_PREVIEW_TOOLTIP}
size="s"
color="hollow"
+ data-test-subj="techPreviewBadge"
/>
@@ -64,7 +65,9 @@ export const IntegrationAssistantCard = React.memo(() => {
{canExecuteConnectors ? (
- navigate(Page.assistant)}>{i18n.ASSISTANT_BUTTON}
+ navigate(Page.assistant)} data-test-subj="assistantButton">
+ {i18n.ASSISTANT_BUTTON}
+
) : (
{i18n.ASSISTANT_BUTTON}
diff --git a/x-pack/plugins/integration_assistant/server/integration_builder/readme_files.ts b/x-pack/plugins/integration_assistant/server/integration_builder/readme_files.ts
index 163b2b04b52f9..5467a1549cea2 100644
--- a/x-pack/plugins/integration_assistant/server/integration_builder/readme_files.ts
+++ b/x-pack/plugins/integration_assistant/server/integration_builder/readme_files.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import nunjucks from 'nunjucks';
+import { Environment, FileSystemLoader } from 'nunjucks';
import { join as joinPath } from 'path';
import { createSync, ensureDirSync } from '../util';
@@ -17,6 +17,8 @@ export function createReadme(packageDir: string, integrationName: string, fields
function createPackageReadme(packageDir: string, integrationName: string, fields: object[]) {
const dirPath = joinPath(packageDir, 'docs/');
+ // The readme nunjucks template files should be named in the format `somename_readme.md.njk` and not just `readme.md.njk`
+ // since any file with `readme.*` pattern is skipped in build process in buildkite.
createReadmeFile(dirPath, 'package_readme.md.njk', integrationName, fields);
}
@@ -33,10 +35,17 @@ function createReadmeFile(
) {
ensureDirSync(targetDir);
- const template = nunjucks.render(templateName, {
+ const templatesPath = joinPath(__dirname, '../templates');
+ const env = new Environment(new FileSystemLoader(templatesPath), {
+ autoescape: false,
+ });
+
+ const template = env.getTemplate(templateName);
+
+ const renderedTemplate = template.render({
package_name: integrationName,
fields,
});
- createSync(joinPath(targetDir, 'README.md'), template);
+ createSync(joinPath(targetDir, 'README.md'), renderedTemplate);
}
diff --git a/x-pack/plugins/integration_assistant/server/templates/build_readme.md.njk b/x-pack/plugins/integration_assistant/server/templates/build_readme.md.njk
index e23fa4af9efe8..1b58e55aebd37 100644
--- a/x-pack/plugins/integration_assistant/server/templates/build_readme.md.njk
+++ b/x-pack/plugins/integration_assistant/server/templates/build_readme.md.njk
@@ -1,4 +1,4 @@
-{% include "readme.njk" %}
+{% include "./description_readme.njk" %}
{% for data_stream in fields %}
### {{ data_stream.datastream }}
diff --git a/x-pack/plugins/integration_assistant/server/templates/readme.njk b/x-pack/plugins/integration_assistant/server/templates/description_readme.njk
similarity index 100%
rename from x-pack/plugins/integration_assistant/server/templates/readme.njk
rename to x-pack/plugins/integration_assistant/server/templates/description_readme.njk
diff --git a/x-pack/plugins/integration_assistant/server/templates/package_readme.md.njk b/x-pack/plugins/integration_assistant/server/templates/package_readme.md.njk
index b47e3491b5bc2..bd56aba5ac1e5 100644
--- a/x-pack/plugins/integration_assistant/server/templates/package_readme.md.njk
+++ b/x-pack/plugins/integration_assistant/server/templates/package_readme.md.njk
@@ -1,4 +1,4 @@
-{% include "readme.njk" %}
+{% include "./description_readme.njk" %}
{% for data_stream in fields %}
### {{ data_stream.datastream }}
From 4d54cfe2bc3d28d238bc3d56186692081a5d5a9c Mon Sep 17 00:00:00 2001
From: Maxim Palenov
Date: Thu, 10 Oct 2024 12:44:56 +0300
Subject: [PATCH 38/87] [Security Solution] Add upgrade prebuilt rule flyout
layout details (#195166)
**Addresses:** https://github.com/elastic/kibana/issues/171520
**Design:**
[Figma](https://www.figma.com/file/gLHm8LpTtSkAUQHrkG3RHU/%5B8.7%5D-%5BRules%5D-Rule-Immutability%2FCustomization?type=design&node-id=3903%3A88369&mode=design&t=rMjxtGjBNKbCjedE-1)
(internal)
## Summary
This PR extends prebuilt rule flyout layout with design details
including field state, rule state callout and little UI fixes.
## Screenshots
---
.../comparison_side/comparison_side.tsx | 21 ++++--
.../comparison_side_help_info.tsx | 43 +++++++++++
.../comparison_side/translations.ts | 7 ++
.../field_upgrade_conflicts_resolver.tsx | 10 ++-
...ield_upgrade_conflicts_resolver_header.tsx | 16 +++--
.../field_upgrade_state_info.tsx | 55 ++++++++++++++
.../field_upgrade_state_info/index.ts | 8 +++
.../field_upgrade_state_info/translations.tsx | 60 ++++++++++++++++
.../components/rule_upgrade_callout/index.ts | 8 +++
.../rule_upgrade_callout.tsx | 71 +++++++++++++++++++
.../rule_upgrade_callout/translations.tsx | 58 +++++++++++++++
.../rule_upgrade_conflicts_resolver.tsx | 3 +-
.../components/rule_upgrade_info_bar.tsx | 2 +-
.../components/translations.tsx | 30 ++++----
.../three_way_diff/final_side/final_side.tsx | 4 +-
.../three_way_diff/final_side/translations.ts | 6 +-
.../rule_upgrade_conflicts_resolver_tab.tsx | 5 +-
.../field_upgrade_state.ts | 12 ++++
.../fields_upgrade_state.ts | 10 +++
.../model/prebuilt_rule_upgrade/index.ts | 12 ++++
.../rule_upgrade_state.ts | 27 +++++++
.../rules_upgrade_state.ts | 11 +++
.../set_rule_field_resolved_value.ts | 16 +++++
.../upgrade_prebuilt_rules_table.tsx | 2 +-
.../upgrade_prebuilt_rules_table_buttons.tsx | 2 +-
.../upgrade_prebuilt_rules_table_context.tsx | 2 +-
.../use_prebuilt_rules_upgrade_state.ts | 60 ++++++++++------
...e_upgrade_prebuilt_rules_table_columns.tsx | 2 +-
28 files changed, 504 insertions(+), 59 deletions(-)
create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side_help_info.tsx
create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_state_info/field_upgrade_state_info.tsx
create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_state_info/index.ts
create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_state_info/translations.tsx
create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_callout/index.ts
create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_callout/rule_upgrade_callout.tsx
create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_callout/translations.tsx
create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/field_upgrade_state.ts
create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/fields_upgrade_state.ts
create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/index.ts
create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/rule_upgrade_state.ts
create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/rules_upgrade_state.ts
create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/set_rule_field_resolved_value.ts
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side.tsx
index 9ef207b0bb998..2592469beaabb 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side.tsx
@@ -6,6 +6,7 @@
*/
import React, { useState } from 'react';
+import { EuiFlexGroup, EuiTitle } from '@elastic/eui';
import { VersionsPicker } from '../versions_picker/versions_picker';
import type { Version } from '../versions_picker/constants';
import { SelectedVersions } from '../versions_picker/constants';
@@ -17,6 +18,8 @@ import type {
import { getSubfieldChanges } from './get_subfield_changes';
import { SubfieldChanges } from './subfield_changes';
import { SideHeader } from '../components/side_header';
+import { ComparisonSideHelpInfo } from './comparison_side_help_info';
+import * as i18n from './translations';
interface ComparisonSideProps {
fieldName: FieldName;
@@ -43,11 +46,19 @@ export function ComparisonSide({
return (
<>
-
+
+
+
+ {i18n.TITLE}
+
+
+
+
+
>
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side_help_info.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side_help_info.tsx
new file mode 100644
index 0000000000000..a2b7e1a360150
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side_help_info.tsx
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { useToggle } from 'react-use';
+import { EuiPopover, EuiText, EuiButtonIcon } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+
+/**
+ * Theme doesn't expose width variables. Using provided size variables will require
+ * multiplying it by another magic constant.
+ *
+ * 320px width looks
+ * like a [commonly used width in EUI](https://github.com/search?q=repo%3Aelastic%2Feui%20320&type=code).
+ */
+const POPOVER_WIDTH = 320;
+
+export function ComparisonSideHelpInfo(): JSX.Element {
+ const [isPopoverOpen, togglePopover] = useToggle(false);
+
+ const button = (
+
+ );
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/translations.ts
index d60c78646b5ad..8208892ac298d 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/translations.ts
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/translations.ts
@@ -7,6 +7,13 @@
import { i18n } from '@kbn/i18n';
+export const TITLE = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.upgradeRules.comparisonSide.title',
+ {
+ defaultMessage: 'Diff view',
+ }
+);
+
export const NO_CHANGES = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.comparisonSide.noChangesLabel',
{
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_conflicts_resolver.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_conflicts_resolver.tsx
index eeafddfc21f03..a750c163814a0 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_conflicts_resolver.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_conflicts_resolver.tsx
@@ -16,18 +16,21 @@ import type {
ThreeWayDiff,
} from '../../../../../../../common/api/detection_engine';
import { ThreeWayDiffConflict } from '../../../../../../../common/api/detection_engine';
+import type { FieldUpgradeState } from '../../../../model/prebuilt_rule_upgrade';
import { ComparisonSide } from '../comparison_side/comparison_side';
import { FinalSide } from '../final_side/final_side';
import { FieldUpgradeConflictsResolverHeader } from './field_upgrade_conflicts_resolver_header';
interface FieldUpgradeConflictsResolverProps {
fieldName: FieldName;
+ fieldUpgradeState: FieldUpgradeState;
fieldThreeWayDiff: RuleFieldsDiff[FieldName];
finalDiffableRule: DiffableRule;
}
export function FieldUpgradeConflictsResolver({
fieldName,
+ fieldUpgradeState,
fieldThreeWayDiff,
finalDiffableRule,
}: FieldUpgradeConflictsResolverProps): JSX.Element {
@@ -37,7 +40,12 @@ export function FieldUpgradeConflictsResolver
}
+ header={
+
+ }
initialIsOpen={hasConflict}
data-test-subj="ruleUpgradePerFieldDiff"
>
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_conflicts_resolver_header.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_conflicts_resolver_header.tsx
index 2821a0a179b91..a096f025873a5 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_conflicts_resolver_header.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_conflicts_resolver_header.tsx
@@ -7,19 +7,27 @@
import React from 'react';
import { camelCase, startCase } from 'lodash';
-import { EuiTitle } from '@elastic/eui';
+import { EuiFlexGroup, EuiTitle } from '@elastic/eui';
import { fieldToDisplayNameMap } from '../../diff_components/translations';
+import type { FieldUpgradeState } from '../../../../model/prebuilt_rule_upgrade';
+import { FieldUpgradeStateInfo } from './field_upgrade_state_info';
interface FieldUpgradeConflictsResolverHeaderProps {
fieldName: string;
+ fieldUpgradeState: FieldUpgradeState;
}
export function FieldUpgradeConflictsResolverHeader({
fieldName,
+ fieldUpgradeState,
}: FieldUpgradeConflictsResolverHeaderProps): JSX.Element {
return (
-
- {fieldToDisplayNameMap[fieldName] ?? startCase(camelCase(fieldName))}
-
+
+
+ {fieldToDisplayNameMap[fieldName] ?? startCase(camelCase(fieldName))}
+
+
+
+
);
}
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_state_info/field_upgrade_state_info.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_state_info/field_upgrade_state_info.tsx
new file mode 100644
index 0000000000000..c49fc18e2c6ba
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_state_info/field_upgrade_state_info.tsx
@@ -0,0 +1,55 @@
+/*
+ * 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 { EuiIcon, EuiText } from '@elastic/eui';
+import { FieldUpgradeState } from '../../../../../model/prebuilt_rule_upgrade';
+import * as i18n from './translations';
+
+interface FieldUpgradeStateInfoProps {
+ state: FieldUpgradeState;
+}
+
+export function FieldUpgradeStateInfo({ state }: FieldUpgradeStateInfoProps): JSX.Element {
+ switch (state) {
+ case FieldUpgradeState.Accepted:
+ return (
+ <>
+
+
+ {i18n.UPDATE_ACCEPTED}
+ {i18n.SEPARATOR}
+ {i18n.UPDATE_ACCEPTED_DESCRIPTION}
+
+ >
+ );
+
+ case FieldUpgradeState.SolvableConflict:
+ return (
+ <>
+
+
+ {i18n.SOLVABLE_CONFLICT}
+ {i18n.SEPARATOR}
+ {i18n.SOLVABLE_CONFLICT_DESCRIPTION}
+
+ >
+ );
+
+ case FieldUpgradeState.NonSolvableConflict:
+ return (
+ <>
+
+
+ {i18n.NON_SOLVABLE_CONFLICT}
+ {i18n.SEPARATOR}
+ {i18n.NON_SOLVABLE_CONFLICT_DESCRIPTION}
+
+ >
+ );
+ }
+}
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_state_info/index.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_state_info/index.ts
new file mode 100644
index 0000000000000..69915cc64cdcc
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_state_info/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 './field_upgrade_state_info';
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_state_info/translations.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_state_info/translations.tsx
new file mode 100644
index 0000000000000..36349b5029a87
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/field_upgrade_state_info/translations.tsx
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const UPDATE_ACCEPTED = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.updateAccepted',
+ {
+ defaultMessage: 'Update accepted',
+ }
+);
+
+export const UPDATE_ACCEPTED_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.updateAcceptedDescription',
+ {
+ defaultMessage:
+ 'You can still make changes, please review/accept all other conflicts before updating the rule.',
+ }
+);
+
+export const SOLVABLE_CONFLICT = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.solvableConflict',
+ {
+ defaultMessage: 'Solved conflict',
+ }
+);
+
+export const SOLVABLE_CONFLICT_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.solvableConflictDescription',
+ {
+ defaultMessage:
+ 'We have suggested an update for this modified field, please review before accepting.',
+ }
+);
+
+export const NON_SOLVABLE_CONFLICT = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.nonSolvableConflict',
+ {
+ defaultMessage: 'Solved conflict',
+ }
+);
+
+export const NON_SOLVABLE_CONFLICT_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.nonSolvableConflictDescription',
+ {
+ defaultMessage:
+ 'We have suggested an update for this modified field, please review before accepting.',
+ }
+);
+
+export const SEPARATOR = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.separator',
+ {
+ defaultMessage: ' - ',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_callout/index.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_callout/index.ts
new file mode 100644
index 0000000000000..75ff48ff541a1
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_callout/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 './rule_upgrade_callout';
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_callout/rule_upgrade_callout.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_callout/rule_upgrade_callout.tsx
new file mode 100644
index 0000000000000..852ab0c91c58e
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_callout/rule_upgrade_callout.tsx
@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useMemo } from 'react';
+import { EuiCallOut } from '@elastic/eui';
+import type { RuleUpgradeState } from '../../../../../model/prebuilt_rule_upgrade';
+import { FieldUpgradeState } from '../../../../../model/prebuilt_rule_upgrade';
+import * as i18n from './translations';
+
+interface RuleUpgradeCalloutProps {
+ ruleUpgradeState: RuleUpgradeState;
+}
+
+export function RuleUpgradeCallout({ ruleUpgradeState }: RuleUpgradeCalloutProps): JSX.Element {
+ const fieldsUpgradeState = ruleUpgradeState.fieldsUpgradeState;
+ const { numOfNonSolvableConflicts, numOfSolvableConflicts } = useMemo(() => {
+ let numOfFieldsWithNonSolvableConflicts = 0;
+ let numOfFieldsWithSolvableConflicts = 0;
+
+ for (const fieldName of Object.keys(fieldsUpgradeState)) {
+ if (fieldsUpgradeState[fieldName] === FieldUpgradeState.NonSolvableConflict) {
+ numOfFieldsWithNonSolvableConflicts++;
+ }
+
+ if (fieldsUpgradeState[fieldName] === FieldUpgradeState.SolvableConflict) {
+ numOfFieldsWithSolvableConflicts++;
+ }
+ }
+
+ return {
+ numOfNonSolvableConflicts: numOfFieldsWithNonSolvableConflicts,
+ numOfSolvableConflicts: numOfFieldsWithSolvableConflicts,
+ };
+ }, [fieldsUpgradeState]);
+
+ if (numOfNonSolvableConflicts > 0) {
+ return (
+
+ {i18n.RULE_HAS_NON_SOLVABLE_CONFLICTS_DESCRIPTION}
+
+ );
+ }
+
+ if (numOfSolvableConflicts > 0) {
+ return (
+
+ {i18n.RULE_HAS_SOLVABLE_CONFLICTS_DESCRIPTION}
+
+ );
+ }
+
+ return (
+
+ {i18n.RULE_IS_READY_FOR_UPGRADE_DESCRIPTION}
+
+ );
+}
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_callout/translations.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_callout/translations.tsx
new file mode 100644
index 0000000000000..be9ee761388d0
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_callout/translations.tsx
@@ -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 { i18n } from '@kbn/i18n';
+
+export const RULE_HAS_NON_SOLVABLE_CONFLICTS = (count: number) =>
+ i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.ruleHasNonSolvableConflicts',
+ {
+ values: { count },
+ defaultMessage:
+ '{count} of the fields has a unsolved conflict. Please review and modify accordingly.',
+ }
+ );
+
+export const RULE_HAS_NON_SOLVABLE_CONFLICTS_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.ruleHasNonSolvableConflictsDescription',
+ {
+ defaultMessage:
+ 'Please provide an input for the unsolved conflict. You can also keep the current without the updates, or accept the Elastic update but lose your modifications.',
+ }
+);
+
+export const RULE_HAS_SOLVABLE_CONFLICTS = (count: number) =>
+ i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.ruleHasSolvableConflicts',
+ {
+ values: { count },
+ defaultMessage:
+ '{count} of the fields has an update conflict, please review the suggested update being updating.',
+ }
+ );
+
+export const RULE_HAS_SOLVABLE_CONFLICTS_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.ruleHasSolvableConflictsDescription',
+ {
+ defaultMessage:
+ 'Please review the suggested updated version before accepting the update. You can edit and then save the field if you wish to change it.',
+ }
+);
+
+export const RULE_IS_READY_FOR_UPGRADE = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.ruleIsReadyForUpgrade',
+ {
+ defaultMessage: 'The update is ready to be applied.',
+ }
+);
+
+export const RULE_IS_READY_FOR_UPGRADE_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.ruleIsReadyForUpgradeDescription',
+ {
+ defaultMessage: 'All conflicts have now been reviewed and solved please update the rule.',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_conflicts_resolver.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_conflicts_resolver.tsx
index 57af1b340c776..f60af70c808f5 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_conflicts_resolver.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_conflicts_resolver.tsx
@@ -9,7 +9,7 @@ import React from 'react';
import type {
RuleUpgradeState,
SetRuleFieldResolvedValueFn,
-} from '../../../../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state';
+} from '../../../../model/prebuilt_rule_upgrade';
import { FieldUpgradeConflictsResolver } from './field_upgrade_conflicts_resolver';
interface RuleUpgradeConflictsResolverProps {
@@ -31,6 +31,7 @@ export function RuleUpgradeConflictsResolver({
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_info_bar.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_info_bar.tsx
index 7ecde8059cc2f..970f04f383274 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_info_bar.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/rule_upgrade_info_bar.tsx
@@ -6,7 +6,7 @@
*/
import React from 'react';
-import type { RuleUpgradeState } from '../../../../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state';
+import type { RuleUpgradeState } from '../../../../model/prebuilt_rule_upgrade';
import {
UtilityBar,
UtilityBarGroup,
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/translations.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/translations.tsx
index 620b3ac1c0ba8..27172cb98755c 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/translations.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/translations.tsx
@@ -11,23 +11,21 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useKibana } from '../../../../../../common/lib/kibana/kibana_react';
-export const NUM_OF_FIELDS_WITH_UPDATES = (count: number) =>
- i18n.translate(
- 'xpack.securitySolution.detectionEngine.rules.upgradeRules.diffTab.fieldsWithUpdates',
- {
- values: { count },
- defaultMessage: 'Upgrade has {count} {count, plural, one {field} other {fields}}',
- }
- );
+export const NUM_OF_FIELDS_WITH_UPDATES = (count: number) => (
+ {count} }}
+ />
+);
-export const NUM_OF_CONFLICTS = (count: number) =>
- i18n.translate(
- 'xpack.securitySolution.detectionEngine.rules.upgradeRules.diffTab.numOfConflicts',
- {
- values: { count },
- defaultMessage: '{count} {count, plural, one {conflict} other {conflicts}}',
- }
- );
+export const NUM_OF_CONFLICTS = (count: number) => (
+ {count} }}
+ />
+);
const UPGRADE_RULES_DOCS_LINK = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.updateYourRulesDocsLink',
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/final_side.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/final_side.tsx
index 0685d064b32d0..83190015ebc6d 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/final_side.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/final_side.tsx
@@ -22,9 +22,9 @@ export function FinalSide({ fieldName, finalDiffableRule }: FinalSideProps): JSX
return (
<>
-
+
- {i18n.UPGRADED_VERSION}
+ {i18n.FINAL_UPDATE}
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/translations.ts
index aa9b4885a964d..8f6a10b5681be 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/translations.ts
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_side/translations.ts
@@ -7,9 +7,9 @@
import { i18n } from '@kbn/i18n';
-export const UPGRADED_VERSION = i18n.translate(
- 'xpack.securitySolution.detectionEngine.rules.upgradeRules.upgradedVersion',
+export const FINAL_UPDATE = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.upgradeRules.finalUpdate',
{
- defaultMessage: 'Upgraded version',
+ defaultMessage: 'Final update',
}
);
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade_conflicts_resolver_tab.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade_conflicts_resolver_tab.tsx
index 10823b8045c96..547cd23c7e86e 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade_conflicts_resolver_tab.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade_conflicts_resolver_tab.tsx
@@ -10,9 +10,10 @@ import { EuiSpacer } from '@elastic/eui';
import type {
RuleUpgradeState,
SetRuleFieldResolvedValueFn,
-} from '../../../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state';
+} from '../../../model/prebuilt_rule_upgrade';
import { RuleUpgradeInfoBar } from './components/rule_upgrade_info_bar';
import { RuleUpgradeConflictsResolver } from './components/rule_upgrade_conflicts_resolver';
+import { RuleUpgradeCallout } from './components/rule_upgrade_callout';
interface RuleUpgradeConflictsResolverTabProps {
ruleUpgradeState: RuleUpgradeState;
@@ -28,6 +29,8 @@ export function RuleUpgradeConflictsResolverTab({
+
+
;
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/index.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/index.ts
new file mode 100644
index 0000000000000..57ee30f308f08
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/index.ts
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export * from './field_upgrade_state';
+export * from './fields_upgrade_state';
+export * from './rule_upgrade_state';
+export * from './rules_upgrade_state';
+export * from './set_rule_field_resolved_value';
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/rule_upgrade_state.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/rule_upgrade_state.ts
new file mode 100644
index 0000000000000..0c72361bb29dc
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/rule_upgrade_state.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ type DiffableRule,
+ type RuleUpgradeInfoForReview,
+} from '../../../../../common/api/detection_engine';
+import type { FieldsUpgradeState } from './fields_upgrade_state';
+
+export interface RuleUpgradeState extends RuleUpgradeInfoForReview {
+ /**
+ * Rule containing desired values users expect to see in the upgraded rule.
+ */
+ finalRule: DiffableRule;
+ /**
+ * Indicates whether there are conflicts blocking rule upgrading.
+ */
+ hasUnresolvedConflicts: boolean;
+ /**
+ * Stores a record of field names mapped to field upgrade state.
+ */
+ fieldsUpgradeState: FieldsUpgradeState;
+}
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/rules_upgrade_state.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/rules_upgrade_state.ts
new file mode 100644
index 0000000000000..66709ec34653e
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/rules_upgrade_state.ts
@@ -0,0 +1,11 @@
+/*
+ * 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 { RuleSignatureId } from '../../../../../common/api/detection_engine';
+import type { RuleUpgradeState } from './rule_upgrade_state';
+
+export type RulesUpgradeState = Record;
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/set_rule_field_resolved_value.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/set_rule_field_resolved_value.ts
new file mode 100644
index 0000000000000..c4bb65f162394
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/set_rule_field_resolved_value.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 { DiffableAllFields, RuleObjectId } from '../../../../../common/api/detection_engine';
+
+export type SetRuleFieldResolvedValueFn<
+ FieldName extends keyof DiffableAllFields = keyof DiffableAllFields
+> = (params: {
+ ruleId: RuleObjectId;
+ fieldName: FieldName;
+ resolvedValue: DiffableAllFields[FieldName];
+}) => void;
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx
index 16ba012313f34..2437a5e87866d 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx
@@ -16,6 +16,7 @@ import {
EuiSkeletonTitle,
} from '@elastic/eui';
import React, { useMemo, useState } from 'react';
+import type { RuleUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade';
import * as i18n from '../../../../../detections/pages/detection_engine/rules/translations';
import { RULES_TABLE_INITIAL_PAGE_SIZE, RULES_TABLE_PAGE_SIZE_OPTIONS } from '../constants';
import { RulesChangelogLink } from '../rules_changelog_link';
@@ -23,7 +24,6 @@ import { UpgradePrebuiltRulesTableButtons } from './upgrade_prebuilt_rules_table
import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context';
import { UpgradePrebuiltRulesTableFilters } from './upgrade_prebuilt_rules_table_filters';
import { useUpgradePrebuiltRulesTableColumns } from './use_upgrade_prebuilt_rules_table_columns';
-import type { RuleUpgradeState } from './use_prebuilt_rules_upgrade_state';
const NO_ITEMS_MESSAGE = (
;
-export type SetRuleFieldResolvedValueFn<
- FieldName extends keyof DiffableAllFields = keyof DiffableAllFields
-> = (params: {
- ruleId: RuleObjectId;
- fieldName: FieldName;
- resolvedValue: DiffableAllFields[FieldName];
-}) => void;
-
type RuleResolvedConflicts = Partial;
type RulesResolvedConflicts = Record;
@@ -70,6 +55,10 @@ export function usePrebuiltRulesUpgradeState(
ruleUpgradeInfo,
rulesResolvedConflicts[ruleUpgradeInfo.rule_id] ?? {}
),
+ fieldsUpgradeState: calcFieldsState(
+ ruleUpgradeInfo.diff.fields,
+ rulesResolvedConflicts[ruleUpgradeInfo.rule_id] ?? {}
+ ),
hasUnresolvedConflicts:
getUnacceptedConflictsCount(
ruleUpgradeInfo.diff.fields,
@@ -113,6 +102,35 @@ function convertRuleFieldsDiffToDiffable(
return mergeVersionRule;
}
+function calcFieldsState(
+ ruleFieldsDiff: FieldsDiff>,
+ ruleResolvedConflicts: RuleResolvedConflicts
+): FieldsUpgradeState {
+ const fieldsState: FieldsUpgradeState = {};
+
+ for (const fieldName of Object.keys(ruleFieldsDiff)) {
+ switch (ruleFieldsDiff[fieldName].conflict) {
+ case ThreeWayDiffConflict.NONE:
+ fieldsState[fieldName] = FieldUpgradeState.Accepted;
+ break;
+
+ case ThreeWayDiffConflict.SOLVABLE:
+ fieldsState[fieldName] = FieldUpgradeState.SolvableConflict;
+ break;
+
+ case ThreeWayDiffConflict.NON_SOLVABLE:
+ fieldsState[fieldName] = FieldUpgradeState.NonSolvableConflict;
+ break;
+ }
+ }
+
+ for (const fieldName of Object.keys(ruleResolvedConflicts)) {
+ fieldsState[fieldName] = FieldUpgradeState.Accepted;
+ }
+
+ return fieldsState;
+}
+
function getUnacceptedConflictsCount(
ruleFieldsDiff: FieldsDiff>,
ruleResolvedConflicts: RuleResolvedConflicts
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx
index e7267007d2348..09009c98c2858 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx
@@ -8,6 +8,7 @@
import type { EuiBasicTableColumn } from '@elastic/eui';
import { EuiBadge, EuiButtonEmpty, EuiLink, EuiLoadingSpinner, EuiText } from '@elastic/eui';
import React, { useMemo } from 'react';
+import type { RuleUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade/rule_upgrade_state';
import { RulesTableEmptyColumnName } from '../rules_table_empty_column_name';
import { SHOW_RELATED_INTEGRATIONS_SETTING } from '../../../../../../common/constants';
import type { RuleSignatureId } from '../../../../../../common/api/detection_engine/model/rule_schema';
@@ -22,7 +23,6 @@ import type { Rule } from '../../../../rule_management/logic';
import { getNormalizedSeverity } from '../helpers';
import type { UpgradePrebuiltRulesTableActions } from './upgrade_prebuilt_rules_table_context';
import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context';
-import type { RuleUpgradeState } from './use_prebuilt_rules_upgrade_state';
export type TableColumn = EuiBasicTableColumn;
From 44a42a7a2a22e0ee7ed6d1f8deb1f5f12ca2b155 Mon Sep 17 00:00:00 2001
From: Victor Martinez
Date: Thu, 10 Oct 2024 12:35:03 +0200
Subject: [PATCH 39/87] github-actions: grant write permissions to report to
the issues (#195706)
---
.github/workflows/oblt-github-commands.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/oblt-github-commands.yml b/.github/workflows/oblt-github-commands.yml
index 443c0fa5f9071..48df40f3343d9 100644
--- a/.github/workflows/oblt-github-commands.yml
+++ b/.github/workflows/oblt-github-commands.yml
@@ -14,6 +14,7 @@ on:
permissions:
contents: read
+ issues: write
pull-requests: write
jobs:
From 0caea22006591486fbfd80d7899e116743acd8a2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Felix=20St=C3=BCrmer?=
Date: Thu, 10 Oct 2024 12:46:25 +0200
Subject: [PATCH 40/87] [Logs Overview] Overview component (iteration 1)
(attempt 2) (#195673)
This is a re-submission of https://github.com/elastic/kibana/pull/191899, which was reverted due to
a storybook build problem. This introduces a "Logs Overview" component for use in solution UIs
behind a feature flag.
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Kerry Gallagher <471693+Kerry350@users.noreply.github.com>
Co-authored-by: Elastic Machine
---
.eslintrc.js | 1 +
.github/CODEOWNERS | 1 +
package.json | 5 +
.../src/lib/entity.ts | 12 +
.../src/lib/gaussian_events.ts | 74 +++++
.../src/lib/infra/host.ts | 10 +-
.../src/lib/infra/index.ts | 3 +-
.../src/lib/interval.ts | 18 +-
.../src/lib/logs/index.ts | 21 ++
.../src/lib/poisson_events.test.ts | 53 ++++
.../src/lib/poisson_events.ts | 77 +++++
.../src/lib/timerange.ts | 27 +-
.../distributed_unstructured_logs.ts | 197 ++++++++++++
.../scenarios/helpers/unstructured_logs.ts | 94 ++++++
packages/kbn-apm-synthtrace/tsconfig.json | 1 +
.../settings/setting_ids/index.ts | 1 +
.../src/worker/webpack.config.ts | 12 +
packages/kbn-storybook/src/webpack.config.ts | 11 +
packages/kbn-xstate-utils/kibana.jsonc | 2 +-
.../kbn-xstate-utils/src/console_inspector.ts | 88 ++++++
packages/kbn-xstate-utils/src/index.ts | 1 +
.../server/collectors/management/schema.ts | 6 +
.../server/collectors/management/types.ts | 1 +
src/plugins/telemetry/schema/oss_plugins.json | 6 +
tsconfig.base.json | 2 +
x-pack/.i18nrc.json | 3 +
.../observability/logs_overview/README.md | 3 +
.../observability/logs_overview/index.ts | 21 ++
.../logs_overview/jest.config.js | 12 +
.../observability/logs_overview/kibana.jsonc | 5 +
.../observability/logs_overview/package.json | 7 +
.../discover_link/discover_link.tsx | 110 +++++++
.../src/components/discover_link/index.ts | 8 +
.../src/components/log_categories/index.ts | 8 +
.../log_categories/log_categories.tsx | 94 ++++++
.../log_categories_control_bar.tsx | 44 +++
.../log_categories_error_content.tsx | 44 +++
.../log_categories/log_categories_grid.tsx | 182 +++++++++++
.../log_categories_grid_cell.tsx | 99 ++++++
.../log_categories_grid_change_time_cell.tsx | 54 ++++
.../log_categories_grid_change_type_cell.tsx | 108 +++++++
.../log_categories_grid_count_cell.tsx | 32 ++
.../log_categories_grid_histogram_cell.tsx | 99 ++++++
.../log_categories_grid_pattern_cell.tsx | 60 ++++
.../log_categories_loading_content.tsx | 68 +++++
.../log_categories_result_content.tsx | 87 ++++++
.../src/components/logs_overview/index.ts | 10 +
.../logs_overview/logs_overview.tsx | 64 ++++
.../logs_overview_error_content.tsx | 41 +++
.../logs_overview_loading_content.tsx | 23 ++
.../categorize_documents.ts | 282 ++++++++++++++++++
.../categorize_logs_service.ts | 250 ++++++++++++++++
.../count_documents.ts | 60 ++++
.../services/categorize_logs_service/index.ts | 8 +
.../categorize_logs_service/queries.ts | 151 ++++++++++
.../services/categorize_logs_service/types.ts | 21 ++
.../observability/logs_overview/src/types.ts | 74 +++++
.../logs_overview/src/utils/logs_source.ts | 60 ++++
.../logs_overview/src/utils/xstate5_utils.ts | 13 +
.../observability/logs_overview/tsconfig.json | 39 +++
.../components/app/service_logs/index.tsx | 171 ++++++++++-
.../routing/service_detail/index.tsx | 2 +-
.../apm/public/plugin.ts | 2 +
.../components/tabs/logs/logs_tab_content.tsx | 93 ++++--
.../logs_shared/kibana.jsonc | 5 +-
.../public/components/logs_overview/index.tsx | 8 +
.../logs_overview/logs_overview.mock.tsx | 32 ++
.../logs_overview/logs_overview.tsx | 70 +++++
.../logs_shared/public/index.ts | 1 +
.../logs_shared/public/mocks.tsx | 2 +
.../logs_shared/public/plugin.ts | 23 +-
.../logs_shared/public/types.ts | 12 +-
.../logs_shared/server/feature_flags.ts | 33 ++
.../logs_shared/server/plugin.ts | 28 +-
.../logs_shared/tsconfig.json | 4 +
yarn.lock | 17 ++
76 files changed, 3415 insertions(+), 56 deletions(-)
create mode 100644 packages/kbn-apm-synthtrace-client/src/lib/gaussian_events.ts
create mode 100644 packages/kbn-apm-synthtrace-client/src/lib/poisson_events.test.ts
create mode 100644 packages/kbn-apm-synthtrace-client/src/lib/poisson_events.ts
create mode 100644 packages/kbn-apm-synthtrace/src/scenarios/distributed_unstructured_logs.ts
create mode 100644 packages/kbn-apm-synthtrace/src/scenarios/helpers/unstructured_logs.ts
create mode 100644 packages/kbn-xstate-utils/src/console_inspector.ts
create mode 100644 x-pack/packages/observability/logs_overview/README.md
create mode 100644 x-pack/packages/observability/logs_overview/index.ts
create mode 100644 x-pack/packages/observability/logs_overview/jest.config.js
create mode 100644 x-pack/packages/observability/logs_overview/kibana.jsonc
create mode 100644 x-pack/packages/observability/logs_overview/package.json
create mode 100644 x-pack/packages/observability/logs_overview/src/components/discover_link/discover_link.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/discover_link/index.ts
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/index.ts
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_control_bar.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_error_content.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_cell.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_time_cell.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_type_cell.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_count_cell.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_histogram_cell.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_pattern_cell.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_loading_content.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_result_content.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/logs_overview/index.ts
create mode 100644 x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_error_content.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_loading_content.tsx
create mode 100644 x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_documents.ts
create mode 100644 x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_logs_service.ts
create mode 100644 x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/count_documents.ts
create mode 100644 x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/index.ts
create mode 100644 x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/queries.ts
create mode 100644 x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/types.ts
create mode 100644 x-pack/packages/observability/logs_overview/src/types.ts
create mode 100644 x-pack/packages/observability/logs_overview/src/utils/logs_source.ts
create mode 100644 x-pack/packages/observability/logs_overview/src/utils/xstate5_utils.ts
create mode 100644 x-pack/packages/observability/logs_overview/tsconfig.json
create mode 100644 x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/index.tsx
create mode 100644 x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.mock.tsx
create mode 100644 x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.tsx
create mode 100644 x-pack/plugins/observability_solution/logs_shared/server/feature_flags.ts
diff --git a/.eslintrc.js b/.eslintrc.js
index 797b84522df3f..c604844089ef4 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -978,6 +978,7 @@ module.exports = {
files: [
'x-pack/plugins/observability_solution/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)',
'src/plugins/ai_assistant_management/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)',
+ 'x-pack/packages/observability/logs_overview/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)',
],
rules: {
'@kbn/i18n/strings_should_be_translated_with_i18n': 'warn',
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 9b3c46d065fe1..974a7d39f63b3 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -652,6 +652,7 @@ x-pack/packages/observability/alerting_test_data @elastic/obs-ux-management-team
x-pack/test/cases_api_integration/common/plugins/observability @elastic/response-ops
x-pack/packages/observability/get_padded_alert_time_range_util @elastic/obs-ux-management-team
x-pack/plugins/observability_solution/observability_logs_explorer @elastic/obs-ux-logs-team
+x-pack/packages/observability/logs_overview @elastic/obs-ux-logs-team
x-pack/plugins/observability_solution/observability_onboarding/e2e @elastic/obs-ux-logs-team @elastic/obs-ux-onboarding-team
x-pack/plugins/observability_solution/observability_onboarding @elastic/obs-ux-logs-team @elastic/obs-ux-onboarding-team
x-pack/plugins/observability_solution/observability @elastic/obs-ux-management-team
diff --git a/package.json b/package.json
index 57b84f1c46dcb..58cd08773696f 100644
--- a/package.json
+++ b/package.json
@@ -97,6 +97,7 @@
"@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.cd77847.0",
"@types/react": "~18.2.0",
"@types/react-dom": "~18.2.0",
+ "@xstate5/react/**/xstate": "^5.18.1",
"globby/fast-glob": "^3.2.11"
},
"dependencies": {
@@ -687,6 +688,7 @@
"@kbn/observability-fixtures-plugin": "link:x-pack/test/cases_api_integration/common/plugins/observability",
"@kbn/observability-get-padded-alert-time-range-util": "link:x-pack/packages/observability/get_padded_alert_time_range_util",
"@kbn/observability-logs-explorer-plugin": "link:x-pack/plugins/observability_solution/observability_logs_explorer",
+ "@kbn/observability-logs-overview": "link:x-pack/packages/observability/logs_overview",
"@kbn/observability-onboarding-plugin": "link:x-pack/plugins/observability_solution/observability_onboarding",
"@kbn/observability-plugin": "link:x-pack/plugins/observability_solution/observability",
"@kbn/observability-shared-plugin": "link:x-pack/plugins/observability_solution/observability_shared",
@@ -1050,6 +1052,7 @@
"@turf/helpers": "6.0.1",
"@turf/length": "^6.0.2",
"@xstate/react": "^3.2.2",
+ "@xstate5/react": "npm:@xstate/react@^4.1.2",
"adm-zip": "^0.5.9",
"ai": "^2.2.33",
"ajv": "^8.12.0",
@@ -1283,6 +1286,7 @@
"whatwg-fetch": "^3.0.0",
"xml2js": "^0.5.0",
"xstate": "^4.38.2",
+ "xstate5": "npm:xstate@^5.18.1",
"xterm": "^5.1.0",
"yauzl": "^2.10.0",
"yazl": "^2.5.1",
@@ -1304,6 +1308,7 @@
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-transform-class-properties": "^7.24.7",
+ "@babel/plugin-transform-logical-assignment-operators": "^7.24.7",
"@babel/plugin-transform-numeric-separator": "^7.24.7",
"@babel/plugin-transform-runtime": "^7.24.7",
"@babel/preset-env": "^7.24.7",
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/entity.ts b/packages/kbn-apm-synthtrace-client/src/lib/entity.ts
index 4d522ef07ff0e..b26dbfc7ffb46 100644
--- a/packages/kbn-apm-synthtrace-client/src/lib/entity.ts
+++ b/packages/kbn-apm-synthtrace-client/src/lib/entity.ts
@@ -7,6 +7,8 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
+export type ObjectEntry = [keyof T, T[keyof T]];
+
export type Fields | undefined = undefined> = {
'@timestamp'?: number;
} & (TMeta extends undefined ? {} : Partial<{ meta: TMeta }>);
@@ -27,4 +29,14 @@ export class Entity {
return this;
}
+
+ overrides(overrides: Partial) {
+ const overrideEntries = Object.entries(overrides) as Array>;
+
+ overrideEntries.forEach(([fieldName, value]) => {
+ this.fields[fieldName] = value;
+ });
+
+ return this;
+ }
}
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/gaussian_events.ts b/packages/kbn-apm-synthtrace-client/src/lib/gaussian_events.ts
new file mode 100644
index 0000000000000..4f1db28017d29
--- /dev/null
+++ b/packages/kbn-apm-synthtrace-client/src/lib/gaussian_events.ts
@@ -0,0 +1,74 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", 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 { castArray } from 'lodash';
+import { SynthtraceGenerator } from '../types';
+import { Fields } from './entity';
+import { Serializable } from './serializable';
+
+export class GaussianEvents {
+ constructor(
+ private readonly from: Date,
+ private readonly to: Date,
+ private readonly mean: Date,
+ private readonly width: number,
+ private readonly totalPoints: number
+ ) {}
+
+ *generator(
+ map: (
+ timestamp: number,
+ index: number
+ ) => Serializable | Array>
+ ): SynthtraceGenerator {
+ if (this.totalPoints <= 0) {
+ return;
+ }
+
+ const startTime = this.from.getTime();
+ const endTime = this.to.getTime();
+ const meanTime = this.mean.getTime();
+ const densityInterval = 1 / (this.totalPoints - 1);
+
+ for (let eventIndex = 0; eventIndex < this.totalPoints; eventIndex++) {
+ const quantile = eventIndex * densityInterval;
+
+ const standardScore = Math.sqrt(2) * inverseError(2 * quantile - 1);
+ const timestamp = Math.round(meanTime + standardScore * this.width);
+
+ if (timestamp >= startTime && timestamp <= endTime) {
+ yield* this.generateEvents(timestamp, eventIndex, map);
+ }
+ }
+ }
+
+ private *generateEvents(
+ timestamp: number,
+ eventIndex: number,
+ map: (
+ timestamp: number,
+ index: number
+ ) => Serializable | Array>
+ ): Generator> {
+ const events = castArray(map(timestamp, eventIndex));
+ for (const event of events) {
+ yield event;
+ }
+ }
+}
+
+function inverseError(x: number): number {
+ const a = 0.147;
+ const sign = x < 0 ? -1 : 1;
+
+ const part1 = 2 / (Math.PI * a) + Math.log(1 - x * x) / 2;
+ const part2 = Math.log(1 - x * x) / a;
+
+ return sign * Math.sqrt(Math.sqrt(part1 * part1 - part2) - part1);
+}
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/infra/host.ts b/packages/kbn-apm-synthtrace-client/src/lib/infra/host.ts
index 198949b482be3..30550d64c4df8 100644
--- a/packages/kbn-apm-synthtrace-client/src/lib/infra/host.ts
+++ b/packages/kbn-apm-synthtrace-client/src/lib/infra/host.ts
@@ -27,7 +27,7 @@ interface HostDocument extends Fields {
'cloud.provider'?: string;
}
-class Host extends Entity {
+export class Host extends Entity {
cpu({ cpuTotalValue }: { cpuTotalValue?: number } = {}) {
return new HostMetrics({
...this.fields,
@@ -175,3 +175,11 @@ export function host(name: string): Host {
'cloud.provider': 'gcp',
});
}
+
+export function minimalHost(name: string): Host {
+ return new Host({
+ 'agent.id': 'synthtrace',
+ 'host.hostname': name,
+ 'host.name': name,
+ });
+}
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/infra/index.ts b/packages/kbn-apm-synthtrace-client/src/lib/infra/index.ts
index 853a9549ce02c..2957605cffcd3 100644
--- a/packages/kbn-apm-synthtrace-client/src/lib/infra/index.ts
+++ b/packages/kbn-apm-synthtrace-client/src/lib/infra/index.ts
@@ -8,7 +8,7 @@
*/
import { dockerContainer, DockerContainerMetricsDocument } from './docker_container';
-import { host, HostMetricsDocument } from './host';
+import { host, HostMetricsDocument, minimalHost } from './host';
import { k8sContainer, K8sContainerMetricsDocument } from './k8s_container';
import { pod, PodMetricsDocument } from './pod';
import { awsRds, AWSRdsMetricsDocument } from './aws/rds';
@@ -24,6 +24,7 @@ export type InfraDocument =
export const infra = {
host,
+ minimalHost,
pod,
dockerContainer,
k8sContainer,
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/interval.ts b/packages/kbn-apm-synthtrace-client/src/lib/interval.ts
index 1d56c42e1fe12..5a5ed3ab5fdbe 100644
--- a/packages/kbn-apm-synthtrace-client/src/lib/interval.ts
+++ b/packages/kbn-apm-synthtrace-client/src/lib/interval.ts
@@ -34,6 +34,10 @@ interface IntervalOptions {
rate?: number;
}
+interface StepDetails {
+ stepMilliseconds: number;
+}
+
export class Interval {
private readonly intervalAmount: number;
private readonly intervalUnit: unitOfTime.DurationConstructor;
@@ -46,12 +50,16 @@ export class Interval {
this._rate = options.rate || 1;
}
+ private getIntervalMilliseconds(): number {
+ return moment.duration(this.intervalAmount, this.intervalUnit).asMilliseconds();
+ }
+
private getTimestamps() {
const from = this.options.from.getTime();
const to = this.options.to.getTime();
let time: number = from;
- const diff = moment.duration(this.intervalAmount, this.intervalUnit).asMilliseconds();
+ const diff = this.getIntervalMilliseconds();
const timestamps: number[] = [];
@@ -68,15 +76,19 @@ export class Interval {
*generator(
map: (
timestamp: number,
- index: number
+ index: number,
+ stepDetails: StepDetails
) => Serializable | Array>
): SynthtraceGenerator {
const timestamps = this.getTimestamps();
+ const stepDetails: StepDetails = {
+ stepMilliseconds: this.getIntervalMilliseconds(),
+ };
let index = 0;
for (const timestamp of timestamps) {
- const events = castArray(map(timestamp, index));
+ const events = castArray(map(timestamp, index, stepDetails));
index++;
for (const event of events) {
yield event;
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts b/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts
index e19f0f6fd6565..2bbc59eb37e70 100644
--- a/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts
+++ b/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts
@@ -68,6 +68,7 @@ export type LogDocument = Fields &
'event.duration': number;
'event.start': Date;
'event.end': Date;
+ labels?: Record;
test_field: string | string[];
date: Date;
severity: string;
@@ -156,6 +157,26 @@ function create(logsOptions: LogsOptions = defaultLogsOptions): Log {
).dataset('synth');
}
+function createMinimal({
+ dataset = 'synth',
+ namespace = 'default',
+}: {
+ dataset?: string;
+ namespace?: string;
+} = {}): Log {
+ return new Log(
+ {
+ 'input.type': 'logs',
+ 'data_stream.namespace': namespace,
+ 'data_stream.type': 'logs',
+ 'data_stream.dataset': dataset,
+ 'event.dataset': dataset,
+ },
+ { isLogsDb: false }
+ );
+}
+
export const log = {
create,
+ createMinimal,
};
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.test.ts b/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.test.ts
new file mode 100644
index 0000000000000..0741884550f32
--- /dev/null
+++ b/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.test.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 { PoissonEvents } from './poisson_events';
+import { Serializable } from './serializable';
+
+describe('poisson events', () => {
+ it('generates events within the given time range', () => {
+ const poissonEvents = new PoissonEvents(new Date(1000), new Date(2000), 10);
+
+ const events = Array.from(
+ poissonEvents.generator((timestamp) => new Serializable({ '@timestamp': timestamp }))
+ );
+
+ expect(events.length).toBeGreaterThanOrEqual(1);
+
+ for (const event of events) {
+ expect(event.fields['@timestamp']).toBeGreaterThanOrEqual(1000);
+ expect(event.fields['@timestamp']).toBeLessThanOrEqual(2000);
+ }
+ });
+
+ it('generates at least one event if the rate is greater than 0', () => {
+ const poissonEvents = new PoissonEvents(new Date(1000), new Date(2000), 1);
+
+ const events = Array.from(
+ poissonEvents.generator((timestamp) => new Serializable({ '@timestamp': timestamp }))
+ );
+
+ expect(events.length).toBeGreaterThanOrEqual(1);
+
+ for (const event of events) {
+ expect(event.fields['@timestamp']).toBeGreaterThanOrEqual(1000);
+ expect(event.fields['@timestamp']).toBeLessThanOrEqual(2000);
+ }
+ });
+
+ it('generates no event if the rate is 0', () => {
+ const poissonEvents = new PoissonEvents(new Date(1000), new Date(2000), 0);
+
+ const events = Array.from(
+ poissonEvents.generator((timestamp) => new Serializable({ '@timestamp': timestamp }))
+ );
+
+ expect(events.length).toBe(0);
+ });
+});
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.ts b/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.ts
new file mode 100644
index 0000000000000..e7fd24b8323e7
--- /dev/null
+++ b/packages/kbn-apm-synthtrace-client/src/lib/poisson_events.ts
@@ -0,0 +1,77 @@
+/*
+ * 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 { castArray } from 'lodash';
+import { SynthtraceGenerator } from '../types';
+import { Fields } from './entity';
+import { Serializable } from './serializable';
+
+export class PoissonEvents {
+ constructor(
+ private readonly from: Date,
+ private readonly to: Date,
+ private readonly rate: number
+ ) {}
+
+ private getTotalTimePeriod(): number {
+ return this.to.getTime() - this.from.getTime();
+ }
+
+ private getInterarrivalTime(): number {
+ const distribution = -Math.log(1 - Math.random()) / this.rate;
+ const totalTimePeriod = this.getTotalTimePeriod();
+ return Math.floor(distribution * totalTimePeriod);
+ }
+
+ *generator(
+ map: (
+ timestamp: number,
+ index: number
+ ) => Serializable | Array>
+ ): SynthtraceGenerator {
+ if (this.rate <= 0) {
+ return;
+ }
+
+ let currentTime = this.from.getTime();
+ const endTime = this.to.getTime();
+ let eventIndex = 0;
+
+ while (currentTime < endTime) {
+ const interarrivalTime = this.getInterarrivalTime();
+ currentTime += interarrivalTime;
+
+ if (currentTime < endTime) {
+ yield* this.generateEvents(currentTime, eventIndex, map);
+ eventIndex++;
+ }
+ }
+
+ // ensure at least one event has been emitted
+ if (this.rate > 0 && eventIndex === 0) {
+ const forcedEventTime =
+ this.from.getTime() + Math.floor(Math.random() * this.getTotalTimePeriod());
+ yield* this.generateEvents(forcedEventTime, eventIndex, map);
+ }
+ }
+
+ private *generateEvents(
+ timestamp: number,
+ eventIndex: number,
+ map: (
+ timestamp: number,
+ index: number
+ ) => Serializable | Array>
+ ): Generator> {
+ const events = castArray(map(timestamp, eventIndex));
+ for (const event of events) {
+ yield event;
+ }
+ }
+}
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts b/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts
index ccdea4ee75197..1c6f12414a148 100644
--- a/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts
+++ b/packages/kbn-apm-synthtrace-client/src/lib/timerange.ts
@@ -9,10 +9,12 @@
import datemath from '@kbn/datemath';
import type { Moment } from 'moment';
+import { GaussianEvents } from './gaussian_events';
import { Interval } from './interval';
+import { PoissonEvents } from './poisson_events';
export class Timerange {
- constructor(private from: Date, private to: Date) {}
+ constructor(public readonly from: Date, public readonly to: Date) {}
interval(interval: string) {
return new Interval({ from: this.from, to: this.to, interval });
@@ -21,6 +23,29 @@ export class Timerange {
ratePerMinute(rate: number) {
return this.interval(`1m`).rate(rate);
}
+
+ poissonEvents(rate: number) {
+ return new PoissonEvents(this.from, this.to, rate);
+ }
+
+ gaussianEvents(mean: Date, width: number, totalPoints: number) {
+ return new GaussianEvents(this.from, this.to, mean, width, totalPoints);
+ }
+
+ splitInto(segmentCount: number): Timerange[] {
+ const duration = this.to.getTime() - this.from.getTime();
+ const segmentDuration = duration / segmentCount;
+
+ return Array.from({ length: segmentCount }, (_, i) => {
+ const from = new Date(this.from.getTime() + i * segmentDuration);
+ const to = new Date(from.getTime() + segmentDuration);
+ return new Timerange(from, to);
+ });
+ }
+
+ toString() {
+ return `Timerange(from=${this.from.toISOString()}, to=${this.to.toISOString()})`;
+ }
}
type DateLike = Date | number | Moment | string;
diff --git a/packages/kbn-apm-synthtrace/src/scenarios/distributed_unstructured_logs.ts b/packages/kbn-apm-synthtrace/src/scenarios/distributed_unstructured_logs.ts
new file mode 100644
index 0000000000000..83860635ae64a
--- /dev/null
+++ b/packages/kbn-apm-synthtrace/src/scenarios/distributed_unstructured_logs.ts
@@ -0,0 +1,197 @@
+/*
+ * 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 { infra, LogDocument, log } from '@kbn/apm-synthtrace-client';
+import { fakerEN as faker } from '@faker-js/faker';
+import { z } from '@kbn/zod';
+import { Scenario } from '../cli/scenario';
+import { withClient } from '../lib/utils/with_client';
+import {
+ LogMessageGenerator,
+ generateUnstructuredLogMessage,
+ unstructuredLogMessageGenerators,
+} from './helpers/unstructured_logs';
+
+const scenarioOptsSchema = z.intersection(
+ z.object({
+ randomSeed: z.number().default(0),
+ messageGroup: z
+ .enum([
+ 'httpAccess',
+ 'userAuthentication',
+ 'networkEvent',
+ 'dbOperations',
+ 'taskOperations',
+ 'degradedOperations',
+ 'errorOperations',
+ ])
+ .default('dbOperations'),
+ }),
+ z
+ .discriminatedUnion('distribution', [
+ z.object({
+ distribution: z.literal('uniform'),
+ rate: z.number().default(1),
+ }),
+ z.object({
+ distribution: z.literal('poisson'),
+ rate: z.number().default(1),
+ }),
+ z.object({
+ distribution: z.literal('gaussian'),
+ mean: z.coerce.date().describe('Time of the peak of the gaussian distribution'),
+ width: z.number().default(5000).describe('Width of the gaussian distribution in ms'),
+ totalPoints: z
+ .number()
+ .default(100)
+ .describe('Total number of points in the gaussian distribution'),
+ }),
+ ])
+ .default({ distribution: 'uniform', rate: 1 })
+);
+
+type ScenarioOpts = z.output;
+
+const scenario: Scenario = async (runOptions) => {
+ return {
+ generate: ({ range, clients: { logsEsClient } }) => {
+ const { logger } = runOptions;
+ const scenarioOpts = scenarioOptsSchema.parse(runOptions.scenarioOpts ?? {});
+
+ faker.seed(scenarioOpts.randomSeed);
+ faker.setDefaultRefDate(range.from.toISOString());
+
+ logger.debug(`Generating ${scenarioOpts.distribution} logs...`);
+
+ // Logs Data logic
+ const LOG_LEVELS = ['info', 'debug', 'error', 'warn', 'trace', 'fatal'];
+
+ const clusterDefinions = [
+ {
+ 'orchestrator.cluster.id': faker.string.nanoid(),
+ 'orchestrator.cluster.name': 'synth-cluster-1',
+ 'orchestrator.namespace': 'default',
+ 'cloud.provider': 'gcp',
+ 'cloud.region': 'eu-central-1',
+ 'cloud.availability_zone': 'eu-central-1a',
+ 'cloud.project.id': faker.string.nanoid(),
+ },
+ {
+ 'orchestrator.cluster.id': faker.string.nanoid(),
+ 'orchestrator.cluster.name': 'synth-cluster-2',
+ 'orchestrator.namespace': 'production',
+ 'cloud.provider': 'aws',
+ 'cloud.region': 'us-east-1',
+ 'cloud.availability_zone': 'us-east-1a',
+ 'cloud.project.id': faker.string.nanoid(),
+ },
+ {
+ 'orchestrator.cluster.id': faker.string.nanoid(),
+ 'orchestrator.cluster.name': 'synth-cluster-3',
+ 'orchestrator.namespace': 'kube',
+ 'cloud.provider': 'azure',
+ 'cloud.region': 'area-51',
+ 'cloud.availability_zone': 'area-51a',
+ 'cloud.project.id': faker.string.nanoid(),
+ },
+ ];
+
+ const hostEntities = [
+ {
+ 'host.name': 'host-1',
+ 'agent.id': 'synth-agent-1',
+ 'agent.name': 'nodejs',
+ 'cloud.instance.id': faker.string.nanoid(),
+ 'orchestrator.resource.id': faker.string.nanoid(),
+ ...clusterDefinions[0],
+ },
+ {
+ 'host.name': 'host-2',
+ 'agent.id': 'synth-agent-2',
+ 'agent.name': 'custom',
+ 'cloud.instance.id': faker.string.nanoid(),
+ 'orchestrator.resource.id': faker.string.nanoid(),
+ ...clusterDefinions[1],
+ },
+ {
+ 'host.name': 'host-3',
+ 'agent.id': 'synth-agent-3',
+ 'agent.name': 'python',
+ 'cloud.instance.id': faker.string.nanoid(),
+ 'orchestrator.resource.id': faker.string.nanoid(),
+ ...clusterDefinions[2],
+ },
+ ].map((hostDefinition) =>
+ infra.minimalHost(hostDefinition['host.name']).overrides(hostDefinition)
+ );
+
+ const serviceNames = Array(3)
+ .fill(null)
+ .map((_, idx) => `synth-service-${idx}`);
+
+ const generatorFactory =
+ scenarioOpts.distribution === 'uniform'
+ ? range.interval('1s').rate(scenarioOpts.rate)
+ : scenarioOpts.distribution === 'poisson'
+ ? range.poissonEvents(scenarioOpts.rate)
+ : range.gaussianEvents(scenarioOpts.mean, scenarioOpts.width, scenarioOpts.totalPoints);
+
+ const logs = generatorFactory.generator((timestamp) => {
+ const entity = faker.helpers.arrayElement(hostEntities);
+ const serviceName = faker.helpers.arrayElement(serviceNames);
+ const level = faker.helpers.arrayElement(LOG_LEVELS);
+ const messages = logMessageGenerators[scenarioOpts.messageGroup](faker);
+
+ return messages.map((message) =>
+ log
+ .createMinimal()
+ .message(message)
+ .logLevel(level)
+ .service(serviceName)
+ .overrides({
+ ...entity.fields,
+ labels: {
+ scenario: 'rare',
+ population: scenarioOpts.distribution,
+ },
+ })
+ .timestamp(timestamp)
+ );
+ });
+
+ return [
+ withClient(
+ logsEsClient,
+ logger.perf('generating_logs', () => [logs])
+ ),
+ ];
+ },
+ };
+};
+
+export default scenario;
+
+const logMessageGenerators = {
+ httpAccess: generateUnstructuredLogMessage([unstructuredLogMessageGenerators.httpAccess]),
+ userAuthentication: generateUnstructuredLogMessage([
+ unstructuredLogMessageGenerators.userAuthentication,
+ ]),
+ networkEvent: generateUnstructuredLogMessage([unstructuredLogMessageGenerators.networkEvent]),
+ dbOperations: generateUnstructuredLogMessage([unstructuredLogMessageGenerators.dbOperation]),
+ taskOperations: generateUnstructuredLogMessage([
+ unstructuredLogMessageGenerators.taskStatusSuccess,
+ ]),
+ degradedOperations: generateUnstructuredLogMessage([
+ unstructuredLogMessageGenerators.taskStatusFailure,
+ ]),
+ errorOperations: generateUnstructuredLogMessage([
+ unstructuredLogMessageGenerators.error,
+ unstructuredLogMessageGenerators.restart,
+ ]),
+} satisfies Record;
diff --git a/packages/kbn-apm-synthtrace/src/scenarios/helpers/unstructured_logs.ts b/packages/kbn-apm-synthtrace/src/scenarios/helpers/unstructured_logs.ts
new file mode 100644
index 0000000000000..490bd449e2b60
--- /dev/null
+++ b/packages/kbn-apm-synthtrace/src/scenarios/helpers/unstructured_logs.ts
@@ -0,0 +1,94 @@
+/*
+ * 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 { Faker, faker } from '@faker-js/faker';
+
+export type LogMessageGenerator = (f: Faker) => string[];
+
+export const unstructuredLogMessageGenerators = {
+ httpAccess: (f: Faker) => [
+ `${f.internet.ip()} - - [${f.date
+ .past()
+ .toISOString()
+ .replace('T', ' ')
+ .replace(
+ /\..+/,
+ ''
+ )}] "${f.internet.httpMethod()} ${f.internet.url()} HTTP/1.1" ${f.helpers.arrayElement([
+ 200, 301, 404, 500,
+ ])} ${f.number.int({ min: 100, max: 5000 })}`,
+ ],
+ dbOperation: (f: Faker) => [
+ `${f.database.engine()}: ${f.database.column()} ${f.helpers.arrayElement([
+ 'created',
+ 'updated',
+ 'deleted',
+ 'inserted',
+ ])} successfully ${f.number.int({ max: 100000 })} times`,
+ ],
+ taskStatusSuccess: (f: Faker) => [
+ `${f.hacker.noun()}: ${f.word.words()} ${f.helpers.arrayElement([
+ 'triggered',
+ 'executed',
+ 'processed',
+ 'handled',
+ ])} successfully at ${f.date.recent().toISOString()}`,
+ ],
+ taskStatusFailure: (f: Faker) => [
+ `${f.hacker.noun()}: ${f.helpers.arrayElement([
+ 'triggering',
+ 'execution',
+ 'processing',
+ 'handling',
+ ])} of ${f.word.words()} failed at ${f.date.recent().toISOString()}`,
+ ],
+ error: (f: Faker) => [
+ `${f.helpers.arrayElement([
+ 'Error',
+ 'Exception',
+ 'Failure',
+ 'Crash',
+ 'Bug',
+ 'Issue',
+ ])}: ${f.hacker.phrase()}`,
+ `Stopping ${f.number.int(42)} background tasks...`,
+ 'Shutting down process...',
+ ],
+ restart: (f: Faker) => {
+ const service = f.database.engine();
+ return [
+ `Restarting ${service}...`,
+ `Waiting for queue to drain...`,
+ `Service ${service} restarted ${f.helpers.arrayElement([
+ 'successfully',
+ 'with errors',
+ 'with warnings',
+ ])}`,
+ ];
+ },
+ userAuthentication: (f: Faker) => [
+ `User ${f.internet.userName()} ${f.helpers.arrayElement([
+ 'logged in',
+ 'logged out',
+ 'failed to login',
+ ])}`,
+ ],
+ networkEvent: (f: Faker) => [
+ `Network ${f.helpers.arrayElement([
+ 'connection',
+ 'disconnection',
+ 'data transfer',
+ ])} ${f.helpers.arrayElement(['from', 'to'])} ${f.internet.ip()}`,
+ ],
+} satisfies Record;
+
+export const generateUnstructuredLogMessage =
+ (generators: LogMessageGenerator[] = Object.values(unstructuredLogMessageGenerators)) =>
+ (f: Faker = faker) =>
+ f.helpers.arrayElement(generators)(f);
diff --git a/packages/kbn-apm-synthtrace/tsconfig.json b/packages/kbn-apm-synthtrace/tsconfig.json
index d0f5c5801597a..db93e36421b83 100644
--- a/packages/kbn-apm-synthtrace/tsconfig.json
+++ b/packages/kbn-apm-synthtrace/tsconfig.json
@@ -10,6 +10,7 @@
"@kbn/apm-synthtrace-client",
"@kbn/dev-utils",
"@kbn/elastic-agent-utils",
+ "@kbn/zod",
],
"exclude": [
"target/**/*",
diff --git a/packages/kbn-management/settings/setting_ids/index.ts b/packages/kbn-management/settings/setting_ids/index.ts
index 2b8c5de0b71df..e926007f77f25 100644
--- a/packages/kbn-management/settings/setting_ids/index.ts
+++ b/packages/kbn-management/settings/setting_ids/index.ts
@@ -142,6 +142,7 @@ export const OBSERVABILITY_APM_ENABLE_SERVICE_INVENTORY_TABLE_SEARCH_BAR =
'observability:apmEnableServiceInventoryTableSearchBar';
export const OBSERVABILITY_LOGS_EXPLORER_ALLOWED_DATA_VIEWS_ID =
'observability:logsExplorer:allowedDataViews';
+export const OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID = 'observability:newLogsOverview';
export const OBSERVABILITY_ENTITY_CENTRIC_EXPERIENCE = 'observability:entityCentricExperience';
export const OBSERVABILITY_LOGS_DATA_ACCESS_LOG_SOURCES_ID = 'observability:logSources';
export const OBSERVABILITY_ENABLE_LOGS_STREAM = 'observability:enableLogsStream';
diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts
index 539d3098030e0..52a837724480d 100644
--- a/packages/kbn-optimizer/src/worker/webpack.config.ts
+++ b/packages/kbn-optimizer/src/worker/webpack.config.ts
@@ -247,6 +247,18 @@ export function getWebpackConfig(
},
},
},
+ {
+ test: /node_modules\/@?xstate5\/.*\.js$/,
+ use: {
+ loader: 'babel-loader',
+ options: {
+ babelrc: false,
+ envName: worker.dist ? 'production' : 'development',
+ presets: [BABEL_PRESET],
+ plugins: ['@babel/plugin-transform-logical-assignment-operators'],
+ },
+ },
+ },
{
test: /\.(html|md|txt|tmpl)$/,
use: {
diff --git a/packages/kbn-storybook/src/webpack.config.ts b/packages/kbn-storybook/src/webpack.config.ts
index fb901692e7f66..b03d78dbbc190 100644
--- a/packages/kbn-storybook/src/webpack.config.ts
+++ b/packages/kbn-storybook/src/webpack.config.ts
@@ -125,6 +125,17 @@ export default ({ config: storybookConfig }: { config: Configuration }) => {
},
],
},
+ {
+ test: /node_modules\/@?xstate5\/.*\.js$/,
+ use: {
+ loader: 'babel-loader',
+ options: {
+ babelrc: false,
+ presets: [require.resolve('@kbn/babel-preset/webpack_preset')],
+ plugins: ['@babel/plugin-transform-logical-assignment-operators'],
+ },
+ },
+ },
],
},
plugins: [new IgnoreNotFoundExportPlugin()],
diff --git a/packages/kbn-xstate-utils/kibana.jsonc b/packages/kbn-xstate-utils/kibana.jsonc
index cd1151a3f2103..1fb3507854b98 100644
--- a/packages/kbn-xstate-utils/kibana.jsonc
+++ b/packages/kbn-xstate-utils/kibana.jsonc
@@ -1,5 +1,5 @@
{
- "type": "shared-common",
+ "type": "shared-browser",
"id": "@kbn/xstate-utils",
"owner": "@elastic/obs-ux-logs-team"
}
diff --git a/packages/kbn-xstate-utils/src/console_inspector.ts b/packages/kbn-xstate-utils/src/console_inspector.ts
new file mode 100644
index 0000000000000..8792ab44f3c28
--- /dev/null
+++ b/packages/kbn-xstate-utils/src/console_inspector.ts
@@ -0,0 +1,88 @@
+/*
+ * 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 {
+ ActorRefLike,
+ AnyActorRef,
+ InspectedActorEvent,
+ InspectedEventEvent,
+ InspectedSnapshotEvent,
+ InspectionEvent,
+} from 'xstate5';
+import { isDevMode } from './dev_tools';
+
+export const createConsoleInspector = () => {
+ if (!isDevMode()) {
+ return () => {};
+ }
+
+ // eslint-disable-next-line no-console
+ const log = console.info.bind(console);
+
+ const logActorEvent = (actorEvent: InspectedActorEvent) => {
+ if (isActorRef(actorEvent.actorRef)) {
+ log(
+ '✨ %c%s%c is a new actor of type %c%s%c:',
+ ...styleAsActor(actorEvent.actorRef.id),
+ ...styleAsKeyword(actorEvent.type),
+ actorEvent.actorRef
+ );
+ } else {
+ log('✨ New %c%s%c actor without id:', ...styleAsKeyword(actorEvent.type), actorEvent);
+ }
+ };
+
+ const logEventEvent = (eventEvent: InspectedEventEvent) => {
+ if (isActorRef(eventEvent.actorRef)) {
+ log(
+ '🔔 %c%s%c received event %c%s%c from %c%s%c:',
+ ...styleAsActor(eventEvent.actorRef.id),
+ ...styleAsKeyword(eventEvent.event.type),
+ ...styleAsKeyword(eventEvent.sourceRef?.id),
+ eventEvent
+ );
+ } else {
+ log('🔔 Event', ...styleAsKeyword(eventEvent.event.type), ':', eventEvent);
+ }
+ };
+
+ const logSnapshotEvent = (snapshotEvent: InspectedSnapshotEvent) => {
+ if (isActorRef(snapshotEvent.actorRef)) {
+ log(
+ '📸 %c%s%c updated due to %c%s%c:',
+ ...styleAsActor(snapshotEvent.actorRef.id),
+ ...styleAsKeyword(snapshotEvent.event.type),
+ snapshotEvent.snapshot
+ );
+ } else {
+ log('📸 Snapshot due to %c%s%c:', ...styleAsKeyword(snapshotEvent.event.type), snapshotEvent);
+ }
+ };
+
+ return (inspectionEvent: InspectionEvent) => {
+ if (inspectionEvent.type === '@xstate.actor') {
+ logActorEvent(inspectionEvent);
+ } else if (inspectionEvent.type === '@xstate.event') {
+ logEventEvent(inspectionEvent);
+ } else if (inspectionEvent.type === '@xstate.snapshot') {
+ logSnapshotEvent(inspectionEvent);
+ } else {
+ log(`❓ Received inspection event:`, inspectionEvent);
+ }
+ };
+};
+
+const isActorRef = (actorRefLike: ActorRefLike): actorRefLike is AnyActorRef =>
+ 'id' in actorRefLike;
+
+const keywordStyle = 'font-weight: bold';
+const styleAsKeyword = (value: any) => [keywordStyle, value, ''] as const;
+
+const actorStyle = 'font-weight: bold; text-decoration: underline';
+const styleAsActor = (value: any) => [actorStyle, value, ''] as const;
diff --git a/packages/kbn-xstate-utils/src/index.ts b/packages/kbn-xstate-utils/src/index.ts
index 107585ba2096f..3edf83e8a32c2 100644
--- a/packages/kbn-xstate-utils/src/index.ts
+++ b/packages/kbn-xstate-utils/src/index.ts
@@ -9,5 +9,6 @@
export * from './actions';
export * from './dev_tools';
+export * from './console_inspector';
export * from './notification_channel';
export * from './types';
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 dc2d2ad2c5de2..e5ddfbe4dd037 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
@@ -705,4 +705,10 @@ export const stackManagementSchema: MakeSchemaFrom = {
_meta: { description: 'Non-default value of setting.' },
},
},
+ 'observability:newLogsOverview': {
+ type: 'boolean',
+ _meta: {
+ description: 'Enable the new logs overview component.',
+ },
+ },
};
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 ef20ab223dfb6..2acb487e7ed08 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
@@ -56,6 +56,7 @@ export interface UsageStats {
'observability:logsExplorer:allowedDataViews': string[];
'observability:logSources': string[];
'observability:enableLogsStream': boolean;
+ 'observability:newLogsOverview': boolean;
'observability:aiAssistantSimulatedFunctionCalling': boolean;
'observability:aiAssistantSearchConnectorIndexPattern': string;
'visualization:heatmap:maxBuckets': number;
diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json
index 958280d9eba00..830cffc17cf1c 100644
--- a/src/plugins/telemetry/schema/oss_plugins.json
+++ b/src/plugins/telemetry/schema/oss_plugins.json
@@ -10768,6 +10768,12 @@
"description": "Non-default value of setting."
}
},
+ "observability:newLogsOverview": {
+ "type": "boolean",
+ "_meta": {
+ "description": "Enable the new logs overview component."
+ }
+ },
"observability:searchExcludedDataTiers": {
"type": "array",
"items": {
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 3df30d9cf8c30..4bc68d806f043 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -1298,6 +1298,8 @@
"@kbn/observability-get-padded-alert-time-range-util/*": ["x-pack/packages/observability/get_padded_alert_time_range_util/*"],
"@kbn/observability-logs-explorer-plugin": ["x-pack/plugins/observability_solution/observability_logs_explorer"],
"@kbn/observability-logs-explorer-plugin/*": ["x-pack/plugins/observability_solution/observability_logs_explorer/*"],
+ "@kbn/observability-logs-overview": ["x-pack/packages/observability/logs_overview"],
+ "@kbn/observability-logs-overview/*": ["x-pack/packages/observability/logs_overview/*"],
"@kbn/observability-onboarding-e2e": ["x-pack/plugins/observability_solution/observability_onboarding/e2e"],
"@kbn/observability-onboarding-e2e/*": ["x-pack/plugins/observability_solution/observability_onboarding/e2e/*"],
"@kbn/observability-onboarding-plugin": ["x-pack/plugins/observability_solution/observability_onboarding"],
diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json
index a46e291093411..50f2b77b84ad7 100644
--- a/x-pack/.i18nrc.json
+++ b/x-pack/.i18nrc.json
@@ -95,6 +95,9 @@
"xpack.observabilityLogsExplorer": "plugins/observability_solution/observability_logs_explorer",
"xpack.observability_onboarding": "plugins/observability_solution/observability_onboarding",
"xpack.observabilityShared": "plugins/observability_solution/observability_shared",
+ "xpack.observabilityLogsOverview": [
+ "packages/observability/logs_overview/src/components"
+ ],
"xpack.osquery": ["plugins/osquery"],
"xpack.painlessLab": "plugins/painless_lab",
"xpack.profiling": ["plugins/observability_solution/profiling"],
diff --git a/x-pack/packages/observability/logs_overview/README.md b/x-pack/packages/observability/logs_overview/README.md
new file mode 100644
index 0000000000000..20d3f0f02b7df
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/README.md
@@ -0,0 +1,3 @@
+# @kbn/observability-logs-overview
+
+Empty package generated by @kbn/generate
diff --git a/x-pack/packages/observability/logs_overview/index.ts b/x-pack/packages/observability/logs_overview/index.ts
new file mode 100644
index 0000000000000..057d1d3acd152
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/index.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.
+ */
+
+export {
+ LogsOverview,
+ LogsOverviewErrorContent,
+ LogsOverviewLoadingContent,
+ type LogsOverviewDependencies,
+ type LogsOverviewErrorContentProps,
+ type LogsOverviewProps,
+} from './src/components/logs_overview';
+export type {
+ DataViewLogsSourceConfiguration,
+ IndexNameLogsSourceConfiguration,
+ LogsSourceConfiguration,
+ SharedSettingLogsSourceConfiguration,
+} from './src/utils/logs_source';
diff --git a/x-pack/packages/observability/logs_overview/jest.config.js b/x-pack/packages/observability/logs_overview/jest.config.js
new file mode 100644
index 0000000000000..2ee88ee990253
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/jest.config.js
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../../../..',
+ roots: ['/x-pack/packages/observability/logs_overview'],
+};
diff --git a/x-pack/packages/observability/logs_overview/kibana.jsonc b/x-pack/packages/observability/logs_overview/kibana.jsonc
new file mode 100644
index 0000000000000..90b3375086720
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/kibana.jsonc
@@ -0,0 +1,5 @@
+{
+ "type": "shared-browser",
+ "id": "@kbn/observability-logs-overview",
+ "owner": "@elastic/obs-ux-logs-team"
+}
diff --git a/x-pack/packages/observability/logs_overview/package.json b/x-pack/packages/observability/logs_overview/package.json
new file mode 100644
index 0000000000000..77a529e7e59f7
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@kbn/observability-logs-overview",
+ "private": true,
+ "version": "1.0.0",
+ "license": "Elastic License 2.0",
+ "sideEffects": false
+}
diff --git a/x-pack/packages/observability/logs_overview/src/components/discover_link/discover_link.tsx b/x-pack/packages/observability/logs_overview/src/components/discover_link/discover_link.tsx
new file mode 100644
index 0000000000000..fe108289985a9
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/discover_link/discover_link.tsx
@@ -0,0 +1,110 @@
+/*
+ * 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import { EuiButton } from '@elastic/eui';
+import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
+import { FilterStateStore, buildCustomFilter } from '@kbn/es-query';
+import { i18n } from '@kbn/i18n';
+import { getRouterLinkProps } from '@kbn/router-utils';
+import type { SharePluginStart } from '@kbn/share-plugin/public';
+import React, { useCallback, useMemo } from 'react';
+import type { IndexNameLogsSourceConfiguration } from '../../utils/logs_source';
+
+export interface DiscoverLinkProps {
+ documentFilters?: QueryDslQueryContainer[];
+ logsSource: IndexNameLogsSourceConfiguration;
+ timeRange: {
+ start: string;
+ end: string;
+ };
+ dependencies: DiscoverLinkDependencies;
+}
+
+export interface DiscoverLinkDependencies {
+ share: SharePluginStart;
+}
+
+export const DiscoverLink = React.memo(
+ ({ dependencies: { share }, documentFilters, logsSource, timeRange }: DiscoverLinkProps) => {
+ const discoverLocatorParams = useMemo(
+ () => ({
+ dataViewSpec: {
+ id: logsSource.indexName,
+ name: logsSource.indexName,
+ title: logsSource.indexName,
+ timeFieldName: logsSource.timestampField,
+ },
+ timeRange: {
+ from: timeRange.start,
+ to: timeRange.end,
+ },
+ filters: documentFilters?.map((filter) =>
+ buildCustomFilter(
+ logsSource.indexName,
+ filter,
+ false,
+ false,
+ categorizedLogsFilterLabel,
+ FilterStateStore.APP_STATE
+ )
+ ),
+ }),
+ [
+ documentFilters,
+ logsSource.indexName,
+ logsSource.timestampField,
+ timeRange.end,
+ timeRange.start,
+ ]
+ );
+
+ const discoverLocator = useMemo(
+ () => share.url.locators.get('DISCOVER_APP_LOCATOR'),
+ [share.url.locators]
+ );
+
+ const discoverUrl = useMemo(
+ () => discoverLocator?.getRedirectUrl(discoverLocatorParams),
+ [discoverLocatorParams, discoverLocator]
+ );
+
+ const navigateToDiscover = useCallback(() => {
+ discoverLocator?.navigate(discoverLocatorParams);
+ }, [discoverLocatorParams, discoverLocator]);
+
+ const discoverLinkProps = getRouterLinkProps({
+ href: discoverUrl,
+ onClick: navigateToDiscover,
+ });
+
+ return (
+
+ {discoverLinkTitle}
+
+ );
+ }
+);
+
+export const discoverLinkTitle = i18n.translate(
+ 'xpack.observabilityLogsOverview.discoverLinkTitle',
+ {
+ defaultMessage: 'Open in Discover',
+ }
+);
+
+export const categorizedLogsFilterLabel = i18n.translate(
+ 'xpack.observabilityLogsOverview.categorizedLogsFilterLabel',
+ {
+ defaultMessage: 'Categorized log entries',
+ }
+);
diff --git a/x-pack/packages/observability/logs_overview/src/components/discover_link/index.ts b/x-pack/packages/observability/logs_overview/src/components/discover_link/index.ts
new file mode 100644
index 0000000000000..738bf51d4529d
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/discover_link/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 './discover_link';
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/index.ts b/x-pack/packages/observability/logs_overview/src/components/log_categories/index.ts
new file mode 100644
index 0000000000000..786475396237c
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/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 './log_categories';
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories.tsx
new file mode 100644
index 0000000000000..6204667827281
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories.tsx
@@ -0,0 +1,94 @@
+/*
+ * 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import { ISearchGeneric } from '@kbn/search-types';
+import { createConsoleInspector } from '@kbn/xstate-utils';
+import { useMachine } from '@xstate5/react';
+import React, { useCallback } from 'react';
+import {
+ categorizeLogsService,
+ createCategorizeLogsServiceImplementations,
+} from '../../services/categorize_logs_service';
+import { IndexNameLogsSourceConfiguration } from '../../utils/logs_source';
+import { LogCategoriesErrorContent } from './log_categories_error_content';
+import { LogCategoriesLoadingContent } from './log_categories_loading_content';
+import {
+ LogCategoriesResultContent,
+ LogCategoriesResultContentDependencies,
+} from './log_categories_result_content';
+
+export interface LogCategoriesProps {
+ dependencies: LogCategoriesDependencies;
+ documentFilters?: QueryDslQueryContainer[];
+ logsSource: IndexNameLogsSourceConfiguration;
+ // The time range could be made optional if we want to support an internal
+ // time range picker
+ timeRange: {
+ start: string;
+ end: string;
+ };
+}
+
+export type LogCategoriesDependencies = LogCategoriesResultContentDependencies & {
+ search: ISearchGeneric;
+};
+
+export const LogCategories: React.FC = ({
+ dependencies,
+ documentFilters = [],
+ logsSource,
+ timeRange,
+}) => {
+ const [categorizeLogsServiceState, sendToCategorizeLogsService] = useMachine(
+ categorizeLogsService.provide(
+ createCategorizeLogsServiceImplementations({ search: dependencies.search })
+ ),
+ {
+ inspect: consoleInspector,
+ input: {
+ index: logsSource.indexName,
+ startTimestamp: timeRange.start,
+ endTimestamp: timeRange.end,
+ timeField: logsSource.timestampField,
+ messageField: logsSource.messageField,
+ documentFilters,
+ },
+ }
+ );
+
+ const cancelOperation = useCallback(() => {
+ sendToCategorizeLogsService({
+ type: 'cancel',
+ });
+ }, [sendToCategorizeLogsService]);
+
+ if (categorizeLogsServiceState.matches('done')) {
+ return (
+
+ );
+ } else if (categorizeLogsServiceState.matches('failed')) {
+ return ;
+ } else if (categorizeLogsServiceState.matches('countingDocuments')) {
+ return ;
+ } else if (
+ categorizeLogsServiceState.matches('fetchingSampledCategories') ||
+ categorizeLogsServiceState.matches('fetchingRemainingCategories')
+ ) {
+ return ;
+ } else {
+ return null;
+ }
+};
+
+const consoleInspector = createConsoleInspector();
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_control_bar.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_control_bar.tsx
new file mode 100644
index 0000000000000..4538b0ec2fd5d
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_control_bar.tsx
@@ -0,0 +1,44 @@
+/*
+ * 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import type { SharePluginStart } from '@kbn/share-plugin/public';
+import React from 'react';
+import type { IndexNameLogsSourceConfiguration } from '../../utils/logs_source';
+import { DiscoverLink } from '../discover_link';
+
+export interface LogCategoriesControlBarProps {
+ documentFilters?: QueryDslQueryContainer[];
+ logsSource: IndexNameLogsSourceConfiguration;
+ timeRange: {
+ start: string;
+ end: string;
+ };
+ dependencies: LogCategoriesControlBarDependencies;
+}
+
+export interface LogCategoriesControlBarDependencies {
+ share: SharePluginStart;
+}
+
+export const LogCategoriesControlBar: React.FC = React.memo(
+ ({ dependencies, documentFilters, logsSource, timeRange }) => {
+ return (
+
+
+
+
+
+ );
+ }
+);
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_error_content.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_error_content.tsx
new file mode 100644
index 0000000000000..1a335e3265294
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_error_content.tsx
@@ -0,0 +1,44 @@
+/*
+ * 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 { EuiCodeBlock, EuiEmptyPrompt } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+
+export interface LogCategoriesErrorContentProps {
+ error?: Error;
+}
+
+export const LogCategoriesErrorContent: React.FC = ({ error }) => {
+ return (
+ {logsOverviewErrorTitle}}
+ body={
+
+ {error?.stack ?? error?.toString() ?? unknownErrorDescription}
+
+ }
+ layout="vertical"
+ />
+ );
+};
+
+const logsOverviewErrorTitle = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.errorTitle',
+ {
+ defaultMessage: 'Failed to categorize logs',
+ }
+);
+
+const unknownErrorDescription = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.unknownErrorDescription',
+ {
+ defaultMessage: 'An unspecified error occurred.',
+ }
+);
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid.tsx
new file mode 100644
index 0000000000000..d9e960685de99
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid.tsx
@@ -0,0 +1,182 @@
+/*
+ * 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 {
+ EuiDataGrid,
+ EuiDataGridColumnSortingConfig,
+ EuiDataGridPaginationProps,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { createConsoleInspector } from '@kbn/xstate-utils';
+import { useMachine } from '@xstate5/react';
+import _ from 'lodash';
+import React, { useMemo } from 'react';
+import { assign, setup } from 'xstate5';
+import { LogCategory } from '../../types';
+import {
+ LogCategoriesGridCellDependencies,
+ LogCategoriesGridColumnId,
+ createCellContext,
+ logCategoriesGridColumnIds,
+ logCategoriesGridColumns,
+ renderLogCategoriesGridCell,
+} from './log_categories_grid_cell';
+
+export interface LogCategoriesGridProps {
+ dependencies: LogCategoriesGridDependencies;
+ logCategories: LogCategory[];
+}
+
+export type LogCategoriesGridDependencies = LogCategoriesGridCellDependencies;
+
+export const LogCategoriesGrid: React.FC = ({
+ dependencies,
+ logCategories,
+}) => {
+ const [gridState, dispatchGridEvent] = useMachine(gridStateService, {
+ input: {
+ visibleColumns: logCategoriesGridColumns.map(({ id }) => id),
+ },
+ inspect: consoleInspector,
+ });
+
+ const sortedLogCategories = useMemo(() => {
+ const sortingCriteria = gridState.context.sortingColumns.map(
+ ({ id, direction }): [(logCategory: LogCategory) => any, 'asc' | 'desc'] => {
+ switch (id) {
+ case 'count':
+ return [(logCategory: LogCategory) => logCategory.documentCount, direction];
+ case 'change_type':
+ // TODO: use better sorting weight for change types
+ return [(logCategory: LogCategory) => logCategory.change.type, direction];
+ case 'change_time':
+ return [
+ (logCategory: LogCategory) =>
+ 'timestamp' in logCategory.change ? logCategory.change.timestamp ?? '' : '',
+ direction,
+ ];
+ default:
+ return [_.identity, direction];
+ }
+ }
+ );
+ return _.orderBy(
+ logCategories,
+ sortingCriteria.map(([accessor]) => accessor),
+ sortingCriteria.map(([, direction]) => direction)
+ );
+ }, [gridState.context.sortingColumns, logCategories]);
+
+ return (
+
+ dispatchGridEvent({ type: 'changeVisibleColumns', visibleColumns }),
+ }}
+ cellContext={createCellContext(sortedLogCategories, dependencies)}
+ pagination={{
+ ...gridState.context.pagination,
+ onChangeItemsPerPage: (pageSize) => dispatchGridEvent({ type: 'changePageSize', pageSize }),
+ onChangePage: (pageIndex) => dispatchGridEvent({ type: 'changePageIndex', pageIndex }),
+ }}
+ renderCellValue={renderLogCategoriesGridCell}
+ rowCount={sortedLogCategories.length}
+ sorting={{
+ columns: gridState.context.sortingColumns,
+ onSort: (sortingColumns) =>
+ dispatchGridEvent({ type: 'changeSortingColumns', sortingColumns }),
+ }}
+ />
+ );
+};
+
+const gridStateService = setup({
+ types: {
+ context: {} as {
+ visibleColumns: string[];
+ pagination: Pick;
+ sortingColumns: LogCategoriesGridSortingConfig[];
+ },
+ events: {} as
+ | {
+ type: 'changePageSize';
+ pageSize: number;
+ }
+ | {
+ type: 'changePageIndex';
+ pageIndex: number;
+ }
+ | {
+ type: 'changeSortingColumns';
+ sortingColumns: EuiDataGridColumnSortingConfig[];
+ }
+ | {
+ type: 'changeVisibleColumns';
+ visibleColumns: string[];
+ },
+ input: {} as {
+ visibleColumns: string[];
+ },
+ },
+}).createMachine({
+ id: 'logCategoriesGridState',
+ context: ({ input }) => ({
+ visibleColumns: input.visibleColumns,
+ pagination: { pageIndex: 0, pageSize: 20, pageSizeOptions: [10, 20, 50] },
+ sortingColumns: [{ id: 'change_time', direction: 'desc' }],
+ }),
+ on: {
+ changePageSize: {
+ actions: assign(({ context, event }) => ({
+ pagination: {
+ ...context.pagination,
+ pageIndex: 0,
+ pageSize: event.pageSize,
+ },
+ })),
+ },
+ changePageIndex: {
+ actions: assign(({ context, event }) => ({
+ pagination: {
+ ...context.pagination,
+ pageIndex: event.pageIndex,
+ },
+ })),
+ },
+ changeSortingColumns: {
+ actions: assign(({ event }) => ({
+ sortingColumns: event.sortingColumns.filter(
+ (sortingConfig): sortingConfig is LogCategoriesGridSortingConfig =>
+ (logCategoriesGridColumnIds as string[]).includes(sortingConfig.id)
+ ),
+ })),
+ },
+ changeVisibleColumns: {
+ actions: assign(({ event }) => ({
+ visibleColumns: event.visibleColumns,
+ })),
+ },
+ },
+});
+
+const consoleInspector = createConsoleInspector();
+
+const logCategoriesGridLabel = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategoriesGrid.euiDataGrid.logCategoriesLabel',
+ { defaultMessage: 'Log categories' }
+);
+
+interface TypedEuiDataGridColumnSortingConfig
+ extends EuiDataGridColumnSortingConfig {
+ id: ColumnId;
+}
+
+type LogCategoriesGridSortingConfig =
+ TypedEuiDataGridColumnSortingConfig;
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_cell.tsx
new file mode 100644
index 0000000000000..d6ab4969eaf7b
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_cell.tsx
@@ -0,0 +1,99 @@
+/*
+ * 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 { EuiDataGridColumn, RenderCellValue } from '@elastic/eui';
+import React from 'react';
+import { LogCategory } from '../../types';
+import {
+ LogCategoriesGridChangeTimeCell,
+ LogCategoriesGridChangeTimeCellDependencies,
+ logCategoriesGridChangeTimeColumn,
+} from './log_categories_grid_change_time_cell';
+import {
+ LogCategoriesGridChangeTypeCell,
+ logCategoriesGridChangeTypeColumn,
+} from './log_categories_grid_change_type_cell';
+import {
+ LogCategoriesGridCountCell,
+ logCategoriesGridCountColumn,
+} from './log_categories_grid_count_cell';
+import {
+ LogCategoriesGridHistogramCell,
+ LogCategoriesGridHistogramCellDependencies,
+ logCategoriesGridHistoryColumn,
+} from './log_categories_grid_histogram_cell';
+import {
+ LogCategoriesGridPatternCell,
+ logCategoriesGridPatternColumn,
+} from './log_categories_grid_pattern_cell';
+
+export interface LogCategoriesGridCellContext {
+ dependencies: LogCategoriesGridCellDependencies;
+ logCategories: LogCategory[];
+}
+
+export type LogCategoriesGridCellDependencies = LogCategoriesGridHistogramCellDependencies &
+ LogCategoriesGridChangeTimeCellDependencies;
+
+export const renderLogCategoriesGridCell: RenderCellValue = ({
+ rowIndex,
+ columnId,
+ isExpanded,
+ ...rest
+}) => {
+ const { dependencies, logCategories } = getCellContext(rest);
+
+ const logCategory = logCategories[rowIndex];
+
+ switch (columnId as LogCategoriesGridColumnId) {
+ case 'pattern':
+ return ;
+ case 'count':
+ return ;
+ case 'history':
+ return (
+
+ );
+ case 'change_type':
+ return ;
+ case 'change_time':
+ return (
+
+ );
+ default:
+ return <>->;
+ }
+};
+
+export const logCategoriesGridColumns = [
+ logCategoriesGridPatternColumn,
+ logCategoriesGridCountColumn,
+ logCategoriesGridChangeTypeColumn,
+ logCategoriesGridChangeTimeColumn,
+ logCategoriesGridHistoryColumn,
+] satisfies EuiDataGridColumn[];
+
+export const logCategoriesGridColumnIds = logCategoriesGridColumns.map(({ id }) => id);
+
+export type LogCategoriesGridColumnId = (typeof logCategoriesGridColumns)[number]['id'];
+
+const cellContextKey = 'cellContext';
+
+const getCellContext = (cellContext: object): LogCategoriesGridCellContext =>
+ (cellContextKey in cellContext
+ ? cellContext[cellContextKey]
+ : {}) as LogCategoriesGridCellContext;
+
+export const createCellContext = (
+ logCategories: LogCategory[],
+ dependencies: LogCategoriesGridCellDependencies
+): { [cellContextKey]: LogCategoriesGridCellContext } => ({
+ [cellContextKey]: {
+ dependencies,
+ logCategories,
+ },
+});
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_time_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_time_cell.tsx
new file mode 100644
index 0000000000000..5ad8cbdd49346
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_time_cell.tsx
@@ -0,0 +1,54 @@
+/*
+ * 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 { EuiDataGridColumn } from '@elastic/eui';
+import { SettingsStart } from '@kbn/core-ui-settings-browser';
+import { i18n } from '@kbn/i18n';
+import moment from 'moment';
+import React, { useMemo } from 'react';
+import { LogCategory } from '../../types';
+
+export const logCategoriesGridChangeTimeColumn = {
+ id: 'change_time' as const,
+ display: i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategoriesGrid.changeTimeColumnLabel',
+ {
+ defaultMessage: 'Change at',
+ }
+ ),
+ isSortable: true,
+ initialWidth: 220,
+ schema: 'datetime',
+} satisfies EuiDataGridColumn;
+
+export interface LogCategoriesGridChangeTimeCellProps {
+ dependencies: LogCategoriesGridChangeTimeCellDependencies;
+ logCategory: LogCategory;
+}
+
+export interface LogCategoriesGridChangeTimeCellDependencies {
+ uiSettings: SettingsStart;
+}
+
+export const LogCategoriesGridChangeTimeCell: React.FC = ({
+ dependencies,
+ logCategory,
+}) => {
+ const dateFormat = useMemo(
+ () => dependencies.uiSettings.client.get('dateFormat'),
+ [dependencies.uiSettings.client]
+ );
+ if (!('timestamp' in logCategory.change && logCategory.change.timestamp != null)) {
+ return null;
+ }
+
+ if (dateFormat) {
+ return <>{moment(logCategory.change.timestamp).format(dateFormat)}>;
+ } else {
+ return <>{logCategory.change.timestamp}>;
+ }
+};
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_type_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_type_cell.tsx
new file mode 100644
index 0000000000000..af6349bd0e18c
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_change_type_cell.tsx
@@ -0,0 +1,108 @@
+/*
+ * 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 { EuiBadge, EuiDataGridColumn } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { LogCategory } from '../../types';
+
+export const logCategoriesGridChangeTypeColumn = {
+ id: 'change_type' as const,
+ display: i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategoriesGrid.changeTypeColumnLabel',
+ {
+ defaultMessage: 'Change type',
+ }
+ ),
+ isSortable: true,
+ initialWidth: 110,
+} satisfies EuiDataGridColumn;
+
+export interface LogCategoriesGridChangeTypeCellProps {
+ logCategory: LogCategory;
+}
+
+export const LogCategoriesGridChangeTypeCell: React.FC = ({
+ logCategory,
+}) => {
+ switch (logCategory.change.type) {
+ case 'dip':
+ return {dipBadgeLabel} ;
+ case 'spike':
+ return {spikeBadgeLabel} ;
+ case 'step':
+ return {stepBadgeLabel} ;
+ case 'distribution':
+ return {distributionBadgeLabel} ;
+ case 'rare':
+ return {rareBadgeLabel} ;
+ case 'trend':
+ return {trendBadgeLabel} ;
+ case 'other':
+ return {otherBadgeLabel} ;
+ case 'none':
+ return <>->;
+ default:
+ return {unknownBadgeLabel} ;
+ }
+};
+
+const dipBadgeLabel = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.dipChangeTypeBadgeLabel',
+ {
+ defaultMessage: 'Dip',
+ }
+);
+
+const spikeBadgeLabel = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.spikeChangeTypeBadgeLabel',
+ {
+ defaultMessage: 'Spike',
+ }
+);
+
+const stepBadgeLabel = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.spikeChangeTypeBadgeLabel',
+ {
+ defaultMessage: 'Step',
+ }
+);
+
+const distributionBadgeLabel = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.distributionChangeTypeBadgeLabel',
+ {
+ defaultMessage: 'Distribution',
+ }
+);
+
+const trendBadgeLabel = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.spikeChangeTypeBadgeLabel',
+ {
+ defaultMessage: 'Trend',
+ }
+);
+
+const otherBadgeLabel = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.otherChangeTypeBadgeLabel',
+ {
+ defaultMessage: 'Other',
+ }
+);
+
+const unknownBadgeLabel = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.unknownChangeTypeBadgeLabel',
+ {
+ defaultMessage: 'Unknown',
+ }
+);
+
+const rareBadgeLabel = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.rareChangeTypeBadgeLabel',
+ {
+ defaultMessage: 'Rare',
+ }
+);
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_count_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_count_cell.tsx
new file mode 100644
index 0000000000000..f2247aab5212e
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_count_cell.tsx
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiDataGridColumn } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedNumber } from '@kbn/i18n-react';
+import React from 'react';
+import { LogCategory } from '../../types';
+
+export const logCategoriesGridCountColumn = {
+ id: 'count' as const,
+ display: i18n.translate('xpack.observabilityLogsOverview.logCategoriesGrid.countColumnLabel', {
+ defaultMessage: 'Events',
+ }),
+ isSortable: true,
+ schema: 'numeric',
+ initialWidth: 100,
+} satisfies EuiDataGridColumn;
+
+export interface LogCategoriesGridCountCellProps {
+ logCategory: LogCategory;
+}
+
+export const LogCategoriesGridCountCell: React.FC = ({
+ logCategory,
+}) => {
+ return ;
+};
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_histogram_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_histogram_cell.tsx
new file mode 100644
index 0000000000000..2fb50b0f2f3b4
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_histogram_cell.tsx
@@ -0,0 +1,99 @@
+/*
+ * 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 {
+ BarSeries,
+ Chart,
+ LineAnnotation,
+ LineAnnotationStyle,
+ PartialTheme,
+ Settings,
+ Tooltip,
+ TooltipType,
+} from '@elastic/charts';
+import { EuiDataGridColumn } from '@elastic/eui';
+import { ChartsPluginStart } from '@kbn/charts-plugin/public';
+import { i18n } from '@kbn/i18n';
+import { RecursivePartial } from '@kbn/utility-types';
+import React from 'react';
+import { LogCategory, LogCategoryHistogramBucket } from '../../types';
+
+export const logCategoriesGridHistoryColumn = {
+ id: 'history' as const,
+ display: i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategoriesGrid.histogramColumnLabel',
+ {
+ defaultMessage: 'Timeline',
+ }
+ ),
+ isSortable: false,
+ initialWidth: 250,
+ isExpandable: false,
+} satisfies EuiDataGridColumn;
+
+export interface LogCategoriesGridHistogramCellProps {
+ dependencies: LogCategoriesGridHistogramCellDependencies;
+ logCategory: LogCategory;
+}
+
+export interface LogCategoriesGridHistogramCellDependencies {
+ charts: ChartsPluginStart;
+}
+
+export const LogCategoriesGridHistogramCell: React.FC = ({
+ dependencies: { charts },
+ logCategory,
+}) => {
+ const baseTheme = charts.theme.useChartsBaseTheme();
+ const sparklineTheme = charts.theme.useSparklineOverrides();
+
+ return (
+
+
+
+
+ {'timestamp' in logCategory.change && logCategory.change.timestamp != null ? (
+
+ ) : null}
+
+ );
+};
+
+const localThemeOverrides: PartialTheme = {
+ scales: {
+ histogramPadding: 0.1,
+ },
+ background: {
+ color: 'transparent',
+ },
+};
+
+const annotationStyle: RecursivePartial = {
+ line: {
+ strokeWidth: 2,
+ },
+};
+
+const timestampAccessor = (histogram: LogCategoryHistogramBucket) =>
+ new Date(histogram.timestamp).getTime();
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_pattern_cell.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_pattern_cell.tsx
new file mode 100644
index 0000000000000..d507487a99e3c
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_grid_pattern_cell.tsx
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiDataGridColumn, useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/react';
+import { i18n } from '@kbn/i18n';
+import React, { useMemo } from 'react';
+import { LogCategory } from '../../types';
+
+export const logCategoriesGridPatternColumn = {
+ id: 'pattern' as const,
+ display: i18n.translate('xpack.observabilityLogsOverview.logCategoriesGrid.patternColumnLabel', {
+ defaultMessage: 'Pattern',
+ }),
+ isSortable: false,
+ schema: 'string',
+} satisfies EuiDataGridColumn;
+
+export interface LogCategoriesGridPatternCellProps {
+ logCategory: LogCategory;
+}
+
+export const LogCategoriesGridPatternCell: React.FC = ({
+ logCategory,
+}) => {
+ const theme = useEuiTheme();
+ const { euiTheme } = theme;
+ const termsList = useMemo(() => logCategory.terms.split(' '), [logCategory.terms]);
+
+ const commonStyle = css`
+ display: inline-block;
+ font-family: ${euiTheme.font.familyCode};
+ margin-right: ${euiTheme.size.xs};
+ `;
+
+ const termStyle = css`
+ ${commonStyle};
+ `;
+
+ const separatorStyle = css`
+ ${commonStyle};
+ color: ${euiTheme.colors.successText};
+ `;
+
+ return (
+
+ *
+ {termsList.map((term, index) => (
+
+ {term}
+ *
+
+ ))}
+
+ );
+};
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_loading_content.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_loading_content.tsx
new file mode 100644
index 0000000000000..0fde469fe717d
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_loading_content.tsx
@@ -0,0 +1,68 @@
+/*
+ * 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 { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui';
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+
+export interface LogCategoriesLoadingContentProps {
+ onCancel?: () => void;
+ stage: 'counting' | 'categorizing';
+}
+
+export const LogCategoriesLoadingContent: React.FC = ({
+ onCancel,
+ stage,
+}) => {
+ return (
+ }
+ title={
+
+ {stage === 'counting'
+ ? logCategoriesLoadingStateCountingTitle
+ : logCategoriesLoadingStateCategorizingTitle}
+
+ }
+ actions={
+ onCancel != null
+ ? [
+ {
+ onCancel();
+ }}
+ >
+ {logCategoriesLoadingStateCancelButtonLabel}
+ ,
+ ]
+ : []
+ }
+ />
+ );
+};
+
+const logCategoriesLoadingStateCountingTitle = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategoriesGrid.loadingStageCountingTitle',
+ {
+ defaultMessage: 'Estimating log volume',
+ }
+);
+
+const logCategoriesLoadingStateCategorizingTitle = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategoriesGrid.loadingStageCategorizingTitle',
+ {
+ defaultMessage: 'Categorizing logs',
+ }
+);
+
+const logCategoriesLoadingStateCancelButtonLabel = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategoriesGrid.loadingStateCancelButtonLabel',
+ {
+ defaultMessage: 'Cancel',
+ }
+);
diff --git a/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_result_content.tsx b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_result_content.tsx
new file mode 100644
index 0000000000000..e16bdda7cb44a
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/log_categories/log_categories_result_content.tsx
@@ -0,0 +1,87 @@
+/*
+ * 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { LogCategory } from '../../types';
+import { IndexNameLogsSourceConfiguration } from '../../utils/logs_source';
+import {
+ LogCategoriesControlBar,
+ LogCategoriesControlBarDependencies,
+} from './log_categories_control_bar';
+import { LogCategoriesGrid, LogCategoriesGridDependencies } from './log_categories_grid';
+
+export interface LogCategoriesResultContentProps {
+ dependencies: LogCategoriesResultContentDependencies;
+ documentFilters?: QueryDslQueryContainer[];
+ logCategories: LogCategory[];
+ logsSource: IndexNameLogsSourceConfiguration;
+ timeRange: {
+ start: string;
+ end: string;
+ };
+}
+
+export type LogCategoriesResultContentDependencies = LogCategoriesControlBarDependencies &
+ LogCategoriesGridDependencies;
+
+export const LogCategoriesResultContent: React.FC = ({
+ dependencies,
+ documentFilters,
+ logCategories,
+ logsSource,
+ timeRange,
+}) => {
+ if (logCategories.length === 0) {
+ return ;
+ } else {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+};
+
+export const LogCategoriesEmptyResultContent: React.FC = () => {
+ return (
+ {emptyResultContentDescription}
}
+ color="subdued"
+ layout="horizontal"
+ title={{emptyResultContentTitle} }
+ titleSize="m"
+ />
+ );
+};
+
+const emptyResultContentTitle = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.emptyResultContentTitle',
+ {
+ defaultMessage: 'No log categories found',
+ }
+);
+
+const emptyResultContentDescription = i18n.translate(
+ 'xpack.observabilityLogsOverview.logCategories.emptyResultContentDescription',
+ {
+ defaultMessage:
+ 'No suitable documents within the time range. Try searching for a longer time period.',
+ }
+);
diff --git a/x-pack/packages/observability/logs_overview/src/components/logs_overview/index.ts b/x-pack/packages/observability/logs_overview/src/components/logs_overview/index.ts
new file mode 100644
index 0000000000000..878f634f078ad
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/logs_overview/index.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export * from './logs_overview';
+export * from './logs_overview_error_content';
+export * from './logs_overview_loading_content';
diff --git a/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview.tsx b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview.tsx
new file mode 100644
index 0000000000000..988656eb1571e
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview.tsx
@@ -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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import { type LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public';
+import React from 'react';
+import useAsync from 'react-use/lib/useAsync';
+import { LogsSourceConfiguration, normalizeLogsSource } from '../../utils/logs_source';
+import { LogCategories, LogCategoriesDependencies } from '../log_categories';
+import { LogsOverviewErrorContent } from './logs_overview_error_content';
+import { LogsOverviewLoadingContent } from './logs_overview_loading_content';
+
+export interface LogsOverviewProps {
+ dependencies: LogsOverviewDependencies;
+ documentFilters?: QueryDslQueryContainer[];
+ logsSource?: LogsSourceConfiguration;
+ timeRange: {
+ start: string;
+ end: string;
+ };
+}
+
+export type LogsOverviewDependencies = LogCategoriesDependencies & {
+ logsDataAccess: LogsDataAccessPluginStart;
+};
+
+export const LogsOverview: React.FC = React.memo(
+ ({
+ dependencies,
+ documentFilters = defaultDocumentFilters,
+ logsSource = defaultLogsSource,
+ timeRange,
+ }) => {
+ const normalizedLogsSource = useAsync(
+ () => normalizeLogsSource({ logsDataAccess: dependencies.logsDataAccess })(logsSource),
+ [dependencies.logsDataAccess, logsSource]
+ );
+
+ if (normalizedLogsSource.loading) {
+ return ;
+ }
+
+ if (normalizedLogsSource.error != null || normalizedLogsSource.value == null) {
+ return ;
+ }
+
+ return (
+
+ );
+ }
+);
+
+const defaultDocumentFilters: QueryDslQueryContainer[] = [];
+
+const defaultLogsSource: LogsSourceConfiguration = { type: 'shared_setting' };
diff --git a/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_error_content.tsx b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_error_content.tsx
new file mode 100644
index 0000000000000..73586756bb908
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_error_content.tsx
@@ -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 { EuiCodeBlock, EuiEmptyPrompt } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+
+export interface LogsOverviewErrorContentProps {
+ error?: Error;
+}
+
+export const LogsOverviewErrorContent: React.FC = ({ error }) => {
+ return (
+ {logsOverviewErrorTitle}}
+ body={
+
+ {error?.stack ?? error?.toString() ?? unknownErrorDescription}
+
+ }
+ layout="vertical"
+ />
+ );
+};
+
+const logsOverviewErrorTitle = i18n.translate('xpack.observabilityLogsOverview.errorTitle', {
+ defaultMessage: 'Error',
+});
+
+const unknownErrorDescription = i18n.translate(
+ 'xpack.observabilityLogsOverview.unknownErrorDescription',
+ {
+ defaultMessage: 'An unspecified error occurred.',
+ }
+);
diff --git a/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_loading_content.tsx b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_loading_content.tsx
new file mode 100644
index 0000000000000..7645fdb90f0ac
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/components/logs_overview/logs_overview_loading_content.tsx
@@ -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 { EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+
+export const LogsOverviewLoadingContent: React.FC = ({}) => {
+ return (
+ }
+ title={{logsOverviewLoadingTitle} }
+ />
+ );
+};
+
+const logsOverviewLoadingTitle = i18n.translate('xpack.observabilityLogsOverview.loadingTitle', {
+ defaultMessage: 'Loading',
+});
diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_documents.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_documents.ts
new file mode 100644
index 0000000000000..7260efe63d435
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_documents.ts
@@ -0,0 +1,282 @@
+/*
+ * 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 { ISearchGeneric } from '@kbn/search-types';
+import { lastValueFrom } from 'rxjs';
+import { fromPromise } from 'xstate5';
+import { createRandomSamplerWrapper } from '@kbn/ml-random-sampler-utils';
+import { z } from '@kbn/zod';
+import { LogCategorizationParams } from './types';
+import { createCategorizationRequestParams } from './queries';
+import { LogCategory, LogCategoryChange } from '../../types';
+
+// the fraction of a category's histogram below which the category is considered rare
+const rarityThreshold = 0.2;
+const maxCategoriesCount = 1000;
+
+export const categorizeDocuments = ({ search }: { search: ISearchGeneric }) =>
+ fromPromise<
+ {
+ categories: LogCategory[];
+ hasReachedLimit: boolean;
+ },
+ LogCategorizationParams & {
+ samplingProbability: number;
+ ignoredCategoryTerms: string[];
+ minDocsPerCategory: number;
+ }
+ >(
+ async ({
+ input: {
+ index,
+ endTimestamp,
+ startTimestamp,
+ timeField,
+ messageField,
+ samplingProbability,
+ ignoredCategoryTerms,
+ documentFilters = [],
+ minDocsPerCategory,
+ },
+ signal,
+ }) => {
+ const randomSampler = createRandomSamplerWrapper({
+ probability: samplingProbability,
+ seed: 1,
+ });
+
+ const requestParams = createCategorizationRequestParams({
+ index,
+ timeField,
+ messageField,
+ startTimestamp,
+ endTimestamp,
+ randomSampler,
+ additionalFilters: documentFilters,
+ ignoredCategoryTerms,
+ minDocsPerCategory,
+ maxCategoriesCount,
+ });
+
+ const { rawResponse } = await lastValueFrom(
+ search({ params: requestParams }, { abortSignal: signal })
+ );
+
+ if (rawResponse.aggregations == null) {
+ throw new Error('No aggregations found in large categories response');
+ }
+
+ const logCategoriesAggResult = randomSampler.unwrap(rawResponse.aggregations);
+
+ if (!('categories' in logCategoriesAggResult)) {
+ throw new Error('No categorization aggregation found in large categories response');
+ }
+
+ const logCategories =
+ (logCategoriesAggResult.categories.buckets as unknown[]).map(mapCategoryBucket) ?? [];
+
+ return {
+ categories: logCategories,
+ hasReachedLimit: logCategories.length >= maxCategoriesCount,
+ };
+ }
+ );
+
+const mapCategoryBucket = (bucket: any): LogCategory =>
+ esCategoryBucketSchema
+ .transform((parsedBucket) => ({
+ change: mapChangePoint(parsedBucket),
+ documentCount: parsedBucket.doc_count,
+ histogram: parsedBucket.histogram,
+ terms: parsedBucket.key,
+ }))
+ .parse(bucket);
+
+const mapChangePoint = ({ change, histogram }: EsCategoryBucket): LogCategoryChange => {
+ switch (change.type) {
+ case 'stationary':
+ if (isRareInHistogram(histogram)) {
+ return {
+ type: 'rare',
+ timestamp: findFirstNonZeroBucket(histogram)?.timestamp ?? histogram[0].timestamp,
+ };
+ } else {
+ return {
+ type: 'none',
+ };
+ }
+ case 'dip':
+ case 'spike':
+ return {
+ type: change.type,
+ timestamp: change.bucket.key,
+ };
+ case 'step_change':
+ return {
+ type: 'step',
+ timestamp: change.bucket.key,
+ };
+ case 'distribution_change':
+ return {
+ type: 'distribution',
+ timestamp: change.bucket.key,
+ };
+ case 'trend_change':
+ return {
+ type: 'trend',
+ timestamp: change.bucket.key,
+ correlationCoefficient: change.details.r_value,
+ };
+ case 'unknown':
+ return {
+ type: 'unknown',
+ rawChange: change.rawChange,
+ };
+ case 'non_stationary':
+ default:
+ return {
+ type: 'other',
+ };
+ }
+};
+
+/**
+ * The official types are lacking the change_point aggregation
+ */
+const esChangePointBucketSchema = z.object({
+ key: z.string().datetime(),
+ doc_count: z.number(),
+});
+
+const esChangePointDetailsSchema = z.object({
+ p_value: z.number(),
+});
+
+const esChangePointCorrelationSchema = esChangePointDetailsSchema.extend({
+ r_value: z.number(),
+});
+
+const esChangePointSchema = z.union([
+ z
+ .object({
+ bucket: esChangePointBucketSchema,
+ type: z.object({
+ dip: esChangePointDetailsSchema,
+ }),
+ })
+ .transform(({ bucket, type: { dip: details } }) => ({
+ type: 'dip' as const,
+ bucket,
+ details,
+ })),
+ z
+ .object({
+ bucket: esChangePointBucketSchema,
+ type: z.object({
+ spike: esChangePointDetailsSchema,
+ }),
+ })
+ .transform(({ bucket, type: { spike: details } }) => ({
+ type: 'spike' as const,
+ bucket,
+ details,
+ })),
+ z
+ .object({
+ bucket: esChangePointBucketSchema,
+ type: z.object({
+ step_change: esChangePointDetailsSchema,
+ }),
+ })
+ .transform(({ bucket, type: { step_change: details } }) => ({
+ type: 'step_change' as const,
+ bucket,
+ details,
+ })),
+ z
+ .object({
+ bucket: esChangePointBucketSchema,
+ type: z.object({
+ trend_change: esChangePointCorrelationSchema,
+ }),
+ })
+ .transform(({ bucket, type: { trend_change: details } }) => ({
+ type: 'trend_change' as const,
+ bucket,
+ details,
+ })),
+ z
+ .object({
+ bucket: esChangePointBucketSchema,
+ type: z.object({
+ distribution_change: esChangePointDetailsSchema,
+ }),
+ })
+ .transform(({ bucket, type: { distribution_change: details } }) => ({
+ type: 'distribution_change' as const,
+ bucket,
+ details,
+ })),
+ z
+ .object({
+ type: z.object({
+ non_stationary: esChangePointCorrelationSchema.extend({
+ trend: z.enum(['increasing', 'decreasing']),
+ }),
+ }),
+ })
+ .transform(({ type: { non_stationary: details } }) => ({
+ type: 'non_stationary' as const,
+ details,
+ })),
+ z
+ .object({
+ type: z.object({
+ stationary: z.object({}),
+ }),
+ })
+ .transform(() => ({ type: 'stationary' as const })),
+ z
+ .object({
+ type: z.object({}),
+ })
+ .transform((value) => ({ type: 'unknown' as const, rawChange: JSON.stringify(value) })),
+]);
+
+const esHistogramSchema = z
+ .object({
+ buckets: z.array(
+ z
+ .object({
+ key_as_string: z.string(),
+ doc_count: z.number(),
+ })
+ .transform((bucket) => ({
+ timestamp: bucket.key_as_string,
+ documentCount: bucket.doc_count,
+ }))
+ ),
+ })
+ .transform(({ buckets }) => buckets);
+
+type EsHistogram = z.output;
+
+const esCategoryBucketSchema = z.object({
+ key: z.string(),
+ doc_count: z.number(),
+ change: esChangePointSchema,
+ histogram: esHistogramSchema,
+});
+
+type EsCategoryBucket = z.output;
+
+const isRareInHistogram = (histogram: EsHistogram): boolean =>
+ histogram.filter((bucket) => bucket.documentCount > 0).length <
+ histogram.length * rarityThreshold;
+
+const findFirstNonZeroBucket = (histogram: EsHistogram) =>
+ histogram.find((bucket) => bucket.documentCount > 0);
diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_logs_service.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_logs_service.ts
new file mode 100644
index 0000000000000..deeb758d2d737
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/categorize_logs_service.ts
@@ -0,0 +1,250 @@
+/*
+ * 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 { MachineImplementationsFrom, assign, setup } from 'xstate5';
+import { LogCategory } from '../../types';
+import { getPlaceholderFor } from '../../utils/xstate5_utils';
+import { categorizeDocuments } from './categorize_documents';
+import { countDocuments } from './count_documents';
+import { CategorizeLogsServiceDependencies, LogCategorizationParams } from './types';
+
+export const categorizeLogsService = setup({
+ types: {
+ input: {} as LogCategorizationParams,
+ output: {} as {
+ categories: LogCategory[];
+ documentCount: number;
+ hasReachedLimit: boolean;
+ samplingProbability: number;
+ },
+ context: {} as {
+ categories: LogCategory[];
+ documentCount: number;
+ error?: Error;
+ hasReachedLimit: boolean;
+ parameters: LogCategorizationParams;
+ samplingProbability: number;
+ },
+ events: {} as {
+ type: 'cancel';
+ },
+ },
+ actors: {
+ countDocuments: getPlaceholderFor(countDocuments),
+ categorizeDocuments: getPlaceholderFor(categorizeDocuments),
+ },
+ actions: {
+ storeError: assign((_, params: { error: unknown }) => ({
+ error: params.error instanceof Error ? params.error : new Error(String(params.error)),
+ })),
+ storeCategories: assign(
+ ({ context }, params: { categories: LogCategory[]; hasReachedLimit: boolean }) => ({
+ categories: [...context.categories, ...params.categories],
+ hasReachedLimit: params.hasReachedLimit,
+ })
+ ),
+ storeDocumentCount: assign(
+ (_, params: { documentCount: number; samplingProbability: number }) => ({
+ documentCount: params.documentCount,
+ samplingProbability: params.samplingProbability,
+ })
+ ),
+ },
+ guards: {
+ hasTooFewDocuments: (_guardArgs, params: { documentCount: number }) => params.documentCount < 1,
+ requiresSampling: (_guardArgs, params: { samplingProbability: number }) =>
+ params.samplingProbability < 1,
+ },
+}).createMachine({
+ /** @xstate-layout N4IgpgJg5mDOIC5QGMCGAXMUD2AnAlgF5gAy2UsAdMtgK4B26+9UAItsrQLZiOwDEEbPTCVmAN2wBrUWkw4CxMhWp1GzNh2690sBBI4Z8wgNoAGALrmLiUAAdssfE2G2QAD0QBmMwA5KACy+AQFmob4AjABMwQBsADQgAJ6IkYEAnJkA7FmxZlERmQGxAL4liXJYeESk5FQ0DEws7Jw8fILCogYy1BhVirUqDerNWm26+vSScsb01iYRNkggDk4u9G6eCD7+QSFhftFxiSkIvgCsWZSxEVlRsbFZ52Zm515lFX0KNcr1ak2aVo6ARCERiKbSWRfapKOqqRoaFraPiTaZGUyWExRJb2RzOWabbx+QLBULhI7FE7eWL+F45GnRPIRZkfECVb6wob-RFjYH8MC4XB4Sh2AA2GAAZnguL15DDBn8EaMgSiDDMMVZLG5VvjXMstjsSftyTFKclEOdzgFKF5zukvA8zBFnl50udWez5b94SNAcjdPw0PRkGBRdZtXj1oTtsS9mTDqaEuaEBF8udKFkIr5fK6olkzOksgEPdCBt6JWB0MgABYaADKqC4YsgAGFS-g4B0wd0oXKBg2m6LW+24OHljqo-rEMzbpQos8-K7fC9CknTrF0rEbbb0oVMoWIgF3eU2e3OVQK1XaywB82IG2+x2BAKhbgReL0FLcDLPf3G3eH36J8x1xNYCSnFNmSuecXhzdJlydTcqQQLJfHSOc0PyLJN3SMxYiPEtH3PShLxret-yHe8RwEIMQzDLVx0jcDQC2GdoIXOCENXZDsyiOcAiiKJ0iiPDLi8V1CKA4jSOvKAACUwC4VBmA0QDvk7UEughHpfxqBSlJUlg1OqUcGNA3UNggrMs347IjzdaIvGQwSvECXI8k3Z43gEiJJI5BUSMrMiWH05T6FU6j+UFYUxUlaVZSksBQsMqBjIIUycRWJi9RY6dIn8KIAjsu1zkc5CAmiG1fBiaIzB8B0QmPT4iICmSNGS8KjMi2jQxArKwJyjw8pswriocqInOTLwIi3ASD1yQpswCd5WXobAIDgNxdPPCMBss3KEAAWjXRBDvTfcLsu9Jlr8r04WGAEkXGeBGL26MBOQzIt2ut4cwmirCt8W6yzhNqbwo4dH0216LOjTMIjnBdYhK1DYgdHjihtZbUIdWIXJuYGflBoLZI6iKoZe8zJwOw9KtGt1kbuTcsmQrwi0oeCQjzZ5blwt1Cek5TKN22GIIKZbAgKC45pyLyeLwtz4Kyabs1QgWAs0kXqaGhBxdcnzpaE2XXmch0MORmaBJeLwjbKMogA */
+ id: 'categorizeLogs',
+ context: ({ input }) => ({
+ categories: [],
+ documentCount: 0,
+ hasReachedLimit: false,
+ parameters: input,
+ samplingProbability: 1,
+ }),
+ initial: 'countingDocuments',
+ states: {
+ countingDocuments: {
+ invoke: {
+ src: 'countDocuments',
+ input: ({ context }) => context.parameters,
+ onDone: [
+ {
+ target: 'done',
+ guard: {
+ type: 'hasTooFewDocuments',
+ params: ({ event }) => event.output,
+ },
+ actions: [
+ {
+ type: 'storeDocumentCount',
+ params: ({ event }) => event.output,
+ },
+ ],
+ },
+ {
+ target: 'fetchingSampledCategories',
+ guard: {
+ type: 'requiresSampling',
+ params: ({ event }) => event.output,
+ },
+ actions: [
+ {
+ type: 'storeDocumentCount',
+ params: ({ event }) => event.output,
+ },
+ ],
+ },
+ {
+ target: 'fetchingRemainingCategories',
+ actions: [
+ {
+ type: 'storeDocumentCount',
+ params: ({ event }) => event.output,
+ },
+ ],
+ },
+ ],
+ onError: {
+ target: 'failed',
+ actions: [
+ {
+ type: 'storeError',
+ params: ({ event }) => ({ error: event.error }),
+ },
+ ],
+ },
+ },
+
+ on: {
+ cancel: {
+ target: 'failed',
+ actions: [
+ {
+ type: 'storeError',
+ params: () => ({ error: new Error('Counting cancelled') }),
+ },
+ ],
+ },
+ },
+ },
+
+ fetchingSampledCategories: {
+ invoke: {
+ src: 'categorizeDocuments',
+ id: 'categorizeSampledCategories',
+ input: ({ context }) => ({
+ ...context.parameters,
+ samplingProbability: context.samplingProbability,
+ ignoredCategoryTerms: [],
+ minDocsPerCategory: 10,
+ }),
+ onDone: {
+ target: 'fetchingRemainingCategories',
+ actions: [
+ {
+ type: 'storeCategories',
+ params: ({ event }) => event.output,
+ },
+ ],
+ },
+ onError: {
+ target: 'failed',
+ actions: [
+ {
+ type: 'storeError',
+ params: ({ event }) => ({ error: event.error }),
+ },
+ ],
+ },
+ },
+
+ on: {
+ cancel: {
+ target: 'failed',
+ actions: [
+ {
+ type: 'storeError',
+ params: () => ({ error: new Error('Categorization cancelled') }),
+ },
+ ],
+ },
+ },
+ },
+
+ fetchingRemainingCategories: {
+ invoke: {
+ src: 'categorizeDocuments',
+ id: 'categorizeRemainingCategories',
+ input: ({ context }) => ({
+ ...context.parameters,
+ samplingProbability: 1,
+ ignoredCategoryTerms: context.categories.map((category) => category.terms),
+ minDocsPerCategory: 0,
+ }),
+ onDone: {
+ target: 'done',
+ actions: [
+ {
+ type: 'storeCategories',
+ params: ({ event }) => event.output,
+ },
+ ],
+ },
+ onError: {
+ target: 'failed',
+ actions: [
+ {
+ type: 'storeError',
+ params: ({ event }) => ({ error: event.error }),
+ },
+ ],
+ },
+ },
+
+ on: {
+ cancel: {
+ target: 'failed',
+ actions: [
+ {
+ type: 'storeError',
+ params: () => ({ error: new Error('Categorization cancelled') }),
+ },
+ ],
+ },
+ },
+ },
+
+ failed: {
+ type: 'final',
+ },
+
+ done: {
+ type: 'final',
+ },
+ },
+ output: ({ context }) => ({
+ categories: context.categories,
+ documentCount: context.documentCount,
+ hasReachedLimit: context.hasReachedLimit,
+ samplingProbability: context.samplingProbability,
+ }),
+});
+
+export const createCategorizeLogsServiceImplementations = ({
+ search,
+}: CategorizeLogsServiceDependencies): MachineImplementationsFrom<
+ typeof categorizeLogsService
+> => ({
+ actors: {
+ categorizeDocuments: categorizeDocuments({ search }),
+ countDocuments: countDocuments({ search }),
+ },
+});
diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/count_documents.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/count_documents.ts
new file mode 100644
index 0000000000000..359f9ddac2bd8
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/count_documents.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { getSampleProbability } from '@kbn/ml-random-sampler-utils';
+import { ISearchGeneric } from '@kbn/search-types';
+import { lastValueFrom } from 'rxjs';
+import { fromPromise } from 'xstate5';
+import { LogCategorizationParams } from './types';
+import { createCategorizationQuery } from './queries';
+
+export const countDocuments = ({ search }: { search: ISearchGeneric }) =>
+ fromPromise<
+ {
+ documentCount: number;
+ samplingProbability: number;
+ },
+ LogCategorizationParams
+ >(
+ async ({
+ input: { index, endTimestamp, startTimestamp, timeField, messageField, documentFilters },
+ signal,
+ }) => {
+ const { rawResponse: totalHitsResponse } = await lastValueFrom(
+ search(
+ {
+ params: {
+ index,
+ size: 0,
+ track_total_hits: true,
+ query: createCategorizationQuery({
+ messageField,
+ timeField,
+ startTimestamp,
+ endTimestamp,
+ additionalFilters: documentFilters,
+ }),
+ },
+ },
+ { abortSignal: signal }
+ )
+ );
+
+ const documentCount =
+ totalHitsResponse.hits.total == null
+ ? 0
+ : typeof totalHitsResponse.hits.total === 'number'
+ ? totalHitsResponse.hits.total
+ : totalHitsResponse.hits.total.value;
+ const samplingProbability = getSampleProbability(documentCount);
+
+ return {
+ documentCount,
+ samplingProbability,
+ };
+ }
+ );
diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/index.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/index.ts
new file mode 100644
index 0000000000000..149359b7d2015
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/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 './categorize_logs_service';
diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/queries.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/queries.ts
new file mode 100644
index 0000000000000..aef12da303bcc
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/queries.ts
@@ -0,0 +1,151 @@
+/*
+ * 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import { calculateAuto } from '@kbn/calculate-auto';
+import { RandomSamplerWrapper } from '@kbn/ml-random-sampler-utils';
+import moment from 'moment';
+
+const isoTimestampFormat = "YYYY-MM-DD'T'HH:mm:ss.SSS'Z'";
+
+export const createCategorizationQuery = ({
+ messageField,
+ timeField,
+ startTimestamp,
+ endTimestamp,
+ additionalFilters = [],
+ ignoredCategoryTerms = [],
+}: {
+ messageField: string;
+ timeField: string;
+ startTimestamp: string;
+ endTimestamp: string;
+ additionalFilters?: QueryDslQueryContainer[];
+ ignoredCategoryTerms?: string[];
+}): QueryDslQueryContainer => {
+ return {
+ bool: {
+ filter: [
+ {
+ exists: {
+ field: messageField,
+ },
+ },
+ {
+ range: {
+ [timeField]: {
+ gte: startTimestamp,
+ lte: endTimestamp,
+ format: 'strict_date_time',
+ },
+ },
+ },
+ ...additionalFilters,
+ ],
+ must_not: ignoredCategoryTerms.map(createCategoryQuery(messageField)),
+ },
+ };
+};
+
+export const createCategorizationRequestParams = ({
+ index,
+ timeField,
+ messageField,
+ startTimestamp,
+ endTimestamp,
+ randomSampler,
+ minDocsPerCategory = 0,
+ additionalFilters = [],
+ ignoredCategoryTerms = [],
+ maxCategoriesCount = 1000,
+}: {
+ startTimestamp: string;
+ endTimestamp: string;
+ index: string;
+ timeField: string;
+ messageField: string;
+ randomSampler: RandomSamplerWrapper;
+ minDocsPerCategory?: number;
+ additionalFilters?: QueryDslQueryContainer[];
+ ignoredCategoryTerms?: string[];
+ maxCategoriesCount?: number;
+}) => {
+ const startMoment = moment(startTimestamp, isoTimestampFormat);
+ const endMoment = moment(endTimestamp, isoTimestampFormat);
+ const fixedIntervalDuration = calculateAuto.atLeast(
+ 24,
+ moment.duration(endMoment.diff(startMoment))
+ );
+ const fixedIntervalSize = `${Math.ceil(fixedIntervalDuration?.asMinutes() ?? 1)}m`;
+
+ return {
+ index,
+ size: 0,
+ track_total_hits: false,
+ query: createCategorizationQuery({
+ messageField,
+ timeField,
+ startTimestamp,
+ endTimestamp,
+ additionalFilters,
+ ignoredCategoryTerms,
+ }),
+ aggs: randomSampler.wrap({
+ histogram: {
+ date_histogram: {
+ field: timeField,
+ fixed_interval: fixedIntervalSize,
+ extended_bounds: {
+ min: startTimestamp,
+ max: endTimestamp,
+ },
+ },
+ },
+ categories: {
+ categorize_text: {
+ field: messageField,
+ size: maxCategoriesCount,
+ categorization_analyzer: {
+ tokenizer: 'standard',
+ },
+ ...(minDocsPerCategory > 0 ? { min_doc_count: minDocsPerCategory } : {}),
+ },
+ aggs: {
+ histogram: {
+ date_histogram: {
+ field: timeField,
+ fixed_interval: fixedIntervalSize,
+ extended_bounds: {
+ min: startTimestamp,
+ max: endTimestamp,
+ },
+ },
+ },
+ change: {
+ // @ts-expect-error the official types don't support the change_point aggregation
+ change_point: {
+ buckets_path: 'histogram>_count',
+ },
+ },
+ },
+ },
+ }),
+ };
+};
+
+export const createCategoryQuery =
+ (messageField: string) =>
+ (categoryTerms: string): QueryDslQueryContainer => ({
+ match: {
+ [messageField]: {
+ query: categoryTerms,
+ operator: 'AND' as const,
+ fuzziness: 0,
+ auto_generate_synonyms_phrase_query: false,
+ },
+ },
+ });
diff --git a/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/types.ts b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/types.ts
new file mode 100644
index 0000000000000..e094317a98d62
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/services/categorize_logs_service/types.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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import { ISearchGeneric } from '@kbn/search-types';
+
+export interface CategorizeLogsServiceDependencies {
+ search: ISearchGeneric;
+}
+
+export interface LogCategorizationParams {
+ documentFilters: QueryDslQueryContainer[];
+ endTimestamp: string;
+ index: string;
+ messageField: string;
+ startTimestamp: string;
+ timeField: string;
+}
diff --git a/x-pack/packages/observability/logs_overview/src/types.ts b/x-pack/packages/observability/logs_overview/src/types.ts
new file mode 100644
index 0000000000000..4c3d27eca7e7c
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/types.ts
@@ -0,0 +1,74 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface LogCategory {
+ change: LogCategoryChange;
+ documentCount: number;
+ histogram: LogCategoryHistogramBucket[];
+ terms: string;
+}
+
+export type LogCategoryChange =
+ | LogCategoryNoChange
+ | LogCategoryRareChange
+ | LogCategorySpikeChange
+ | LogCategoryDipChange
+ | LogCategoryStepChange
+ | LogCategoryDistributionChange
+ | LogCategoryTrendChange
+ | LogCategoryOtherChange
+ | LogCategoryUnknownChange;
+
+export interface LogCategoryNoChange {
+ type: 'none';
+}
+
+export interface LogCategoryRareChange {
+ type: 'rare';
+ timestamp: string;
+}
+
+export interface LogCategorySpikeChange {
+ type: 'spike';
+ timestamp: string;
+}
+
+export interface LogCategoryDipChange {
+ type: 'dip';
+ timestamp: string;
+}
+
+export interface LogCategoryStepChange {
+ type: 'step';
+ timestamp: string;
+}
+
+export interface LogCategoryTrendChange {
+ type: 'trend';
+ timestamp: string;
+ correlationCoefficient: number;
+}
+
+export interface LogCategoryDistributionChange {
+ type: 'distribution';
+ timestamp: string;
+}
+
+export interface LogCategoryOtherChange {
+ type: 'other';
+ timestamp?: string;
+}
+
+export interface LogCategoryUnknownChange {
+ type: 'unknown';
+ rawChange: string;
+}
+
+export interface LogCategoryHistogramBucket {
+ documentCount: number;
+ timestamp: string;
+}
diff --git a/x-pack/packages/observability/logs_overview/src/utils/logs_source.ts b/x-pack/packages/observability/logs_overview/src/utils/logs_source.ts
new file mode 100644
index 0000000000000..0c8767c8702d4
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/utils/logs_source.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { type AbstractDataView } from '@kbn/data-views-plugin/common';
+import { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public';
+
+export type LogsSourceConfiguration =
+ | SharedSettingLogsSourceConfiguration
+ | IndexNameLogsSourceConfiguration
+ | DataViewLogsSourceConfiguration;
+
+export interface SharedSettingLogsSourceConfiguration {
+ type: 'shared_setting';
+ timestampField?: string;
+ messageField?: string;
+}
+
+export interface IndexNameLogsSourceConfiguration {
+ type: 'index_name';
+ indexName: string;
+ timestampField: string;
+ messageField: string;
+}
+
+export interface DataViewLogsSourceConfiguration {
+ type: 'data_view';
+ dataView: AbstractDataView;
+ messageField?: string;
+}
+
+export const normalizeLogsSource =
+ ({ logsDataAccess }: { logsDataAccess: LogsDataAccessPluginStart }) =>
+ async (logsSource: LogsSourceConfiguration): Promise => {
+ switch (logsSource.type) {
+ case 'index_name':
+ return logsSource;
+ case 'shared_setting':
+ const logSourcesFromSharedSettings =
+ await logsDataAccess.services.logSourcesService.getLogSources();
+ return {
+ type: 'index_name',
+ indexName: logSourcesFromSharedSettings
+ .map((logSource) => logSource.indexPattern)
+ .join(','),
+ timestampField: logsSource.timestampField ?? '@timestamp',
+ messageField: logsSource.messageField ?? 'message',
+ };
+ case 'data_view':
+ return {
+ type: 'index_name',
+ indexName: logsSource.dataView.getIndexPattern(),
+ timestampField: logsSource.dataView.timeFieldName ?? '@timestamp',
+ messageField: logsSource.messageField ?? 'message',
+ };
+ }
+ };
diff --git a/x-pack/packages/observability/logs_overview/src/utils/xstate5_utils.ts b/x-pack/packages/observability/logs_overview/src/utils/xstate5_utils.ts
new file mode 100644
index 0000000000000..3df0bf4ea3988
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/src/utils/xstate5_utils.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const getPlaceholderFor = any>(
+ implementationFactory: ImplementationFactory
+): ReturnType =>
+ (() => {
+ throw new Error('Not implemented');
+ }) as ReturnType;
diff --git a/x-pack/packages/observability/logs_overview/tsconfig.json b/x-pack/packages/observability/logs_overview/tsconfig.json
new file mode 100644
index 0000000000000..886062ae8855f
--- /dev/null
+++ b/x-pack/packages/observability/logs_overview/tsconfig.json
@@ -0,0 +1,39 @@
+{
+ "extends": "../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "target/types",
+ "types": [
+ "jest",
+ "node",
+ "react",
+ "@kbn/ambient-ui-types",
+ "@kbn/ambient-storybook-types",
+ "@emotion/react/types/css-prop"
+ ]
+ },
+ "include": [
+ "**/*.ts",
+ "**/*.tsx",
+ ],
+ "exclude": [
+ "target/**/*"
+ ],
+ "kbn_references": [
+ "@kbn/data-views-plugin",
+ "@kbn/i18n",
+ "@kbn/search-types",
+ "@kbn/xstate-utils",
+ "@kbn/core-ui-settings-browser",
+ "@kbn/i18n-react",
+ "@kbn/charts-plugin",
+ "@kbn/utility-types",
+ "@kbn/logs-data-access-plugin",
+ "@kbn/ml-random-sampler-utils",
+ "@kbn/zod",
+ "@kbn/calculate-auto",
+ "@kbn/discover-plugin",
+ "@kbn/es-query",
+ "@kbn/router-utils",
+ "@kbn/share-plugin",
+ ]
+}
diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_logs/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/service_logs/index.tsx
index 4df52758ceda3..a1dadbf186b91 100644
--- a/x-pack/plugins/observability_solution/apm/public/components/app/service_logs/index.tsx
+++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_logs/index.tsx
@@ -5,19 +5,36 @@
* 2.0.
*/
-import React from 'react';
+import React, { useMemo } from 'react';
import moment from 'moment';
+import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { LogStream } from '@kbn/logs-shared-plugin/public';
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
-import { useFetcher } from '../../../hooks/use_fetcher';
-import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
-import { APIReturnType } from '../../../services/rest/create_call_apm_api';
-
import { CONTAINER_ID, SERVICE_ENVIRONMENT, SERVICE_NAME } from '../../../../common/es_fields/apm';
+import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
+import { useKibana } from '../../../context/kibana_context/use_kibana';
import { useAnyOfApmParams } from '../../../hooks/use_apm_params';
+import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import { useTimeRange } from '../../../hooks/use_time_range';
+import { APIReturnType } from '../../../services/rest/create_call_apm_api';
export function ServiceLogs() {
+ const {
+ services: {
+ logsShared: { LogsOverview },
+ },
+ } = useKibana();
+
+ const isLogsOverviewEnabled = LogsOverview.useIsEnabled();
+
+ if (isLogsOverviewEnabled) {
+ return ;
+ } else {
+ return ;
+ }
+}
+
+export function ClassicServiceLogsStream() {
const { serviceName } = useApmServiceContext();
const {
@@ -58,6 +75,54 @@ export function ServiceLogs() {
);
}
+export function ServiceLogsOverview() {
+ const {
+ services: { logsShared },
+ } = useKibana();
+ const { serviceName } = useApmServiceContext();
+ const {
+ query: { environment, kuery, rangeFrom, rangeTo },
+ } = useAnyOfApmParams('/services/{serviceName}/logs');
+ const { start, end } = useTimeRange({ rangeFrom, rangeTo });
+ const timeRange = useMemo(() => ({ start, end }), [start, end]);
+
+ const { data: logFilters, status } = useFetcher(
+ async (callApmApi) => {
+ if (start == null || end == null) {
+ return;
+ }
+
+ const { containerIds } = await callApmApi(
+ 'GET /internal/apm/services/{serviceName}/infrastructure_attributes',
+ {
+ params: {
+ path: { serviceName },
+ query: {
+ environment,
+ kuery,
+ start,
+ end,
+ },
+ },
+ }
+ );
+
+ return [getInfrastructureFilter({ containerIds, environment, serviceName })];
+ },
+ [environment, kuery, serviceName, start, end]
+ );
+
+ if (status === FETCH_STATUS.SUCCESS) {
+ return ;
+ } else if (status === FETCH_STATUS.FAILURE) {
+ return (
+
+ );
+ } else {
+ return ;
+ }
+}
+
export function getInfrastructureKQLFilter({
data,
serviceName,
@@ -84,3 +149,99 @@ export function getInfrastructureKQLFilter({
return [serviceNameAndEnvironmentCorrelation, ...containerIdCorrelation].join(' or ');
}
+
+export function getInfrastructureFilter({
+ containerIds,
+ environment,
+ serviceName,
+}: {
+ containerIds: string[];
+ environment: string;
+ serviceName: string;
+}): QueryDslQueryContainer {
+ return {
+ bool: {
+ should: [
+ ...getServiceShouldClauses({ environment, serviceName }),
+ ...getContainerShouldClauses({ containerIds }),
+ ],
+ minimum_should_match: 1,
+ },
+ };
+}
+
+export function getServiceShouldClauses({
+ environment,
+ serviceName,
+}: {
+ environment: string;
+ serviceName: string;
+}): QueryDslQueryContainer[] {
+ const serviceNameFilter: QueryDslQueryContainer = {
+ term: {
+ [SERVICE_NAME]: serviceName,
+ },
+ };
+
+ if (environment === ENVIRONMENT_ALL.value) {
+ return [serviceNameFilter];
+ } else {
+ return [
+ {
+ bool: {
+ filter: [
+ serviceNameFilter,
+ {
+ term: {
+ [SERVICE_ENVIRONMENT]: environment,
+ },
+ },
+ ],
+ },
+ },
+ {
+ bool: {
+ filter: [serviceNameFilter],
+ must_not: [
+ {
+ exists: {
+ field: SERVICE_ENVIRONMENT,
+ },
+ },
+ ],
+ },
+ },
+ ];
+ }
+}
+
+export function getContainerShouldClauses({
+ containerIds = [],
+}: {
+ containerIds: string[];
+}): QueryDslQueryContainer[] {
+ if (containerIds.length === 0) {
+ return [];
+ }
+
+ return [
+ {
+ bool: {
+ filter: [
+ {
+ terms: {
+ [CONTAINER_ID]: containerIds,
+ },
+ },
+ ],
+ must_not: [
+ {
+ term: {
+ [SERVICE_NAME]: '*',
+ },
+ },
+ ],
+ },
+ },
+ ];
+}
diff --git a/x-pack/plugins/observability_solution/apm/public/components/routing/service_detail/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/routing/service_detail/index.tsx
index d746e0464fd40..8a4a1c32877c5 100644
--- a/x-pack/plugins/observability_solution/apm/public/components/routing/service_detail/index.tsx
+++ b/x-pack/plugins/observability_solution/apm/public/components/routing/service_detail/index.tsx
@@ -330,7 +330,7 @@ export const serviceDetailRoute = {
}),
element: ,
searchBarOptions: {
- showUnifiedSearchBar: false,
+ showQueryInput: false,
},
}),
'/services/{serviceName}/infrastructure': {
diff --git a/x-pack/plugins/observability_solution/apm/public/plugin.ts b/x-pack/plugins/observability_solution/apm/public/plugin.ts
index 9a9f45f42a39e..b21bdedac9ef8 100644
--- a/x-pack/plugins/observability_solution/apm/public/plugin.ts
+++ b/x-pack/plugins/observability_solution/apm/public/plugin.ts
@@ -69,6 +69,7 @@ import { from } from 'rxjs';
import { map } from 'rxjs';
import type { CloudSetup } from '@kbn/cloud-plugin/public';
import type { ServerlessPluginStart } from '@kbn/serverless/public';
+import { LogsSharedClientStartExports } from '@kbn/logs-shared-plugin/public';
import type { ConfigSchema } from '.';
import { registerApmRuleTypes } from './components/alerting/rule_types/register_apm_rule_types';
import { registerEmbeddables } from './embeddable/register_embeddables';
@@ -142,6 +143,7 @@ export interface ApmPluginStartDeps {
dashboard: DashboardStart;
metricsDataAccess: MetricsDataPluginStart;
uiSettings: IUiSettingsClient;
+ logsShared: LogsSharedClientStartExports;
}
const applicationsTitle = i18n.translate('xpack.apm.navigation.rootTitle', {
diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx b/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx
index 27344ccd1f108..68a5db6d4d484 100644
--- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx
+++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx
@@ -5,21 +5,37 @@
* 2.0.
*/
-import React, { useMemo } from 'react';
+import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { LogStream } from '@kbn/logs-shared-plugin/public';
-import { i18n } from '@kbn/i18n';
+import React, { useMemo } from 'react';
import { InfraLoadingPanel } from '../../../../../../components/loading';
+import { useKibanaContextForPlugin } from '../../../../../../hooks/use_kibana';
+import { useLogViewReference } from '../../../../../../hooks/use_log_view_reference';
+import { buildCombinedAssetFilter } from '../../../../../../utils/filters/build';
import { useHostsViewContext } from '../../../hooks/use_hosts_view';
-import { useUnifiedSearchContext } from '../../../hooks/use_unified_search';
import { useLogsSearchUrlState } from '../../../hooks/use_logs_search_url_state';
+import { useUnifiedSearchContext } from '../../../hooks/use_unified_search';
import { LogsLinkToStream } from './logs_link_to_stream';
import { LogsSearchBar } from './logs_search_bar';
-import { buildCombinedAssetFilter } from '../../../../../../utils/filters/build';
-import { useLogViewReference } from '../../../../../../hooks/use_log_view_reference';
export const LogsTabContent = () => {
+ const {
+ services: {
+ logsShared: { LogsOverview },
+ },
+ } = useKibanaContextForPlugin();
+ const isLogsOverviewEnabled = LogsOverview.useIsEnabled();
+ if (isLogsOverviewEnabled) {
+ return ;
+ } else {
+ return ;
+ }
+};
+
+export const LogsTabLogStreamContent = () => {
const [filterQuery] = useLogsSearchUrlState();
const { getDateRangeAsTimestamp } = useUnifiedSearchContext();
const { from, to } = useMemo(() => getDateRangeAsTimestamp(), [getDateRangeAsTimestamp]);
@@ -53,22 +69,7 @@ export const LogsTabContent = () => {
}, [filterQuery.query, hostNodes]);
if (loading || logViewLoading || !logView) {
- return (
-
-
-
- }
- />
-
-
- );
+ return ;
}
return (
@@ -112,3 +113,53 @@ const createHostsFilterQueryParam = (hostNodes: string[]): string => {
return hostsQueryParam;
};
+
+const LogsTabLogsOverviewContent = () => {
+ const {
+ services: {
+ logsShared: { LogsOverview },
+ },
+ } = useKibanaContextForPlugin();
+
+ const { parsedDateRange } = useUnifiedSearchContext();
+ const timeRange = useMemo(
+ () => ({ start: parsedDateRange.from, end: parsedDateRange.to }),
+ [parsedDateRange.from, parsedDateRange.to]
+ );
+
+ const { hostNodes, loading, error } = useHostsViewContext();
+ const logFilters = useMemo(
+ () => [
+ buildCombinedAssetFilter({
+ field: 'host.name',
+ values: hostNodes.map((p) => p.name),
+ }).query as QueryDslQueryContainer,
+ ],
+ [hostNodes]
+ );
+
+ if (loading) {
+ return ;
+ } else if (error != null) {
+ return ;
+ } else {
+ return ;
+ }
+};
+
+const LogsTabLoadingContent = () => (
+
+
+
+ }
+ />
+
+
+);
diff --git a/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc b/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc
index ea93fd326dac7..10c8fe32cfe9c 100644
--- a/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc
+++ b/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc
@@ -9,13 +9,14 @@
"browser": true,
"configPath": ["xpack", "logs_shared"],
"requiredPlugins": [
+ "charts",
"data",
"dataViews",
"discoverShared",
- "usageCollection",
+ "logsDataAccess",
"observabilityShared",
"share",
- "logsDataAccess"
+ "usageCollection",
],
"optionalPlugins": [
"observabilityAIAssistant",
diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/index.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/index.tsx
new file mode 100644
index 0000000000000..627cdc8447eea
--- /dev/null
+++ b/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/index.tsx
@@ -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 './logs_overview';
diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.mock.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.mock.tsx
new file mode 100644
index 0000000000000..435766bff793d
--- /dev/null
+++ b/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.mock.tsx
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import type {
+ LogsOverviewProps,
+ SelfContainedLogsOverviewComponent,
+ SelfContainedLogsOverviewHelpers,
+} from './logs_overview';
+
+export const createLogsOverviewMock = () => {
+ const LogsOverviewMock = jest.fn(LogsOverviewMockImpl) as unknown as ILogsOverviewMock;
+
+ LogsOverviewMock.useIsEnabled = jest.fn(() => true);
+
+ LogsOverviewMock.ErrorContent = jest.fn(() =>
);
+
+ LogsOverviewMock.LoadingContent = jest.fn(() =>
);
+
+ return LogsOverviewMock;
+};
+
+const LogsOverviewMockImpl = (_props: LogsOverviewProps) => {
+ return
;
+};
+
+type ILogsOverviewMock = jest.Mocked &
+ jest.Mocked;
diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.tsx
new file mode 100644
index 0000000000000..7b60aee5be57c
--- /dev/null
+++ b/x-pack/plugins/observability_solution/logs_shared/public/components/logs_overview/logs_overview.tsx
@@ -0,0 +1,70 @@
+/*
+ * 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 { OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID } from '@kbn/management-settings-ids';
+import type {
+ LogsOverviewProps as FullLogsOverviewProps,
+ LogsOverviewDependencies,
+ LogsOverviewErrorContentProps,
+} from '@kbn/observability-logs-overview';
+import { dynamic } from '@kbn/shared-ux-utility';
+import React from 'react';
+import useObservable from 'react-use/lib/useObservable';
+
+const LazyLogsOverview = dynamic(() =>
+ import('@kbn/observability-logs-overview').then((mod) => ({ default: mod.LogsOverview }))
+);
+
+const LazyLogsOverviewErrorContent = dynamic(() =>
+ import('@kbn/observability-logs-overview').then((mod) => ({
+ default: mod.LogsOverviewErrorContent,
+ }))
+);
+
+const LazyLogsOverviewLoadingContent = dynamic(() =>
+ import('@kbn/observability-logs-overview').then((mod) => ({
+ default: mod.LogsOverviewLoadingContent,
+ }))
+);
+
+export type LogsOverviewProps = Omit;
+
+export interface SelfContainedLogsOverviewHelpers {
+ useIsEnabled: () => boolean;
+ ErrorContent: React.ComponentType;
+ LoadingContent: React.ComponentType;
+}
+
+export type SelfContainedLogsOverviewComponent = React.ComponentType;
+
+export type SelfContainedLogsOverview = SelfContainedLogsOverviewComponent &
+ SelfContainedLogsOverviewHelpers;
+
+export const createLogsOverview = (
+ dependencies: LogsOverviewDependencies
+): SelfContainedLogsOverview => {
+ const SelfContainedLogsOverview = (props: LogsOverviewProps) => {
+ return ;
+ };
+
+ const isEnabled$ = dependencies.uiSettings.client.get$(
+ OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID,
+ defaultIsEnabled
+ );
+
+ SelfContainedLogsOverview.useIsEnabled = (): boolean => {
+ return useObservable(isEnabled$, defaultIsEnabled);
+ };
+
+ SelfContainedLogsOverview.ErrorContent = LazyLogsOverviewErrorContent;
+
+ SelfContainedLogsOverview.LoadingContent = LazyLogsOverviewLoadingContent;
+
+ return SelfContainedLogsOverview;
+};
+
+const defaultIsEnabled = false;
diff --git a/x-pack/plugins/observability_solution/logs_shared/public/index.ts b/x-pack/plugins/observability_solution/logs_shared/public/index.ts
index a602b25786116..3d601c9936f2d 100644
--- a/x-pack/plugins/observability_solution/logs_shared/public/index.ts
+++ b/x-pack/plugins/observability_solution/logs_shared/public/index.ts
@@ -50,6 +50,7 @@ export type {
UpdatedDateRange,
VisibleInterval,
} from './components/logging/log_text_stream/scrollable_log_text_stream_view';
+export type { LogsOverviewProps } from './components/logs_overview';
export const WithSummary = dynamic(() => import('./containers/logs/log_summary/with_summary'));
export const LogEntryFlyout = dynamic(
diff --git a/x-pack/plugins/observability_solution/logs_shared/public/mocks.tsx b/x-pack/plugins/observability_solution/logs_shared/public/mocks.tsx
index a9b0ebd6a6aa3..ffb867abbcc17 100644
--- a/x-pack/plugins/observability_solution/logs_shared/public/mocks.tsx
+++ b/x-pack/plugins/observability_solution/logs_shared/public/mocks.tsx
@@ -6,12 +6,14 @@
*/
import { createLogAIAssistantMock } from './components/log_ai_assistant/log_ai_assistant.mock';
+import { createLogsOverviewMock } from './components/logs_overview/logs_overview.mock';
import { createLogViewsServiceStartMock } from './services/log_views/log_views_service.mock';
import { LogsSharedClientStartExports } from './types';
export const createLogsSharedPluginStartMock = (): jest.Mocked => ({
logViews: createLogViewsServiceStartMock(),
LogAIAssistant: createLogAIAssistantMock(),
+ LogsOverview: createLogsOverviewMock(),
});
export const _ensureTypeCompatibility = (): LogsSharedClientStartExports =>
diff --git a/x-pack/plugins/observability_solution/logs_shared/public/plugin.ts b/x-pack/plugins/observability_solution/logs_shared/public/plugin.ts
index d6f4ac81fe266..fc17e9b17cc82 100644
--- a/x-pack/plugins/observability_solution/logs_shared/public/plugin.ts
+++ b/x-pack/plugins/observability_solution/logs_shared/public/plugin.ts
@@ -12,6 +12,7 @@ import {
TraceLogsLocatorDefinition,
} from '../common/locators';
import { createLogAIAssistant, createLogsAIAssistantRenderer } from './components/log_ai_assistant';
+import { createLogsOverview } from './components/logs_overview';
import { LogViewsService } from './services/log_views';
import {
LogsSharedClientCoreSetup,
@@ -51,8 +52,16 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass {
}
public start(core: CoreStart, plugins: LogsSharedClientStartDeps) {
- const { http } = core;
- const { data, dataViews, discoverShared, observabilityAIAssistant, logsDataAccess } = plugins;
+ const { http, settings } = core;
+ const {
+ charts,
+ data,
+ dataViews,
+ discoverShared,
+ logsDataAccess,
+ observabilityAIAssistant,
+ share,
+ } = plugins;
const logViews = this.logViews.start({
http,
@@ -61,9 +70,18 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass {
search: data.search,
});
+ const LogsOverview = createLogsOverview({
+ charts,
+ logsDataAccess,
+ search: data.search.search,
+ uiSettings: settings,
+ share,
+ });
+
if (!observabilityAIAssistant) {
return {
logViews,
+ LogsOverview,
};
}
@@ -77,6 +95,7 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass {
return {
logViews,
LogAIAssistant,
+ LogsOverview,
};
}
diff --git a/x-pack/plugins/observability_solution/logs_shared/public/types.ts b/x-pack/plugins/observability_solution/logs_shared/public/types.ts
index 58b180ee8b6ef..4237c28c621b8 100644
--- a/x-pack/plugins/observability_solution/logs_shared/public/types.ts
+++ b/x-pack/plugins/observability_solution/logs_shared/public/types.ts
@@ -5,19 +5,19 @@
* 2.0.
*/
+import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
import type { CoreSetup, CoreStart, Plugin as PluginClass } from '@kbn/core/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { DiscoverSharedPublicStart } from '@kbn/discover-shared-plugin/public';
-import { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public';
+import type { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public';
import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public';
import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
-
-import { LogsSharedLocators } from '../common/locators';
+import type { LogsSharedLocators } from '../common/locators';
import type { LogAIAssistantProps } from './components/log_ai_assistant/log_ai_assistant';
-// import type { OsqueryPluginStart } from '../../osquery/public';
-import { LogViewsServiceSetup, LogViewsServiceStart } from './services/log_views';
+import type { SelfContainedLogsOverview } from './components/logs_overview';
+import type { LogViewsServiceSetup, LogViewsServiceStart } from './services/log_views';
// Our own setup and start contract values
export interface LogsSharedClientSetupExports {
@@ -28,6 +28,7 @@ export interface LogsSharedClientSetupExports {
export interface LogsSharedClientStartExports {
logViews: LogViewsServiceStart;
LogAIAssistant?: (props: Omit) => JSX.Element;
+ LogsOverview: SelfContainedLogsOverview;
}
export interface LogsSharedClientSetupDeps {
@@ -35,6 +36,7 @@ export interface LogsSharedClientSetupDeps {
}
export interface LogsSharedClientStartDeps {
+ charts: ChartsPluginStart;
data: DataPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
discoverShared: DiscoverSharedPublicStart;
diff --git a/x-pack/plugins/observability_solution/logs_shared/server/feature_flags.ts b/x-pack/plugins/observability_solution/logs_shared/server/feature_flags.ts
new file mode 100644
index 0000000000000..0298416bd3f26
--- /dev/null
+++ b/x-pack/plugins/observability_solution/logs_shared/server/feature_flags.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 { schema } from '@kbn/config-schema';
+import { UiSettingsParams } from '@kbn/core-ui-settings-common';
+import { i18n } from '@kbn/i18n';
+import { OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID } from '@kbn/management-settings-ids';
+
+const technicalPreviewLabel = i18n.translate('xpack.logsShared.technicalPreviewSettingLabel', {
+ defaultMessage: 'Technical Preview',
+});
+
+export const featureFlagUiSettings: Record = {
+ [OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID]: {
+ category: ['observability'],
+ name: i18n.translate('xpack.logsShared.newLogsOverviewSettingName', {
+ defaultMessage: 'New logs overview',
+ }),
+ value: false,
+ description: i18n.translate('xpack.logsShared.newLogsOverviewSettingDescription', {
+ defaultMessage: '{technicalPreviewLabel} Enable the new logs overview experience.',
+
+ values: { technicalPreviewLabel: `[${technicalPreviewLabel}] ` },
+ }),
+ type: 'boolean',
+ schema: schema.boolean(),
+ requiresPageReload: true,
+ },
+};
diff --git a/x-pack/plugins/observability_solution/logs_shared/server/plugin.ts b/x-pack/plugins/observability_solution/logs_shared/server/plugin.ts
index 7c97e175ed64f..d1f6399104fc2 100644
--- a/x-pack/plugins/observability_solution/logs_shared/server/plugin.ts
+++ b/x-pack/plugins/observability_solution/logs_shared/server/plugin.ts
@@ -5,8 +5,19 @@
* 2.0.
*/
-import { PluginInitializerContext, CoreStart, Plugin, Logger } from '@kbn/core/server';
-
+import { CoreStart, Logger, Plugin, PluginInitializerContext } from '@kbn/core/server';
+import { defaultLogViewId } from '../common/log_views';
+import { LogsSharedConfig } from '../common/plugin_config';
+import { registerDeprecations } from './deprecations';
+import { featureFlagUiSettings } from './feature_flags';
+import { KibanaFramework } from './lib/adapters/framework/kibana_framework_adapter';
+import { LogsSharedKibanaLogEntriesAdapter } from './lib/adapters/log_entries/kibana_log_entries_adapter';
+import { LogsSharedLogEntriesDomain } from './lib/domains/log_entries_domain';
+import { LogsSharedBackendLibs, LogsSharedDomainLibs } from './lib/logs_shared_types';
+import { initLogsSharedServer } from './logs_shared_server';
+import { logViewSavedObjectType } from './saved_objects';
+import { LogEntriesService } from './services/log_entries';
+import { LogViewsService } from './services/log_views';
import {
LogsSharedPluginCoreSetup,
LogsSharedPluginSetup,
@@ -15,17 +26,6 @@ import {
LogsSharedServerPluginStartDeps,
UsageCollector,
} from './types';
-import { logViewSavedObjectType } from './saved_objects';
-import { initLogsSharedServer } from './logs_shared_server';
-import { LogViewsService } from './services/log_views';
-import { KibanaFramework } from './lib/adapters/framework/kibana_framework_adapter';
-import { LogsSharedBackendLibs, LogsSharedDomainLibs } from './lib/logs_shared_types';
-import { LogsSharedLogEntriesDomain } from './lib/domains/log_entries_domain';
-import { LogsSharedKibanaLogEntriesAdapter } from './lib/adapters/log_entries/kibana_log_entries_adapter';
-import { LogEntriesService } from './services/log_entries';
-import { LogsSharedConfig } from '../common/plugin_config';
-import { registerDeprecations } from './deprecations';
-import { defaultLogViewId } from '../common/log_views';
export class LogsSharedPlugin
implements
@@ -88,6 +88,8 @@ export class LogsSharedPlugin
registerDeprecations({ core });
+ core.uiSettings.register(featureFlagUiSettings);
+
return {
...domainLibs,
logViews,
diff --git a/x-pack/plugins/observability_solution/logs_shared/tsconfig.json b/x-pack/plugins/observability_solution/logs_shared/tsconfig.json
index 38cbba7c252c0..788f55c9b6fc5 100644
--- a/x-pack/plugins/observability_solution/logs_shared/tsconfig.json
+++ b/x-pack/plugins/observability_solution/logs_shared/tsconfig.json
@@ -44,5 +44,9 @@
"@kbn/logs-data-access-plugin",
"@kbn/core-deprecations-common",
"@kbn/core-deprecations-server",
+ "@kbn/management-settings-ids",
+ "@kbn/observability-logs-overview",
+ "@kbn/charts-plugin",
+ "@kbn/core-ui-settings-common",
]
}
diff --git a/yarn.lock b/yarn.lock
index 54a38b2c0e5d3..019de6121540e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5879,6 +5879,10 @@
version "0.0.0"
uid ""
+"@kbn/observability-logs-overview@link:x-pack/packages/observability/logs_overview":
+ version "0.0.0"
+ uid ""
+
"@kbn/observability-onboarding-e2e@link:x-pack/plugins/observability_solution/observability_onboarding/e2e":
version "0.0.0"
uid ""
@@ -12105,6 +12109,14 @@
use-isomorphic-layout-effect "^1.1.2"
use-sync-external-store "^1.0.0"
+"@xstate5/react@npm:@xstate/react@^4.1.2":
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/@xstate/react/-/react-4.1.2.tgz#4bfcdf2d9e9ef1eaea7388d1896649345e6679cd"
+ integrity sha512-orAidFrKCrU0ZwN5l/ABPlBfW2ziRDT2RrYoktRlZ0WRoLvA2E/uAC1JpZt43mCLtc8jrdwYCgJiqx1V8NvGTw==
+ dependencies:
+ use-isomorphic-layout-effect "^1.1.2"
+ use-sync-external-store "^1.2.0"
+
"@xtuc/ieee754@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
@@ -32800,6 +32812,11 @@ xpath@^0.0.33:
resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.33.tgz#5136b6094227c5df92002e7c3a13516a5074eb07"
integrity sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA==
+"xstate5@npm:xstate@^5.18.1", xstate@^5.18.1:
+ version "5.18.1"
+ resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.18.1.tgz#c4d43ceaba6e6c31705d36bd96e285de4be4f7f4"
+ integrity sha512-m02IqcCQbaE/kBQLunwub/5i8epvkD2mFutnL17Oeg1eXTShe1sRF4D5mhv1dlaFO4vbW5gRGRhraeAD5c938g==
+
xstate@^4.38.2:
version "4.38.2"
resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.2.tgz#1b74544fc9c8c6c713ba77f81c6017e65aa89804"
From a6e22cf581975cab828b62926484dc2104a19432 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Thu, 10 Oct 2024 13:33:41 +0200
Subject: [PATCH 41/87] [ES|QL][Inspector] Display cluster details tab for CCS
data sources (#195373)
## Summary
It displays correctly the cluster details if they come in the response.
To test it you will need a CCS index as the `_clusters` property only
comes for these indexes.
Other than that it just works out of the bloom as the response is
exactly the same as the search api. If we were sending the response
correctly in the inspector (it wants: `rawResonse: {....}` and not just
the response as we get it), it would have worked without any change from
our side.
![image
(63)](https://github.com/user-attachments/assets/c3a93616-4a6d-468c-8968-e1f1692cffc1)
---
src/plugins/data/common/search/expressions/esql.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/plugins/data/common/search/expressions/esql.ts b/src/plugins/data/common/search/expressions/esql.ts
index b6cb039683c9b..966500710fd45 100644
--- a/src/plugins/data/common/search/expressions/esql.ts
+++ b/src/plugins/data/common/search/expressions/esql.ts
@@ -289,7 +289,7 @@ export const getEsqlFn = ({ getStartDependencies }: EsqlFnArguments) => {
}),
})
.json(params)
- .ok({ json: rawResponse, requestParams });
+ .ok({ json: { rawResponse }, requestParams });
},
error(error) {
logInspectorRequest()
From 8ea2846ae91b35a7d26838f27367302d33d27be3 Mon Sep 17 00:00:00 2001
From: Jeramy Soucy
Date: Thu, 10 Oct 2024 13:38:43 +0200
Subject: [PATCH 42/87] Removes supertest and superuser from platform security
serverless API tests (#194922)
Closes #186467
## Summary
Removes remaining usages of `supertest` and `superuser` from platform
security serverless API tests. Utilizes admin privileges when testing
disabled routes, viewer privileges for all other routes. Uses cookie
authentication for internal API calls.
### Tests
-
x-pack/test_serverless/api_integration/test_suites/common/platform_security/authentication.ts
-
x-pack/test_serverless/api_integration/test_suites/common/platform_security/authorization.ts
- Flaky test runner:
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7089
---
.../platform_security/authentication.ts | 105 +++++++++---------
.../common/platform_security/authorization.ts | 4 +-
2 files changed, 57 insertions(+), 52 deletions(-)
diff --git a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authentication.ts b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authentication.ts
index 7f31db43a3f00..041c005855d0f 100644
--- a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authentication.ts
+++ b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authentication.ts
@@ -6,51 +6,58 @@
*/
import expect from 'expect';
+import { SupertestWithRoleScopeType } from '@kbn/test-suites-xpack/api_integration/deployment_agnostic/services';
import { FtrProviderContext } from '../../../ftr_provider_context';
-import { RoleCredentials } from '../../../../shared/services';
export default function ({ getService }: FtrProviderContext) {
- const supertest = getService('supertest');
- const config = getService('config');
-
+ const roleScopedSupertest = getService('roleScopedSupertest');
const svlCommonApi = getService('svlCommonApi');
- const svlUserManager = getService('svlUserManager');
- const supertestWithoutAuth = getService('supertestWithoutAuth');
- let roleAuthc: RoleCredentials;
+ let supertestAdminWithApiKey: SupertestWithRoleScopeType;
+ let supertestViewerWithApiKey: SupertestWithRoleScopeType;
+ let supertestViewerWithCookieCredentials: SupertestWithRoleScopeType;
+
describe('security/authentication', function () {
before(async () => {
- roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin');
+ supertestAdminWithApiKey = await roleScopedSupertest.getSupertestWithRoleScope('admin');
+ supertestViewerWithApiKey = await roleScopedSupertest.getSupertestWithRoleScope('viewer');
+ supertestViewerWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope(
+ 'viewer',
+ {
+ useCookieHeader: true,
+ withCommonHeaders: true,
+ }
+ );
});
after(async () => {
- await svlUserManager.invalidateM2mApiKeyWithRoleScope(roleAuthc);
+ await supertestAdminWithApiKey.destroy();
+ await supertestViewerWithApiKey.destroy();
+ await supertestViewerWithCookieCredentials.destroy();
});
describe('route access', () => {
describe('disabled', () => {
// ToDo: uncomment when we disable login
// it('login', async () => {
- // const { body, status } = await supertestWithoutAuth
- // .post('/internal/security/login')
- // .set(svlCommonApi.getInternalRequestHeader()).set(roleAuthc.apiKeyHeader)
+ // const { body, status } = await supertestAdminWithApiKey
+ // .post('/internal/security/login');
// svlCommonApi.assertApiNotFound(body, status);
// });
it('logout (deprecated)', async () => {
- const { body, status } = await supertestWithoutAuth
+ const { body, status } = await supertestAdminWithApiKey
.get('/api/security/v1/logout')
- .set(svlCommonApi.getInternalRequestHeader())
- .set(roleAuthc.apiKeyHeader);
+ .set(svlCommonApi.getInternalRequestHeader());
svlCommonApi.assertApiNotFound(body, status);
});
it('get current user (deprecated)', async () => {
- const { body, status } = await supertest
+ const { body, status } = await supertestAdminWithApiKey
.get('/internal/security/v1/me')
.set(svlCommonApi.getInternalRequestHeader());
svlCommonApi.assertApiNotFound(body, status);
});
it('acknowledge access agreement', async () => {
- const { body, status } = await supertest
+ const { body, status } = await supertestAdminWithApiKey
.post('/internal/security/access_agreement/acknowledge')
.set(svlCommonApi.getInternalRequestHeader());
svlCommonApi.assertApiNotFound(body, status);
@@ -58,56 +65,56 @@ export default function ({ getService }: FtrProviderContext) {
describe('OIDC', () => {
it('OIDC implicit', async () => {
- const { body, status } = await supertest
+ const { body, status } = await supertestAdminWithApiKey
.get('/api/security/oidc/implicit')
.set(svlCommonApi.getInternalRequestHeader());
svlCommonApi.assertApiNotFound(body, status);
});
it('OIDC implicit (deprecated)', async () => {
- const { body, status } = await supertest
+ const { body, status } = await supertestAdminWithApiKey
.get('/api/security/v1/oidc/implicit')
.set(svlCommonApi.getInternalRequestHeader());
svlCommonApi.assertApiNotFound(body, status);
});
it('OIDC implicit.js', async () => {
- const { body, status } = await supertest
+ const { body, status } = await supertestAdminWithApiKey
.get('/internal/security/oidc/implicit.js')
.set(svlCommonApi.getInternalRequestHeader());
svlCommonApi.assertApiNotFound(body, status);
});
it('OIDC callback', async () => {
- const { body, status } = await supertest
+ const { body, status } = await supertestAdminWithApiKey
.get('/api/security/oidc/callback')
.set(svlCommonApi.getInternalRequestHeader());
svlCommonApi.assertApiNotFound(body, status);
});
it('OIDC callback (deprecated)', async () => {
- const { body, status } = await supertest
+ const { body, status } = await supertestAdminWithApiKey
.get('/api/security/v1/oidc')
.set(svlCommonApi.getInternalRequestHeader());
svlCommonApi.assertApiNotFound(body, status);
});
it('OIDC login', async () => {
- const { body, status } = await supertest
+ const { body, status } = await supertestAdminWithApiKey
.post('/api/security/oidc/initiate_login')
.set(svlCommonApi.getInternalRequestHeader());
svlCommonApi.assertApiNotFound(body, status);
});
it('OIDC login (deprecated)', async () => {
- const { body, status } = await supertest
+ const { body, status } = await supertestAdminWithApiKey
.post('/api/security/v1/oidc')
.set(svlCommonApi.getInternalRequestHeader());
svlCommonApi.assertApiNotFound(body, status);
});
it('OIDC 3rd party login', async () => {
- const { body, status } = await supertest
+ const { body, status } = await supertestAdminWithApiKey
.get('/api/security/oidc/initiate_login')
.set(svlCommonApi.getInternalRequestHeader());
svlCommonApi.assertApiNotFound(body, status);
@@ -115,7 +122,7 @@ export default function ({ getService }: FtrProviderContext) {
});
it('SAML callback (deprecated)', async () => {
- const { body, status } = await supertest
+ const { body, status } = await supertestAdminWithApiKey
.post('/api/security/v1/saml')
.set(svlCommonApi.getInternalRequestHeader());
svlCommonApi.assertApiNotFound(body, status);
@@ -127,9 +134,9 @@ export default function ({ getService }: FtrProviderContext) {
let body: any;
let status: number;
- ({ body, status } = await supertest
- .get('/internal/security/me')
- .set(svlCommonApi.getCommonRequestHeader()));
+ ({ body, status } = await supertestViewerWithCookieCredentials.get(
+ '/internal/security/me'
+ ));
// expect a rejection because we're not using the internal header
expect(body).toEqual({
statusCode: 400,
@@ -140,24 +147,22 @@ export default function ({ getService }: FtrProviderContext) {
});
expect(status).toBe(400);
- ({ body, status } = await supertest
+ ({ body, status } = await supertestViewerWithCookieCredentials
.get('/internal/security/me')
.set(svlCommonApi.getInternalRequestHeader()));
// expect success because we're using the internal header
- expect(body).toEqual({
- authentication_provider: { name: '__http__', type: 'http' },
- authentication_realm: { name: 'file1', type: 'file' },
- authentication_type: 'realm',
- elastic_cloud_user: false,
- email: null,
- enabled: true,
- full_name: null,
- lookup_realm: { name: 'file1', type: 'file' },
- metadata: {},
- operator: true,
- roles: ['superuser'],
- username: config.get('servers.kibana.username'),
- });
+ expect(body).toEqual(
+ expect.objectContaining({
+ authentication_provider: { name: 'cloud-saml-kibana', type: 'saml' },
+ authentication_type: 'token',
+ authentication_realm: {
+ name: 'cloud-saml-kibana',
+ type: 'saml',
+ },
+ enabled: true,
+ full_name: 'test viewer',
+ })
+ );
expect(status).toBe(200);
});
@@ -166,9 +171,9 @@ export default function ({ getService }: FtrProviderContext) {
let body: any;
let status: number;
- ({ body, status } = await supertest
- .post('/internal/security/login')
- .set(svlCommonApi.getCommonRequestHeader()));
+ ({ body, status } = await supertestViewerWithCookieCredentials.post(
+ '/internal/security/login'
+ ));
// expect a rejection because we're not using the internal header
expect(body).toEqual({
statusCode: 400,
@@ -179,7 +184,7 @@ export default function ({ getService }: FtrProviderContext) {
});
expect(status).toBe(400);
- ({ body, status } = await supertest
+ ({ body, status } = await supertestViewerWithCookieCredentials
.post('/internal/security/login')
.set(svlCommonApi.getInternalRequestHeader()));
expect(status).not.toBe(404);
@@ -188,12 +193,12 @@ export default function ({ getService }: FtrProviderContext) {
describe('public', () => {
it('logout', async () => {
- const { status } = await supertest.get('/api/security/logout');
+ const { status } = await supertestViewerWithApiKey.get('/api/security/logout');
expect(status).toBe(302);
});
it('SAML callback', async () => {
- const { body, status } = await supertest
+ const { body, status } = await supertestViewerWithApiKey
.post('/api/security/saml/callback')
.set(svlCommonApi.getCommonRequestHeader())
.send({
diff --git a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authorization.ts b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authorization.ts
index bc01b14848eff..bd706132d4874 100644
--- a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authorization.ts
+++ b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authorization.ts
@@ -75,7 +75,7 @@ export default function ({ getService }: FtrProviderContext) {
it('get role', async () => {
const { body, status } = await supertestAdminWithApiKey.get(
- '/api/security/role/superuser'
+ '/api/security/role/someRole' // mame of the role doesn't matter, we're checking the endpoint doesn't exist
);
svlCommonApi.assertApiNotFound(body, status);
});
@@ -87,7 +87,7 @@ export default function ({ getService }: FtrProviderContext) {
it('delete role', async () => {
const { body, status } = await supertestAdminWithApiKey.delete(
- '/api/security/role/superuser'
+ '/api/security/role/someRole' // mame of the role doesn't matter, we're checking the endpoint doesn't exist
);
svlCommonApi.assertApiNotFound(body, status);
});
From 446ad9475ba4d419066977f776b4fcd20f8a8cc0 Mon Sep 17 00:00:00 2001
From: Krzysztof Kowalczyk
Date: Thu, 10 Oct 2024 14:08:31 +0200
Subject: [PATCH 43/87] Fix theme switch success toast layout (#195717)
## Summary
This PR fixes the layout of `Color theme updated` toast to match [EUI
guidelines on success
toasts](https://eui.elastic.co/#/display/toast#success).
Fixes: #165979
## Visuals
| Previous | New |
|-----------------|-----------------|
|![image](https://github.com/user-attachments/assets/4f191907-b708-41ab-81a1-2dba708045f7)
|
![image](https://github.com/user-attachments/assets/48dce2dd-e751-455e-8bc5-81bf288c3b85)
|
### Checklist
Delete any items that are not applicable to this PR.
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
---
.../src/hooks/use_update_user_profile.tsx | 36 ++++++++++---------
1 file changed, 19 insertions(+), 17 deletions(-)
diff --git a/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx b/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx
index 8c276dc533f6c..edf11d43b2c84 100644
--- a/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx
+++ b/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx
@@ -74,23 +74,25 @@ export const useUpdateUserProfile = ({
{
title: notificationTitle,
text: (
-
-
- {pageReloadText}
- window.location.reload()}
- data-test-subj="windowReloadButton"
- >
- {i18n.translate(
- 'userProfileComponents.updateUserProfile.notification.requiresPageReloadButtonLabel',
- {
- defaultMessage: 'Reload page',
- }
- )}
-
-
-
+ <>
+ {pageReloadText}
+
+
+ window.location.reload()}
+ data-test-subj="windowReloadButton"
+ >
+ {i18n.translate(
+ 'userProfileComponents.updateUserProfile.notification.requiresPageReloadButtonLabel',
+ {
+ defaultMessage: 'Reload page',
+ }
+ )}
+
+
+
+ >
),
},
{
From d86ce77217a26747b39ddf240e5703efba1a0cb0 Mon Sep 17 00:00:00 2001
From: Ignacio Rivas
Date: Thu, 10 Oct 2024 14:16:42 +0200
Subject: [PATCH 44/87] Remove kbn-ace, ace and brace dependencies (#195703)
---
.github/CODEOWNERS | 1 -
NOTICE.txt | 31 -
api_docs/kbn_ace.devdocs.json | 210 ---
api_docs/kbn_ace.mdx | 33 -
api_docs/plugin_directory.mdx | 2 -
.../monorepo-packages.asciidoc | 3 +-
package.json | 3 -
packages/kbn-ace/README.md | 20 -
packages/kbn-ace/index.ts | 17 -
packages/kbn-ace/kibana.jsonc | 5 -
packages/kbn-ace/package.json | 6 -
packages/kbn-ace/src/ace/modes/index.ts | 17 -
.../elasticsearch_sql_highlight_rules.ts | 104 --
.../src/ace/modes/lexer_rules/index.ts | 12 -
.../lexer_rules/script_highlight_rules.ts | 73 -
.../lexer_rules/x_json_highlight_rules.ts | 184 ---
.../kbn-ace/src/ace/modes/x_json/index.ts | 10 -
.../src/ace/modes/x_json/worker/index.ts | 16 -
.../src/ace/modes/x_json/worker/worker.d.ts | 15 -
.../modes/x_json/worker/x_json.ace.worker.js | 1265 -----------------
.../kbn-ace/src/ace/modes/x_json/x_json.ts | 57 -
packages/kbn-ace/tsconfig.json | 16 -
.../src/import_resolver.ts | 5 -
.../integration_tests/import_resolver.test.ts | 6 -
packages/kbn-test/src/jest/resolver.js | 2 +-
packages/kbn-ui-shared-deps-npm/BUILD.bazel | 1 -
.../kbn-ui-shared-deps-npm/webpack.config.js | 1 -
src/core/public/styles/_ace_overrides.scss | 202 ---
src/core/public/styles/_index.scss | 1 -
.../ace/_ui_ace_keyboard_mode.scss | 24 -
.../__packages_do_not_import__/ace/index.ts | 10 -
.../ace/use_ui_ace_keyboard_mode.tsx | 113 --
src/plugins/es_ui_shared/public/ace/index.ts | 10 -
src/plugins/es_ui_shared/public/index.ts | 3 +-
.../static/forms/components/index.ts | 16 -
src/plugins/es_ui_shared/tsconfig.json | 1 -
.../public/default_editor.tsx | 1 -
.../management/data_views/_scripted_fields.ts | 4 -
.../_scripted_fields_classic_table.ts | 4 -
tsconfig.base.json | 2 -
.../components/detail_panel/detail_panel.js | 2 -
.../edit_role_mapping_page.test.tsx | 5 -
.../json_rule_editor.test.tsx | 7 -
.../rule_editor_panel/json_rule_editor.tsx | 4 -
.../rule_editor_panel.test.tsx | 5 -
x-pack/plugins/security/tsconfig.json | 1 -
.../expression/es_query_expression.test.tsx | 1 -
.../es_query/expression/expression.test.tsx | 1 -
.../es_query/expression/expression.tsx | 1 -
yarn.lock | 22 +-
50 files changed, 4 insertions(+), 2551 deletions(-)
delete mode 100644 api_docs/kbn_ace.devdocs.json
delete mode 100644 api_docs/kbn_ace.mdx
delete mode 100644 packages/kbn-ace/README.md
delete mode 100644 packages/kbn-ace/index.ts
delete mode 100644 packages/kbn-ace/kibana.jsonc
delete mode 100644 packages/kbn-ace/package.json
delete mode 100644 packages/kbn-ace/src/ace/modes/index.ts
delete mode 100644 packages/kbn-ace/src/ace/modes/lexer_rules/elasticsearch_sql_highlight_rules.ts
delete mode 100644 packages/kbn-ace/src/ace/modes/lexer_rules/index.ts
delete mode 100644 packages/kbn-ace/src/ace/modes/lexer_rules/script_highlight_rules.ts
delete mode 100644 packages/kbn-ace/src/ace/modes/lexer_rules/x_json_highlight_rules.ts
delete mode 100644 packages/kbn-ace/src/ace/modes/x_json/index.ts
delete mode 100644 packages/kbn-ace/src/ace/modes/x_json/worker/index.ts
delete mode 100644 packages/kbn-ace/src/ace/modes/x_json/worker/worker.d.ts
delete mode 100644 packages/kbn-ace/src/ace/modes/x_json/worker/x_json.ace.worker.js
delete mode 100644 packages/kbn-ace/src/ace/modes/x_json/x_json.ts
delete mode 100644 packages/kbn-ace/tsconfig.json
delete mode 100644 src/core/public/styles/_ace_overrides.scss
delete mode 100644 src/plugins/es_ui_shared/__packages_do_not_import__/ace/_ui_ace_keyboard_mode.scss
delete mode 100644 src/plugins/es_ui_shared/__packages_do_not_import__/ace/index.ts
delete mode 100644 src/plugins/es_ui_shared/__packages_do_not_import__/ace/use_ui_ace_keyboard_mode.tsx
delete mode 100644 src/plugins/es_ui_shared/public/ace/index.ts
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 974a7d39f63b3..204c7b8198768 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -6,7 +6,6 @@
####
x-pack/test/alerting_api_integration/common/plugins/aad @elastic/response-ops
-packages/kbn-ace @elastic/kibana-management
x-pack/plugins/actions @elastic/response-ops
x-pack/test/alerting_api_integration/common/plugins/actions_simulators @elastic/response-ops
packages/kbn-actions-types @elastic/response-ops
diff --git a/NOTICE.txt b/NOTICE.txt
index 80d49de19e5db..bdd6a95e57b04 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -68,37 +68,6 @@ Author Tobias Koppers @sokra
---
This product has relied on ASTExplorer that is licensed under MIT.
----
-This product includes code that is based on Ace editor, which was available
-under a "BSD" license.
-
-Distributed under the BSD license:
-
-Copyright (c) 2010, Ajax.org B.V.
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
- * Redistributions of source code must retain the above copyright
- notice, this list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above copyright
- notice, this list of conditions and the following disclaimer in the
- documentation and/or other materials provided with the distribution.
- * Neither the name of Ajax.org B.V. nor the
- names of its contributors may be used to endorse or promote products
- derived from this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
-DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
-ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
---
This product includes code that is based on flot-charts, which was available
under a "MIT" license.
diff --git a/api_docs/kbn_ace.devdocs.json b/api_docs/kbn_ace.devdocs.json
deleted file mode 100644
index 31b9c39264e4d..0000000000000
--- a/api_docs/kbn_ace.devdocs.json
+++ /dev/null
@@ -1,210 +0,0 @@
-{
- "id": "@kbn/ace",
- "client": {
- "classes": [],
- "functions": [],
- "interfaces": [],
- "enums": [],
- "misc": [],
- "objects": []
- },
- "server": {
- "classes": [],
- "functions": [],
- "interfaces": [],
- "enums": [],
- "misc": [],
- "objects": []
- },
- "common": {
- "classes": [],
- "functions": [
- {
- "parentPluginId": "@kbn/ace",
- "id": "def-common.addToRules",
- "type": "Function",
- "tags": [],
- "label": "addToRules",
- "description": [],
- "signature": [
- "(otherRules: any, embedUnder: any) => void"
- ],
- "path": "packages/kbn-ace/src/ace/modes/lexer_rules/x_json_highlight_rules.ts",
- "deprecated": false,
- "trackAdoption": false,
- "children": [
- {
- "parentPluginId": "@kbn/ace",
- "id": "def-common.addToRules.$1",
- "type": "Any",
- "tags": [],
- "label": "otherRules",
- "description": [],
- "signature": [
- "any"
- ],
- "path": "packages/kbn-ace/src/ace/modes/lexer_rules/x_json_highlight_rules.ts",
- "deprecated": false,
- "trackAdoption": false,
- "isRequired": true
- },
- {
- "parentPluginId": "@kbn/ace",
- "id": "def-common.addToRules.$2",
- "type": "Any",
- "tags": [],
- "label": "embedUnder",
- "description": [],
- "signature": [
- "any"
- ],
- "path": "packages/kbn-ace/src/ace/modes/lexer_rules/x_json_highlight_rules.ts",
- "deprecated": false,
- "trackAdoption": false,
- "isRequired": true
- }
- ],
- "returnComment": [],
- "initialIsOpen": false
- },
- {
- "parentPluginId": "@kbn/ace",
- "id": "def-common.ElasticsearchSqlHighlightRules",
- "type": "Function",
- "tags": [],
- "label": "ElasticsearchSqlHighlightRules",
- "description": [],
- "signature": [
- "(this: any) => void"
- ],
- "path": "packages/kbn-ace/src/ace/modes/lexer_rules/elasticsearch_sql_highlight_rules.ts",
- "deprecated": false,
- "trackAdoption": false,
- "returnComment": [],
- "children": [],
- "initialIsOpen": false
- },
- {
- "parentPluginId": "@kbn/ace",
- "id": "def-common.installXJsonMode",
- "type": "Function",
- "tags": [],
- "label": "installXJsonMode",
- "description": [],
- "signature": [
- "(editor: ",
- "Editor",
- ") => void"
- ],
- "path": "packages/kbn-ace/src/ace/modes/x_json/x_json.ts",
- "deprecated": false,
- "trackAdoption": false,
- "children": [
- {
- "parentPluginId": "@kbn/ace",
- "id": "def-common.installXJsonMode.$1",
- "type": "Object",
- "tags": [],
- "label": "editor",
- "description": [],
- "signature": [
- "Editor"
- ],
- "path": "packages/kbn-ace/src/ace/modes/x_json/x_json.ts",
- "deprecated": false,
- "trackAdoption": false,
- "isRequired": true
- }
- ],
- "returnComment": [],
- "initialIsOpen": false
- },
- {
- "parentPluginId": "@kbn/ace",
- "id": "def-common.ScriptHighlightRules",
- "type": "Function",
- "tags": [],
- "label": "ScriptHighlightRules",
- "description": [],
- "signature": [
- "(this: any) => void"
- ],
- "path": "packages/kbn-ace/src/ace/modes/lexer_rules/script_highlight_rules.ts",
- "deprecated": false,
- "trackAdoption": false,
- "children": [
- {
- "parentPluginId": "@kbn/ace",
- "id": "def-common.ScriptHighlightRules.$1",
- "type": "Any",
- "tags": [],
- "label": "this",
- "description": [],
- "signature": [
- "any"
- ],
- "path": "packages/kbn-ace/src/ace/modes/lexer_rules/script_highlight_rules.ts",
- "deprecated": false,
- "trackAdoption": false,
- "isRequired": true
- }
- ],
- "returnComment": [],
- "initialIsOpen": false
- },
- {
- "parentPluginId": "@kbn/ace",
- "id": "def-common.XJsonHighlightRules",
- "type": "Function",
- "tags": [],
- "label": "XJsonHighlightRules",
- "description": [],
- "signature": [
- "(this: any) => void"
- ],
- "path": "packages/kbn-ace/src/ace/modes/lexer_rules/x_json_highlight_rules.ts",
- "deprecated": false,
- "trackAdoption": false,
- "children": [
- {
- "parentPluginId": "@kbn/ace",
- "id": "def-common.XJsonHighlightRules.$1",
- "type": "Any",
- "tags": [],
- "label": "this",
- "description": [],
- "signature": [
- "any"
- ],
- "path": "packages/kbn-ace/src/ace/modes/lexer_rules/x_json_highlight_rules.ts",
- "deprecated": false,
- "trackAdoption": false,
- "isRequired": true
- }
- ],
- "returnComment": [],
- "initialIsOpen": false
- }
- ],
- "interfaces": [],
- "enums": [],
- "misc": [
- {
- "parentPluginId": "@kbn/ace",
- "id": "def-common.XJsonMode",
- "type": "Any",
- "tags": [],
- "label": "XJsonMode",
- "description": [],
- "signature": [
- "any"
- ],
- "path": "packages/kbn-ace/src/ace/modes/x_json/x_json.ts",
- "deprecated": false,
- "trackAdoption": false,
- "initialIsOpen": false
- }
- ],
- "objects": []
- }
-}
\ No newline at end of file
diff --git a/api_docs/kbn_ace.mdx b/api_docs/kbn_ace.mdx
deleted file mode 100644
index 64aba3c6788e8..0000000000000
--- a/api_docs/kbn_ace.mdx
+++ /dev/null
@@ -1,33 +0,0 @@
----
-####
-#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system.
-#### Reach out in #docs-engineering for more info.
-####
-id: kibKbnAcePluginApi
-slug: /kibana-dev-docs/api/kbn-ace
-title: "@kbn/ace"
-image: https://source.unsplash.com/400x175/?github
-description: API docs for the @kbn/ace plugin
-date: 2024-10-09
-tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ace']
----
-import kbnAceObj from './kbn_ace.devdocs.json';
-
-
-
-Contact [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) for questions regarding this plugin.
-
-**Code health stats**
-
-| Public API count | Any count | Items lacking comments | Missing exports |
-|-------------------|-----------|------------------------|-----------------|
-| 11 | 5 | 11 | 0 |
-
-## Common
-
-### Functions
-
-
-### Consts, variables and types
-
-
diff --git a/api_docs/plugin_directory.mdx b/api_docs/plugin_directory.mdx
index a5a2307c4d6db..959b02632bf07 100644
--- a/api_docs/plugin_directory.mdx
+++ b/api_docs/plugin_directory.mdx
@@ -242,7 +242,6 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana']
| Package name | Maintaining team | Description | API Cnt | Any Cnt | Missing comments | Missing exports |
|--------------|----------------|-----------|--------------|----------|---------------|--------|
-| | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 11 | 5 | 11 | 0 |
| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 14 | 0 | 14 | 0 |
| | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 36 | 0 | 0 | 0 |
| | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 2 | 0 | 0 | 0 |
@@ -797,4 +796,3 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana']
| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 9 | 0 | 4 | 0 |
| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 1254 | 0 | 4 | 0 |
| | [@elastic/security-detection-rule-management](https://github.com/orgs/elastic/teams/security-detection-rule-management) | - | 20 | 0 | 10 | 0 |
-
diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc
index 50095f8b7018f..0b97a425001ec 100644
--- a/docs/developer/getting-started/monorepo-packages.asciidoc
+++ b/docs/developer/getting-started/monorepo-packages.asciidoc
@@ -41,7 +41,6 @@ yarn kbn watch
[discrete]
=== List of Already Migrated Packages to Bazel
-- @kbn/ace
- @kbn/analytics
- @kbn/apm-config-loader
- @kbn/apm-utils
@@ -93,4 +92,4 @@ yarn kbn watch
- @kbn/ui-shared-deps-npm
- @kbn/ui-shared-deps-src
- @kbn/utility-types
-- @kbn/utils
+- @kbn/utils
\ No newline at end of file
diff --git a/package.json b/package.json
index 58cd08773696f..d258e35a67b27 100644
--- a/package.json
+++ b/package.json
@@ -154,7 +154,6 @@
"@hapi/wreck": "^18.1.0",
"@hello-pangea/dnd": "16.6.0",
"@kbn/aad-fixtures-plugin": "link:x-pack/test/alerting_api_integration/common/plugins/aad",
- "@kbn/ace": "link:packages/kbn-ace",
"@kbn/actions-plugin": "link:x-pack/plugins/actions",
"@kbn/actions-simulators-plugin": "link:x-pack/test/alerting_api_integration/common/plugins/actions_simulators",
"@kbn/actions-types": "link:packages/kbn-actions-types",
@@ -1066,7 +1065,6 @@
"bitmap-sdf": "^1.0.3",
"blurhash": "^2.0.1",
"borc": "3.0.0",
- "brace": "0.11.1",
"brok": "^6.0.0",
"byte-size": "^8.1.0",
"cacheable-lookup": "6",
@@ -1204,7 +1202,6 @@
"re-resizable": "^6.9.9",
"re2js": "0.4.2",
"react": "^17.0.2",
- "react-ace": "^7.0.5",
"react-diff-view": "^3.2.1",
"react-dom": "^17.0.2",
"react-dropzone": "^4.2.9",
diff --git a/packages/kbn-ace/README.md b/packages/kbn-ace/README.md
deleted file mode 100644
index c11d5cc2f24b8..0000000000000
--- a/packages/kbn-ace/README.md
+++ /dev/null
@@ -1,20 +0,0 @@
-# @kbn/ace
-
-This package contains the XJSON mode for brace. This is an extension of the `brace/mode/json` mode.
-
-This package also contains an import of the entire brace editor which is used for creating the custom XJSON worker.
-
-## Note to plugins
-_This code should not be eagerly loaded_.
-
-Make sure imports of this package are behind a lazy-load `import()` statement.
-
-Your plugin should already be loading application code this way in the `mount` function.
-
-## Deprecated
-
-This package is considered deprecated and will be removed in future.
-
-New and existing editor functionality should use Monaco.
-
-_Do not add new functionality to this package_. Build new functionality for Monaco and use it instead.
diff --git a/packages/kbn-ace/index.ts b/packages/kbn-ace/index.ts
deleted file mode 100644
index c9cc0b7a73e86..0000000000000
--- a/packages/kbn-ace/index.ts
+++ /dev/null
@@ -1,17 +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", 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".
- */
-
-export {
- ElasticsearchSqlHighlightRules,
- ScriptHighlightRules,
- XJsonHighlightRules,
- addXJsonToRules,
- XJsonMode,
- installXJsonMode,
-} from './src/ace/modes';
diff --git a/packages/kbn-ace/kibana.jsonc b/packages/kbn-ace/kibana.jsonc
deleted file mode 100644
index 0a01d96a6b1c6..0000000000000
--- a/packages/kbn-ace/kibana.jsonc
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "type": "shared-common",
- "id": "@kbn/ace",
- "owner": "@elastic/kibana-management"
-}
diff --git a/packages/kbn-ace/package.json b/packages/kbn-ace/package.json
deleted file mode 100644
index 3d3ed36941978..0000000000000
--- a/packages/kbn-ace/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "@kbn/ace",
- "version": "1.0.0",
- "private": true,
- "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
-}
\ No newline at end of file
diff --git a/packages/kbn-ace/src/ace/modes/index.ts b/packages/kbn-ace/src/ace/modes/index.ts
deleted file mode 100644
index ffbb385663e48..0000000000000
--- a/packages/kbn-ace/src/ace/modes/index.ts
+++ /dev/null
@@ -1,17 +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", 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".
- */
-
-export {
- ElasticsearchSqlHighlightRules,
- ScriptHighlightRules,
- XJsonHighlightRules,
- addXJsonToRules,
-} from './lexer_rules';
-
-export { installXJsonMode, XJsonMode } from './x_json';
diff --git a/packages/kbn-ace/src/ace/modes/lexer_rules/elasticsearch_sql_highlight_rules.ts b/packages/kbn-ace/src/ace/modes/lexer_rules/elasticsearch_sql_highlight_rules.ts
deleted file mode 100644
index a4cb60529281d..0000000000000
--- a/packages/kbn-ace/src/ace/modes/lexer_rules/elasticsearch_sql_highlight_rules.ts
+++ /dev/null
@@ -1,104 +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", 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 ace from 'brace';
-
-const { TextHighlightRules } = ace.acequire('ace/mode/text_highlight_rules');
-const oop = ace.acequire('ace/lib/oop');
-
-export const ElasticsearchSqlHighlightRules = function (this: any) {
- // See https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-commands.html
- const keywords =
- 'describe|between|in|like|not|and|or|desc|select|from|where|having|group|by|order' +
- 'asc|desc|pivot|for|in|as|show|columns|include|frozen|tables|escape|limit|rlike|all|distinct|is';
-
- const builtinConstants = 'true|false';
-
- // See https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-syntax-show-functions.html
- const builtinFunctions =
- 'avg|count|first|first_value|last|last_value|max|min|sum|kurtosis|mad|percentile|percentile_rank|skewness' +
- '|stddev_pop|sum_of_squares|var_pop|histogram|case|coalesce|greatest|ifnull|iif|isnull|least|nullif|nvl' +
- '|curdate|current_date|current_time|current_timestamp|curtime|dateadd|datediff|datepart|datetrunc|date_add' +
- '|date_diff|date_part|date_trunc|day|dayname|dayofmonth|dayofweek|dayofyear|day_name|day_of_month|day_of_week' +
- '|day_of_year|dom|dow|doy|hour|hour_of_day|idow|isodayofweek|isodow|isoweek|isoweekofyear|iso_day_of_week|iso_week_of_year' +
- '|iw|iwoy|minute|minute_of_day|minute_of_hour|month|monthname|month_name|month_of_year|now|quarter|second|second_of_minute' +
- '|timestampadd|timestampdiff|timestamp_add|timestamp_diff|today|week|week_of_year|year|abs|acos|asin|atan|atan2|cbrt' +
- '|ceil|ceiling|cos|cosh|cot|degrees|e|exp|expm1|floor|log|log10|mod|pi|power|radians|rand|random|round|sign|signum|sin' +
- '|sinh|sqrt|tan|truncate|ascii|bit_length|char|character_length|char_length|concat|insert|lcase|left|length|locate' +
- '|ltrim|octet_length|position|repeat|replace|right|rtrim|space|substring|ucase|cast|convert|database|user|st_astext|st_aswkt' +
- '|st_distance|st_geometrytype|st_geomfromtext|st_wkttosql|st_x|st_y|st_z|score';
-
- // See https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-data-types.html
- const dataTypes =
- 'null|boolean|byte|short|integer|long|double|float|half_float|scaled_float|keyword|text|binary|date|ip|object|nested|time' +
- '|interval_year|interval_month|interval_day|interval_hour|interval_minute|interval_second|interval_year_to_month' +
- 'inteval_day_to_hour|interval_day_to_minute|interval_day_to_second|interval_hour_to_minute|interval_hour_to_second' +
- 'interval_minute_to_second|geo_point|geo_shape|shape';
-
- const keywordMapper = this.createKeywordMapper(
- {
- keyword: [keywords, builtinFunctions, builtinConstants, dataTypes].join('|'),
- },
- 'identifier',
- true
- );
-
- this.$rules = {
- start: [
- {
- token: 'comment',
- regex: '--.*$',
- },
- {
- token: 'comment',
- start: '/\\*',
- end: '\\*/',
- },
- {
- token: 'string', // " string
- regex: '".*?"',
- },
- {
- token: 'constant', // ' string
- regex: "'.*?'",
- },
- {
- token: 'string', // ` string (apache drill)
- regex: '`.*?`',
- },
- {
- token: 'entity.name.function', // float
- regex: '[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b',
- },
- {
- token: keywordMapper,
- regex: '[a-zA-Z_$][a-zA-Z0-9_$]*\\b',
- },
- {
- token: 'keyword.operator',
- regex: '⇐|<⇒|\\*|\\.|\\:\\:|\\+|\\-|\\/|\\/\\/|%|&|\\^|~|<|>|<=|=>|==|!=|<>|=',
- },
- {
- token: 'paren.lparen',
- regex: '[\\(]',
- },
- {
- token: 'paren.rparen',
- regex: '[\\)]',
- },
- {
- token: 'text',
- regex: '\\s+',
- },
- ],
- };
- this.normalizeRules();
-};
-
-oop.inherits(ElasticsearchSqlHighlightRules, TextHighlightRules);
diff --git a/packages/kbn-ace/src/ace/modes/lexer_rules/index.ts b/packages/kbn-ace/src/ace/modes/lexer_rules/index.ts
deleted file mode 100644
index aa8c6af19c10f..0000000000000
--- a/packages/kbn-ace/src/ace/modes/lexer_rules/index.ts
+++ /dev/null
@@ -1,12 +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", 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".
- */
-
-export { ElasticsearchSqlHighlightRules } from './elasticsearch_sql_highlight_rules';
-export { ScriptHighlightRules } from './script_highlight_rules';
-export { XJsonHighlightRules, addToRules as addXJsonToRules } from './x_json_highlight_rules';
diff --git a/packages/kbn-ace/src/ace/modes/lexer_rules/script_highlight_rules.ts b/packages/kbn-ace/src/ace/modes/lexer_rules/script_highlight_rules.ts
deleted file mode 100644
index 64e8a1a6594bd..0000000000000
--- a/packages/kbn-ace/src/ace/modes/lexer_rules/script_highlight_rules.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", 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 ace from 'brace';
-const oop = ace.acequire('ace/lib/oop');
-const { TextHighlightRules } = ace.acequire('ace/mode/text_highlight_rules');
-const painlessKeywords =
- 'def|int|long|byte|String|float|double|char|null|if|else|while|do|for|continue|break|new|try|catch|throw|this|instanceof|return|ctx';
-
-export function ScriptHighlightRules(this: any) {
- this.name = 'ScriptHighlightRules';
- this.$rules = {
- start: [
- {
- token: 'script.comment',
- regex: '\\/\\/.*$',
- },
- {
- token: 'script.string.regexp',
- regex: '[/](?:(?:\\[(?:\\\\]|[^\\]])+\\])|(?:\\\\/|[^\\]/]))*[/]\\w*\\s*(?=[).,;]|$)',
- },
- {
- token: 'script.string', // single line
- regex: "['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']",
- },
- {
- token: 'script.constant.numeric', // hex
- regex: '0[xX][0-9a-fA-F]+\\b',
- },
- {
- token: 'script.constant.numeric', // float
- regex: '[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b',
- },
- {
- token: 'script.constant.language.boolean',
- regex: '(?:true|false)\\b',
- },
- {
- token: 'script.keyword',
- regex: painlessKeywords,
- },
- {
- token: 'script.text',
- regex: '[a-zA-Z_$][a-zA-Z0-9_$]*\\b',
- },
- {
- token: 'script.keyword.operator',
- regex:
- '\\?\\.|\\*\\.|=~|==~|!|%|&|\\*|\\-\\-|\\-|\\+\\+|\\+|~|===|==|=|!=|!==|<=|>=|<<=|>>=|>>>=|<>|<|>|->|!|&&|\\|\\||\\?\\:|\\*=|%=|\\+=|\\-=|&=|\\^=|\\b(?:in|instanceof|new|typeof|void)',
- },
- {
- token: 'script.lparen',
- regex: '[[({]',
- },
- {
- token: 'script.rparen',
- regex: '[\\])}]',
- },
- {
- token: 'script.text',
- regex: '\\s+',
- },
- ],
- };
-}
-
-oop.inherits(ScriptHighlightRules, TextHighlightRules);
diff --git a/packages/kbn-ace/src/ace/modes/lexer_rules/x_json_highlight_rules.ts b/packages/kbn-ace/src/ace/modes/lexer_rules/x_json_highlight_rules.ts
deleted file mode 100644
index f69e2fbbf5d8a..0000000000000
--- a/packages/kbn-ace/src/ace/modes/lexer_rules/x_json_highlight_rules.ts
+++ /dev/null
@@ -1,184 +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", 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 { defaultsDeep } from 'lodash';
-import ace from 'brace';
-import 'brace/mode/json';
-
-import { ElasticsearchSqlHighlightRules } from './elasticsearch_sql_highlight_rules';
-import { ScriptHighlightRules } from './script_highlight_rules';
-
-const { JsonHighlightRules } = ace.acequire('ace/mode/json_highlight_rules');
-const oop = ace.acequire('ace/lib/oop');
-
-const jsonRules = function (root: any) {
- root = root ? root : 'json';
- const rules: any = {};
- const xJsonRules = [
- {
- token: [
- 'variable',
- 'whitespace',
- 'ace.punctuation.colon',
- 'whitespace',
- 'punctuation.start_triple_quote',
- ],
- regex: '("(?:[^"]*_)?script"|"inline"|"source")(\\s*?)(:)(\\s*?)(""")',
- next: 'script-start',
- merge: false,
- push: true,
- },
- {
- token: 'variable', // single line
- regex: '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]\\s*(?=:)',
- },
- {
- token: 'punctuation.start_triple_quote',
- regex: '"""',
- next: 'string_literal',
- merge: false,
- push: true,
- },
- {
- token: 'string', // single line
- regex: '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]',
- },
- {
- token: 'constant.numeric', // hex
- regex: '0[xX][0-9a-fA-F]+\\b',
- },
- {
- token: 'constant.numeric', // float
- regex: '[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b',
- },
- {
- token: 'constant.language.boolean',
- regex: '(?:true|false)\\b',
- },
- {
- token: 'invalid.illegal', // single quoted strings are not allowed
- regex: "['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']",
- },
- {
- token: 'invalid.illegal', // comments are not allowed
- regex: '\\/\\/.*$',
- },
- {
- token: 'paren.lparen',
- merge: false,
- regex: '{',
- next: root,
- push: true,
- },
- {
- token: 'paren.lparen',
- merge: false,
- regex: '[[(]',
- },
- {
- token: 'paren.rparen',
- merge: false,
- regex: '[\\])]',
- },
- {
- token: 'paren.rparen',
- regex: '}',
- merge: false,
- next: 'pop',
- },
- {
- token: 'punctuation.comma',
- regex: ',',
- },
- {
- token: 'punctuation.colon',
- regex: ':',
- },
- {
- token: 'whitespace',
- regex: '\\s+',
- },
- {
- token: 'text',
- regex: '.+?',
- },
- ];
-
- rules[root] = xJsonRules;
- rules[root + '-sql'] = [
- {
- token: [
- 'variable',
- 'whitespace',
- 'ace.punctuation.colon',
- 'whitespace',
- 'punctuation.start_triple_quote',
- ],
- regex: '("query")(\\s*?)(:)(\\s*?)(""")',
- next: 'sql-start',
- merge: false,
- push: true,
- },
- ].concat(xJsonRules as any);
-
- rules.string_literal = [
- {
- token: 'punctuation.end_triple_quote',
- regex: '"""',
- next: 'pop',
- },
- {
- token: 'multi_string',
- regex: '.',
- },
- ];
- return rules;
-};
-
-export function XJsonHighlightRules(this: any) {
- this.$rules = {
- ...jsonRules('start'),
- };
-
- this.embedRules(ScriptHighlightRules, 'script-', [
- {
- token: 'punctuation.end_triple_quote',
- regex: '"""',
- next: 'pop',
- },
- ]);
-
- this.embedRules(ElasticsearchSqlHighlightRules, 'sql-', [
- {
- token: 'punctuation.end_triple_quote',
- regex: '"""',
- next: 'pop',
- },
- ]);
-}
-
-oop.inherits(XJsonHighlightRules, JsonHighlightRules);
-
-export function addToRules(otherRules: any, embedUnder: any) {
- otherRules.$rules = defaultsDeep(otherRules.$rules, jsonRules(embedUnder));
- otherRules.embedRules(ScriptHighlightRules, 'script-', [
- {
- token: 'punctuation.end_triple_quote',
- regex: '"""',
- next: 'pop',
- },
- ]);
- otherRules.embedRules(ElasticsearchSqlHighlightRules, 'sql-', [
- {
- token: 'punctuation.end_triple_quote',
- regex: '"""',
- next: 'pop',
- },
- ]);
-}
diff --git a/packages/kbn-ace/src/ace/modes/x_json/index.ts b/packages/kbn-ace/src/ace/modes/x_json/index.ts
deleted file mode 100644
index a1651c9e06979..0000000000000
--- a/packages/kbn-ace/src/ace/modes/x_json/index.ts
+++ /dev/null
@@ -1,10 +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", 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".
- */
-
-export { installXJsonMode, XJsonMode } from './x_json';
diff --git a/packages/kbn-ace/src/ace/modes/x_json/worker/index.ts b/packages/kbn-ace/src/ace/modes/x_json/worker/index.ts
deleted file mode 100644
index b09099ed9ad01..0000000000000
--- a/packages/kbn-ace/src/ace/modes/x_json/worker/index.ts
+++ /dev/null
@@ -1,16 +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", 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".
- */
-
-// @ts-ignore
-import src from '!!raw-loader!./x_json.ace.worker';
-
-export const workerModule = {
- id: 'ace/mode/json_worker',
- src,
-};
diff --git a/packages/kbn-ace/src/ace/modes/x_json/worker/worker.d.ts b/packages/kbn-ace/src/ace/modes/x_json/worker/worker.d.ts
deleted file mode 100644
index 34598ea61003b..0000000000000
--- a/packages/kbn-ace/src/ace/modes/x_json/worker/worker.d.ts
+++ /dev/null
@@ -1,15 +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", 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".
- */
-
-// Satisfy TS's requirements that the module be declared per './index.ts'.
-declare module '!!raw-loader!./worker.js' {
- const content: string;
- // eslint-disable-next-line import/no-default-export
- export default content;
-}
diff --git a/packages/kbn-ace/src/ace/modes/x_json/worker/x_json.ace.worker.js b/packages/kbn-ace/src/ace/modes/x_json/worker/x_json.ace.worker.js
deleted file mode 100644
index 63ca258e524d4..0000000000000
--- a/packages/kbn-ace/src/ace/modes/x_json/worker/x_json.ace.worker.js
+++ /dev/null
@@ -1,1265 +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", 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".
- */
-
-/* @notice
- *
- * This product includes code that is based on Ace editor, which was available
- * under a "BSD" license.
- *
- * Distributed under the BSD license:
- *
- * Copyright (c) 2010, Ajax.org B.V.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- * * Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- * * Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- * * Neither the name of Ajax.org B.V. nor the
- * names of its contributors may be used to endorse or promote products
- * derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-/* eslint-disable prettier/prettier,no-var,eqeqeq,no-use-before-define,block-scoped-var,no-undef,
- guard-for-in,one-var,strict,no-redeclare,no-sequences,no-proto,new-cap,no-nested-ternary,no-unused-vars,
- prefer-const,no-empty,no-extend-native,camelcase */
-/*
- This file is loaded up as a blob by Brace to hand to Ace to load as Jsonp
- (hence the redefining of everything). It is based on the json
- mode from the brace distro.
-
- It is very likely that this file will be removed in future but for now it enables
- extended JSON parsing, like e.g. """{}""" (triple quotes)
-*/
-// @internal
-// @ts-nocheck
-"no use strict";
-! function(window) {
- function resolveModuleId(id, paths) {
- for (var testPath = id, tail = ""; testPath;) {
- var alias = paths[testPath];
- if ("string" == typeof alias) return alias + tail;
- if (alias) return alias.location.replace(/\/*$/, "/") + (tail || alias.main || alias.name);
- if (alias === !1) return "";
- var i = testPath.lastIndexOf("/");
- if (-1 === i) break;
- tail = testPath.substr(i) + tail, testPath = testPath.slice(0, i)
- }
- return id
- }
- if (!(void 0 !== window.window && window.document || window.acequire && window.define)) {
- window.console || (window.console = function() {
- var msgs = Array.prototype.slice.call(arguments, 0);
- postMessage({
- type: "log",
- data: msgs
- })
- }, window.console.error = window.console.warn = window.console.log = window.console.trace = window.console), window.window = window, window.ace = window, window.onerror = function(message, file, line, col, err) {
- postMessage({
- type: "error",
- data: {
- message: message,
- data: err.data,
- file: file,
- line: line,
- col: col,
- stack: err.stack
- }
- })
- }, window.normalizeModule = function(parentId, moduleName) {
- if (-1 !== moduleName.indexOf("!")) {
- var chunks = moduleName.split("!");
- return window.normalizeModule(parentId, chunks[0]) + "!" + window.normalizeModule(parentId, chunks[1])
- }
- if ("." == moduleName.charAt(0)) {
- var base = parentId.split("/").slice(0, -1).join("/");
- for (moduleName = (base ? base + "/" : "") + moduleName; - 1 !== moduleName.indexOf(".") && previous != moduleName;) {
- var previous = moduleName;
- moduleName = moduleName.replace(/^\.\//, "").replace(/\/\.\//, "/").replace(/[^\/]+\/\.\.\//, "")
- }
- }
- return moduleName
- }, window.acequire = function acequire(parentId, id) {
- if (id || (id = parentId, parentId = null), !id.charAt) throw Error("worker.js acequire() accepts only (parentId, id) as arguments");
- id = window.normalizeModule(parentId, id);
- var module = window.acequire.modules[id];
- if (module) return module.initialized || (module.initialized = !0, module.exports = module.factory().exports), module.exports;
- if (!window.acequire.tlns) return console.log("unable to load " + id);
- var path = resolveModuleId(id, window.acequire.tlns);
- return ".js" != path.slice(-3) && (path += ".js"), window.acequire.id = id, window.acequire.modules[id] = {}, importScripts(path), window.acequire(parentId, id)
- }, window.acequire.modules = {}, window.acequire.tlns = {}, window.define = function(id, deps, factory) {
- if (2 == arguments.length ? (factory = deps, "string" != typeof id && (deps = id, id = window.acequire.id)) : 1 == arguments.length && (factory = id, deps = [], id = window.acequire.id), "function" != typeof factory) return window.acequire.modules[id] = {
- exports: factory,
- initialized: !0
- }, void 0;
- deps.length || (deps = ["require", "exports", "module"]);
- var req = function(childId) {
- return window.acequire(id, childId)
- };
- window.acequire.modules[id] = {
- exports: {},
- factory: function() {
- var module = this,
- returnExports = factory.apply(this, deps.map(function(dep) {
- switch (dep) {
- case "require":
- return req;
- case "exports":
- return module.exports;
- case "module":
- return module;
- default:
- return req(dep)
- }
- }));
- return returnExports && (module.exports = returnExports), module
- }
- }
- }, window.define.amd = {}, acequire.tlns = {}, window.initBaseUrls = function(topLevelNamespaces) {
- for (var i in topLevelNamespaces) acequire.tlns[i] = topLevelNamespaces[i]
- }, window.initSender = function() {
- var EventEmitter = window.acequire("ace/lib/event_emitter").EventEmitter,
- oop = window.acequire("ace/lib/oop"),
- Sender = function() {};
- return function() {
- oop.implement(this, EventEmitter), this.callback = function(data, callbackId) {
- postMessage({
- type: "call",
- id: callbackId,
- data: data
- })
- }, this.emit = function(name, data) {
- postMessage({
- type: "event",
- name: name,
- data: data
- })
- }
- }.call(Sender.prototype), new Sender
- };
- var main = window.main = null,
- sender = window.sender = null;
- window.onmessage = function(e) {
- var msg = e.data;
- if (msg.event && sender) sender._signal(msg.event, msg.data);
- else if (msg.command)
- if (main[msg.command]) main[msg.command].apply(main, msg.args);
- else {
- if (!window[msg.command]) throw Error("Unknown command:" + msg.command);
- window[msg.command].apply(window, msg.args)
- }
- else if (msg.init) {
- window.initBaseUrls(msg.tlns), acequire("ace/lib/es5-shim"), sender = window.sender = window.initSender();
- var clazz = acequire(msg.module)[msg.classname];
- main = window.main = new clazz(sender)
- }
- }
- }
-}(this), ace.define("ace/lib/oop", ["require", "exports", "module"], function(acequire, exports) {
- "use strict";
- exports.inherits = function(ctor, superCtor) {
- ctor.super_ = superCtor, ctor.prototype = Object.create(superCtor.prototype, {
- constructor: {
- value: ctor,
- enumerable: !1,
- writable: !0,
- configurable: !0
- }
- })
- }, exports.mixin = function(obj, mixin) {
- for (var key in mixin) obj[key] = mixin[key];
- return obj
- }, exports.implement = function(proto, mixin) {
- exports.mixin(proto, mixin)
- }
-}), ace.define("ace/range", ["require", "exports", "module"], function(acequire, exports) {
- "use strict";
- var comparePoints = function(p1, p2) {
- return p1.row - p2.row || p1.column - p2.column
- },
- Range = function(startRow, startColumn, endRow, endColumn) {
- this.start = {
- row: startRow,
- column: startColumn
- }, this.end = {
- row: endRow,
- column: endColumn
- }
- };
- (function() {
- this.isEqual = function(range) {
- return this.start.row === range.start.row && this.end.row === range.end.row && this.start.column === range.start.column && this.end.column === range.end.column
- }, this.toString = function() {
- return "Range: [" + this.start.row + "/" + this.start.column + "] -> [" + this.end.row + "/" + this.end.column + "]"
- }, this.contains = function(row, column) {
- return 0 == this.compare(row, column)
- }, this.compareRange = function(range) {
- var cmp, end = range.end,
- start = range.start;
- return cmp = this.compare(end.row, end.column), 1 == cmp ? (cmp = this.compare(start.row, start.column), 1 == cmp ? 2 : 0 == cmp ? 1 : 0) : -1 == cmp ? -2 : (cmp = this.compare(start.row, start.column), -1 == cmp ? -1 : 1 == cmp ? 42 : 0)
- }, this.comparePoint = function(p) {
- return this.compare(p.row, p.column)
- }, this.containsRange = function(range) {
- return 0 == this.comparePoint(range.start) && 0 == this.comparePoint(range.end)
- }, this.intersects = function(range) {
- var cmp = this.compareRange(range);
- return -1 == cmp || 0 == cmp || 1 == cmp
- }, this.isEnd = function(row, column) {
- return this.end.row == row && this.end.column == column
- }, this.isStart = function(row, column) {
- return this.start.row == row && this.start.column == column
- }, this.setStart = function(row, column) {
- "object" == typeof row ? (this.start.column = row.column, this.start.row = row.row) : (this.start.row = row, this.start.column = column)
- }, this.setEnd = function(row, column) {
- "object" == typeof row ? (this.end.column = row.column, this.end.row = row.row) : (this.end.row = row, this.end.column = column)
- }, this.inside = function(row, column) {
- return 0 == this.compare(row, column) ? this.isEnd(row, column) || this.isStart(row, column) ? !1 : !0 : !1
- }, this.insideStart = function(row, column) {
- return 0 == this.compare(row, column) ? this.isEnd(row, column) ? !1 : !0 : !1
- }, this.insideEnd = function(row, column) {
- return 0 == this.compare(row, column) ? this.isStart(row, column) ? !1 : !0 : !1
- }, this.compare = function(row, column) {
- return this.isMultiLine() || row !== this.start.row ? this.start.row > row ? -1 : row > this.end.row ? 1 : this.start.row === row ? column >= this.start.column ? 0 : -1 : this.end.row === row ? this.end.column >= column ? 0 : 1 : 0 : this.start.column > column ? -1 : column > this.end.column ? 1 : 0
- }, this.compareStart = function(row, column) {
- return this.start.row == row && this.start.column == column ? -1 : this.compare(row, column)
- }, this.compareEnd = function(row, column) {
- return this.end.row == row && this.end.column == column ? 1 : this.compare(row, column)
- }, this.compareInside = function(row, column) {
- return this.end.row == row && this.end.column == column ? 1 : this.start.row == row && this.start.column == column ? -1 : this.compare(row, column)
- }, this.clipRows = function(firstRow, lastRow) {
- if (this.end.row > lastRow) var end = {
- row: lastRow + 1,
- column: 0
- };
- else if (firstRow > this.end.row) var end = {
- row: firstRow,
- column: 0
- };
- if (this.start.row > lastRow) var start = {
- row: lastRow + 1,
- column: 0
- };
- else if (firstRow > this.start.row) var start = {
- row: firstRow,
- column: 0
- };
- return Range.fromPoints(start || this.start, end || this.end)
- }, this.extend = function(row, column) {
- var cmp = this.compare(row, column);
- if (0 == cmp) return this;
- if (-1 == cmp) var start = {
- row: row,
- column: column
- };
- else var end = {
- row: row,
- column: column
- };
- return Range.fromPoints(start || this.start, end || this.end)
- }, this.isEmpty = function() {
- return this.start.row === this.end.row && this.start.column === this.end.column
- }, this.isMultiLine = function() {
- return this.start.row !== this.end.row
- }, this.clone = function() {
- return Range.fromPoints(this.start, this.end)
- }, this.collapseRows = function() {
- return 0 == this.end.column ? new Range(this.start.row, 0, Math.max(this.start.row, this.end.row - 1), 0) : new Range(this.start.row, 0, this.end.row, 0)
- }, this.toScreenRange = function(session) {
- var screenPosStart = session.documentToScreenPosition(this.start),
- screenPosEnd = session.documentToScreenPosition(this.end);
- return new Range(screenPosStart.row, screenPosStart.column, screenPosEnd.row, screenPosEnd.column)
- }, this.moveBy = function(row, column) {
- this.start.row += row, this.start.column += column, this.end.row += row, this.end.column += column
- }
- }).call(Range.prototype), Range.fromPoints = function(start, end) {
- return new Range(start.row, start.column, end.row, end.column)
- }, Range.comparePoints = comparePoints, Range.comparePoints = function(p1, p2) {
- return p1.row - p2.row || p1.column - p2.column
- }, exports.Range = Range
-}), ace.define("ace/apply_delta", ["require", "exports", "module"], function(acequire, exports) {
- "use strict";
- exports.applyDelta = function(docLines, delta) {
- var row = delta.start.row,
- startColumn = delta.start.column,
- line = docLines[row] || "";
- switch (delta.action) {
- case "insert":
- var lines = delta.lines;
- if (1 === lines.length) docLines[row] = line.substring(0, startColumn) + delta.lines[0] + line.substring(startColumn);
- else {
- var args = [row, 1].concat(delta.lines);
- docLines.splice.apply(docLines, args), docLines[row] = line.substring(0, startColumn) + docLines[row], docLines[row + delta.lines.length - 1] += line.substring(startColumn)
- }
- break;
- case "remove":
- var endColumn = delta.end.column,
- endRow = delta.end.row;
- row === endRow ? docLines[row] = line.substring(0, startColumn) + line.substring(endColumn) : docLines.splice(row, endRow - row + 1, line.substring(0, startColumn) + docLines[endRow].substring(endColumn))
- }
- }
-}), ace.define("ace/lib/event_emitter", ["require", "exports", "module"], function(acequire, exports) {
- "use strict";
- var EventEmitter = {},
- stopPropagation = function() {
- this.propagationStopped = !0
- },
- preventDefault = function() {
- this.defaultPrevented = !0
- };
- EventEmitter._emit = EventEmitter._dispatchEvent = function(eventName, e) {
- this._eventRegistry || (this._eventRegistry = {}), this._defaultHandlers || (this._defaultHandlers = {});
- var listeners = this._eventRegistry[eventName] || [],
- defaultHandler = this._defaultHandlers[eventName];
- if (listeners.length || defaultHandler) {
- "object" == typeof e && e || (e = {}), e.type || (e.type = eventName), e.stopPropagation || (e.stopPropagation = stopPropagation), e.preventDefault || (e.preventDefault = preventDefault), listeners = listeners.slice();
- for (var i = 0; listeners.length > i && (listeners[i](e, this), !e.propagationStopped); i++);
- return defaultHandler && !e.defaultPrevented ? defaultHandler(e, this) : void 0
- }
- }, EventEmitter._signal = function(eventName, e) {
- var listeners = (this._eventRegistry || {})[eventName];
- if (listeners) {
- listeners = listeners.slice();
- for (var i = 0; listeners.length > i; i++) listeners[i](e, this)
- }
- }, EventEmitter.once = function(eventName, callback) {
- var _self = this;
- callback && this.addEventListener(eventName, function newCallback() {
- _self.removeEventListener(eventName, newCallback), callback.apply(null, arguments)
- })
- }, EventEmitter.setDefaultHandler = function(eventName, callback) {
- var handlers = this._defaultHandlers;
- if (handlers || (handlers = this._defaultHandlers = {
- _disabled_: {}
- }), handlers[eventName]) {
- var old = handlers[eventName],
- disabled = handlers._disabled_[eventName];
- disabled || (handlers._disabled_[eventName] = disabled = []), disabled.push(old);
- var i = disabled.indexOf(callback); - 1 != i && disabled.splice(i, 1)
- }
- handlers[eventName] = callback
- }, EventEmitter.removeDefaultHandler = function(eventName, callback) {
- var handlers = this._defaultHandlers;
- if (handlers) {
- var disabled = handlers._disabled_[eventName];
- if (handlers[eventName] == callback) handlers[eventName], disabled && this.setDefaultHandler(eventName, disabled.pop());
- else if (disabled) {
- var i = disabled.indexOf(callback); - 1 != i && disabled.splice(i, 1)
- }
- }
- }, EventEmitter.on = EventEmitter.addEventListener = function(eventName, callback, capturing) {
- this._eventRegistry = this._eventRegistry || {};
- var listeners = this._eventRegistry[eventName];
- return listeners || (listeners = this._eventRegistry[eventName] = []), -1 == listeners.indexOf(callback) && listeners[capturing ? "unshift" : "push"](callback), callback
- }, EventEmitter.off = EventEmitter.removeListener = EventEmitter.removeEventListener = function(eventName, callback) {
- this._eventRegistry = this._eventRegistry || {};
- var listeners = this._eventRegistry[eventName];
- if (listeners) {
- var index = listeners.indexOf(callback); - 1 !== index && listeners.splice(index, 1)
- }
- }, EventEmitter.removeAllListeners = function(eventName) {
- this._eventRegistry && (this._eventRegistry[eventName] = [])
- }, exports.EventEmitter = EventEmitter
-}), ace.define("ace/anchor", ["require", "exports", "module", "ace/lib/oop", "ace/lib/event_emitter"], function(acequire, exports) {
- "use strict";
- var oop = acequire("./lib/oop"),
- EventEmitter = acequire("./lib/event_emitter").EventEmitter,
- Anchor = exports.Anchor = function(doc, row, column) {
- this.$onChange = this.onChange.bind(this), this.attach(doc), column === void 0 ? this.setPosition(row.row, row.column) : this.setPosition(row, column)
- };
- (function() {
- function $pointsInOrder(point1, point2, equalPointsInOrder) {
- var bColIsAfter = equalPointsInOrder ? point1.column <= point2.column : point1.column < point2.column;
- return point1.row < point2.row || point1.row == point2.row && bColIsAfter
- }
-
- function $getTransformedPoint(delta, point, moveIfEqual) {
- var deltaIsInsert = "insert" == delta.action,
- deltaRowShift = (deltaIsInsert ? 1 : -1) * (delta.end.row - delta.start.row),
- deltaColShift = (deltaIsInsert ? 1 : -1) * (delta.end.column - delta.start.column),
- deltaStart = delta.start,
- deltaEnd = deltaIsInsert ? deltaStart : delta.end;
- return $pointsInOrder(point, deltaStart, moveIfEqual) ? {
- row: point.row,
- column: point.column
- } : $pointsInOrder(deltaEnd, point, !moveIfEqual) ? {
- row: point.row + deltaRowShift,
- column: point.column + (point.row == deltaEnd.row ? deltaColShift : 0)
- } : {
- row: deltaStart.row,
- column: deltaStart.column
- }
- }
- oop.implement(this, EventEmitter), this.getPosition = function() {
- return this.$clipPositionToDocument(this.row, this.column)
- }, this.getDocument = function() {
- return this.document
- }, this.$insertRight = !1, this.onChange = function(delta) {
- if (!(delta.start.row == delta.end.row && delta.start.row != this.row || delta.start.row > this.row)) {
- var point = $getTransformedPoint(delta, {
- row: this.row,
- column: this.column
- }, this.$insertRight);
- this.setPosition(point.row, point.column, !0)
- }
- }, this.setPosition = function(row, column, noClip) {
- var pos;
- if (pos = noClip ? {
- row: row,
- column: column
- } : this.$clipPositionToDocument(row, column), this.row != pos.row || this.column != pos.column) {
- var old = {
- row: this.row,
- column: this.column
- };
- this.row = pos.row, this.column = pos.column, this._signal("change", {
- old: old,
- value: pos
- })
- }
- }, this.detach = function() {
- this.document.removeEventListener("change", this.$onChange)
- }, this.attach = function(doc) {
- this.document = doc || this.document, this.document.on("change", this.$onChange)
- }, this.$clipPositionToDocument = function(row, column) {
- var pos = {};
- return row >= this.document.getLength() ? (pos.row = Math.max(0, this.document.getLength() - 1), pos.column = this.document.getLine(pos.row).length) : 0 > row ? (pos.row = 0, pos.column = 0) : (pos.row = row, pos.column = Math.min(this.document.getLine(pos.row).length, Math.max(0, column))), 0 > column && (pos.column = 0), pos
- }
- }).call(Anchor.prototype)
-}), ace.define("ace/document", ["require", "exports", "module", "ace/lib/oop", "ace/apply_delta", "ace/lib/event_emitter", "ace/range", "ace/anchor"], function(acequire, exports) {
- "use strict";
- var oop = acequire("./lib/oop"),
- applyDelta = acequire("./apply_delta").applyDelta,
- EventEmitter = acequire("./lib/event_emitter").EventEmitter,
- Range = acequire("./range").Range,
- Anchor = acequire("./anchor").Anchor,
- Document = function(textOrLines) {
- this.$lines = [""], 0 === textOrLines.length ? this.$lines = [""] : Array.isArray(textOrLines) ? this.insertMergedLines({
- row: 0,
- column: 0
- }, textOrLines) : this.insert({
- row: 0,
- column: 0
- }, textOrLines)
- };
- (function() {
- oop.implement(this, EventEmitter), this.setValue = function(text) {
- var len = this.getLength() - 1;
- this.remove(new Range(0, 0, len, this.getLine(len).length)), this.insert({
- row: 0,
- column: 0
- }, text)
- }, this.getValue = function() {
- return this.getAllLines().join(this.getNewLineCharacter())
- }, this.createAnchor = function(row, column) {
- return new Anchor(this, row, column)
- }, this.$split = 0 === "aaa".split(/a/).length ? function(text) {
- return text.replace(/\r\n|\r/g, "\n").split("\n");
- } : function(text) {
- return text.split(/\r\n|\r|\n/);
- }, this.$detectNewLine = function(text) {
- var match = text.match(/^.*?(\r\n|\r|\n)/m);
- this.$autoNewLine = match ? match[1] : "\n", this._signal("changeNewLineMode")
- }, this.getNewLineCharacter = function() {
- switch (this.$newLineMode) {
- case "windows":
- return "\r\n";
- case "unix":
- return "\n";
- default:
- return this.$autoNewLine || "\n"
- }
- }, this.$autoNewLine = "", this.$newLineMode = "auto", this.setNewLineMode = function(newLineMode) {
- this.$newLineMode !== newLineMode && (this.$newLineMode = newLineMode, this._signal("changeNewLineMode"))
- }, this.getNewLineMode = function() {
- return this.$newLineMode
- }, this.isNewLine = function(text) {
- return "\r\n" == text || "\r" == text || "\n" == text
- }, this.getLine = function(row) {
- return this.$lines[row] || ""
- }, this.getLines = function(firstRow, lastRow) {
- return this.$lines.slice(firstRow, lastRow + 1)
- }, this.getAllLines = function() {
- return this.getLines(0, this.getLength())
- }, this.getLength = function() {
- return this.$lines.length
- }, this.getTextRange = function(range) {
- return this.getLinesForRange(range).join(this.getNewLineCharacter())
- }, this.getLinesForRange = function(range) {
- var lines;
- if (range.start.row === range.end.row) lines = [this.getLine(range.start.row).substring(range.start.column, range.end.column)];
- else {
- lines = this.getLines(range.start.row, range.end.row), lines[0] = (lines[0] || "").substring(range.start.column);
- var l = lines.length - 1;
- range.end.row - range.start.row == l && (lines[l] = lines[l].substring(0, range.end.column))
- }
- return lines
- }, this.insertLines = function(row, lines) {
- return console.warn("Use of document.insertLines is deprecated. Use the insertFullLines method instead."), this.insertFullLines(row, lines)
- }, this.removeLines = function(firstRow, lastRow) {
- return console.warn("Use of document.removeLines is deprecated. Use the removeFullLines method instead."), this.removeFullLines(firstRow, lastRow)
- }, this.insertNewLine = function(position) {
- return console.warn("Use of document.insertNewLine is deprecated. Use insertMergedLines(position, ['', '']) instead."), this.insertMergedLines(position, ["", ""])
- }, this.insert = function(position, text) {
- return 1 >= this.getLength() && this.$detectNewLine(text), this.insertMergedLines(position, this.$split(text))
- }, this.insertInLine = function(position, text) {
- var start = this.clippedPos(position.row, position.column),
- end = this.pos(position.row, position.column + text.length);
- return this.applyDelta({
- start: start,
- end: end,
- action: "insert",
- lines: [text]
- }, !0), this.clonePos(end)
- }, this.clippedPos = function(row, column) {
- var length = this.getLength();
- void 0 === row ? row = length : 0 > row ? row = 0 : row >= length && (row = length - 1, column = void 0);
- var line = this.getLine(row);
- return void 0 == column && (column = line.length), column = Math.min(Math.max(column, 0), line.length), {
- row: row,
- column: column
- }
- }, this.clonePos = function(pos) {
- return {
- row: pos.row,
- column: pos.column
- }
- }, this.pos = function(row, column) {
- return {
- row: row,
- column: column
- }
- }, this.$clipPosition = function(position) {
- var length = this.getLength();
- return position.row >= length ? (position.row = Math.max(0, length - 1), position.column = this.getLine(length - 1).length) : (position.row = Math.max(0, position.row), position.column = Math.min(Math.max(position.column, 0), this.getLine(position.row).length)), position
- }, this.insertFullLines = function(row, lines) {
- row = Math.min(Math.max(row, 0), this.getLength());
- var column = 0;
- this.getLength() > row ? (lines = lines.concat([""]), column = 0) : (lines = [""].concat(lines), row--, column = this.$lines[row].length), this.insertMergedLines({
- row: row,
- column: column
- }, lines)
- }, this.insertMergedLines = function(position, lines) {
- var start = this.clippedPos(position.row, position.column),
- end = {
- row: start.row + lines.length - 1,
- column: (1 == lines.length ? start.column : 0) + lines[lines.length - 1].length
- };
- return this.applyDelta({
- start: start,
- end: end,
- action: "insert",
- lines: lines
- }), this.clonePos(end)
- }, this.remove = function(range) {
- var start = this.clippedPos(range.start.row, range.start.column),
- end = this.clippedPos(range.end.row, range.end.column);
- return this.applyDelta({
- start: start,
- end: end,
- action: "remove",
- lines: this.getLinesForRange({
- start: start,
- end: end
- })
- }), this.clonePos(start)
- }, this.removeInLine = function(row, startColumn, endColumn) {
- var start = this.clippedPos(row, startColumn),
- end = this.clippedPos(row, endColumn);
- return this.applyDelta({
- start: start,
- end: end,
- action: "remove",
- lines: this.getLinesForRange({
- start: start,
- end: end
- })
- }, !0), this.clonePos(start)
- }, this.removeFullLines = function(firstRow, lastRow) {
- firstRow = Math.min(Math.max(0, firstRow), this.getLength() - 1), lastRow = Math.min(Math.max(0, lastRow), this.getLength() - 1);
- var deleteFirstNewLine = lastRow == this.getLength() - 1 && firstRow > 0,
- deleteLastNewLine = this.getLength() - 1 > lastRow,
- startRow = deleteFirstNewLine ? firstRow - 1 : firstRow,
- startCol = deleteFirstNewLine ? this.getLine(startRow).length : 0,
- endRow = deleteLastNewLine ? lastRow + 1 : lastRow,
- endCol = deleteLastNewLine ? 0 : this.getLine(endRow).length,
- range = new Range(startRow, startCol, endRow, endCol),
- deletedLines = this.$lines.slice(firstRow, lastRow + 1);
- return this.applyDelta({
- start: range.start,
- end: range.end,
- action: "remove",
- lines: this.getLinesForRange(range)
- }), deletedLines
- }, this.removeNewLine = function(row) {
- this.getLength() - 1 > row && row >= 0 && this.applyDelta({
- start: this.pos(row, this.getLine(row).length),
- end: this.pos(row + 1, 0),
- action: "remove",
- lines: ["", ""]
- })
- }, this.replace = function(range, text) {
- if (range instanceof Range || (range = Range.fromPoints(range.start, range.end)), 0 === text.length && range.isEmpty()) return range.start;
- if (text == this.getTextRange(range)) return range.end;
- this.remove(range);
- var end;
- return end = text ? this.insert(range.start, text) : range.start
- }, this.applyDeltas = function(deltas) {
- for (var i = 0; deltas.length > i; i++) this.applyDelta(deltas[i])
- }, this.revertDeltas = function(deltas) {
- for (var i = deltas.length - 1; i >= 0; i--) this.revertDelta(deltas[i])
- }, this.applyDelta = function(delta, doNotValidate) {
- var isInsert = "insert" == delta.action;
- (isInsert ? 1 >= delta.lines.length && !delta.lines[0] : !Range.comparePoints(delta.start, delta.end)) || (isInsert && delta.lines.length > 2e4 && this.$splitAndapplyLargeDelta(delta, 2e4), applyDelta(this.$lines, delta, doNotValidate), this._signal("change", delta))
- }, this.$splitAndapplyLargeDelta = function(delta, MAX) {
- for (var lines = delta.lines, l = lines.length, row = delta.start.row, column = delta.start.column, from = 0, to = 0;;) {
- from = to, to += MAX - 1;
- var chunk = lines.slice(from, to);
- if (to > l) {
- delta.lines = chunk, delta.start.row = row + from, delta.start.column = column;
- break
- }
- chunk.push(""), this.applyDelta({
- start: this.pos(row + from, column),
- end: this.pos(row + to, column = 0),
- action: delta.action,
- lines: chunk
- }, !0)
- }
- }, this.revertDelta = function(delta) {
- this.applyDelta({
- start: this.clonePos(delta.start),
- end: this.clonePos(delta.end),
- action: "insert" == delta.action ? "remove" : "insert",
- lines: delta.lines.slice()
- })
- }, this.indexToPosition = function(index, startRow) {
- for (var lines = this.$lines || this.getAllLines(), newlineLength = this.getNewLineCharacter().length, i = startRow || 0, l = lines.length; l > i; i++)
- if (index -= lines[i].length + newlineLength, 0 > index) return {
- row: i,
- column: index + lines[i].length + newlineLength
- };
- return {
- row: l - 1,
- column: lines[l - 1].length
- }
- }, this.positionToIndex = function(pos, startRow) {
- for (var lines = this.$lines || this.getAllLines(), newlineLength = this.getNewLineCharacter().length, index = 0, row = Math.min(pos.row, lines.length), i = startRow || 0; row > i; ++i) index += lines[i].length + newlineLength;
- return index + pos.column
- }
- }).call(Document.prototype), exports.Document = Document
-}), ace.define("ace/lib/lang", ["require", "exports", "module"], function(acequire, exports) {
- "use strict";
- exports.last = function(a) {
- return a[a.length - 1]
- }, exports.stringReverse = function(string) {
- return string.split("").reverse().join("")
- }, exports.stringRepeat = function(string, count) {
- for (var result = ""; count > 0;) 1 & count && (result += string), (count >>= 1) && (string += string);
- return result
- };
- var trimBeginRegexp = /^\s\s*/,
- trimEndRegexp = /\s\s*$/;
- exports.stringTrimLeft = function(string) {
- return string.replace(trimBeginRegexp, "")
- }, exports.stringTrimRight = function(string) {
- return string.replace(trimEndRegexp, "")
- }, exports.copyObject = function(obj) {
- var copy = {};
- for (var key in obj) copy[key] = obj[key];
- return copy
- }, exports.copyArray = function(array) {
- for (var copy = [], i = 0, l = array.length; l > i; i++) copy[i] = array[i] && "object" == typeof array[i] ? this.copyObject(array[i]) : array[i];
- return copy
- }, exports.deepCopy = function deepCopy(obj) {
- if ("object" != typeof obj || !obj) return obj;
- var copy;
- if (Array.isArray(obj)) {
- copy = [];
- for (var key = 0; obj.length > key; key++) copy[key] = deepCopy(obj[key]);
- return copy
- }
- if ("[object Object]" !== Object.prototype.toString.call(obj)) return obj;
- copy = {};
- for (var key in obj) copy[key] = deepCopy(obj[key]);
- return copy
- }, exports.arrayToMap = function(arr) {
- for (var map = {}, i = 0; arr.length > i; i++) map[arr[i]] = 1;
- return map
- }, exports.createMap = function(props) {
- var map = Object.create(null);
- for (var i in props) map[i] = props[i];
- return map
- }, exports.arrayRemove = function(array, value) {
- for (var i = 0; array.length >= i; i++) value === array[i] && array.splice(i, 1)
- }, exports.escapeRegExp = function(str) {
- return str.replace(/([.*+?^${}()|[\]\/\\])/g, "\\$1");
- }, exports.escapeHTML = function(str) {
- return str.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/ i; i += 2) {
- if (Array.isArray(data[i + 1])) var d = {
- action: "insert",
- start: data[i],
- lines: data[i + 1]
- };
- else var d = {
- action: "remove",
- start: data[i],
- end: data[i + 1]
- };
- doc.applyDelta(d, !0)
- }
- return _self.$timeout ? deferredUpdate.schedule(_self.$timeout) : (_self.onUpdate(), void 0)
- })
- };
- (function() {
- this.$timeout = 500, this.setTimeout = function(timeout) {
- this.$timeout = timeout
- }, this.setValue = function(value) {
- this.doc.setValue(value), this.deferredUpdate.schedule(this.$timeout)
- }, this.getValue = function(callbackId) {
- this.sender.callback(this.doc.getValue(), callbackId)
- }, this.onUpdate = function() {}, this.isPending = function() {
- return this.deferredUpdate.isPending()
- }
- }).call(Mirror.prototype)
-}), ace.define("ace/mode/json/json_parse", ["require", "exports", "module"], function() {
- "use strict";
- var at, ch, text, value, escapee = {
- '"': '"',
- "\\": "\\",
- "/": "/",
- b: "\b",
- f: "\f",
- n: "\n",
- r: "\r",
- t: " "
- },
- error = function(m) {
- throw {
- name: "SyntaxError",
- message: m,
- at: at,
- text: text
- }
- },
- reset = function (newAt) {
- ch = text.charAt(newAt);
- at = newAt + 1;
- },
- next = function(c) {
- return c && c !== ch && error("Expected '" + c + "' instead of '" + ch + "'"), ch = text.charAt(at), at += 1, ch
- },
- nextUpTo = function (upTo, errorMessage) {
- let currentAt = at,
- i = text.indexOf(upTo, currentAt);
- if (i < 0) {
- error(errorMessage || 'Expected \'' + upTo + '\'');
- }
- reset(i + upTo.length);
- return text.substring(currentAt, i);
- },
- peek = function (c) {
- return text.substr(at, c.length) === c; // nocommit - double check
- },
- number = function() {
- var number, string = "";
- for ("-" === ch && (string = "-", next("-")); ch >= "0" && "9" >= ch;) string += ch, next();
- if ("." === ch)
- for (string += "."; next() && ch >= "0" && "9" >= ch;) string += ch;
- if ("e" === ch || "E" === ch)
- for (string += ch, next(), ("-" === ch || "+" === ch) && (string += ch, next()); ch >= "0" && "9" >= ch;) string += ch, next();
- return number = +string, isNaN(number) ? (error("Bad number"), void 0) : number
- },
- string = function() {
- var hex, i, uffff, string = "";
- if ('"' === ch) {
- if (peek('""')) {
- // literal
- next('"');
- next('"');
- return nextUpTo('"""', 'failed to find closing \'"""\'');
- } else {
- for (; next();) {
- if ('"' === ch) return next(), string;
- if ("\\" === ch)
- if (next(), "u" === ch) {
- for (uffff = 0, i = 0; 4 > i && (hex = parseInt(next(), 16), isFinite(hex)); i += 1) uffff = 16 * uffff + hex;
- string += String.fromCharCode(uffff)
- } else {
- if ("string" != typeof escapee[ch]) break;
- string += escapee[ch]
- }
- else string += ch
- }
- }
- }
- error("Bad string")
- },
- white = function() {
- for (; ch && " " >= ch;) next()
- },
- word = function() {
- switch (ch) {
- case "t":
- return next("t"), next("r"), next("u"), next("e"), !0;
- case "f":
- return next("f"), next("a"), next("l"), next("s"), next("e"), !1;
- case "n":
- return next("n"), next("u"), next("l"), next("l"), null
- }
- error("Unexpected '" + ch + "'")
- },
- array = function() {
- var array = [];
- if ("[" === ch) {
- if (next("["), white(), "]" === ch) return next("]"), array;
- for (; ch;) {
- if (array.push(value()), white(), "]" === ch) return next("]"), array;
- next(","), white()
- }
- }
- error("Bad array")
- },
- object = function() {
- var key, object = {};
- if ("{" === ch) {
- if (next("{"), white(), "}" === ch) return next("}"), object;
- for (; ch;) {
- if (key = string(), white(), next(":"), Object.hasOwnProperty.call(object, key) && error('Duplicate key "' + key + '"'), object[key] = value(), white(), "}" === ch) return next("}"), object;
- next(","), white()
- }
- }
- error("Bad object")
- };
- return value = function() {
- switch (white(), ch) {
- case "{":
- return object();
- case "[":
- return array();
- case '"':
- return string();
- case "-":
- return number();
- default:
- return ch >= "0" && "9" >= ch ? number() : word()
- }
- },
- function(source, reviver) {
- var result;
- return text = source, at = 0, ch = " ", result = value(), white(), ch && error("Syntax error"), "function" == typeof reviver ? function walk(holder, key) {
- var k, v, value = holder[key];
- if (value && "object" == typeof value)
- for (k in value) Object.hasOwnProperty.call(value, k) && (v = walk(value, k), void 0 !== v ? value[k] = v : delete value[k]);
- return reviver.call(holder, key, value)
- }({
- "": result
- }, "") : result
- }
-}), ace.define("ace/mode/json_worker", ["require", "exports", "module", "ace/lib/oop", "ace/worker/mirror", "ace/mode/json/json_parse"], function(acequire, exports) {
- "use strict";
- var oop = acequire("../lib/oop"),
- Mirror = acequire("../worker/mirror").Mirror,
- parse = acequire("./json/json_parse"),
- JsonWorker = exports.JsonWorker = function(sender) {
- Mirror.call(this, sender), this.setTimeout(200)
- };
- oop.inherits(JsonWorker, Mirror),
- function() {
- this.onUpdate = function() {
- var value = this.doc.getValue(),
- errors = [];
- try {
- value && parse(value)
- } catch (e) {
- var pos = this.doc.indexToPosition(e.at - 1);
- errors.push({
- row: pos.row,
- column: pos.column,
- text: e.message,
- type: "error"
- })
- }
- this.sender.emit("annotate", errors)
- }
- }.call(JsonWorker.prototype)
-}), ace.define("ace/lib/es5-shim", ["require", "exports", "module"], function() {
- function Empty() {}
-
- function doesDefinePropertyWork(object) {
- try {
- return Object.defineProperty(object, "sentinel", {}), "sentinel" in object
- } catch (exception) {}
- }
-
- function toInteger(n) {
- return n = +n, n !== n ? n = 0 : 0 !== n && n !== 1 / 0 && n !== -(1 / 0) && (n = (n > 0 || -1) * Math.floor(Math.abs(n))), n
- }
- Function.prototype.bind || (Function.prototype.bind = function(that) {
- var target = this;
- if ("function" != typeof target) throw new TypeError("Function.prototype.bind called on incompatible " + target);
- var args = slice.call(arguments, 1),
- bound = function() {
- if (this instanceof bound) {
- var result = target.apply(this, args.concat(slice.call(arguments)));
- return Object(result) === result ? result : this
- }
- return target.apply(that, args.concat(slice.call(arguments)))
- };
- return target.prototype && (Empty.prototype = target.prototype, bound.prototype = new Empty, Empty.prototype = null), bound
- });
- var defineGetter, defineSetter, lookupGetter, lookupSetter, supportsAccessors, call = Function.prototype.call,
- prototypeOfArray = Array.prototype,
- prototypeOfObject = Object.prototype,
- slice = prototypeOfArray.slice,
- _toString = call.bind(prototypeOfObject.toString),
- owns = call.bind(prototypeOfObject.hasOwnProperty);
- if ((supportsAccessors = owns(prototypeOfObject, "__defineGetter__")) && (defineGetter = call.bind(prototypeOfObject.__defineGetter__), defineSetter = call.bind(prototypeOfObject.__defineSetter__), lookupGetter = call.bind(prototypeOfObject.__lookupGetter__), lookupSetter = call.bind(prototypeOfObject.__lookupSetter__)), 2 != [1, 2].splice(0).length)
- if (function() {
- function makeArray(l) {
- var a = Array(l + 2);
- return a[0] = a[1] = 0, a
- }
- var lengthBefore, array = [];
- return array.splice.apply(array, makeArray(20)), array.splice.apply(array, makeArray(26)), lengthBefore = array.length, array.splice(5, 0, "XXX"), lengthBefore + 1 == array.length, lengthBefore + 1 == array.length ? !0 : void 0
- }()) {
- var array_splice = Array.prototype.splice;
- Array.prototype.splice = function(start, deleteCount) {
- return arguments.length ? array_splice.apply(this, [void 0 === start ? 0 : start, void 0 === deleteCount ? this.length - start : deleteCount].concat(slice.call(arguments, 2))) : []
- }
- } else Array.prototype.splice = function(pos, removeCount) {
- var length = this.length;
- pos > 0 ? pos > length && (pos = length) : void 0 == pos ? pos = 0 : 0 > pos && (pos = Math.max(length + pos, 0)), length > pos + removeCount || (removeCount = length - pos);
- var removed = this.slice(pos, pos + removeCount),
- insert = slice.call(arguments, 2),
- add = insert.length;
- if (pos === length) add && this.push.apply(this, insert);
- else {
- var remove = Math.min(removeCount, length - pos),
- tailOldPos = pos + remove,
- tailNewPos = tailOldPos + add - remove,
- tailCount = length - tailOldPos,
- lengthAfterRemove = length - remove;
- if (tailOldPos > tailNewPos)
- for (var i = 0; tailCount > i; ++i) this[tailNewPos + i] = this[tailOldPos + i];
- else if (tailNewPos > tailOldPos)
- for (i = tailCount; i--;) this[tailNewPos + i] = this[tailOldPos + i];
- if (add && pos === lengthAfterRemove) this.length = lengthAfterRemove, this.push.apply(this, insert);
- else
- for (this.length = lengthAfterRemove + add, i = 0; add > i; ++i) this[pos + i] = insert[i]
- }
- return removed
- };
- Array.isArray || (Array.isArray = function(obj) {
- return "[object Array]" == _toString(obj)
- });
- var boxedString = Object("a"),
- splitString = "a" != boxedString[0] || !(0 in boxedString);
- if (Array.prototype.forEach || (Array.prototype.forEach = function(fun) {
- var object = toObject(this),
- self = splitString && "[object String]" == _toString(this) ? this.split("") : object,
- thisp = arguments[1],
- i = -1,
- length = self.length >>> 0;
- if ("[object Function]" != _toString(fun)) throw new TypeError;
- for (; length > ++i;) i in self && fun.call(thisp, self[i], i, object)
- }), Array.prototype.map || (Array.prototype.map = function(fun) {
- var object = toObject(this),
- self = splitString && "[object String]" == _toString(this) ? this.split("") : object,
- length = self.length >>> 0,
- result = Array(length),
- thisp = arguments[1];
- if ("[object Function]" != _toString(fun)) throw new TypeError(fun + " is not a function");
- for (var i = 0; length > i; i++) i in self && (result[i] = fun.call(thisp, self[i], i, object));
- return result
- }), Array.prototype.filter || (Array.prototype.filter = function(fun) {
- var value, object = toObject(this),
- self = splitString && "[object String]" == _toString(this) ? this.split("") : object,
- length = self.length >>> 0,
- result = [],
- thisp = arguments[1];
- if ("[object Function]" != _toString(fun)) throw new TypeError(fun + " is not a function");
- for (var i = 0; length > i; i++) i in self && (value = self[i], fun.call(thisp, value, i, object) && result.push(value));
- return result
- }), Array.prototype.every || (Array.prototype.every = function(fun) {
- var object = toObject(this),
- self = splitString && "[object String]" == _toString(this) ? this.split("") : object,
- length = self.length >>> 0,
- thisp = arguments[1];
- if ("[object Function]" != _toString(fun)) throw new TypeError(fun + " is not a function");
- for (var i = 0; length > i; i++)
- if (i in self && !fun.call(thisp, self[i], i, object)) return !1;
- return !0
- }), Array.prototype.some || (Array.prototype.some = function(fun) {
- var object = toObject(this),
- self = splitString && "[object String]" == _toString(this) ? this.split("") : object,
- length = self.length >>> 0,
- thisp = arguments[1];
- if ("[object Function]" != _toString(fun)) throw new TypeError(fun + " is not a function");
- for (var i = 0; length > i; i++)
- if (i in self && fun.call(thisp, self[i], i, object)) return !0;
- return !1
- }), Array.prototype.reduce || (Array.prototype.reduce = function(fun) {
- var object = toObject(this),
- self = splitString && "[object String]" == _toString(this) ? this.split("") : object,
- length = self.length >>> 0;
- if ("[object Function]" != _toString(fun)) throw new TypeError(fun + " is not a function");
- if (!length && 1 == arguments.length) throw new TypeError("reduce of empty array with no initial value");
- var result, i = 0;
- if (arguments.length >= 2) result = arguments[1];
- else
- for (;;) {
- if (i in self) {
- result = self[i++];
- break
- }
- if (++i >= length) throw new TypeError("reduce of empty array with no initial value")
- }
- for (; length > i; i++) i in self && (result = fun.call(void 0, result, self[i], i, object));
- return result
- }), Array.prototype.reduceRight || (Array.prototype.reduceRight = function(fun) {
- var object = toObject(this),
- self = splitString && "[object String]" == _toString(this) ? this.split("") : object,
- length = self.length >>> 0;
- if ("[object Function]" != _toString(fun)) throw new TypeError(fun + " is not a function");
- if (!length && 1 == arguments.length) throw new TypeError("reduceRight of empty array with no initial value");
- var result, i = length - 1;
- if (arguments.length >= 2) result = arguments[1];
- else
- for (;;) {
- if (i in self) {
- result = self[i--];
- break
- }
- if (0 > --i) throw new TypeError("reduceRight of empty array with no initial value")
- }
- do i in this && (result = fun.call(void 0, result, self[i], i, object)); while (i--);
- return result
- }), Array.prototype.indexOf && -1 == [0, 1].indexOf(1, 2) || (Array.prototype.indexOf = function(sought) {
- var self = splitString && "[object String]" == _toString(this) ? this.split("") : toObject(this),
- length = self.length >>> 0;
- if (!length) return -1;
- var i = 0;
- for (arguments.length > 1 && (i = toInteger(arguments[1])), i = i >= 0 ? i : Math.max(0, length + i); length > i; i++)
- if (i in self && self[i] === sought) return i;
- return -1
- }), Array.prototype.lastIndexOf && -1 == [0, 1].lastIndexOf(0, -3) || (Array.prototype.lastIndexOf = function(sought) {
- var self = splitString && "[object String]" == _toString(this) ? this.split("") : toObject(this),
- length = self.length >>> 0;
- if (!length) return -1;
- var i = length - 1;
- for (arguments.length > 1 && (i = Math.min(i, toInteger(arguments[1]))), i = i >= 0 ? i : length - Math.abs(i); i >= 0; i--)
- if (i in self && sought === self[i]) return i;
- return -1
- }), Object.getPrototypeOf || (Object.getPrototypeOf = function(object) {
- return object.__proto__ || (object.constructor ? object.constructor.prototype : prototypeOfObject)
- }), !Object.getOwnPropertyDescriptor) {
- var ERR_NON_OBJECT = "Object.getOwnPropertyDescriptor called on a non-object: ";
- Object.getOwnPropertyDescriptor = function(object, property) {
- if ("object" != typeof object && "function" != typeof object || null === object) throw new TypeError(ERR_NON_OBJECT + object);
- if (owns(object, property)) {
- var descriptor, getter, setter;
- if (descriptor = {
- enumerable: !0,
- configurable: !0
- }, supportsAccessors) {
- var prototype = object.__proto__;
- object.__proto__ = prototypeOfObject;
- var getter = lookupGetter(object, property),
- setter = lookupSetter(object, property);
- if (object.__proto__ = prototype, getter || setter) return getter && (descriptor.get = getter), setter && (descriptor.set = setter), descriptor
- }
- return descriptor.value = object[property], descriptor
- }
- }
- }
- if (Object.getOwnPropertyNames || (Object.getOwnPropertyNames = function(object) {
- return Object.keys(object)
- }), !Object.create) {
- var createEmpty;
- createEmpty = null === Object.prototype.__proto__ ? function() {
- return {
- __proto__: null
- }
- } : function() {
- var empty = {};
- for (var i in empty) empty[i] = null;
- return empty.constructor = empty.hasOwnProperty = empty.propertyIsEnumerable = empty.isPrototypeOf = empty.toLocaleString = empty.toString = empty.valueOf = empty.__proto__ = null, empty
- }, Object.create = function(prototype, properties) {
- var object;
- if (null === prototype) object = createEmpty();
- else {
- if ("object" != typeof prototype) throw new TypeError("typeof prototype[" + typeof prototype + "] != 'object'");
- var Type = function() {};
- Type.prototype = prototype, object = new Type, object.__proto__ = prototype
- }
- return void 0 !== properties && Object.defineProperties(object, properties), object
- }
- }
- if (Object.defineProperty) {
- var definePropertyWorksOnObject = doesDefinePropertyWork({}),
- definePropertyWorksOnDom = "undefined" == typeof document || doesDefinePropertyWork(document.createElement("div"));
- if (!definePropertyWorksOnObject || !definePropertyWorksOnDom) var definePropertyFallback = Object.defineProperty
- }
- if (!Object.defineProperty || definePropertyFallback) {
- var ERR_NON_OBJECT_DESCRIPTOR = "Property description must be an object: ",
- ERR_NON_OBJECT_TARGET = "Object.defineProperty called on non-object: ",
- ERR_ACCESSORS_NOT_SUPPORTED = "getters & setters can not be defined on this javascript engine";
- Object.defineProperty = function(object, property, descriptor) {
- if ("object" != typeof object && "function" != typeof object || null === object) throw new TypeError(ERR_NON_OBJECT_TARGET + object);
- if ("object" != typeof descriptor && "function" != typeof descriptor || null === descriptor) throw new TypeError(ERR_NON_OBJECT_DESCRIPTOR + descriptor);
- if (definePropertyFallback) try {
- return definePropertyFallback.call(Object, object, property, descriptor)
- } catch (exception) {}
- if (owns(descriptor, "value"))
- if (supportsAccessors && (lookupGetter(object, property) || lookupSetter(object, property))) {
- var prototype = object.__proto__;
- object.__proto__ = prototypeOfObject, delete object[property], object[property] = descriptor.value, object.__proto__ = prototype
- } else object[property] = descriptor.value;
- else {
- if (!supportsAccessors) throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED);
- owns(descriptor, "get") && defineGetter(object, property, descriptor.get), owns(descriptor, "set") && defineSetter(object, property, descriptor.set)
- }
- return object
- }
- }
- Object.defineProperties || (Object.defineProperties = function(object, properties) {
- for (var property in properties) owns(properties, property) && Object.defineProperty(object, property, properties[property]);
- return object
- }), Object.seal || (Object.seal = function(object) {
- return object
- }), Object.freeze || (Object.freeze = function(object) {
- return object
- });
- try {
- Object.freeze(function() {})
- } catch (exception) {
- Object.freeze = function(freezeObject) {
- return function(object) {
- return "function" == typeof object ? object : freezeObject(object)
- }
- }(Object.freeze)
- }
- if (Object.preventExtensions || (Object.preventExtensions = function(object) {
- return object
- }), Object.isSealed || (Object.isSealed = function() {
- return !1
- }), Object.isFrozen || (Object.isFrozen = function() {
- return !1
- }), Object.isExtensible || (Object.isExtensible = function(object) {
- if (Object(object) === object) throw new TypeError;
- for (var name = ""; owns(object, name);) name += "?";
- object[name] = !0;
- var returnValue = owns(object, name);
- return delete object[name], returnValue
- }), !Object.keys) {
- var hasDontEnumBug = !0,
- dontEnums = ["toString", "toLocaleString", "valueOf", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "constructor"],
- dontEnumsLength = dontEnums.length;
- for (var key in {
- toString: null
- }) hasDontEnumBug = !1;
- Object.keys = function(object) {
- if ("object" != typeof object && "function" != typeof object || null === object) throw new TypeError("Object.keys called on a non-object");
- var keys = [];
- for (var name in object) owns(object, name) && keys.push(name);
- if (hasDontEnumBug)
- for (var i = 0, ii = dontEnumsLength; ii > i; i++) {
- var dontEnum = dontEnums[i];
- owns(object, dontEnum) && keys.push(dontEnum)
- }
- return keys
- }
- }
- Date.now || (Date.now = function() {
- return (new Date).getTime()
- });
- var ws = " \n\f\r \u2028\u2029";
- if (!String.prototype.trim || ws.trim()) {
- ws = "[" + ws + "]";
- var trimBeginRegexp = RegExp("^" + ws + ws + "*"),
- trimEndRegexp = RegExp(ws + ws + "*$");
- String.prototype.trim = function() {
- return (this + "").replace(trimBeginRegexp, "").replace(trimEndRegexp, "")
- }
- }
- var toObject = function(o) {
- if (null == o) throw new TypeError("can't convert " + o + " to object");
- return Object(o)
- }
-});
diff --git a/packages/kbn-ace/src/ace/modes/x_json/x_json.ts b/packages/kbn-ace/src/ace/modes/x_json/x_json.ts
deleted file mode 100644
index 5a535e237a327..0000000000000
--- a/packages/kbn-ace/src/ace/modes/x_json/x_json.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", 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 ace from 'brace';
-import { XJsonHighlightRules } from '..';
-import { workerModule } from './worker';
-
-const { WorkerClient } = ace.acequire('ace/worker/worker_client');
-
-const oop = ace.acequire('ace/lib/oop');
-
-const { Mode: JSONMode } = ace.acequire('ace/mode/json');
-const { Tokenizer: AceTokenizer } = ace.acequire('ace/tokenizer');
-const { MatchingBraceOutdent } = ace.acequire('ace/mode/matching_brace_outdent');
-const { CstyleBehaviour } = ace.acequire('ace/mode/behaviour/cstyle');
-const { FoldMode: CStyleFoldMode } = ace.acequire('ace/mode/folding/cstyle');
-
-const XJsonMode: any = function XJsonMode(this: any) {
- const ruleset: any = new (XJsonHighlightRules as any)();
- ruleset.normalizeRules();
- this.$tokenizer = new AceTokenizer(ruleset.getRules());
- this.$outdent = new MatchingBraceOutdent();
- this.$behaviour = new CstyleBehaviour();
- this.foldingRules = new CStyleFoldMode();
-};
-
-oop.inherits(XJsonMode, JSONMode);
-
-// Then clobber `createWorker` method to install our worker source. Per ace's wiki: https://github.com/ajaxorg/ace/wiki/Syntax-validation
-(XJsonMode.prototype as any).createWorker = function (session: ace.IEditSession) {
- const xJsonWorker = new WorkerClient(['ace'], workerModule, 'JsonWorker');
-
- xJsonWorker.attachToDocument(session.getDocument());
-
- xJsonWorker.on('annotate', function (e: { data: any }) {
- session.setAnnotations(e.data);
- });
-
- xJsonWorker.on('terminate', function () {
- session.clearAnnotations();
- });
-
- return xJsonWorker;
-};
-
-export { XJsonMode };
-
-export function installXJsonMode(editor: ace.Editor) {
- const session = editor.getSession();
- session.setMode(new XJsonMode());
-}
diff --git a/packages/kbn-ace/tsconfig.json b/packages/kbn-ace/tsconfig.json
deleted file mode 100644
index a545abd7d65a6..0000000000000
--- a/packages/kbn-ace/tsconfig.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "extends": "../../tsconfig.base.json",
- "compilerOptions": {
- "allowJs": false,
- "outDir": "target/types",
- "stripInternal": true,
- "types": ["node"]
- },
- "include": [
- "**/*.ts",
- "src/ace/modes/x_json/worker/x_json.ace.worker.js"
- ],
- "exclude": [
- "target/**/*",
- ]
-}
diff --git a/packages/kbn-import-resolver/src/import_resolver.ts b/packages/kbn-import-resolver/src/import_resolver.ts
index 1b41418a5cb24..9ca16981b2afc 100644
--- a/packages/kbn-import-resolver/src/import_resolver.ts
+++ b/packages/kbn-import-resolver/src/import_resolver.ts
@@ -122,11 +122,6 @@ export class ImportResolver {
return true;
}
- // ignore amd require done by ace syntax plugin
- if (req === 'ace/lib/dom') {
- return true;
- }
-
// typescript validates these imports fine and they're purely virtual thanks to ambient type definitions in @elastic/eui so /shrug
if (
req.startsWith('@elastic/eui/src/components/') ||
diff --git a/packages/kbn-import-resolver/src/integration_tests/import_resolver.test.ts b/packages/kbn-import-resolver/src/integration_tests/import_resolver.test.ts
index f484de7904f06..1089f811b6e98 100644
--- a/packages/kbn-import-resolver/src/integration_tests/import_resolver.test.ts
+++ b/packages/kbn-import-resolver/src/integration_tests/import_resolver.test.ts
@@ -99,12 +99,6 @@ describe('#resolve()', () => {
}
`);
- expect(resolver.resolve('ace/lib/dom', FIXTURES_DIR)).toMatchInlineSnapshot(`
- Object {
- "type": "ignore",
- }
- `);
-
expect(resolver.resolve('@elastic/eui/src/components/foo', FIXTURES_DIR))
.toMatchInlineSnapshot(`
Object {
diff --git a/packages/kbn-test/src/jest/resolver.js b/packages/kbn-test/src/jest/resolver.js
index 27e0b14876587..8f985e9463962 100644
--- a/packages/kbn-test/src/jest/resolver.js
+++ b/packages/kbn-test/src/jest/resolver.js
@@ -70,7 +70,7 @@ module.exports = (request, options) => {
return FILE_MOCK;
}
- if (reqExt === '.worker' && (reqBasename.endsWith('.ace') || reqBasename.endsWith('.editor'))) {
+ if (reqExt === '.worker' && reqBasename.endsWith('.editor')) {
return WORKER_MOCK;
}
}
diff --git a/packages/kbn-ui-shared-deps-npm/BUILD.bazel b/packages/kbn-ui-shared-deps-npm/BUILD.bazel
index 48f234b0bfe10..ad3f3474f1b4e 100644
--- a/packages/kbn-ui-shared-deps-npm/BUILD.bazel
+++ b/packages/kbn-ui-shared-deps-npm/BUILD.bazel
@@ -53,7 +53,6 @@ RUNTIME_DEPS = [
"@npm//jquery",
"@npm//lodash",
"@npm//moment-timezone",
- "@npm//react-ace",
"@npm//react-dom",
"@npm//react-router-dom",
"@npm//react-router-dom-v5-compat",
diff --git a/packages/kbn-ui-shared-deps-npm/webpack.config.js b/packages/kbn-ui-shared-deps-npm/webpack.config.js
index 3b16430aeb724..926a041a72c3d 100644
--- a/packages/kbn-ui-shared-deps-npm/webpack.config.js
+++ b/packages/kbn-ui-shared-deps-npm/webpack.config.js
@@ -88,7 +88,6 @@ module.exports = (_, argv) => {
'moment-timezone/moment-timezone',
'moment-timezone/data/packed/latest.json',
'moment',
- 'react-ace',
'react-dom',
'react-dom/server',
'react-router-dom',
diff --git a/src/core/public/styles/_ace_overrides.scss b/src/core/public/styles/_ace_overrides.scss
deleted file mode 100644
index ca5230b46acd3..0000000000000
--- a/src/core/public/styles/_ace_overrides.scss
+++ /dev/null
@@ -1,202 +0,0 @@
-// SASSTODO: Replace with an EUI editor
-// Intentionally not using the EuiCodeBlock colors here because they actually change
-// hue from light to dark theme. So some colors would change while others wouldn't.
-// Seemed weird, so just hexing all the colors but using the `makeHighContrastColor()`
-// function to ensure accessible contrast.
-
-// In order to override the TM (Textmate) theme of Ace/Brace, everywhere,
-// it is being scoped by a known outer selector
-.kbnBody {
- .ace-tm {
- $aceBackground: tintOrShade($euiColorLightShade, 50%, 0);
-
- background-color: $euiColorLightestShade;
- color: $euiTextColor;
-
- .ace_scrollbar {
- @include euiScrollBar;
- }
-
- .ace_gutter-active-line,
- .ace_marker-layer .ace_active-line {
- background-color: transparentize($euiColorLightShade, .3);
- }
-
- .ace_snippet-marker {
- width: 100%;
- background-color: $aceBackground;
- border: none;
- }
-
- .ace_indent-guide {
- background: linear-gradient(to left, $euiColorMediumShade 0%, $euiColorMediumShade 1px, transparent 1px, transparent 100%);
- }
-
- .ace_search {
- z-index: $euiZLevel1 + 1;
- }
-
- .ace_layer.ace_marker-layer {
- overflow: visible;
- }
-
- .ace_warning {
- color: $euiColorDanger;
- }
-
- .ace_method {
- color: makeHighContrastColor(#DD0A73, $aceBackground);
- }
-
- .ace_url,
- .ace_start_triple_quote,
- .ace_end_triple_quote {
- color: makeHighContrastColor(#00A69B, $aceBackground);
- }
-
- .ace_multi_string {
- color: makeHighContrastColor(#009926, $aceBackground);
- font-style: italic;
- }
-
- .ace_gutter {
- background-color: $euiColorEmptyShade;
- color: $euiColorDarkShade;
- border-left: 1px solid $aceBackground;
- }
-
- .ace_print-margin {
- width: 1px;
- background: $euiColorLightShade;
- }
-
- .ace_fold {
- background-color: #6B72E6;
- }
-
- .ace_cursor {
- color: $euiColorFullShade;
- }
-
- .ace_invisible {
- color: $euiColorLightShade;
- }
-
- .ace_storage,
- .ace_keyword {
- color: makeHighContrastColor(#0079A5, $aceBackground);
- }
-
- .ace_constant {
- color: makeHighContrastColor(#900, $aceBackground);
- }
-
- .ace_constant.ace_buildin {
- color: makeHighContrastColor(rgb(88, 72, 246), $aceBackground);
- }
-
- .ace_constant.ace_language {
- color: makeHighContrastColor(rgb(88, 92, 246), $aceBackground);
- }
-
- .ace_constant.ace_library {
- color: makeHighContrastColor(#009926, $aceBackground);
- }
-
- .ace_invalid {
- background-color: euiCallOutColor('danger', 'background');
- color: euiCallOutColor('danger', 'foreground');
- }
-
- .ace_support.ace_function {
- color: makeHighContrastColor(rgb(60, 76, 114), $aceBackground);
- }
-
- .ace_support.ace_constant {
- color: makeHighContrastColor(#009926, $aceBackground);
- }
-
- .ace_support.ace_type,
- .ace_support.ace_class {
- color: makeHighContrastColor(rgb(109, 121, 222), $aceBackground);
- }
-
- .ace_keyword.ace_operator {
- color: makeHighContrastColor($euiColorDarkShade, $aceBackground);
- }
-
- .ace_string {
- color: makeHighContrastColor(#009926, $aceBackground);
- }
-
- .ace_comment {
- color: makeHighContrastColor(rgb(76, 136, 107), $aceBackground);
- }
-
- .ace_comment.ace_doc {
- color: makeHighContrastColor(#0079A5, $aceBackground);
- }
-
- .ace_comment.ace_doc.ace_tag {
- color: makeHighContrastColor($euiColorMediumShade, $aceBackground);
- }
-
- .ace_constant.ace_numeric {
- color: makeHighContrastColor(#0079A5, $aceBackground);
- }
-
- .ace_variable {
- color: makeHighContrastColor(#0079A5, $aceBackground);
- }
-
- .ace_xml-pe {
- color: makeHighContrastColor($euiColorDarkShade, $aceBackground);
- }
-
- .ace_entity.ace_name.ace_function {
- color: makeHighContrastColor(#0000A2, $aceBackground);
- }
-
- .ace_heading {
- color: makeHighContrastColor(rgb(12, 7, 255), $aceBackground);
- }
-
- .ace_list {
- color: makeHighContrastColor(rgb(185, 6, 144), $aceBackground);
- }
-
- .ace_meta.ace_tag {
- color: makeHighContrastColor(rgb(0, 22, 142), $aceBackground);
- }
-
- .ace_string.ace_regex {
- color: makeHighContrastColor(rgb(255, 0, 0), $aceBackground);
- }
-
- .ace_marker-layer .ace_selection {
- background: tintOrShade($euiColorPrimary, 70%, 70%);
- }
-
- &.ace_multiselect .ace_selection.ace_start {
- box-shadow: 0 0 3px 0 $euiColorEmptyShade;
- }
-
- .ace_marker-layer .ace_step {
- background: tintOrShade($euiColorWarning, 80%, 80%);
- }
-
- .ace_marker-layer .ace_stack {
- background: tintOrShade($euiColorSuccess, 80%, 80%);
- }
-
- .ace_marker-layer .ace_bracket {
- margin: -1px 0 0 -1px;
- border: $euiBorderThin;
- }
-
- .ace_marker-layer .ace_selected-word {
- background: $euiColorLightestShade;
- border: $euiBorderThin;
- }
- }
-}
diff --git a/src/core/public/styles/_index.scss b/src/core/public/styles/_index.scss
index 42981c7e07398..cfdb1c7192dcd 100644
--- a/src/core/public/styles/_index.scss
+++ b/src/core/public/styles/_index.scss
@@ -1,4 +1,3 @@
@import './base';
-@import './ace_overrides';
@import './chrome/index';
@import './rendering/index';
diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/ace/_ui_ace_keyboard_mode.scss b/src/plugins/es_ui_shared/__packages_do_not_import__/ace/_ui_ace_keyboard_mode.scss
deleted file mode 100644
index 2ad92f3506b20..0000000000000
--- a/src/plugins/es_ui_shared/__packages_do_not_import__/ace/_ui_ace_keyboard_mode.scss
+++ /dev/null
@@ -1,24 +0,0 @@
-.kbnUiAceKeyboardHint {
- position: absolute;
- top: 0;
- bottom: 0;
- right: 0;
- left: 0;
- background: transparentize($euiColorEmptyShade, .3);
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- text-align: center;
- opacity: 0;
-
- &:focus {
- opacity: 1;
- border: 2px solid $euiColorPrimary;
- z-index: $euiZLevel1;
- }
-
- &.kbnUiAceKeyboardHint-isInactive {
- display: none;
- }
-}
diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/ace/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/ace/index.ts
deleted file mode 100644
index 6214a2609462c..0000000000000
--- a/src/plugins/es_ui_shared/__packages_do_not_import__/ace/index.ts
+++ /dev/null
@@ -1,10 +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", 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".
- */
-
-export { useUIAceKeyboardMode } from './use_ui_ace_keyboard_mode';
diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/ace/use_ui_ace_keyboard_mode.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/ace/use_ui_ace_keyboard_mode.tsx
deleted file mode 100644
index f1fe888104783..0000000000000
--- a/src/plugins/es_ui_shared/__packages_do_not_import__/ace/use_ui_ace_keyboard_mode.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the "Elastic License
- * 2.0", 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 React, { useEffect, useRef } from 'react';
-import * as ReactDOM from 'react-dom';
-import { keys, EuiText } from '@elastic/eui';
-import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
-
-import './_ui_ace_keyboard_mode.scss';
-import type { AnalyticsServiceStart, I18nStart, ThemeServiceStart } from '@kbn/core/public';
-
-interface StartServices {
- analytics: Pick;
- i18n: I18nStart;
- theme: Pick;
-}
-
-const OverlayText = (startServices: StartServices) => (
- // The point of this element is for accessibility purposes, so ignore eslint error
- // in this case
- //
-
-
- Press Enter to start editing.
-
- When you’re done, press Escape to stop editing.
-
-);
-
-export function useUIAceKeyboardMode(
- aceTextAreaElement: HTMLTextAreaElement | null,
- startServices: StartServices,
- isAccessibilityOverlayEnabled: boolean = true
-) {
- const overlayMountNode = useRef(null);
- const autoCompleteVisibleRef = useRef(false);
- useEffect(() => {
- function onDismissOverlay(event: KeyboardEvent) {
- if (event.key === keys.ENTER) {
- event.preventDefault();
- aceTextAreaElement!.focus();
- }
- }
-
- function enableOverlay() {
- if (overlayMountNode.current) {
- overlayMountNode.current.focus();
- }
- }
-
- const isAutoCompleteVisible = () => {
- const autoCompleter = document.querySelector('.ace_autocomplete');
- if (!autoCompleter) {
- return false;
- }
- // The autoComplete is just hidden when it's closed, not removed from the DOM.
- return autoCompleter.style.display !== 'none';
- };
-
- const documentKeyDownListener = () => {
- autoCompleteVisibleRef.current = isAutoCompleteVisible();
- };
-
- const aceKeydownListener = (event: KeyboardEvent) => {
- if (event.key === keys.ESCAPE && !autoCompleteVisibleRef.current) {
- event.preventDefault();
- event.stopPropagation();
- enableOverlay();
- }
- };
- if (aceTextAreaElement && isAccessibilityOverlayEnabled) {
- // We don't control HTML elements inside of ace so we imperatively create an element
- // that acts as a container and insert it just before ace's textarea element
- // so that the overlay lives at the correct spot in the DOM hierarchy.
- overlayMountNode.current = document.createElement('div');
- overlayMountNode.current.className = 'kbnUiAceKeyboardHint';
- overlayMountNode.current.setAttribute('role', 'application');
- overlayMountNode.current.tabIndex = 0;
- overlayMountNode.current.addEventListener('focus', enableOverlay);
- overlayMountNode.current.addEventListener('keydown', onDismissOverlay);
-
- ReactDOM.render( , overlayMountNode.current);
-
- aceTextAreaElement.parentElement!.insertBefore(overlayMountNode.current, aceTextAreaElement);
- aceTextAreaElement.setAttribute('tabindex', '-1');
-
- // Order of events:
- // 1. Document capture event fires first and we check whether an autocomplete menu is open on keydown
- // (not ideal because this is scoped to the entire document).
- // 2. Ace changes it's state (like hiding or showing autocomplete menu)
- // 3. We check what button was pressed and whether autocomplete was visible then determine
- // whether it should act like a dismiss or if we should display an overlay.
- document.addEventListener('keydown', documentKeyDownListener, { capture: true });
- aceTextAreaElement.addEventListener('keydown', aceKeydownListener);
- }
- return () => {
- if (aceTextAreaElement && isAccessibilityOverlayEnabled) {
- document.removeEventListener('keydown', documentKeyDownListener, { capture: true });
- aceTextAreaElement.removeEventListener('keydown', aceKeydownListener);
- const textAreaContainer = aceTextAreaElement.parentElement;
- if (textAreaContainer && textAreaContainer.contains(overlayMountNode.current!)) {
- textAreaContainer.removeChild(overlayMountNode.current!);
- }
- }
- };
- }, [aceTextAreaElement, startServices, isAccessibilityOverlayEnabled]);
-}
diff --git a/src/plugins/es_ui_shared/public/ace/index.ts b/src/plugins/es_ui_shared/public/ace/index.ts
deleted file mode 100644
index 9d010117e560e..0000000000000
--- a/src/plugins/es_ui_shared/public/ace/index.ts
+++ /dev/null
@@ -1,10 +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", 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".
- */
-
-export { useUIAceKeyboardMode } from '../../__packages_do_not_import__/ace';
diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts
index 3b3ccc3fca08f..ddcdb84fa5758 100644
--- a/src/plugins/es_ui_shared/public/index.ts
+++ b/src/plugins/es_ui_shared/public/index.ts
@@ -12,7 +12,6 @@
* In the future, each top level folder should be exported like that to avoid naming collision
*/
import * as Forms from './forms';
-import * as ace from './ace';
import * as GlobalFlyout from './global_flyout';
import * as XJson from './xjson';
@@ -47,7 +46,7 @@ export {
useAuthorizationContext,
} from './authorization';
-export { Forms, ace, GlobalFlyout, XJson };
+export { Forms, GlobalFlyout, XJson };
export { extractQueryParams, attemptToURIDecode } from './url';
diff --git a/src/plugins/es_ui_shared/static/forms/components/index.ts b/src/plugins/es_ui_shared/static/forms/components/index.ts
index 4ccfeed19dbfe..2e5dd03390eb7 100644
--- a/src/plugins/es_ui_shared/static/forms/components/index.ts
+++ b/src/plugins/es_ui_shared/static/forms/components/index.ts
@@ -7,22 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-/*
-@TODO
-
-The react-ace and brace/mode/json imports below are loaded eagerly - before this plugin is explicitly loaded by users. This makes
-the brace JSON mode, used for JSON syntax highlighting and grammar checking, available across all of Kibana plugins.
-
-This is not ideal because we are loading JS that is not necessary for Kibana to start, but the alternative
-is breaking JSON mode for an unknown number of ace editors across Kibana - not all components reference the underlying
-EuiCodeEditor (for instance, explicitly).
-
-Importing here is a way of preventing a more sophisticated solution to this problem since we want to, eventually,
-migrate all code editors over to Monaco. Once that is done, we should remove this import.
- */
-import 'react-ace';
-import 'brace/mode/json';
-
export * from './field';
export * from './form_row';
export * from './fields';
diff --git a/src/plugins/es_ui_shared/tsconfig.json b/src/plugins/es_ui_shared/tsconfig.json
index f3dc3bb39a31d..2747f41b0f370 100644
--- a/src/plugins/es_ui_shared/tsconfig.json
+++ b/src/plugins/es_ui_shared/tsconfig.json
@@ -24,7 +24,6 @@
"@kbn/storybook",
"@kbn/shared-ux-link-redirect-app",
"@kbn/code-editor",
- "@kbn/react-kibana-context-render",
"@kbn/core-application-common",
],
"exclude": [
diff --git a/src/plugins/vis_default_editor/public/default_editor.tsx b/src/plugins/vis_default_editor/public/default_editor.tsx
index f61450c8e85e0..dc9e83e8c3b43 100644
--- a/src/plugins/vis_default_editor/public/default_editor.tsx
+++ b/src/plugins/vis_default_editor/public/default_editor.tsx
@@ -8,7 +8,6 @@
*/
import './index.scss';
-import 'brace/mode/json';
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { EventEmitter } from 'events';
diff --git a/test/functional/apps/management/data_views/_scripted_fields.ts b/test/functional/apps/management/data_views/_scripted_fields.ts
index 172537bf4e73a..f86ae72aa5047 100644
--- a/test/functional/apps/management/data_views/_scripted_fields.ts
+++ b/test/functional/apps/management/data_views/_scripted_fields.ts
@@ -19,10 +19,6 @@
// 3. Filter in Discover by the scripted field
// 4. Visualize with aggregation on the scripted field by clicking unifiedFieldList.clickFieldListItemVisualize
-// NOTE: Scripted field input is managed by Ace editor, which automatically
-// appends closing braces, for exmaple, if you type opening square brace [
-// it will automatically insert a a closing square brace ], etc.
-
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
diff --git a/test/functional/apps/management/data_views/_scripted_fields_classic_table.ts b/test/functional/apps/management/data_views/_scripted_fields_classic_table.ts
index 063e0b960d52e..4f3d30222e496 100644
--- a/test/functional/apps/management/data_views/_scripted_fields_classic_table.ts
+++ b/test/functional/apps/management/data_views/_scripted_fields_classic_table.ts
@@ -19,10 +19,6 @@
// 3. Filter in Discover by the scripted field
// 4. Visualize with aggregation on the scripted field by clicking unifiedFieldList.clickFieldListItemVisualize
-// NOTE: Scripted field input is managed by Ace editor, which automatically
-// appends closing braces, for exmaple, if you type opening square brace [
-// it will automatically insert a a closing square brace ], etc.
-
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 4bc68d806f043..a05f287c0f9ef 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -6,8 +6,6 @@
// START AUTOMATED PACKAGE LISTING
"@kbn/aad-fixtures-plugin": ["x-pack/test/alerting_api_integration/common/plugins/aad"],
"@kbn/aad-fixtures-plugin/*": ["x-pack/test/alerting_api_integration/common/plugins/aad/*"],
- "@kbn/ace": ["packages/kbn-ace"],
- "@kbn/ace/*": ["packages/kbn-ace/*"],
"@kbn/actions-plugin": ["x-pack/plugins/actions"],
"@kbn/actions-plugin/*": ["x-pack/plugins/actions/*"],
"@kbn/actions-simulators-plugin": ["x-pack/test/alerting_api_integration/common/plugins/actions_simulators"],
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js
index 351f1bd77592f..c0b6c0f4c9a09 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js
@@ -30,8 +30,6 @@ import {
EuiTextColor,
EuiTitle,
} from '@elastic/eui';
-import 'react-ace';
-import 'brace/theme/textmate';
import { getIndexListUri } from '@kbn/index-management-plugin/public';
import { routing } from '../../../../../services/routing';
diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx
index e74d2f7703f31..63c395b1f4bbc 100644
--- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx
+++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx
@@ -5,11 +5,6 @@
* 2.0.
*/
-// brace/ace uses the Worker class, which is not currently provided by JSDOM.
-// This is not required for the tests to pass, but it rather suppresses lengthy
-// warnings in the console which adds unnecessary noise to the test output.
-import '@kbn/web-worker-stub';
-
import React from 'react';
import { coreMock, scopedHistoryMock } from '@kbn/core/public/mocks';
diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx
index 64e332bd130bd..9db22a251779b 100644
--- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx
+++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx
@@ -5,13 +5,6 @@
* 2.0.
*/
-import 'brace';
-import 'brace/mode/json';
-// brace/ace uses the Worker class, which is not currently provided by JSDOM.
-// This is not required for the tests to pass, but it rather suppresses lengthy
-// warnings in the console which adds unnecessary noise to the test output.
-import '@kbn/web-worker-stub';
-
import React from 'react';
import { act } from 'react-dom/test-utils';
import '@kbn/code-editor-mock/jest_helper';
diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx
index 963bbf3a35cfc..121f694517a83 100644
--- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx
+++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx
@@ -5,10 +5,6 @@
* 2.0.
*/
-import 'react-ace';
-import 'brace/mode/json';
-import 'brace/theme/github';
-
import { EuiButton, EuiFormRow, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
import React, { Fragment, useState } from 'react';
diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx
index d01229cdce8a9..21ece31571ae1 100644
--- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx
+++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx
@@ -5,11 +5,6 @@
* 2.0.
*/
-// brace/ace uses the Worker class, which is not currently provided by JSDOM.
-// This is not required for the tests to pass, but it rather suppresses lengthy
-// warnings in the console which adds unnecessary noise to the test output.
-import '@kbn/web-worker-stub';
-
import { EuiErrorBoundary } from '@elastic/eui';
import React from 'react';
diff --git a/x-pack/plugins/security/tsconfig.json b/x-pack/plugins/security/tsconfig.json
index 535e221f8e5fb..2d5509d2d6d42 100644
--- a/x-pack/plugins/security/tsconfig.json
+++ b/x-pack/plugins/security/tsconfig.json
@@ -51,7 +51,6 @@
"@kbn/core-saved-objects-api-server-internal",
"@kbn/core-saved-objects-api-server-mocks",
"@kbn/logging-mocks",
- "@kbn/web-worker-stub",
"@kbn/core-saved-objects-utils-server",
"@kbn/core-saved-objects-api-server",
"@kbn/core-saved-objects-base-server-internal",
diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/es_query_expression.test.tsx b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/es_query_expression.test.tsx
index 6686d56173de4..e1d17b79e612d 100644
--- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/es_query_expression.test.tsx
+++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/es_query_expression.test.tsx
@@ -6,7 +6,6 @@
*/
import React from 'react';
-import 'brace';
import { of, Subject } from 'rxjs';
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import { act } from 'react-dom/test-utils';
diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/expression.test.tsx b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/expression.test.tsx
index 64d075b7ba723..568a8cf226ae2 100644
--- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/expression.test.tsx
+++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/expression.test.tsx
@@ -6,7 +6,6 @@
*/
import type { DataView } from '@kbn/data-views-plugin/public';
import { mountWithIntl } from '@kbn/test-jest-helpers';
-import 'brace';
import React, { useState } from 'react';
import { docLinksServiceMock } from '@kbn/core/public/mocks';
import { httpServiceMock } from '@kbn/core/public/mocks';
diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/expression.tsx b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/expression.tsx
index 196d138c68964..2f0c46a5e34c5 100644
--- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/expression.tsx
+++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/expression.tsx
@@ -7,7 +7,6 @@
import React, { memo, PropsWithChildren, useCallback } from 'react';
import deepEqual from 'fast-deep-equal';
-import 'brace/theme/github';
import { EuiCallOut, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
diff --git a/yarn.lock b/yarn.lock
index 019de6121540e..911cbb9d9f175 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3295,10 +3295,6 @@
version "0.0.0"
uid ""
-"@kbn/ace@link:packages/kbn-ace":
- version "0.0.0"
- uid ""
-
"@kbn/actions-plugin@link:x-pack/plugins/actions":
version "0.0.0"
uid ""
@@ -13579,11 +13575,6 @@ brace-expansion@^2.0.1:
dependencies:
balanced-match "^1.0.0"
-brace@0.11.1, brace@^0.11.1:
- version "0.11.1"
- resolved "https://registry.yarnpkg.com/brace/-/brace-0.11.1.tgz#4896fcc9d544eef45f4bb7660db320d3b379fe58"
- integrity sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg=
-
braces@^2.3.1:
version "2.3.2"
resolved "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz"
@@ -16326,7 +16317,7 @@ diacritics@^1.3.0:
resolved "https://registry.yarnpkg.com/diacritics/-/diacritics-1.3.0.tgz#3efa87323ebb863e6696cebb0082d48ff3d6f7a1"
integrity sha1-PvqHMj67hj5mls67AILUj/PW96E=
-diff-match-patch@^1.0.0, diff-match-patch@^1.0.4, diff-match-patch@^1.0.5:
+diff-match-patch@^1.0.0, diff-match-patch@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37"
integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==
@@ -26617,17 +26608,6 @@ re2js@0.4.2:
resolved "https://registry.yarnpkg.com/re2js/-/re2js-0.4.2.tgz#e344697e64d128ea65c121d6581e67ee5bfa5feb"
integrity sha512-wuv0p0BGbrVIkobV8zh82WjDurXko0QNCgaif6DdRAljgVm2iio4PVYCwjAxGaWen1/QZXWDM67dIslmz7AIbA==
-react-ace@^7.0.5:
- version "7.0.5"
- resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-7.0.5.tgz#798299fd52ddf3a3dcc92afc5865538463544f01"
- integrity sha512-3iI+Rg2bZXCn9K984ll2OF4u9SGcJH96Q1KsUgs9v4M2WePS4YeEHfW2nrxuqJrAkE5kZbxaCE79k6kqK0YBjg==
- dependencies:
- brace "^0.11.1"
- diff-match-patch "^1.0.4"
- lodash.get "^4.4.2"
- lodash.isequal "^4.5.0"
- prop-types "^15.7.2"
-
react-clientside-effect@^1.2.6:
version "1.2.6"
resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz#29f9b14e944a376b03fb650eed2a754dd128ea3a"
From 7927ebf2a6e3bc459acb6d3217cb87ba8f837e09 Mon Sep 17 00:00:00 2001
From: Irene Blanco
Date: Thu, 10 Oct 2024 14:32:37 +0200
Subject: [PATCH 45/87] [Inventory] Check permissions before registering the
Inventory plugin in observabilityShared navigation (#195557)
## Summary
Fixes https://github.com/elastic/kibana/issues/195360 and
https://github.com/elastic/kibana/issues/195560
This PR fixes a bug where the Inventory plugin is improperly registered
in the ObservabilityShared navigation, even in spaces that lack the
required permissions or for user roles that don't have permissions. As a
result, the Inventory link appears in the navigation whenever the
space/user has access to any other Observability plugin.
### Space permissions
#### Before
|Space config|ObservabilityShared navigation|
|-|-|
|![Image](https://github.com/user-attachments/assets/53f51d01-faae-4795-b84b-da636a2e46d3)|![Image](https://github.com/user-attachments/assets/d6c98df5-6975-4e95-be24-7e53e6e1ee02)|
##### After
|Space config|ObservabilityShared navigation|
|-|-|
|![Screenshot 2024-10-09 at 11 47
34](https://github.com/user-attachments/assets/2f5be4c0-4f32-4103-b43a-059e435f730c)|![Screenshot
2024-10-09 at 11 47
12](https://github.com/user-attachments/assets/9dce6095-0a65-4c1d-973f-8a96c330fd08)|
|![Screenshot 2024-10-09 at 11 47
59](https://github.com/user-attachments/assets/f697e646-c034-41d8-b546-925ba4c9fb3a)|![Screenshot
2024-10-09 at 11 48
09](https://github.com/user-attachments/assets/200cf3d3-b7a3-4a42-84ec-48dcf563ad37)|
### User permissions
#### Before
|Role config|ObservabilityShared navigation|
|-|-|
|![Image](https://github.com/user-attachments/assets/74e52c43-0da9-4878-813d-049c1f9f2f83)|![Image](https://github.com/user-attachments/assets/4ffb48a9-81f0-48bd-9156-a98e3361c279)|
#### After
|Role config|ObservabilityShared navigation|
|-|-|
|![Image](https://github.com/user-attachments/assets/74e52c43-0da9-4878-813d-049c1f9f2f83)| |
---
.../.storybook/get_mock_inventory_context.tsx | 2 +
.../inventory/kibana.jsonc | 2 +-
.../inventory/public/plugin.ts | 75 ++++++++++++-------
.../inventory/public/types.ts | 2 +
.../inventory/tsconfig.json | 3 +-
5 files changed, 54 insertions(+), 30 deletions(-)
diff --git a/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx b/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx
index 51aaeebc655f2..d90ce08aab1c6 100644
--- a/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx
+++ b/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx
@@ -13,6 +13,7 @@ import type { InferencePublicStart } from '@kbn/inference-plugin/public';
import type { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
+import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import type { InventoryKibanaContext } from '../public/hooks/use_kibana';
import type { ITelemetryClient } from '../public/services/telemetry/types';
@@ -33,5 +34,6 @@ export function getMockInventoryContext(): InventoryKibanaContext {
fetch: jest.fn(),
stream: jest.fn(),
},
+ spaces: {} as unknown as SpacesPluginStart,
};
}
diff --git a/x-pack/plugins/observability_solution/inventory/kibana.jsonc b/x-pack/plugins/observability_solution/inventory/kibana.jsonc
index f60cf36183b24..28556c7bcc583 100644
--- a/x-pack/plugins/observability_solution/inventory/kibana.jsonc
+++ b/x-pack/plugins/observability_solution/inventory/kibana.jsonc
@@ -19,7 +19,7 @@
"share"
],
"requiredBundles": ["kibanaReact"],
- "optionalPlugins": [],
+ "optionalPlugins": ["spaces"],
"extraPublicDirs": []
}
}
diff --git a/x-pack/plugins/observability_solution/inventory/public/plugin.ts b/x-pack/plugins/observability_solution/inventory/public/plugin.ts
index c02a57b45f691..30e3a1eed3681 100644
--- a/x-pack/plugins/observability_solution/inventory/public/plugin.ts
+++ b/x-pack/plugins/observability_solution/inventory/public/plugin.ts
@@ -17,7 +17,7 @@ import {
import { INVENTORY_APP_ID } from '@kbn/deeplinks-observability/constants';
import { i18n } from '@kbn/i18n';
import type { Logger } from '@kbn/logging';
-import { from, map } from 'rxjs';
+import { from, map, mergeMap, of } from 'rxjs';
import { createCallInventoryAPI } from './api';
import { TelemetryService } from './services/telemetry/telemetry_service';
import { InventoryServices } from './services/types';
@@ -54,34 +54,53 @@ export class InventoryPlugin
'observability:entityCentricExperience',
true
);
+ const getStartServices = coreSetup.getStartServices();
- if (isEntityCentricExperienceSettingEnabled) {
- pluginsSetup.observabilityShared.navigation.registerSections(
- from(coreSetup.getStartServices()).pipe(
- map(([coreStart, pluginsStart]) => {
- return [
- {
- label: '',
- sortKey: 300,
- entries: [
- {
- label: i18n.translate('xpack.inventory.inventoryLinkTitle', {
- defaultMessage: 'Inventory',
- }),
- app: INVENTORY_APP_ID,
- path: '/',
- matchPath(currentPath: string) {
- return ['/', ''].some((testPath) => currentPath.startsWith(testPath));
- },
- isTechnicalPreview: true,
+ const hideInventory$ = from(getStartServices).pipe(
+ mergeMap(([coreStart, pluginsStart]) => {
+ if (pluginsStart.spaces) {
+ return from(pluginsStart.spaces.getActiveSpace()).pipe(
+ map(
+ (space) =>
+ space.disabledFeatures.includes(INVENTORY_APP_ID) ||
+ !coreStart.application.capabilities.inventory.show
+ )
+ );
+ }
+
+ return of(!coreStart.application.capabilities.inventory.show);
+ })
+ );
+
+ const sections$ = hideInventory$.pipe(
+ map((hideInventory) => {
+ if (isEntityCentricExperienceSettingEnabled && !hideInventory) {
+ return [
+ {
+ label: '',
+ sortKey: 300,
+ entries: [
+ {
+ label: i18n.translate('xpack.inventory.inventoryLinkTitle', {
+ defaultMessage: 'Inventory',
+ }),
+ app: INVENTORY_APP_ID,
+ path: '/',
+ matchPath(currentPath: string) {
+ return ['/', ''].some((testPath) => currentPath.startsWith(testPath));
},
- ],
- },
- ];
- })
- )
- );
- }
+ isTechnicalPreview: true,
+ },
+ ],
+ },
+ ];
+ }
+ return [];
+ })
+ );
+
+ pluginsSetup.observabilityShared.navigation.registerSections(sections$);
+
this.telemetry.setup({ analytics: coreSetup.analytics });
const telemetry = this.telemetry.start();
@@ -102,7 +121,7 @@ export class InventoryPlugin
// Load application bundle and Get start services
const [{ renderApp }, [coreStart, pluginsStart]] = await Promise.all([
import('./application'),
- coreSetup.getStartServices(),
+ getStartServices,
]);
const services: InventoryServices = {
diff --git a/x-pack/plugins/observability_solution/inventory/public/types.ts b/x-pack/plugins/observability_solution/inventory/public/types.ts
index 2393b1b55e2b6..48fe7e7eed1c7 100644
--- a/x-pack/plugins/observability_solution/inventory/public/types.ts
+++ b/x-pack/plugins/observability_solution/inventory/public/types.ts
@@ -17,6 +17,7 @@ import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/publi
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public';
+import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
/* eslint-disable @typescript-eslint/no-empty-interface*/
@@ -38,6 +39,7 @@ export interface InventoryStartDependencies {
data: DataPublicPluginStart;
entityManager: EntityManagerPublicPluginStart;
share: SharePluginStart;
+ spaces?: SpacesPluginStart;
}
export interface InventoryPublicSetup {}
diff --git a/x-pack/plugins/observability_solution/inventory/tsconfig.json b/x-pack/plugins/observability_solution/inventory/tsconfig.json
index 54fcfe7e3a11f..20b5e2e37232a 100644
--- a/x-pack/plugins/observability_solution/inventory/tsconfig.json
+++ b/x-pack/plugins/observability_solution/inventory/tsconfig.json
@@ -45,6 +45,7 @@
"@kbn/config-schema",
"@kbn/elastic-agent-utils",
"@kbn/custom-icons",
- "@kbn/ui-theme"
+ "@kbn/ui-theme",
+ "@kbn/spaces-plugin"
]
}
From e0e4ec10e3c329f933bed0a01dbeaecdf79cfa99 Mon Sep 17 00:00:00 2001
From: Marco Antonio Ghiani
Date: Thu, 10 Oct 2024 14:33:18 +0200
Subject: [PATCH 46/87] [Logs ML] Check permissions before granting access to
Logs ML pages (#195278)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## 📓 Summary
Closes #191206
This work fixes issues while accessing the Logs Anomalies and Logs
Categories pages due to a lack of user privileges.
The privileges were correctly handled until
https://github.com/elastic/kibana/pull/168234 was merged, which
introduced a call to retrieve ml formats information higher in the React
hierarchy before the privileges could be asserted for the logged user.
This was resulting in the call failing and letting the user stack in
loading states or erroneous error pages.
These changes lift the license + ML read privileges checks higher in the
hierarchy so we can display the right prompts before calling the ml
formats API, which will resolve correctly if the user has the right
privileges.
### User without valid license
### User with a valid license (or Trial), but no ML privileges
### User with a valid license (or Trial) and only Read ML privileges
### User with a valid license (or Trial) and All ML privileges, which
are the requirements to work with ML Logs features
---------
Co-authored-by: Marco Antonio Ghiani
---
.../missing_results_privileges_prompt.tsx | 1 +
.../missing_setup_privileges_prompt.tsx | 1 +
.../pages/logs/log_entry_categories/page.tsx | 27 +++++-
.../log_entry_categories/page_content.tsx | 28 +------
.../public/pages/logs/log_entry_rate/page.tsx | 28 ++++++-
.../logs/log_entry_rate/page_content.tsx | 28 +------
.../infra/logs/log_entry_categories_tab.ts | 82 ++++++++++++++++--
.../apps/infra/logs/log_entry_rate_tab.ts | 84 +++++++++++++++++--
.../services/logs_ui/log_entry_categories.ts | 8 ++
.../services/logs_ui/log_entry_rate.ts | 8 ++
10 files changed, 230 insertions(+), 65 deletions(-)
diff --git a/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_results_privileges_prompt.tsx b/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_results_privileges_prompt.tsx
index 97eeeabe8721b..dce819ffb0930 100644
--- a/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_results_privileges_prompt.tsx
+++ b/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_results_privileges_prompt.tsx
@@ -16,6 +16,7 @@ import { UserManagementLink } from './user_management_link';
export const MissingResultsPrivilegesPrompt: React.FunctionComponent = () => (
{missingMlPrivilegesTitle}}
body={{missingMlResultsPrivilegesDescription}
}
actions={ }
diff --git a/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_setup_privileges_prompt.tsx b/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_setup_privileges_prompt.tsx
index f959c5035d1a4..4e2a360b55ceb 100644
--- a/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_setup_privileges_prompt.tsx
+++ b/x-pack/plugins/observability_solution/infra/public/components/logging/log_analysis_setup/missing_setup_privileges_prompt.tsx
@@ -16,6 +16,7 @@ import { UserManagementLink } from './user_management_link';
export const MissingSetupPrivilegesPrompt: React.FunctionComponent = () => (
{missingMlPrivilegesTitle}}
body={{missingMlSetupPrivilegesDescription}
}
actions={ }
diff --git a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page.tsx b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page.tsx
index f5b1e89c69e0b..650a5b119d755 100644
--- a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page.tsx
+++ b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page.tsx
@@ -7,8 +7,11 @@
import { EuiErrorBoundary } from '@elastic/eui';
import React from 'react';
+import { MissingResultsPrivilegesPrompt } from '../../../components/logging/log_analysis_setup';
+import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis';
+import { SubscriptionSplashPage } from '../../../components/subscription_splash_content';
import { useLogsBreadcrumbs } from '../../../hooks/use_logs_breadcrumbs';
-import { LogEntryCategoriesPageContent } from './page_content';
+import { CategoriesPageTemplate, LogEntryCategoriesPageContent } from './page_content';
import { LogEntryCategoriesPageProviders } from './page_providers';
import { logCategoriesTitle } from '../../../translations';
import { LogMlJobIdFormatsShimProvider } from '../shared/use_log_ml_job_id_formats_shim';
@@ -20,6 +23,28 @@ export const LogEntryCategoriesPage = () => {
},
]);
+ const { hasLogAnalysisReadCapabilities, hasLogAnalysisCapabilites } =
+ useLogAnalysisCapabilitiesContext();
+
+ if (!hasLogAnalysisCapabilites) {
+ return (
+
+ );
+ }
+
+ if (!hasLogAnalysisReadCapabilities) {
+ return (
+
+
+
+ );
+ }
+
return (
diff --git a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page_content.tsx b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page_content.tsx
index c58ffc5f36e84..8059cdcb093e2 100644
--- a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page_content.tsx
+++ b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_categories/page_content.tsx
@@ -13,14 +13,12 @@ import { isJobStatusWithResults, logEntryCategoriesJobType } from '../../../../c
import { LoadingPage } from '../../../components/loading_page';
import {
LogAnalysisSetupStatusUnknownPrompt,
- MissingResultsPrivilegesPrompt,
MissingSetupPrivilegesPrompt,
} from '../../../components/logging/log_analysis_setup';
import {
LogAnalysisSetupFlyout,
useLogAnalysisSetupFlyoutStateContext,
} from '../../../components/logging/log_analysis_setup/setup_flyout';
-import { SubscriptionSplashPage } from '../../../components/subscription_splash_content';
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis';
import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { LogsPageTemplate } from '../shared/page_template';
@@ -33,11 +31,8 @@ const logCategoriesTitle = i18n.translate('xpack.infra.logs.logCategoriesTitle',
});
export const LogEntryCategoriesPageContent = () => {
- const {
- hasLogAnalysisCapabilites,
- hasLogAnalysisReadCapabilities,
- hasLogAnalysisSetupCapabilities,
- } = useLogAnalysisCapabilitiesContext();
+ const { hasLogAnalysisReadCapabilities, hasLogAnalysisSetupCapabilities } =
+ useLogAnalysisCapabilitiesContext();
const { fetchJobStatus, setupStatus, jobStatus } = useLogEntryCategoriesModuleContext();
@@ -55,22 +50,7 @@ export const LogEntryCategoriesPageContent = () => {
const { idFormats } = useLogMlJobIdFormatsShimContext();
- if (!hasLogAnalysisCapabilites) {
- return (
-
- );
- } else if (!hasLogAnalysisReadCapabilities) {
- return (
-
-
-
- );
- } else if (setupStatus.type === 'initializing') {
+ if (setupStatus.type === 'initializing') {
return (
{
const allowedSetupModules = ['logs_ui_categories' as const];
-const CategoriesPageTemplate: React.FC = ({
+export const CategoriesPageTemplate: React.FC = ({
children,
...rest
}) => {
diff --git a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page.tsx b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page.tsx
index 97841745ae13a..ed46ea9dc2680 100644
--- a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page.tsx
+++ b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page.tsx
@@ -7,7 +7,10 @@
import { EuiErrorBoundary } from '@elastic/eui';
import React from 'react';
-import { LogEntryRatePageContent } from './page_content';
+import { SubscriptionSplashPage } from '../../../components/subscription_splash_content';
+import { MissingResultsPrivilegesPrompt } from '../../../components/logging/log_analysis_setup';
+import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis';
+import { AnomaliesPageTemplate, LogEntryRatePageContent } from './page_content';
import { LogEntryRatePageProviders } from './page_providers';
import { useLogsBreadcrumbs } from '../../../hooks/use_logs_breadcrumbs';
import { logsAnomaliesTitle } from '../../../translations';
@@ -19,6 +22,29 @@ export const LogEntryRatePage = () => {
text: logsAnomaliesTitle,
},
]);
+
+ const { hasLogAnalysisReadCapabilities, hasLogAnalysisCapabilites } =
+ useLogAnalysisCapabilitiesContext();
+
+ if (!hasLogAnalysisCapabilites) {
+ return (
+
+ );
+ }
+
+ if (!hasLogAnalysisReadCapabilities) {
+ return (
+
+
+
+ );
+ }
+
return (
diff --git a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page_content.tsx b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page_content.tsx
index 3ac8d7d9137d1..350094b5df6a3 100644
--- a/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page_content.tsx
+++ b/x-pack/plugins/observability_solution/infra/public/pages/logs/log_entry_rate/page_content.tsx
@@ -18,14 +18,12 @@ import {
import { LoadingPage } from '../../../components/loading_page';
import {
LogAnalysisSetupStatusUnknownPrompt,
- MissingResultsPrivilegesPrompt,
MissingSetupPrivilegesPrompt,
} from '../../../components/logging/log_analysis_setup';
import {
LogAnalysisSetupFlyout,
useLogAnalysisSetupFlyoutStateContext,
} from '../../../components/logging/log_analysis_setup/setup_flyout';
-import { SubscriptionSplashPage } from '../../../components/subscription_splash_content';
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis';
import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { useLogEntryRateModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_rate';
@@ -41,11 +39,8 @@ const logsAnomaliesTitle = i18n.translate('xpack.infra.logs.anomaliesPageTitle',
});
export const LogEntryRatePageContent = memo(() => {
- const {
- hasLogAnalysisCapabilites,
- hasLogAnalysisReadCapabilities,
- hasLogAnalysisSetupCapabilities,
- } = useLogAnalysisCapabilitiesContext();
+ const { hasLogAnalysisReadCapabilities, hasLogAnalysisSetupCapabilities } =
+ useLogAnalysisCapabilitiesContext();
const {
fetchJobStatus: fetchLogEntryCategoriesJobStatus,
@@ -96,22 +91,7 @@ export const LogEntryRatePageContent = memo(() => {
const { idFormats } = useLogMlJobIdFormatsShimContext();
- if (!hasLogAnalysisCapabilites) {
- return (
-
- );
- } else if (!hasLogAnalysisReadCapabilities) {
- return (
-
-
-
- );
- } else if (
+ if (
logEntryCategoriesSetupStatus.type === 'initializing' ||
logEntryRateSetupStatus.type === 'initializing'
) {
@@ -159,7 +139,7 @@ export const LogEntryRatePageContent = memo(() => {
}
});
-const AnomaliesPageTemplate: React.FC = ({
+export const AnomaliesPageTemplate: React.FC = ({
children,
...rest
}) => {
diff --git a/x-pack/test/functional/apps/infra/logs/log_entry_categories_tab.ts b/x-pack/test/functional/apps/infra/logs/log_entry_categories_tab.ts
index 33396497fc83c..0d4a5440ebd58 100644
--- a/x-pack/test/functional/apps/infra/logs/log_entry_categories_tab.ts
+++ b/x-pack/test/functional/apps/infra/logs/log_entry_categories_tab.ts
@@ -9,14 +9,54 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
-export default ({ getService }: FtrProviderContext) => {
+export default ({ getPageObjects, getService }: FtrProviderContext) => {
+ const PageObjects = getPageObjects(['security']);
const esArchiver = getService('esArchiver');
const logsUi = getService('logsUi');
const retry = getService('retry');
+ const security = getService('security');
describe('Log Entry Categories Tab', function () {
this.tags('includeFirefox');
+ const loginWithMLPrivileges = async (privileges: Record) => {
+ await security.role.create('global_logs_role', {
+ elasticsearch: {
+ cluster: ['all'],
+ indices: [{ names: ['*'], privileges: ['read', 'view_index_metadata'] }],
+ },
+ kibana: [
+ {
+ feature: {
+ logs: ['read'],
+ ...privileges,
+ },
+ spaces: ['*'],
+ },
+ ],
+ });
+
+ await security.user.create('global_logs_read_user', {
+ password: 'global_logs_read_user-password',
+ roles: ['global_logs_role'],
+ full_name: 'logs test user',
+ });
+
+ await PageObjects.security.forceLogout();
+
+ await PageObjects.security.login('global_logs_read_user', 'global_logs_read_user-password', {
+ expectSpaceSelector: false,
+ });
+ };
+
+ const logoutAndDeleteUser = async () => {
+ await PageObjects.security.forceLogout();
+ await Promise.all([
+ security.role.delete('global_logs_role'),
+ security.user.delete('global_logs_read_user'),
+ ]);
+ };
+
describe('with a trial license', () => {
it('Shows no data page when indices do not exist', async () => {
await logsUi.logEntryCategoriesPage.navigateTo();
@@ -26,14 +66,42 @@ export default ({ getService }: FtrProviderContext) => {
});
});
- it('shows setup page when indices exist', async () => {
- await esArchiver.load('x-pack/test/functional/es_archives/infra/simple_logs');
- await logsUi.logEntryCategoriesPage.navigateTo();
+ describe('when indices exists', () => {
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs');
+ });
- await retry.try(async () => {
- expect(await logsUi.logEntryCategoriesPage.getSetupScreen()).to.be.ok();
+ after(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs');
+ });
+
+ it('shows setup page when indices exist', async () => {
+ await logsUi.logEntryCategoriesPage.navigateTo();
+
+ await retry.try(async () => {
+ expect(await logsUi.logEntryCategoriesPage.getSetupScreen()).to.be.ok();
+ });
+ });
+
+ it('shows required ml read privileges prompt when the user has not any ml privileges', async () => {
+ await loginWithMLPrivileges({});
+ await logsUi.logEntryCategoriesPage.navigateTo();
+
+ await retry.try(async () => {
+ expect(await logsUi.logEntryCategoriesPage.getNoMlReadPrivilegesPrompt()).to.be.ok();
+ });
+ await logoutAndDeleteUser();
+ });
+
+ it('shows required ml all privileges prompt when the user has only ml read privileges', async () => {
+ await loginWithMLPrivileges({ ml: ['read'] });
+ await logsUi.logEntryCategoriesPage.navigateTo();
+
+ await retry.try(async () => {
+ expect(await logsUi.logEntryCategoriesPage.getNoMlAllPrivilegesPrompt()).to.be.ok();
+ });
+ await logoutAndDeleteUser();
});
- await esArchiver.unload('x-pack/test/functional/es_archives/infra/simple_logs');
});
});
});
diff --git a/x-pack/test/functional/apps/infra/logs/log_entry_rate_tab.ts b/x-pack/test/functional/apps/infra/logs/log_entry_rate_tab.ts
index b2b4b5bcfc0be..35aa6ec6ca4ae 100644
--- a/x-pack/test/functional/apps/infra/logs/log_entry_rate_tab.ts
+++ b/x-pack/test/functional/apps/infra/logs/log_entry_rate_tab.ts
@@ -9,16 +9,56 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
-export default ({ getService }: FtrProviderContext) => {
+export default ({ getPageObjects, getService }: FtrProviderContext) => {
+ const PageObjects = getPageObjects(['security']);
const logsUi = getService('logsUi');
const retry = getService('retry');
const esArchiver = getService('esArchiver');
+ const security = getService('security');
describe('Log Entry Rate Tab', function () {
this.tags('includeFirefox');
+ const loginWithMLPrivileges = async (privileges: Record) => {
+ await security.role.create('global_logs_role', {
+ elasticsearch: {
+ cluster: ['all'],
+ indices: [{ names: ['*'], privileges: ['read', 'view_index_metadata'] }],
+ },
+ kibana: [
+ {
+ feature: {
+ logs: ['read'],
+ ...privileges,
+ },
+ spaces: ['*'],
+ },
+ ],
+ });
+
+ await security.user.create('global_logs_read_user', {
+ password: 'global_logs_read_user-password',
+ roles: ['global_logs_role'],
+ full_name: 'logs test user',
+ });
+
+ await PageObjects.security.forceLogout();
+
+ await PageObjects.security.login('global_logs_read_user', 'global_logs_read_user-password', {
+ expectSpaceSelector: false,
+ });
+ };
+
+ const logoutAndDeleteUser = async () => {
+ await PageObjects.security.forceLogout();
+ await Promise.all([
+ security.role.delete('global_logs_role'),
+ security.user.delete('global_logs_read_user'),
+ ]);
+ };
+
describe('with a trial license', () => {
- it('Shows no data page when indices do not exist', async () => {
+ it('shows no data page when indices do not exist', async () => {
await logsUi.logEntryRatePage.navigateTo();
await retry.try(async () => {
@@ -26,14 +66,42 @@ export default ({ getService }: FtrProviderContext) => {
});
});
- it('shows setup page when indices exist', async () => {
- await esArchiver.load('x-pack/test/functional/es_archives/infra/simple_logs');
- await logsUi.logEntryRatePage.navigateTo();
+ describe('when indices exists', () => {
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs');
+ });
- await retry.try(async () => {
- expect(await logsUi.logEntryRatePage.getSetupScreen()).to.be.ok();
+ after(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs');
+ });
+
+ it('shows setup page when indices exist', async () => {
+ await logsUi.logEntryRatePage.navigateTo();
+
+ await retry.try(async () => {
+ expect(await logsUi.logEntryRatePage.getSetupScreen()).to.be.ok();
+ });
+ });
+
+ it('shows required ml read privileges prompt when the user has not any ml privileges', async () => {
+ await loginWithMLPrivileges({});
+ await logsUi.logEntryRatePage.navigateTo();
+
+ await retry.try(async () => {
+ expect(await logsUi.logEntryRatePage.getNoMlReadPrivilegesPrompt()).to.be.ok();
+ });
+ await logoutAndDeleteUser();
+ });
+
+ it('shows required ml all privileges prompt when the user has only ml read privileges', async () => {
+ await loginWithMLPrivileges({ ml: ['read'] });
+ await logsUi.logEntryRatePage.navigateTo();
+
+ await retry.try(async () => {
+ expect(await logsUi.logEntryRatePage.getNoMlAllPrivilegesPrompt()).to.be.ok();
+ });
+ await logoutAndDeleteUser();
});
- await esArchiver.unload('x-pack/test/functional/es_archives/infra/simple_logs');
});
});
});
diff --git a/x-pack/test/functional/services/logs_ui/log_entry_categories.ts b/x-pack/test/functional/services/logs_ui/log_entry_categories.ts
index 77098bd918ea6..d270b510bffbd 100644
--- a/x-pack/test/functional/services/logs_ui/log_entry_categories.ts
+++ b/x-pack/test/functional/services/logs_ui/log_entry_categories.ts
@@ -24,5 +24,13 @@ export function LogEntryCategoriesPageProvider({ getPageObjects, getService }: F
async getSetupScreen(): Promise {
return await testSubjects.find('logEntryCategoriesSetupPage');
},
+
+ getNoMlReadPrivilegesPrompt() {
+ return testSubjects.find('logsMissingMLReadPrivileges');
+ },
+
+ getNoMlAllPrivilegesPrompt() {
+ return testSubjects.find('logsMissingMLAllPrivileges');
+ },
};
}
diff --git a/x-pack/test/functional/services/logs_ui/log_entry_rate.ts b/x-pack/test/functional/services/logs_ui/log_entry_rate.ts
index f8a68f6c924e0..9b704db9eb021 100644
--- a/x-pack/test/functional/services/logs_ui/log_entry_rate.ts
+++ b/x-pack/test/functional/services/logs_ui/log_entry_rate.ts
@@ -29,6 +29,14 @@ export function LogEntryRatePageProvider({ getPageObjects, getService }: FtrProv
return await testSubjects.find('noDataPage');
},
+ getNoMlReadPrivilegesPrompt() {
+ return testSubjects.find('logsMissingMLReadPrivileges');
+ },
+
+ getNoMlAllPrivilegesPrompt() {
+ return testSubjects.find('logsMissingMLAllPrivileges');
+ },
+
async startJobSetup() {
await testSubjects.click('infraLogEntryRateSetupContentMlSetupButton');
},
From 2759994e2d53b294a3a049f69bd56fc2e8477e77 Mon Sep 17 00:00:00 2001
From: Elena Shostak <165678770+elena-shostak@users.noreply.github.com>
Date: Thu, 10 Oct 2024 14:40:15 +0200
Subject: [PATCH 47/87] [Authz] Adjusted forbidden message for new security
route configuration (#195368)
## Summary
Adjusted forbidden message for new security route configuration to be
consistent with ES.
__Closes: https://github.com/elastic/kibana/issues/195365__
---
.../server/authorization/api_authorization.ts | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/x-pack/plugins/security/server/authorization/api_authorization.ts b/x-pack/plugins/security/server/authorization/api_authorization.ts
index ba38d9ca0aa20..9c67ff8bdff8b 100644
--- a/x-pack/plugins/security/server/authorization/api_authorization.ts
+++ b/x-pack/plugins/security/server/authorization/api_authorization.ts
@@ -87,17 +87,17 @@ export function initAPIAuthorization(
const missingPrivileges = Object.keys(kibanaPrivileges).filter(
(key) => !kibanaPrivileges[key]
);
- logger.warn(
- `User not authorized for "${request.url.pathname}${
- request.url.search
- }", responding with 403: missing privileges: ${missingPrivileges.join(', ')}`
- );
+ const forbiddenMessage = `API [${request.route.method.toUpperCase()} ${
+ request.url.pathname
+ }${
+ request.url.search
+ }] is unauthorized for user, this action is granted by the Kibana privileges [${missingPrivileges}]`;
+
+ logger.warn(forbiddenMessage);
return response.forbidden({
body: {
- message: `User not authorized for ${request.url.pathname}${
- request.url.search
- }, missing privileges: ${missingPrivileges.join(', ')}`,
+ message: forbiddenMessage,
},
});
}
From e6c2750151d04152f8270d56279048e6f019696d Mon Sep 17 00:00:00 2001
From: Kfir Peled <61654899+kfirpeled@users.noreply.github.com>
Date: Thu, 10 Oct 2024 14:05:37 +0100
Subject: [PATCH 48/87] [Cloud Security] Refactoring tests (#195675)
---
.../apis/cloud_security_posture/helper.ts | 69 +----------
.../status/status_index_timeout.ts | 27 ++--
.../status/status_indexed.ts | 49 ++++----
.../status/status_indexing.ts | 28 ++---
.../status/status_unprivileged.ts | 115 ++++++++++--------
.../test/cloud_security_posture_api/utils.ts | 11 +-
.../cloud_security_metering.ts | 80 +++++-------
.../serverless_metering/mock_data.ts | 2 +
.../status/status_indexed.ts | 39 +++---
.../status/status_indexing.ts | 32 +++--
.../cloud_security_posture/telemetry.ts | 69 ++++-------
11 files changed, 220 insertions(+), 301 deletions(-)
diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/helper.ts b/x-pack/test/api_integration/apis/cloud_security_posture/helper.ts
index 51f98b5389a9d..3ad0ef88ef75a 100644
--- a/x-pack/test/api_integration/apis/cloud_security_posture/helper.ts
+++ b/x-pack/test/api_integration/apis/cloud_security_posture/helper.ts
@@ -6,59 +6,12 @@
*/
import type { Agent as SuperTestAgent } from 'supertest';
-import { Client } from '@elastic/elasticsearch';
-import expect from '@kbn/expect';
+
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
-import type { IndexDetails } from '@kbn/cloud-security-posture-common';
import { CLOUD_SECURITY_PLUGIN_VERSION } from '@kbn/cloud-security-posture-plugin/common/constants';
import { SecurityService } from '@kbn/ftr-common-functional-ui-services';
import { RoleCredentials } from '@kbn/ftr-common-functional-services';
-export const deleteIndex = async (es: Client, indexToBeDeleted: string[]) => {
- return Promise.all([
- ...indexToBeDeleted.map((indexes) =>
- es.deleteByQuery({
- index: indexes,
- query: {
- match_all: {},
- },
- ignore_unavailable: true,
- refresh: true,
- })
- ),
- ]);
-};
-
-export const bulkIndex = async (es: Client, findingsMock: T[], indexName: string) => {
- const operations = findingsMock.flatMap((finding) => [
- { create: { _index: indexName } }, // Action description
- {
- ...finding,
- '@timestamp': new Date().toISOString(),
- }, // Data to index
- ]);
-
- await es.bulk({
- body: operations, // Bulk API expects 'body' for operations
- refresh: true,
- });
-};
-
-export const addIndex = async (es: Client, findingsMock: T[], indexName: string) => {
- await Promise.all([
- ...findingsMock.map((finding) =>
- es.index({
- index: indexName,
- body: {
- ...finding,
- '@timestamp': new Date().toISOString(),
- },
- refresh: true,
- })
- ),
- ]);
-};
-
export async function createPackagePolicy(
supertest: SuperTestAgent,
agentPolicyId: string,
@@ -233,10 +186,10 @@ export const createUser = async (security: SecurityService, userName: string, ro
});
};
-export const createCSPOnlyRole = async (
+export const createCSPRole = async (
security: SecurityService,
roleName: string,
- indicesName: string
+ indicesName?: string[]
) => {
await security.role.create(roleName, {
kibana: [
@@ -245,12 +198,12 @@ export const createCSPOnlyRole = async (
spaces: ['*'],
},
],
- ...(indicesName.length !== 0
+ ...(indicesName && indicesName.length > 0
? {
elasticsearch: {
indices: [
{
- names: [indicesName],
+ names: indicesName,
privileges: ['read'],
},
],
@@ -267,15 +220,3 @@ export const deleteRole = async (security: SecurityService, roleName: string) =>
export const deleteUser = async (security: SecurityService, userName: string) => {
await security.user.delete(userName);
};
-
-export const assertIndexStatus = (
- indicesDetails: IndexDetails[],
- indexName: string,
- expectedStatus: string
-) => {
- const actualValue = indicesDetails.find((idx) => idx.index === indexName)?.status;
- expect(actualValue).to.eql(
- expectedStatus,
- `expected ${indexName} status to be ${expectedStatus} but got ${actualValue} instead`
- );
-};
diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_index_timeout.ts b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_index_timeout.ts
index ce0c9014478dc..a2949a9f35253 100644
--- a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_index_timeout.ts
+++ b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_index_timeout.ts
@@ -13,16 +13,10 @@ import {
LATEST_FINDINGS_INDEX_DEFAULT_NS,
VULNERABILITIES_INDEX_DEFAULT_NS,
} from '@kbn/cloud-security-posture-plugin/common/constants';
+import { EsIndexDataProvider } from '../../../../cloud_security_posture_api/utils';
import { generateAgent } from '../../../../fleet_api_integration/helpers';
import { FtrProviderContext } from '../../../ftr_provider_context';
-import { deleteIndex, createPackagePolicy } from '../helper';
-
-const INDEX_ARRAY = [
- FINDINGS_INDEX_DEFAULT_NS,
- LATEST_FINDINGS_INDEX_DEFAULT_NS,
- CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN,
- VULNERABILITIES_INDEX_DEFAULT_NS,
-];
+import { createPackagePolicy } from '../helper';
const currentTimeMinusFourHours = new Date(Date.now() - 21600000).toISOString();
const currentTimeMinusTenMinutes = new Date(Date.now() - 600000).toISOString();
@@ -35,6 +29,13 @@ export default function (providerContext: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const fleetAndAgents = getService('fleetAndAgents');
+ const findingsIndex = new EsIndexDataProvider(es, FINDINGS_INDEX_DEFAULT_NS);
+ const latestFindingsIndex = new EsIndexDataProvider(es, LATEST_FINDINGS_INDEX_DEFAULT_NS);
+ const vulnerabilitiesIndex = new EsIndexDataProvider(es, VULNERABILITIES_INDEX_DEFAULT_NS);
+ const cdrVulnerabilitiesIndex = new EsIndexDataProvider(
+ es,
+ CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN
+ );
describe('GET /internal/cloud_security_posture/status', () => {
let agentPolicyId: string;
@@ -84,12 +85,20 @@ export default function (providerContext: FtrProviderContext) {
.expect(200);
await generateAgent(providerContext, 'healthy', `Agent policy test 2`, agentPolicyId);
- await deleteIndex(es, INDEX_ARRAY);
+ await findingsIndex.deleteAll();
+ await latestFindingsIndex.deleteAll();
+ await vulnerabilitiesIndex.deleteAll();
+ await cdrVulnerabilitiesIndex.deleteAll();
});
afterEach(async () => {
await kibanaServer.savedObjects.cleanStandardList();
await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
+
+ await findingsIndex.deleteAll();
+ await latestFindingsIndex.deleteAll();
+ await vulnerabilitiesIndex.deleteAll();
+ await cdrVulnerabilitiesIndex.deleteAll();
});
it(`Should return index-timeout when installed kspm, has findings only on logs-cloud_security_posture.findings-default* and it has been more than 10 minutes since the installation`, async () => {
diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexed.ts b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexed.ts
index 504bb9f504516..ec8b6a09f8bb2 100644
--- a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexed.ts
+++ b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexed.ts
@@ -8,28 +8,25 @@ import expect from '@kbn/expect';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import { CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN } from '@kbn/cloud-security-posture-common';
import type { CspSetupStatus } from '@kbn/cloud-security-posture-common';
-import {
- FINDINGS_INDEX_DEFAULT_NS,
- LATEST_FINDINGS_INDEX_DEFAULT_NS,
- VULNERABILITIES_INDEX_DEFAULT_NS,
-} from '@kbn/cloud-security-posture-plugin/common/constants';
+import { LATEST_FINDINGS_INDEX_DEFAULT_NS } from '@kbn/cloud-security-posture-plugin/common/constants';
import { FtrProviderContext } from '../../../ftr_provider_context';
-import { deleteIndex, addIndex, createPackagePolicy } from '../helper';
+import { EsIndexDataProvider } from '../../../../cloud_security_posture_api/utils';
+import { createPackagePolicy } from '../helper';
import { findingsMockData, vulnerabilityMockData } from '../mock_data';
-const INDEX_ARRAY = [
- FINDINGS_INDEX_DEFAULT_NS,
- LATEST_FINDINGS_INDEX_DEFAULT_NS,
- CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN,
- VULNERABILITIES_INDEX_DEFAULT_NS,
-];
-
export default function (providerContext: FtrProviderContext) {
const { getService } = providerContext;
const supertest = getService('supertest');
const es = getService('es');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
+ const latestFindingsIndex = new EsIndexDataProvider(es, LATEST_FINDINGS_INDEX_DEFAULT_NS);
+ const latestVulnerabilitiesIndex = new EsIndexDataProvider(
+ es,
+ CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN
+ );
+ const mock3PIndex = 'security_solution-mock-3p-integration.misconfiguration_latest';
+ const _3pIndex = new EsIndexDataProvider(es, mock3PIndex);
describe('GET /internal/cloud_security_posture/status', () => {
let agentPolicyId: string;
@@ -50,19 +47,21 @@ export default function (providerContext: FtrProviderContext) {
agentPolicyId = agentPolicyResponse.item.id;
- await deleteIndex(es, INDEX_ARRAY);
- await addIndex(es, findingsMockData, LATEST_FINDINGS_INDEX_DEFAULT_NS);
- await addIndex(es, vulnerabilityMockData, CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN);
+ await latestFindingsIndex.deleteAll();
+ await latestVulnerabilitiesIndex.deleteAll();
+ await _3pIndex.deleteAll();
});
afterEach(async () => {
- await deleteIndex(es, INDEX_ARRAY);
+ await latestFindingsIndex.deleteAll();
+ await latestVulnerabilitiesIndex.deleteAll();
+ await _3pIndex.destroyIndex();
await kibanaServer.savedObjects.cleanStandardList();
await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
});
it(`Return hasMisconfigurationsFindings true when there are latest findings but no installed integrations`, async () => {
- await addIndex(es, findingsMockData, LATEST_FINDINGS_INDEX_DEFAULT_NS);
+ await latestFindingsIndex.addBulk(findingsMockData);
const { body: res }: { body: CspSetupStatus } = await supertest
.get(`/internal/cloud_security_posture/status`)
@@ -77,9 +76,7 @@ export default function (providerContext: FtrProviderContext) {
});
it(`Return hasMisconfigurationsFindings true when there are only findings in third party index`, async () => {
- await deleteIndex(es, INDEX_ARRAY);
- const mock3PIndex = 'security_solution-mock-3p-integration.misconfiguration_latest';
- await addIndex(es, findingsMockData, mock3PIndex);
+ await _3pIndex.addBulk(findingsMockData);
const { body: res }: { body: CspSetupStatus } = await supertest
.get(`/internal/cloud_security_posture/status`)
@@ -91,13 +88,9 @@ export default function (providerContext: FtrProviderContext) {
true,
`expected hasMisconfigurationsFindings to be true but got ${res.hasMisconfigurationsFindings} instead`
);
-
- await deleteIndex(es, [mock3PIndex]);
});
it(`Return hasMisconfigurationsFindings false when there are no findings`, async () => {
- await deleteIndex(es, INDEX_ARRAY);
-
const { body: res }: { body: CspSetupStatus } = await supertest
.get(`/internal/cloud_security_posture/status`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
@@ -120,6 +113,8 @@ export default function (providerContext: FtrProviderContext) {
'kspm'
);
+ await latestFindingsIndex.addBulk(findingsMockData);
+
const { body: res }: { body: CspSetupStatus } = await supertest
.get(`/internal/cloud_security_posture/status`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
@@ -142,6 +137,8 @@ export default function (providerContext: FtrProviderContext) {
'cspm'
);
+ await latestFindingsIndex.addBulk(findingsMockData);
+
const { body: res }: { body: CspSetupStatus } = await supertest
.get(`/internal/cloud_security_posture/status`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
@@ -164,6 +161,8 @@ export default function (providerContext: FtrProviderContext) {
'vuln_mgmt'
);
+ await latestVulnerabilitiesIndex.addBulk(vulnerabilityMockData);
+
const { body: res }: { body: CspSetupStatus } = await supertest
.get(`/internal/cloud_security_posture/status`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexing.ts b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexing.ts
index 4d66d8460b9a4..16ee02083e34c 100644
--- a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexing.ts
+++ b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_indexing.ts
@@ -7,29 +7,23 @@
import expect from '@kbn/expect';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import type { CspSetupStatus } from '@kbn/cloud-security-posture-common';
-import { CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN } from '@kbn/cloud-security-posture-common';
import {
FINDINGS_INDEX_DEFAULT_NS,
- LATEST_FINDINGS_INDEX_DEFAULT_NS,
VULNERABILITIES_INDEX_DEFAULT_NS,
} from '@kbn/cloud-security-posture-plugin/common/constants';
import { FtrProviderContext } from '../../../ftr_provider_context';
-import { deleteIndex, addIndex, createPackagePolicy } from '../helper';
+import { EsIndexDataProvider } from '../../../../cloud_security_posture_api/utils';
+import { createPackagePolicy } from '../helper';
import { findingsMockData, vulnerabilityMockData } from '../mock_data';
-const INDEX_ARRAY = [
- FINDINGS_INDEX_DEFAULT_NS,
- LATEST_FINDINGS_INDEX_DEFAULT_NS,
- CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN,
- VULNERABILITIES_INDEX_DEFAULT_NS,
-];
-
export default function (providerContext: FtrProviderContext) {
const { getService } = providerContext;
const supertest = getService('supertest');
const es = getService('es');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
+ const findingsIndex = new EsIndexDataProvider(es, FINDINGS_INDEX_DEFAULT_NS);
+ const vulnerabilitiesIndex = new EsIndexDataProvider(es, VULNERABILITIES_INDEX_DEFAULT_NS);
describe('GET /internal/cloud_security_posture/status', () => {
let agentPolicyId: string;
@@ -49,13 +43,13 @@ export default function (providerContext: FtrProviderContext) {
});
agentPolicyId = agentPolicyResponse.item.id;
- await deleteIndex(es, INDEX_ARRAY);
- await addIndex(es, findingsMockData, FINDINGS_INDEX_DEFAULT_NS);
- await addIndex(es, vulnerabilityMockData, VULNERABILITIES_INDEX_DEFAULT_NS);
+ await findingsIndex.deleteAll();
+ await vulnerabilitiesIndex.deleteAll();
});
afterEach(async () => {
- await deleteIndex(es, INDEX_ARRAY);
+ await findingsIndex.deleteAll();
+ await vulnerabilitiesIndex.deleteAll();
await kibanaServer.savedObjects.cleanStandardList();
await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
});
@@ -70,6 +64,8 @@ export default function (providerContext: FtrProviderContext) {
'kspm'
);
+ await findingsIndex.addBulk(findingsMockData);
+
const { body: res }: { body: CspSetupStatus } = await supertest
.get(`/internal/cloud_security_posture/status`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
@@ -92,6 +88,8 @@ export default function (providerContext: FtrProviderContext) {
'cspm'
);
+ await findingsIndex.addBulk(findingsMockData);
+
const { body: res }: { body: CspSetupStatus } = await supertest
.get(`/internal/cloud_security_posture/status`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
@@ -114,6 +112,8 @@ export default function (providerContext: FtrProviderContext) {
'vuln_mgmt'
);
+ await vulnerabilitiesIndex.addBulk(vulnerabilityMockData);
+
const { body: res }: { body: CspSetupStatus } = await supertest
.get(`/internal/cloud_security_posture/status`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_unprivileged.ts b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_unprivileged.ts
index 7c09e4b51f679..5d0f6207e904a 100644
--- a/x-pack/test/api_integration/apis/cloud_security_posture/status/status_unprivileged.ts
+++ b/x-pack/test/api_integration/apis/cloud_security_posture/status/status_unprivileged.ts
@@ -13,16 +13,9 @@ import {
LATEST_FINDINGS_INDEX_DEFAULT_NS,
FINDINGS_INDEX_PATTERN,
} from '@kbn/cloud-security-posture-plugin/common/constants';
+import { find, without } from 'lodash';
import { FtrProviderContext } from '../../../ftr_provider_context';
-import {
- createPackagePolicy,
- createUser,
- createCSPOnlyRole,
- deleteRole,
- deleteUser,
- deleteIndex,
- assertIndexStatus,
-} from '../helper';
+import { createPackagePolicy, createUser, createCSPRole, deleteRole, deleteUser } from '../helper';
const UNPRIVILEGED_ROLE = 'unprivileged_test_role';
const UNPRIVILEGED_USERNAME = 'unprivileged_test_user';
@@ -32,27 +25,36 @@ export default function (providerContext: FtrProviderContext) {
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
const esArchiver = getService('esArchiver');
- const es = getService('es');
const kibanaServer = getService('kibanaServer');
const security = getService('security');
+ const allIndices = [
+ LATEST_FINDINGS_INDEX_DEFAULT_NS,
+ FINDINGS_INDEX_PATTERN,
+ BENCHMARK_SCORE_INDEX_DEFAULT_NS,
+ CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN,
+ ];
+
describe('GET /internal/cloud_security_posture/status', () => {
let agentPolicyId: string;
describe('STATUS = UNPRIVILEGED TEST', () => {
before(async () => {
- await createCSPOnlyRole(security, UNPRIVILEGED_ROLE, '');
+ await createCSPRole(security, UNPRIVILEGED_ROLE);
await createUser(security, UNPRIVILEGED_USERNAME, UNPRIVILEGED_ROLE);
+ await esArchiver.loadIfNeeded(
+ 'x-pack/test/functional/es_archives/fleet/empty_fleet_server'
+ );
});
after(async () => {
await deleteUser(security, UNPRIVILEGED_USERNAME);
await deleteRole(security, UNPRIVILEGED_ROLE);
+ await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
});
beforeEach(async () => {
await kibanaServer.savedObjects.cleanStandardList();
- await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
const { body: agentPolicyResponse } = await supertest
.post(`/api/fleet/agent_policies`)
@@ -67,7 +69,6 @@ export default function (providerContext: FtrProviderContext) {
});
afterEach(async () => {
- await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
await kibanaServer.savedObjects.cleanStandardList();
});
@@ -106,7 +107,6 @@ export default function (providerContext: FtrProviderContext) {
describe('status = unprivileged test indices', () => {
beforeEach(async () => {
await kibanaServer.savedObjects.cleanStandardList();
- await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
const { body: agentPolicyResponse } = await supertest
.post(`/api/fleet/agent_policies`)
@@ -124,11 +124,21 @@ export default function (providerContext: FtrProviderContext) {
await deleteUser(security, UNPRIVILEGED_USERNAME);
await deleteRole(security, UNPRIVILEGED_ROLE);
await kibanaServer.savedObjects.cleanStandardList();
+ });
+
+ before(async () => {
+ await esArchiver.loadIfNeeded(
+ 'x-pack/test/functional/es_archives/fleet/empty_fleet_server'
+ );
+ });
+
+ after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
});
it(`Return unprivileged when missing access to findings_latest index`, async () => {
- await createCSPOnlyRole(security, UNPRIVILEGED_ROLE, LATEST_FINDINGS_INDEX_DEFAULT_NS);
+ const privilegedIndices = without(allIndices, LATEST_FINDINGS_INDEX_DEFAULT_NS);
+ await createCSPRole(security, UNPRIVILEGED_ROLE, privilegedIndices);
await createUser(security, UNPRIVILEGED_USERNAME, UNPRIVILEGED_ROLE);
await createPackagePolicy(
@@ -149,30 +159,30 @@ export default function (providerContext: FtrProviderContext) {
expect(res.kspm.status).to.eql(
'unprivileged',
- `expected unprivileged but got ${res.kspm.status} instead`
+ `kspm status expected unprivileged but got ${res.kspm.status} instead`
);
expect(res.cspm.status).to.eql(
'unprivileged',
- `expected unprivileged but got ${res.cspm.status} instead`
+ `cspm status expected unprivileged but got ${res.cspm.status} instead`
);
expect(res.vuln_mgmt.status).to.eql(
- 'unprivileged',
- `expected unprivileged but got ${res.vuln_mgmt.status} instead`
+ 'not-installed',
+ `cnvm status expected not_installed but got ${res.vuln_mgmt.status} instead`
);
- assertIndexStatus(res.indicesDetails, LATEST_FINDINGS_INDEX_DEFAULT_NS, 'empty');
- assertIndexStatus(res.indicesDetails, FINDINGS_INDEX_PATTERN, 'empty');
- assertIndexStatus(res.indicesDetails, BENCHMARK_SCORE_INDEX_DEFAULT_NS, 'unprivileged');
- assertIndexStatus(
- res.indicesDetails,
- CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN,
+ expect(res).to.have.property('indicesDetails');
+ expect(find(res.indicesDetails, { index: LATEST_FINDINGS_INDEX_DEFAULT_NS })?.status).eql(
'unprivileged'
);
+
+ privilegedIndices.forEach((index) => {
+ expect(find(res.indicesDetails, { index })?.status).not.eql('unprivileged');
+ });
});
it(`Return unprivileged when missing access to score index`, async () => {
- await deleteIndex(es, [BENCHMARK_SCORE_INDEX_DEFAULT_NS]);
- await createCSPOnlyRole(security, UNPRIVILEGED_ROLE, BENCHMARK_SCORE_INDEX_DEFAULT_NS);
+ const privilegedIndices = without(allIndices, BENCHMARK_SCORE_INDEX_DEFAULT_NS);
+ await createCSPRole(security, UNPRIVILEGED_ROLE, privilegedIndices);
await createUser(security, UNPRIVILEGED_USERNAME, UNPRIVILEGED_ROLE);
await createPackagePolicy(
@@ -193,33 +203,33 @@ export default function (providerContext: FtrProviderContext) {
expect(res.kspm.status).to.eql(
'unprivileged',
- `expected unprivileged but got ${res.kspm.status} instead`
+ `kspm status expected unprivileged but got ${res.kspm.status} instead`
);
expect(res.cspm.status).to.eql(
'unprivileged',
- `expected unprivileged but got ${res.cspm.status} instead`
+ `cspm status expected unprivileged but got ${res.cspm.status} instead`
);
expect(res.vuln_mgmt.status).to.eql(
'unprivileged',
- `expected unprivileged but got ${res.vuln_mgmt.status} instead`
+ `cnvm status expected unprivileged but got ${res.vuln_mgmt.status} instead`
);
- assertIndexStatus(res.indicesDetails, LATEST_FINDINGS_INDEX_DEFAULT_NS, 'unprivileged');
- assertIndexStatus(res.indicesDetails, FINDINGS_INDEX_PATTERN, 'empty');
- assertIndexStatus(res.indicesDetails, BENCHMARK_SCORE_INDEX_DEFAULT_NS, 'empty');
- assertIndexStatus(
- res.indicesDetails,
- CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN,
+ expect(res).to.have.property('indicesDetails');
+ expect(find(res.indicesDetails, { index: BENCHMARK_SCORE_INDEX_DEFAULT_NS })?.status).eql(
'unprivileged'
);
+
+ privilegedIndices.forEach((index) => {
+ expect(find(res.indicesDetails, { index })?.status).not.eql('unprivileged');
+ });
});
it(`Return unprivileged when missing access to vulnerabilities_latest index`, async () => {
- await createCSPOnlyRole(
- security,
- UNPRIVILEGED_ROLE,
+ const privilegedIndices = without(
+ allIndices,
CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN
);
+ await createCSPRole(security, UNPRIVILEGED_ROLE, privilegedIndices);
await createUser(security, UNPRIVILEGED_USERNAME, UNPRIVILEGED_ROLE);
await createPackagePolicy(
@@ -239,26 +249,27 @@ export default function (providerContext: FtrProviderContext) {
.expect(200);
expect(res.kspm.status).to.eql(
- 'unprivileged',
- `expected unprivileged but got ${res.kspm.status} instead`
+ 'not-deployed',
+ `kspm status expected unprivileged but got ${res.kspm.status} instead`
);
expect(res.cspm.status).to.eql(
- 'unprivileged',
- `expected unprivileged but got ${res.cspm.status} instead`
+ 'not-installed',
+ `cspm status expected unprivileged but got ${res.cspm.status} instead`
);
expect(res.vuln_mgmt.status).to.eql(
'unprivileged',
- `expected unprivileged but got ${res.vuln_mgmt.status} instead`
+ `cnvm status expected unprivileged but got ${res.vuln_mgmt.status} instead`
);
- assertIndexStatus(res.indicesDetails, LATEST_FINDINGS_INDEX_DEFAULT_NS, 'unprivileged');
- assertIndexStatus(res.indicesDetails, FINDINGS_INDEX_PATTERN, 'empty');
- assertIndexStatus(res.indicesDetails, BENCHMARK_SCORE_INDEX_DEFAULT_NS, 'unprivileged');
- assertIndexStatus(
- res.indicesDetails,
- CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN,
- 'empty'
- );
+ expect(res).to.have.property('indicesDetails');
+ expect(
+ find(res.indicesDetails, { index: CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN })
+ ?.status
+ ).eql('unprivileged');
+
+ privilegedIndices.forEach((index) => {
+ expect(find(res.indicesDetails, { index })?.status).not.eql('unprivileged');
+ });
});
});
});
diff --git a/x-pack/test/cloud_security_posture_api/utils.ts b/x-pack/test/cloud_security_posture_api/utils.ts
index 6f0d86419a349..9f0805c2e85c1 100644
--- a/x-pack/test/cloud_security_posture_api/utils.ts
+++ b/x-pack/test/cloud_security_posture_api/utils.ts
@@ -23,7 +23,7 @@ export const waitForPluginInitialized = ({
}: {
retry: RetryService;
logger: ToolingLog;
- supertest: Agent;
+ supertest: Pick;
}): Promise =>
retry.try(async () => {
logger.debug('Check CSP plugin is initialized');
@@ -44,13 +44,16 @@ export class EsIndexDataProvider {
this.index = index;
}
- addBulk(docs: Array>, overrideTimestamp = true) {
+ async addBulk(docs: Array>, overrideTimestamp = true) {
const operations = docs.flatMap((doc) => [
- { index: { _index: this.index } },
+ { create: { _index: this.index } },
{ ...doc, ...(overrideTimestamp ? { '@timestamp': new Date().toISOString() } : {}) },
]);
- return this.es.bulk({ refresh: 'wait_for', index: this.index, operations });
+ const resp = await this.es.bulk({ refresh: 'wait_for', index: this.index, operations });
+ expect(resp.errors).eql(false, `Error in bulk indexing: ${JSON.stringify(resp)}`);
+
+ return resp;
}
async deleteAll() {
diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/cloud_security_metering.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/cloud_security_metering.ts
index b3db98c829afd..f3d613a41d590 100644
--- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/cloud_security_metering.ts
+++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/cloud_security_metering.ts
@@ -10,11 +10,10 @@ import { CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN } from '@kbn/cloud-secu
import { LATEST_FINDINGS_INDEX_DEFAULT_NS } from '@kbn/cloud-security-posture-plugin/common/constants';
import * as http from 'http';
import {
- deleteIndex,
createPackagePolicy,
createCloudDefendPackagePolicy,
- bulkIndex,
} from '@kbn/test-suites-xpack/api_integration/apis/cloud_security_posture/helper';
+import { EsIndexDataProvider } from '@kbn/test-suites-xpack/cloud_security_posture_api/utils';
import { RoleCredentials } from '../../../../../shared/services';
import { getMockFindings, getMockDefendForContainersHeartbeats } from './mock_data';
import type { FtrProviderContext } from '../../../../ftr_provider_context';
@@ -32,6 +31,12 @@ export default function (providerContext: FtrProviderContext) {
const svlCommonApi = getService('svlCommonApi');
const svlUserManager = getService('svlUserManager');
const supertestWithoutAuth = getService('supertestWithoutAuth');
+ const findingsIndex = new EsIndexDataProvider(es, LATEST_FINDINGS_INDEX_DEFAULT_NS);
+ const cloudDefinedIndex = new EsIndexDataProvider(es, CLOUD_DEFEND_HEARTBEAT_INDEX_DEFAULT_NS);
+ const vulnerabilitiesIndex = new EsIndexDataProvider(
+ es,
+ CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN
+ );
/*
This test aims to intercept the usage API request sent by the metering background task manager.
@@ -67,25 +72,17 @@ export default function (providerContext: FtrProviderContext) {
agentPolicyId = agentPolicyResponse.item.id;
- await deleteIndex(es, [
- LATEST_FINDINGS_INDEX_DEFAULT_NS,
- CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN,
- CLOUD_DEFEND_HEARTBEAT_INDEX_DEFAULT_NS,
- ]);
+ await findingsIndex.deleteAll();
+ await vulnerabilitiesIndex.deleteAll();
+ await cloudDefinedIndex.deleteAll();
});
afterEach(async () => {
- await deleteIndex(es, [
- LATEST_FINDINGS_INDEX_DEFAULT_NS,
- CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN,
- ]);
await kibanaServer.savedObjects.cleanStandardList();
await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
- await deleteIndex(es, [
- LATEST_FINDINGS_INDEX_DEFAULT_NS,
- CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN,
- CLOUD_DEFEND_HEARTBEAT_INDEX_DEFAULT_NS,
- ]);
+ await findingsIndex.deleteAll();
+ await vulnerabilitiesIndex.deleteAll();
+ await cloudDefinedIndex.deleteAll();
});
after(async () => {
await svlUserManager.invalidateM2mApiKeyWithRoleScope(roleAuthc);
@@ -116,11 +113,7 @@ export default function (providerContext: FtrProviderContext) {
numberOfFindings: 10,
});
- await bulkIndex(
- es,
- [...billableFindings, ...notBillableFindings],
- LATEST_FINDINGS_INDEX_DEFAULT_NS
- );
+ await findingsIndex.addBulk([...billableFindings, ...notBillableFindings]);
let interceptedRequestBody: UsageRecord[] = [];
await retry.try(async () => {
@@ -160,11 +153,7 @@ export default function (providerContext: FtrProviderContext) {
numberOfFindings: 11,
});
- await bulkIndex(
- es,
- [...billableFindings, ...notBillableFindings],
- LATEST_FINDINGS_INDEX_DEFAULT_NS
- );
+ await findingsIndex.addBulk([...billableFindings, ...notBillableFindings]);
let interceptedRequestBody: UsageRecord[] = [];
@@ -199,7 +188,7 @@ export default function (providerContext: FtrProviderContext) {
numberOfFindings: 2,
});
- await bulkIndex(es, billableFindings, CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN);
+ await vulnerabilitiesIndex.addBulk(billableFindings);
let interceptedRequestBody: UsageRecord[] = [];
@@ -233,11 +222,11 @@ export default function (providerContext: FtrProviderContext) {
isBlockActionEnables: false,
numberOfHearbeats: 2,
});
- await bulkIndex(
- es,
- [...blockActionEnabledHeartbeats, ...blockActionDisabledHeartbeats],
- CLOUD_DEFEND_HEARTBEAT_INDEX_DEFAULT_NS
- );
+
+ await cloudDefinedIndex.addBulk([
+ ...blockActionEnabledHeartbeats,
+ ...blockActionDisabledHeartbeats,
+ ]);
let interceptedRequestBody: UsageRecord[] = [];
@@ -315,22 +304,17 @@ export default function (providerContext: FtrProviderContext) {
});
await Promise.all([
- bulkIndex(
- es,
- [
- ...billableFindingsCSPM,
- ...notBillableFindingsCSPM,
- ...billableFindingsKSPM,
- ...notBillableFindingsKSPM,
- ],
- LATEST_FINDINGS_INDEX_DEFAULT_NS
- ),
- bulkIndex(es, [...billableFindingsCNVM], CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN),
- bulkIndex(
- es,
- [...blockActionEnabledHeartbeats, ...blockActionDisabledHeartbeats],
- CLOUD_DEFEND_HEARTBEAT_INDEX_DEFAULT_NS
- ),
+ findingsIndex.addBulk([
+ ...billableFindingsCSPM,
+ ...notBillableFindingsCSPM,
+ ...billableFindingsKSPM,
+ ...notBillableFindingsKSPM,
+ ]),
+ vulnerabilitiesIndex.addBulk([...billableFindingsCNVM]),
+ cloudDefinedIndex.addBulk([
+ ...blockActionEnabledHeartbeats,
+ ...blockActionDisabledHeartbeats,
+ ]),
]);
// Intercept and verify usage API request
diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/mock_data.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/mock_data.ts
index 5e5844eaaf3b5..1991b53b85b35 100644
--- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/mock_data.ts
+++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/mock_data.ts
@@ -82,6 +82,8 @@ const mockFiniding = (postureType: string, isBillableAsset?: boolean) => {
},
};
}
+
+ throw new Error('Invalid posture type');
};
export const getMockDefendForContainersHeartbeats = ({
diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexed.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexed.ts
index a9da3a42cdfc8..b53163796a6ee 100644
--- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexed.ts
+++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexed.ts
@@ -8,16 +8,9 @@ import expect from '@kbn/expect';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import type { CspSetupStatus } from '@kbn/cloud-security-posture-common';
import { CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN } from '@kbn/cloud-security-posture-common';
-import {
- FINDINGS_INDEX_DEFAULT_NS,
- LATEST_FINDINGS_INDEX_DEFAULT_NS,
- VULNERABILITIES_INDEX_DEFAULT_NS,
-} from '@kbn/cloud-security-posture-plugin/common/constants';
-import {
- deleteIndex,
- addIndex,
- createPackagePolicy,
-} from '@kbn/test-suites-xpack/api_integration/apis/cloud_security_posture/helper';
+import { LATEST_FINDINGS_INDEX_DEFAULT_NS } from '@kbn/cloud-security-posture-plugin/common/constants';
+import { createPackagePolicy } from '@kbn/test-suites-xpack/api_integration/apis/cloud_security_posture/helper';
+import { EsIndexDataProvider } from '@kbn/test-suites-xpack/cloud_security_posture_api/utils';
import {
findingsMockData,
vulnerabilityMockData,
@@ -25,13 +18,6 @@ import {
import { FtrProviderContext } from '../../../../ftr_provider_context';
import { RoleCredentials } from '../../../../../shared/services';
-const INDEX_ARRAY = [
- FINDINGS_INDEX_DEFAULT_NS,
- LATEST_FINDINGS_INDEX_DEFAULT_NS,
- CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN,
- VULNERABILITIES_INDEX_DEFAULT_NS,
-];
-
export default function (providerContext: FtrProviderContext) {
const { getService } = providerContext;
const es = getService('es');
@@ -40,6 +26,11 @@ export default function (providerContext: FtrProviderContext) {
const supertestWithoutAuth = getService('supertestWithoutAuth');
const svlCommonApi = getService('svlCommonApi');
const svlUserManager = getService('svlUserManager');
+ const latestFindingsIndex = new EsIndexDataProvider(es, LATEST_FINDINGS_INDEX_DEFAULT_NS);
+ const latestVulnerabilitiesIndex = new EsIndexDataProvider(
+ es,
+ CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN
+ );
describe('GET /internal/cloud_security_posture/status', function () {
// security_exception: action [indices:admin/create] is unauthorized for user [elastic] with effective roles [superuser] on restricted indices [.fleet-actions-7], this action is granted by the index privileges [create_index,manage,all]
@@ -74,13 +65,13 @@ export default function (providerContext: FtrProviderContext) {
agentPolicyId = agentPolicyResponse.item.id;
- await deleteIndex(es, INDEX_ARRAY);
- await addIndex(es, findingsMockData, LATEST_FINDINGS_INDEX_DEFAULT_NS);
- await addIndex(es, vulnerabilityMockData, CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN);
+ await latestFindingsIndex.deleteAll();
+ await latestVulnerabilitiesIndex.deleteAll();
});
afterEach(async () => {
- await deleteIndex(es, INDEX_ARRAY);
+ await latestFindingsIndex.deleteAll();
+ await latestVulnerabilitiesIndex.deleteAll();
await kibanaServer.savedObjects.cleanStandardList();
await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
});
@@ -98,6 +89,8 @@ export default function (providerContext: FtrProviderContext) {
internalRequestHeader
);
+ await latestFindingsIndex.addBulk(findingsMockData);
+
const { body: res }: { body: CspSetupStatus } = await supertestWithoutAuth
.get(`/internal/cloud_security_posture/status`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
@@ -124,6 +117,8 @@ export default function (providerContext: FtrProviderContext) {
internalRequestHeader
);
+ await latestFindingsIndex.addBulk(findingsMockData);
+
const { body: res }: { body: CspSetupStatus } = await supertestWithoutAuth
.get(`/internal/cloud_security_posture/status`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
@@ -150,6 +145,8 @@ export default function (providerContext: FtrProviderContext) {
internalRequestHeader
);
+ await latestVulnerabilitiesIndex.addBulk(vulnerabilityMockData);
+
const { body: res }: { body: CspSetupStatus } = await supertestWithoutAuth
.get(`/internal/cloud_security_posture/status`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexing.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexing.ts
index ec6a5835e6aa3..e531f2a5cc14e 100644
--- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexing.ts
+++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexing.ts
@@ -7,31 +7,19 @@
import expect from '@kbn/expect';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import type { CspSetupStatus } from '@kbn/cloud-security-posture-common';
-import { CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN } from '@kbn/cloud-security-posture-common';
import {
FINDINGS_INDEX_DEFAULT_NS,
- LATEST_FINDINGS_INDEX_DEFAULT_NS,
VULNERABILITIES_INDEX_DEFAULT_NS,
} from '@kbn/cloud-security-posture-plugin/common/constants';
-import {
- deleteIndex,
- addIndex,
- createPackagePolicy,
-} from '@kbn/test-suites-xpack/api_integration/apis/cloud_security_posture/helper';
+import { createPackagePolicy } from '@kbn/test-suites-xpack/api_integration/apis/cloud_security_posture/helper';
import {
findingsMockData,
vulnerabilityMockData,
} from '@kbn/test-suites-xpack/api_integration/apis/cloud_security_posture/mock_data';
+import { EsIndexDataProvider } from '@kbn/test-suites-xpack/cloud_security_posture_api/utils';
import { FtrProviderContext } from '../../../../ftr_provider_context';
import { RoleCredentials } from '../../../../../shared/services';
-const INDEX_ARRAY = [
- FINDINGS_INDEX_DEFAULT_NS,
- LATEST_FINDINGS_INDEX_DEFAULT_NS,
- CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN,
- VULNERABILITIES_INDEX_DEFAULT_NS,
-];
-
export default function (providerContext: FtrProviderContext) {
const { getService } = providerContext;
const es = getService('es');
@@ -40,6 +28,8 @@ export default function (providerContext: FtrProviderContext) {
const supertestWithoutAuth = getService('supertestWithoutAuth');
const svlCommonApi = getService('svlCommonApi');
const svlUserManager = getService('svlUserManager');
+ const findingsIndex = new EsIndexDataProvider(es, FINDINGS_INDEX_DEFAULT_NS);
+ const vulnerabilitiesIndex = new EsIndexDataProvider(es, VULNERABILITIES_INDEX_DEFAULT_NS);
describe('GET /internal/cloud_security_posture/status', function () {
// security_exception: action [indices:admin/create] is unauthorized for user [elastic] with effective roles [superuser] on restricted indices [.fleet-actions-7], this action is granted by the index privileges [create_index,manage,all]
@@ -73,13 +63,13 @@ export default function (providerContext: FtrProviderContext) {
});
agentPolicyId = agentPolicyResponse.item.id;
- await deleteIndex(es, INDEX_ARRAY);
- await addIndex(es, findingsMockData, FINDINGS_INDEX_DEFAULT_NS);
- await addIndex(es, vulnerabilityMockData, VULNERABILITIES_INDEX_DEFAULT_NS);
+ await findingsIndex.deleteAll();
+ await vulnerabilitiesIndex.deleteAll();
});
afterEach(async () => {
- await deleteIndex(es, INDEX_ARRAY);
+ await findingsIndex.deleteAll();
+ await vulnerabilitiesIndex.deleteAll();
await kibanaServer.savedObjects.cleanStandardList();
await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
});
@@ -97,6 +87,8 @@ export default function (providerContext: FtrProviderContext) {
internalRequestHeader
);
+ await findingsIndex.addBulk(findingsMockData);
+
const { body: res }: { body: CspSetupStatus } = await supertestWithoutAuth
.get(`/internal/cloud_security_posture/status`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
@@ -123,6 +115,8 @@ export default function (providerContext: FtrProviderContext) {
internalRequestHeader
);
+ await findingsIndex.addBulk(findingsMockData);
+
const { body: res }: { body: CspSetupStatus } = await supertestWithoutAuth
.get(`/internal/cloud_security_posture/status`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
@@ -149,6 +143,8 @@ export default function (providerContext: FtrProviderContext) {
internalRequestHeader
);
+ await vulnerabilitiesIndex.addBulk(vulnerabilityMockData);
+
const { body: res }: { body: CspSetupStatus } = await supertestWithoutAuth
.get(`/internal/cloud_security_posture/status`)
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/telemetry.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/telemetry.ts
index 62cf85b47d997..15700419a7e96 100644
--- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/telemetry.ts
+++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/telemetry.ts
@@ -7,11 +7,12 @@
import expect from '@kbn/expect';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
-import {
- data as telemetryMockData,
- MockTelemetryFindings,
-} from '@kbn/test-suites-xpack/cloud_security_posture_api/telemetry/data';
+import { data as telemetryMockData } from '@kbn/test-suites-xpack/cloud_security_posture_api/telemetry/data';
import { createPackagePolicy } from '@kbn/test-suites-xpack/api_integration/apis/cloud_security_posture/helper';
+import {
+ waitForPluginInitialized,
+ EsIndexDataProvider,
+} from '@kbn/test-suites-xpack/cloud_security_posture_api/utils';
import { SupertestWithRoleScopeType } from '@kbn/test-suites-xpack/api_integration/deployment_agnostic/services';
import type { FtrProviderContext } from '../../../ftr_provider_context';
import { RoleCredentials } from '../../../../shared/services';
@@ -21,7 +22,7 @@ const FINDINGS_INDEX = 'logs-cloud_security_posture.findings_latest-default';
export default function ({ getService }: FtrProviderContext) {
const retry = getService('retry');
const es = getService('es');
- const log = getService('log');
+ const logger = getService('log');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const supertestWithoutAuth = getService('supertestWithoutAuth');
@@ -33,24 +34,7 @@ export default function ({ getService }: FtrProviderContext) {
let roleAuthc: RoleCredentials;
let internalRequestHeader: { 'x-elastic-internal-origin': string; 'kbn-xsrf': string };
- const index = {
- remove: () =>
- es.deleteByQuery({
- index: FINDINGS_INDEX,
- query: { match_all: {} },
- refresh: true,
- }),
-
- add: async (mockTelemetryFindings: MockTelemetryFindings[]) => {
- const operations = mockTelemetryFindings.flatMap((doc) => [
- { index: { _index: FINDINGS_INDEX } },
- doc,
- ]);
-
- const response = await es.bulk({ refresh: 'wait_for', index: FINDINGS_INDEX, operations });
- expect(response.errors).to.eql(false);
- },
- };
+ const findingsIndex = new EsIndexDataProvider(es, FINDINGS_INDEX);
describe('Verify cloud_security_posture telemetry payloads', function () {
// security_exception: action [indices:admin/create] is unauthorized for user [elastic] with effective roles [superuser] on restricted indices [.fleet-actions-7], this action is granted by the index privileges [create_index,manage,all]
@@ -95,22 +79,11 @@ export default function ({ getService }: FtrProviderContext) {
internalRequestHeader
);
- log.debug('Check CSP plugin is initialized');
- await retry.try(async () => {
- const supertestAdminWithHttpHeaderV1 = await roleScopedSupertest.getSupertestWithRoleScope(
- 'admin',
- {
- useCookieHeader: true,
- withInternalHeaders: true,
- withCustomHeaders: { [ELASTIC_HTTP_VERSION_HEADER]: '1' },
- }
- );
- const response = await supertestAdminWithHttpHeaderV1
- .get('/internal/cloud_security_posture/status?check=init')
- .expect(200);
- expect(response.body).to.eql({ isPluginInitialized: true });
- log.debug('CSP plugin is initialized');
+ const supertestAdmin = await roleScopedSupertest.getSupertestWithRoleScope('admin', {
+ useCookieHeader: true,
+ withInternalHeaders: true,
});
+ await waitForPluginInitialized({ logger, retry, supertest: supertestAdmin });
});
after(async () => {
@@ -120,11 +93,11 @@ export default function ({ getService }: FtrProviderContext) {
});
afterEach(async () => {
- await index.remove();
+ await findingsIndex.deleteAll();
});
it('includes only KSPM findings', async () => {
- await index.add(telemetryMockData.kspmFindings);
+ await findingsIndex.addBulk(telemetryMockData.kspmFindings);
const {
body: [{ stats: apiResponse }],
@@ -175,7 +148,7 @@ export default function ({ getService }: FtrProviderContext) {
});
it('includes only CSPM findings', async () => {
- await index.add(telemetryMockData.cspmFindings);
+ await findingsIndex.addBulk(telemetryMockData.cspmFindings);
const {
body: [{ stats: apiResponse }],
@@ -218,8 +191,10 @@ export default function ({ getService }: FtrProviderContext) {
});
it('includes CSPM and KSPM findings', async () => {
- await index.add(telemetryMockData.kspmFindings);
- await index.add(telemetryMockData.cspmFindings);
+ await findingsIndex.addBulk([
+ ...telemetryMockData.kspmFindings,
+ ...telemetryMockData.cspmFindings,
+ ]);
const {
body: [{ stats: apiResponse }],
@@ -294,7 +269,7 @@ export default function ({ getService }: FtrProviderContext) {
});
it(`'includes only KSPM findings without posture_type'`, async () => {
- await index.add(telemetryMockData.kspmFindingsNoPostureType);
+ await findingsIndex.addBulk(telemetryMockData.kspmFindingsNoPostureType);
const {
body: [{ stats: apiResponse }],
@@ -346,8 +321,10 @@ export default function ({ getService }: FtrProviderContext) {
});
it('includes KSPM findings without posture_type and CSPM findings as well', async () => {
- await index.add(telemetryMockData.kspmFindingsNoPostureType);
- await index.add(telemetryMockData.cspmFindings);
+ await findingsIndex.addBulk([
+ ...telemetryMockData.kspmFindingsNoPostureType,
+ ...telemetryMockData.cspmFindings,
+ ]);
const {
body: [{ stats: apiResponse }],
From 8a3a05927bdbe264c491b4034ff5d81674f3db73 Mon Sep 17 00:00:00 2001
From: Sander Philipse <94373878+sphilipse@users.noreply.github.com>
Date: Thu, 10 Oct 2024 15:11:49 +0200
Subject: [PATCH 49/87] Extract AI assistant to package (#194552)
## Summary
This extracts the Observability AI Assistant into a shared package so
Search and Observability can both consume it.
A few notes:
This still relies on significantly tight coupling with the Obs AI
assistant plugin, which we will want to slowly decouple over time. It
means that currently to consume this in multiple places, you need to
provide a number of plugins for useKibana. Hopefully we can get rid of
that and replace them with props eventually and make the interface a
little less plugin-dependent.
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.github/CODEOWNERS | 1 +
package.json | 1 +
.../test_suites/core_plugins/rendering.ts | 2 +
tsconfig.base.json | 2 +
x-pack/.i18nrc.json | 63 ++++--
x-pack/packages/kbn-ai-assistant/README.md | 3 +
x-pack/packages/kbn-ai-assistant/index.ts | 7 +
.../packages/kbn-ai-assistant/jest.config.js | 18 ++
x-pack/packages/kbn-ai-assistant/kibana.jsonc | 5 +
x-pack/packages/kbn-ai-assistant/package.json | 7 +
.../packages/kbn-ai-assistant/setup_tests.ts | 9 +
.../src/assets/elastic_ai_assistant.png | Bin 0 -> 95099 bytes
.../buttons/ask_assistant_button.stories.tsx | 0
.../src}/buttons/ask_assistant_button.tsx | 31 ++-
...xpand_conversation_list_button.stories.tsx | 0
.../hide_expand_conversation_list_button.tsx | 4 +-
.../src}/buttons/new_chat_button.stories.tsx | 0
.../src}/buttons/new_chat_button.tsx | 2 +-
.../src}/chat/chat_actions_menu.tsx | 65 +++----
.../src}/chat/chat_body.stories.tsx | 4 +-
.../src}/chat/chat_body.test.tsx | 0
.../kbn-ai-assistant/src}/chat/chat_body.tsx | 32 +--
.../src}/chat/chat_consolidated_items.tsx | 13 +-
.../src}/chat/chat_flyout.stories.tsx | 5 +-
.../src}/chat/chat_flyout.tsx | 41 ++--
.../src}/chat/chat_header.stories.tsx | 0
.../src}/chat/chat_header.tsx | 45 ++---
.../src}/chat/chat_inline_edit.tsx | 0
.../kbn-ai-assistant/src}/chat/chat_item.tsx | 2 +-
.../src}/chat/chat_item_actions.tsx | 36 ++--
.../src}/chat/chat_item_avatar.tsx | 0
...chat_item_content_inline_prompt_editor.tsx | 0
.../src}/chat/chat_item_title.tsx | 12 +-
.../src}/chat/chat_timeline.stories.tsx | 8 +-
.../src}/chat/chat_timeline.tsx | 4 +-
.../src}/chat/conversation_list.stories.tsx | 4 +-
.../src}/chat/conversation_list.tsx | 65 +++----
.../kbn-ai-assistant/src}/chat/disclaimer.tsx | 2 +-
.../chat/function_list_popover.stories.tsx | 2 +-
.../src}/chat/function_list_popover.tsx | 28 ++-
.../src}/chat/incorrect_license_panel.tsx | 21 +-
.../kbn-ai-assistant/src/chat/index.ts | 11 ++
.../chat/knowledge_base_callout.stories.tsx | 2 +-
.../src}/chat/knowledge_base_callout.tsx | 14 +-
.../simulated_function_calling_callout.tsx | 2 +-
.../src}/chat/starter_prompts.tsx | 8 +-
.../src}/chat/welcome_message.tsx | 26 ++-
.../src}/chat/welcome_message_connectors.tsx | 37 ++--
.../chat/welcome_message_knowledge_base.tsx | 37 ++--
...ssage_knowledge_base_setup_error_panel.tsx | 30 ++-
.../src/conversation}/conversation_view.tsx | 69 +++----
.../hooks/__storybook_mocks__/use_chat.ts | 0
.../__storybook_mocks__/use_conversation.ts | 0
.../use_conversation_list.ts | 0
.../__storybook_mocks__/use_conversations.ts | 0
.../__storybook_mocks__/use_current_user.ts | 0
.../use_genai_connectors.ts | 0
.../__storybook_mocks__/use_knowledge_base.ts | 0
.../kbn-ai-assistant/src/hooks/index.ts | 10 +
.../src/hooks/use_abortable_async.ts | 87 +++++++++
.../src/hooks/use_ai_assistant_app_service.ts | 20 ++
.../hooks/use_ai_assistant_chat_service.ts | 15 ++
.../src}/hooks/use_confirm_modal.tsx | 0
.../src}/hooks/use_conversation.test.tsx | 34 ++--
.../src}/hooks/use_conversation.ts | 20 +-
.../src}/hooks/use_conversation_key.ts | 0
.../src}/hooks/use_conversation_list.ts | 9 +-
.../src}/hooks/use_current_user.ts | 4 +-
.../src}/hooks/use_genai_connectors.ts | 11 +-
.../src}/hooks/use_json_editor_model.ts | 4 +-
.../kbn-ai-assistant/src/hooks/use_kibana.ts | 13 ++
.../src}/hooks/use_knowledge_base.tsx | 17 +-
.../src}/hooks/use_last_used_prompts.ts | 0
.../kbn-ai-assistant/src/hooks/use_license.ts | 36 ++++
.../hooks/use_license_management_locator.ts | 6 +-
.../src/hooks/use_local_storage.test.ts | 77 ++++++++
.../src/hooks/use_local_storage.ts | 60 ++++++
.../kbn-ai-assistant/src}/hooks/use_once.ts | 0
.../hooks/use_simulated_function_calling.ts | 2 +-
x-pack/packages/kbn-ai-assistant/src/i18n.ts | 20 ++
x-pack/packages/kbn-ai-assistant/src/index.ts | 11 ++
.../prompt_editor/prompt_editor.stories.tsx | 4 +-
.../src}/prompt_editor/prompt_editor.tsx | 4 +-
.../prompt_editor/prompt_editor_function.tsx | 4 +-
.../prompt_editor_natural_language.tsx | 4 +-
.../kbn-ai-assistant/src}/render_function.tsx | 5 +-
.../src}/service/create_app_service.ts | 8 +-
.../kbn-ai-assistant/src/types/index.ts | 20 ++
.../kbn-ai-assistant/src}/utils/builders.ts | 0
.../utils/create_initialized_object.test.ts | 0
.../src}/utils/create_initialized_object.ts | 0
.../src}/utils/create_mock_chat_service.ts | 0
.../src}/utils/get_role_translation.ts | 13 +-
..._timeline_items_from_conversation.test.tsx | 8 +-
.../get_timeline_items_from_conversation.tsx | 14 +-
.../src/utils/non_nullable.ts} | 6 +-
.../src/utils/safe_json_parse.ts} | 12 +-
.../utils/storybook_decorator.stories.tsx | 29 +--
.../packages/kbn-ai-assistant/tsconfig.json | 39 ++++
.../public/assets/elastic_ai_assistant.png | Bin 0 -> 95099 bytes
.../public/index.ts | 3 +
.../public/application.tsx | 8 +-
.../public/components/nav_control/index.tsx | 17 +-
.../nav_control/lazy_nav_control.tsx | 4 +-
...lity_ai_assistant_app_service_provider.tsx | 16 --
.../hooks/__storybook_mocks__/use_kibana.ts | 5 +-
.../hooks/use_nav_control_screen_context.ts | 4 +-
..._observability_ai_assistant_app_service.ts | 20 --
.../public/i18n.ts | 27 ---
.../public/plugin.tsx | 4 +-
.../public/routes/config.tsx | 6 +-
.../conversation_view_with_props.tsx | 43 +++++
.../public/utils/shared_providers.tsx | 11 +-
.../tsconfig.json | 28 ++-
x-pack/plugins/search_assistant/kibana.jsonc | 10 +-
.../search_assistant/public/application.tsx | 15 +-
.../public/components/page_template.tsx | 12 ++
.../conversation_view_with_props.tsx | 35 ++++
.../public/components/routes/router.tsx | 32 +++
.../public/components/search_assistant.tsx | 24 ---
.../plugins/search_assistant/public/index.ts | 18 +-
.../plugins/search_assistant/public/plugin.ts | 56 +++++-
.../search_assistant/public/router.tsx | 20 --
.../plugins/search_assistant/public/types.ts | 2 -
.../plugins/search_assistant/server/config.ts | 10 +-
x-pack/plugins/search_assistant/tsconfig.json | 6 +-
.../translations/translations/fr-FR.json | 182 +++++++++---------
.../translations/translations/ja-JP.json | 182 +++++++++---------
.../translations/translations/zh-CN.json | 182 +++++++++---------
yarn.lock | 4 +
130 files changed, 1414 insertions(+), 978 deletions(-)
create mode 100644 x-pack/packages/kbn-ai-assistant/README.md
create mode 100644 x-pack/packages/kbn-ai-assistant/index.ts
create mode 100644 x-pack/packages/kbn-ai-assistant/jest.config.js
create mode 100644 x-pack/packages/kbn-ai-assistant/kibana.jsonc
create mode 100644 x-pack/packages/kbn-ai-assistant/package.json
create mode 100644 x-pack/packages/kbn-ai-assistant/setup_tests.ts
create mode 100644 x-pack/packages/kbn-ai-assistant/src/assets/elastic_ai_assistant.png
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/buttons/ask_assistant_button.stories.tsx (100%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/buttons/ask_assistant_button.tsx (71%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/buttons/hide_expand_conversation_list_button.stories.tsx (100%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/buttons/hide_expand_conversation_list_button.tsx (83%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/buttons/new_chat_button.stories.tsx (100%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/buttons/new_chat_button.tsx (92%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/chat_actions_menu.tsx (65%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/chat_body.stories.tsx (98%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/chat_body.test.tsx (100%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/chat_body.tsx (93%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/chat_consolidated_items.tsx (89%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/chat_flyout.stories.tsx (86%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/chat_flyout.tsx (88%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/chat_header.stories.tsx (100%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/chat_header.tsx (81%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/chat_inline_edit.tsx (100%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/chat_item.tsx (98%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/chat_item_actions.tsx (73%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/chat_item_avatar.tsx (100%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/chat_item_content_inline_prompt_editor.tsx (100%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/chat_item_title.tsx (71%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/chat_timeline.stories.tsx (98%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/chat_timeline.tsx (96%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/conversation_list.stories.tsx (93%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/conversation_list.tsx (80%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/disclaimer.tsx (91%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/function_list_popover.stories.tsx (91%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/function_list_popover.tsx (85%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/incorrect_license_panel.tsx (74%)
create mode 100644 x-pack/packages/kbn-ai-assistant/src/chat/index.ts
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/knowledge_base_callout.stories.tsx (95%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/knowledge_base_callout.tsx (85%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/simulated_function_calling_callout.tsx (90%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/starter_prompts.tsx (85%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/welcome_message.tsx (83%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/welcome_message_connectors.tsx (65%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/welcome_message_knowledge_base.tsx (81%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/chat/welcome_message_knowledge_base_setup_error_panel.tsx (80%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/routes/conversations => packages/kbn-ai-assistant/src/conversation}/conversation_view.tsx (71%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/hooks/__storybook_mocks__/use_chat.ts (100%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/hooks/__storybook_mocks__/use_conversation.ts (100%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/hooks/__storybook_mocks__/use_conversation_list.ts (100%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/hooks/__storybook_mocks__/use_conversations.ts (100%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/hooks/__storybook_mocks__/use_current_user.ts (100%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/hooks/__storybook_mocks__/use_genai_connectors.ts (100%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/hooks/__storybook_mocks__/use_knowledge_base.ts (100%)
create mode 100644 x-pack/packages/kbn-ai-assistant/src/hooks/index.ts
create mode 100644 x-pack/packages/kbn-ai-assistant/src/hooks/use_abortable_async.ts
create mode 100644 x-pack/packages/kbn-ai-assistant/src/hooks/use_ai_assistant_app_service.ts
create mode 100644 x-pack/packages/kbn-ai-assistant/src/hooks/use_ai_assistant_chat_service.ts
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/hooks/use_confirm_modal.tsx (100%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/hooks/use_conversation.test.tsx (94%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/hooks/use_conversation.ts (91%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/hooks/use_conversation_key.ts (100%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/hooks/use_conversation_list.ts (86%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/hooks/use_current_user.ts (84%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/hooks/use_genai_connectors.ts (66%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/hooks/use_json_editor_model.ts (92%)
create mode 100644 x-pack/packages/kbn-ai-assistant/src/hooks/use_kibana.ts
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/hooks/use_knowledge_base.tsx (82%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/hooks/use_last_used_prompts.ts (100%)
create mode 100644 x-pack/packages/kbn-ai-assistant/src/hooks/use_license.ts
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/hooks/use_license_management_locator.ts (89%)
create mode 100644 x-pack/packages/kbn-ai-assistant/src/hooks/use_local_storage.test.ts
create mode 100644 x-pack/packages/kbn-ai-assistant/src/hooks/use_local_storage.ts
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/hooks/use_once.ts (100%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/hooks/use_simulated_function_calling.ts (89%)
create mode 100644 x-pack/packages/kbn-ai-assistant/src/i18n.ts
create mode 100644 x-pack/packages/kbn-ai-assistant/src/index.ts
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/prompt_editor/prompt_editor.stories.tsx (96%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/prompt_editor/prompt_editor.tsx (97%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/prompt_editor/prompt_editor_function.tsx (96%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/prompt_editor/prompt_editor_natural_language.tsx (95%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/components => packages/kbn-ai-assistant/src}/render_function.tsx (80%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/service/create_app_service.ts (63%)
create mode 100644 x-pack/packages/kbn-ai-assistant/src/types/index.ts
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/utils/builders.ts (100%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/utils/create_initialized_object.test.ts (100%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/utils/create_initialized_object.ts (100%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/utils/create_mock_chat_service.ts (100%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/utils/get_role_translation.ts (61%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/utils/get_timeline_items_from_conversation.test.tsx (98%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/utils/get_timeline_items_from_conversation.tsx (94%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/utils/get_settings_href.ts => packages/kbn-ai-assistant/src/utils/non_nullable.ts} (57%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public/utils/get_settings_kb_href.ts => packages/kbn-ai-assistant/src/utils/safe_json_parse.ts} (52%)
rename x-pack/{plugins/observability_solution/observability_ai_assistant_app/public => packages/kbn-ai-assistant/src}/utils/storybook_decorator.stories.tsx (57%)
create mode 100644 x-pack/packages/kbn-ai-assistant/tsconfig.json
create mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant/public/assets/elastic_ai_assistant.png
delete mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant_app/public/context/observability_ai_assistant_app_service_provider.tsx
delete mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_observability_ai_assistant_app_service.ts
delete mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant_app/public/i18n.ts
create mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant_app/public/routes/conversations/conversation_view_with_props.tsx
create mode 100644 x-pack/plugins/search_assistant/public/components/page_template.tsx
create mode 100644 x-pack/plugins/search_assistant/public/components/routes/conversations/conversation_view_with_props.tsx
create mode 100644 x-pack/plugins/search_assistant/public/components/routes/router.tsx
delete mode 100644 x-pack/plugins/search_assistant/public/components/search_assistant.tsx
delete mode 100644 x-pack/plugins/search_assistant/public/router.tsx
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 204c7b8198768..10496d5351ef6 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -10,6 +10,7 @@ x-pack/plugins/actions @elastic/response-ops
x-pack/test/alerting_api_integration/common/plugins/actions_simulators @elastic/response-ops
packages/kbn-actions-types @elastic/response-ops
src/plugins/advanced_settings @elastic/appex-sharedux @elastic/kibana-management
+x-pack/packages/kbn-ai-assistant @elastic/search-kibana
src/plugins/ai_assistant_management/selection @elastic/obs-knowledge-team
x-pack/packages/ml/aiops_change_point_detection @elastic/ml-ui
x-pack/packages/ml/aiops_common @elastic/ml-ui
diff --git a/package.json b/package.json
index d258e35a67b27..734ce9cce5128 100644
--- a/package.json
+++ b/package.json
@@ -158,6 +158,7 @@
"@kbn/actions-simulators-plugin": "link:x-pack/test/alerting_api_integration/common/plugins/actions_simulators",
"@kbn/actions-types": "link:packages/kbn-actions-types",
"@kbn/advanced-settings-plugin": "link:src/plugins/advanced_settings",
+ "@kbn/ai-assistant": "link:x-pack/packages/kbn-ai-assistant",
"@kbn/ai-assistant-management-plugin": "link:src/plugins/ai_assistant_management/selection",
"@kbn/aiops-change-point-detection": "link:x-pack/packages/ml/aiops_change_point_detection",
"@kbn/aiops-common": "link:x-pack/packages/ml/aiops_common",
diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts
index 0054750a55b24..6c9d805c43b30 100644
--- a/test/plugin_functional/test_suites/core_plugins/rendering.ts
+++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts
@@ -315,6 +315,8 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
// 'xpack.reporting.poll.jobsRefresh.intervalErrorMultiplier (number)',
'xpack.rollup.ui.enabled (boolean?)',
'xpack.saved_object_tagging.cache_refresh_interval (duration?)',
+
+ 'xpack.searchAssistant.ui.enabled (boolean?)',
'xpack.searchInferenceEndpoints.ui.enabled (boolean?)',
'xpack.searchPlayground.ui.enabled (boolean?)',
'xpack.security.loginAssistanceMessage (string?)',
diff --git a/tsconfig.base.json b/tsconfig.base.json
index a05f287c0f9ef..188c96734d2ce 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -14,6 +14,8 @@
"@kbn/actions-types/*": ["packages/kbn-actions-types/*"],
"@kbn/advanced-settings-plugin": ["src/plugins/advanced_settings"],
"@kbn/advanced-settings-plugin/*": ["src/plugins/advanced_settings/*"],
+ "@kbn/ai-assistant": ["x-pack/packages/kbn-ai-assistant"],
+ "@kbn/ai-assistant/*": ["x-pack/packages/kbn-ai-assistant/*"],
"@kbn/ai-assistant-management-plugin": ["src/plugins/ai_assistant_management/selection"],
"@kbn/ai-assistant-management-plugin/*": ["src/plugins/ai_assistant_management/selection/*"],
"@kbn/aiops-change-point-detection": ["x-pack/packages/ml/aiops_change_point_detection"],
diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json
index 50f2b77b84ad7..7afbc9dc704c4 100644
--- a/x-pack/.i18nrc.json
+++ b/x-pack/.i18nrc.json
@@ -8,6 +8,7 @@
"packages/ml/aiops_log_rate_analysis",
"plugins/aiops"
],
+ "xpack.aiAssistant": "packages/kbn-ai-assistant",
"xpack.alerting": "plugins/alerting",
"xpack.eventLog": "plugins/event_log",
"xpack.stackAlerts": "plugins/stack_alerts",
@@ -44,9 +45,15 @@
"xpack.dataVisualizer": "plugins/data_visualizer",
"xpack.exploratoryView": "plugins/observability_solution/exploratory_view",
"xpack.fileUpload": "plugins/file_upload",
- "xpack.globalSearch": ["plugins/global_search"],
- "xpack.globalSearchBar": ["plugins/global_search_bar"],
- "xpack.graph": ["plugins/graph"],
+ "xpack.globalSearch": [
+ "plugins/global_search"
+ ],
+ "xpack.globalSearchBar": [
+ "plugins/global_search_bar"
+ ],
+ "xpack.graph": [
+ "plugins/graph"
+ ],
"xpack.grokDebugger": "plugins/grokdebugger",
"xpack.idxMgmt": "plugins/index_management",
"xpack.idxMgmtPackage": "packages/index-management",
@@ -68,9 +75,13 @@
"xpack.licenseMgmt": "plugins/license_management",
"xpack.licensing": "plugins/licensing",
"xpack.lists": "plugins/lists",
- "xpack.logstash": ["plugins/logstash"],
+ "xpack.logstash": [
+ "plugins/logstash"
+ ],
"xpack.main": "legacy/plugins/xpack_main",
- "xpack.maps": ["plugins/maps"],
+ "xpack.maps": [
+ "plugins/maps"
+ ],
"xpack.metricsData": "plugins/observability_solution/metrics_data_access",
"xpack.ml": [
"packages/ml/anomaly_utils",
@@ -85,7 +96,9 @@
"packages/ml/ui_actions",
"plugins/ml"
],
- "xpack.monitoring": ["plugins/monitoring"],
+ "xpack.monitoring": [
+ "plugins/monitoring"
+ ],
"xpack.observability": "plugins/observability_solution/observability",
"xpack.observabilityAiAssistant": [
"plugins/observability_solution/observability_ai_assistant",
@@ -100,10 +113,17 @@
],
"xpack.osquery": ["plugins/osquery"],
"xpack.painlessLab": "plugins/painless_lab",
- "xpack.profiling": ["plugins/observability_solution/profiling"],
+ "xpack.profiling": [
+ "plugins/observability_solution/profiling"
+ ],
"xpack.remoteClusters": "plugins/remote_clusters",
- "xpack.reporting": ["plugins/reporting"],
- "xpack.rollupJobs": ["packages/rollup", "plugins/rollup"],
+ "xpack.reporting": [
+ "plugins/reporting"
+ ],
+ "xpack.rollupJobs": [
+ "packages/rollup",
+ "plugins/rollup"
+ ],
"xpack.runtimeFields": "plugins/runtime_fields",
"xpack.screenshotting": "plugins/screenshotting",
"xpack.searchSharedUI": "packages/search/shared_ui",
@@ -114,7 +134,10 @@
"xpack.searchInferenceEndpoints": "plugins/search_inference_endpoints",
"xpack.searchAssistant": "plugins/search_assistant",
"xpack.searchProfiler": "plugins/searchprofiler",
- "xpack.security": ["plugins/security", "packages/security"],
+ "xpack.security": [
+ "plugins/security",
+ "packages/security"
+ ],
"xpack.server": "legacy/server",
"xpack.serverless": "plugins/serverless",
"xpack.serverlessSearch": "plugins/serverless_search",
@@ -126,20 +149,30 @@
"xpack.slo": "plugins/observability_solution/slo",
"xpack.snapshotRestore": "plugins/snapshot_restore",
"xpack.spaces": "plugins/spaces",
- "xpack.savedObjectsTagging": ["plugins/saved_objects_tagging"],
+ "xpack.savedObjectsTagging": [
+ "plugins/saved_objects_tagging"
+ ],
"xpack.taskManager": "legacy/plugins/task_manager",
"xpack.threatIntelligence": "plugins/threat_intelligence",
"xpack.timelines": "plugins/timelines",
"xpack.transform": "plugins/transform",
"xpack.triggersActionsUI": "plugins/triggers_actions_ui",
"xpack.upgradeAssistant": "plugins/upgrade_assistant",
- "xpack.uptime": ["plugins/observability_solution/uptime"],
- "xpack.synthetics": ["plugins/observability_solution/synthetics"],
- "xpack.ux": ["plugins/observability_solution/ux"],
+ "xpack.uptime": [
+ "plugins/observability_solution/uptime"
+ ],
+ "xpack.synthetics": [
+ "plugins/observability_solution/synthetics"
+ ],
+ "xpack.ux": [
+ "plugins/observability_solution/ux"
+ ],
"xpack.urlDrilldown": "plugins/drilldowns/url_drilldown",
"xpack.watcher": "plugins/watcher"
},
- "exclude": ["examples"],
+ "exclude": [
+ "examples"
+ ],
"translations": [
"@kbn/translations-plugin/translations/zh-CN.json",
"@kbn/translations-plugin/translations/ja-JP.json",
diff --git a/x-pack/packages/kbn-ai-assistant/README.md b/x-pack/packages/kbn-ai-assistant/README.md
new file mode 100644
index 0000000000000..d28f93431baa9
--- /dev/null
+++ b/x-pack/packages/kbn-ai-assistant/README.md
@@ -0,0 +1,3 @@
+# @kbn/ai-assistant
+
+Provides components, types and context to render the AI Assistant in plugins.
diff --git a/x-pack/packages/kbn-ai-assistant/index.ts b/x-pack/packages/kbn-ai-assistant/index.ts
new file mode 100644
index 0000000000000..cf53082cfa4b0
--- /dev/null
+++ b/x-pack/packages/kbn-ai-assistant/index.ts
@@ -0,0 +1,7 @@
+/*
+ * 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 './src';
diff --git a/x-pack/packages/kbn-ai-assistant/jest.config.js b/x-pack/packages/kbn-ai-assistant/jest.config.js
new file mode 100644
index 0000000000000..37d30bae01fa9
--- /dev/null
+++ b/x-pack/packages/kbn-ai-assistant/jest.config.js
@@ -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.
+ */
+
+module.exports = {
+ coverageDirectory: '/target/kibana-coverage/jest/x-pack/packages/kbn_ai_assistant_src',
+ coverageReporters: ['text', 'html'],
+ collectCoverageFrom: [
+ '/x-pack/packages/kbn-ai-assistant/src/**/*.{ts,tsx}',
+ '!/x-pack/packages/kbn-ai-assistant/src/*.test.{ts,tsx}',
+ ],
+ preset: '@kbn/test',
+ rootDir: '../../..',
+ roots: ['/x-pack/packages/kbn-ai-assistant'],
+};
diff --git a/x-pack/packages/kbn-ai-assistant/kibana.jsonc b/x-pack/packages/kbn-ai-assistant/kibana.jsonc
new file mode 100644
index 0000000000000..4cddd90431e39
--- /dev/null
+++ b/x-pack/packages/kbn-ai-assistant/kibana.jsonc
@@ -0,0 +1,5 @@
+{
+ "id": "@kbn/ai-assistant",
+ "owner": "@elastic/search-kibana",
+ "type": "shared-browser"
+}
diff --git a/x-pack/packages/kbn-ai-assistant/package.json b/x-pack/packages/kbn-ai-assistant/package.json
new file mode 100644
index 0000000000000..159ed64f288fd
--- /dev/null
+++ b/x-pack/packages/kbn-ai-assistant/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@kbn/ai-assistant",
+ "private": true,
+ "version": "1.0.0",
+ "license": "Elastic License 2.0",
+ "sideEffects": false
+}
diff --git a/x-pack/packages/kbn-ai-assistant/setup_tests.ts b/x-pack/packages/kbn-ai-assistant/setup_tests.ts
new file mode 100644
index 0000000000000..72e0edd0d07f7
--- /dev/null
+++ b/x-pack/packages/kbn-ai-assistant/setup_tests.ts
@@ -0,0 +1,9 @@
+/*
+ * 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.
+ */
+
+// eslint-disable-next-line import/no-extraneous-dependencies
+import '@testing-library/jest-dom';
diff --git a/x-pack/packages/kbn-ai-assistant/src/assets/elastic_ai_assistant.png b/x-pack/packages/kbn-ai-assistant/src/assets/elastic_ai_assistant.png
new file mode 100644
index 0000000000000000000000000000000000000000..af1064557968369b9d426fde8eb9891240436f55
GIT binary patch
literal 95099
zcmY&<1y~(Hujo0rJH_3lxVy{29ZGR`cXxMpha$yVT#6pNKyi0>_wwky|Gn?M-EY6i
zW;4lTzL8{-jZ#*WMn=F#0002UvN95?000Cv000z)gZaRSFPQRv6i^nT3Zei&LjvNf
z5%kA9sfmoL0s!FsK^Pbc06c%70uKQIHx>Zk*bo5V%LD*$9CO=M1U^25nrX|LD<}Zy
zKVUcjG!O#-`2hhxZU7J;@Q>RE1ONdE{ufpS(*B1A0sx4x0zmzT=F3O~?g={?+;K
z60#8Ze>oOH{0}t*bs^;cz`&2T0|3dY`V=1pyrYbc3jlzK`R@S&vU71ixanA_YrASI
z$n%*v*fANIIvATVdD=Pts|677h5&VPj@v`}{%i*~QD=)yVU+y$i*EGx;Ar5@s$Y&Q^}DRu1;0|M(giJGi+D
zl9T@v^uO1Cx6{?i{QpX_clnQ69||)6`@+o1#KQc)yg#Z6{KN7oIa`^1NdCuPh*jXf
zDE~j)f8+=-{}cZIH0HlG{V(i?szL|?%>UbMLI`;JTd)9t2tZasRNWJJ+6C*GyWmCr
ztXLrGK-r5>52~?F{br6mrqKMAgnz-wn196lhVr-ho9(r!3C&4r&gmM@)w0LJRr$sQ
z*hyuZhe_2|F4}a2K2;*BSSA{aA3m9MdId0Z(0##sgGmaX3~6W%7kH2u@^rvl;d}6U
z;4`@pU+1@SaP2svV%V+Rb096m8ECt(u!-10Hg%2CgJXO`&oO?3XX=wLBIFWo77*xW
zX+DjYev!kB<$l&m4!yYW#K>krygy|lQFtWOV=!3ZtmW(M&FyIW5Bj%b1hH-#iGZu@LwMgdV`EnAeF^
z(_0;puS}dM8)d%cXl+S)ga>k)b^85e=i0I(?K5V(>CW~@Z4CF@E~dR2K$ya=h05GecUk%>$>OH#mEQWNz`sC4wqTuMzck#BG)G&t80
zaM9^MI#WOEVi0E3k|8KhBoUDbZPF|%1CX$BA6SKF3&B3fVm^|a)sM~Q$
zH_Ps0mPYrOP${%T;4xI?s9cS6iH9(S>#j-*r`b*BkeH}g3UyX094|1oGa!{x{yBo%
z10pfNvzo{~Df0Si6!(Ml+I(E6{>wVJ5h;LExCsgs5)
zg}f;fI1rKbdbwL3UjwGt{Me|A)3iyV5ud$u`?`5n#7EQXu4O#zG6)ivFvw&|T{6iX
z?$Ab0)1{|&vUA;7wJSqXu~|HLHI1O{`6fX~kS?p`HptFuSTuVqUS;D<@$y70sQ6Nu
z6BWSq!-w9oh0_Bzr22`2Ryj(?8-LC@;bXW2LnMD4cFXK7XG0Q7t1A66C|x2_45UhfD5aM!P>pBbm&R7KuL+^g#fg+$-mKdIm!rF8
zu?phw_0Kugs`LO;+U8AayVVKoEh*SC0Dy0F=H$WTd+dD
z;}PJ{aAv`49L)ZZGA_E(0wpR#L0{wh;Pqjq=?%PPbPWb$08dtX-1p@=lAk-i6}z&^
zG0AaxIDxSodrZNUu6X162v`6JEsyTQJoQT2a#>WGQxE{E(xtTuiMiP2x6n008?xH%
zMB6%*bda-34^9mg^*(Mv!&wA0)n@Aiihy*~j!Koodf)K#*|=_PFT0fB9(mB12eI;p
z7G6_PweT4wfdB}TY>A`poU&bVr*Lj9<3*e8a(cSP9te!3tliNzkC8pg&00Mogs;$8
z^!%8RAahlBV5+Hf(ish{Y`3V%LI<2YS5j=6r$?=#Lz}7vD)cICj-cO`wJH|^8mR={
z4CT-Co?R%dfPO~Va3A)p&X{kEgHQfp6{s~5Oiqcf!Qd9`q>*V+|SRp
z!6N;1IRuAxy-F-JGBI?mUY$xr>R9~1AmEo%q$wn8HSrS{{FS1)y6()8^%~SZi-AP9
zsWT5rtp@CYRu-c(cHc$bjUcHAb>e(Q_Fy4Iv@0W|bqZ+@{-;UsfV-rgB!cpHWA``Y
zI&ptn)B~OKXB&z2y_%G~Q@r#zXvxWFjwzO{
zNV%y+@l_@}aMn3Q=|&J<%}=~#(-Co~PHkhnqlgz4e>ZT1@Q_k#P*pjwCsc(s&I5&J
z-S?H0J#RGHx!sMlsNnpQlB
zJxBAC)k>fBo)li7<0dQ01pXrd$)=}94F)U-iFK_YTJ2*bN2jb}d(P34y2Tr3OV}3;
z>|zNg>#Of$*XKMpHOdcW7UEZ)-hbAL8?7}I2Ri$JvC!`)2qtqufrjaHba9snJ<6>2
z@`UegDpGDlp4pRJ=&7r6glk|VdKCk1bwX(B3K28&y^%3@3nFNG86dlEPSgsH-NAsL
zv6y^{vtl1(2A?-3G*_RXPqG6>E{jChJ%>|D0QAeRKFNj^KY#B=%SAeWg|O#r1WMi1
z(Ju1Gy}#TLRBuY(k~v(vhJurg0db8UA7fem-w2QobzAzai7*V;996xP1_e(8uZ)}2
zbShc`=j&^fE|=%m9Mcop@k=vOWnx}GbK?$UYHt?5a6p2tKkk)&c7qzLi*A+_)5Z0Q
zBu@I=SH8)(w&0i`EW0iTckXVRW))952&DSaWgyj+G`BRm!nx8?oX=43)jX`f1_o`Z
zN^$5jd=4xp^Lr4|<&@$WAsy0)(Dcll4NeB-6b|ch3P;-rwP}G3Sp1UtBJ|XsFLhZ$
z1(|J7Q(akSU)c#WtTeB_b9~;C%1~5wCN5h|sFntPY-Yq~dLvqK5V<`jay;lS<5fxI
zNb{xR@*xc7gPE6MIBH=(V|c>20N`%PYMYFG5@;v@Pq}4_5e0HaM?sqK*bQbT%br=Q
zb?0yDc$>MXbEn@YMPhmSTgDt-StSx`T~THh6#eMD75{kWM1^o?Tyvi5~{3iBG&;))7|TMtd&T168Kmp8Tjb
zaAmu=Sn0r7j5-X`4H@j;$b>0KM#7b&70GUHX1oZwN*|JbS46zHDJYAO_A2Ehxcf`f
zavI5FmKX>-MCllVSRB&Hl#mTllkxG__kU1OG4-2q`_LA$N=;S+#0L&6U$gfrPJ`~$
zsfTm||A;_e=S0fK>asv0#tSGh_9@@owOk6>v%%3c &rA`|ktIWHGB6dn8QFmmY|Y6Q303WWX{l0s*zDK6O%U+MmY
z^j4tVEvdyGIM#@f2DwwOx_t@?!>V*)7p2&K$Q;e3KFqE3<;tNwee^L(!&?Z^*R7m2
z+~L&pptsy3`T_3Veiqx8O@X5!bBLsNIfmX+Jp0TdvF|Q_9!%k
za9iCv{iouOI;F31AM+SmNL;(NKh+R-4#aB!&&J2R_{9~`PWS%H%O@P$@4HGjqrl#1D=Z;2y)+P&l4EAv^mAgVhhi|nOWGR0
z^f>td8;vuu8Ld)o@Zvt}?3P3*CW@Xt+pPART)vN?U`tDLF^*XMWObB|rDQ
zW|{V+G=j&r-4J!2r5Hf4#&jue+(MPrZJ-F;L*fW^k}PV%fe2l+0Y)OCYJM_0Ciiae
z4K+zHp@Cr%u*jYLmvj7QAay*m6kan1=BJ#^eB~&u&Fc0r6U7D}w)^~;<6}i0gmGeo
z)SJ&)g5O;^KPGc31O$9oCN)`=3)uaWNU(m4vDRRUxo=pCsqfdeXYyF69N(_`Y{)&Yd|o|nkcbrlg*sCcEAa^Ci36h$gGzfOdRqJAaVRxnd!ic1Sd$a_TsjvfSy}XZYm%CG8|ffT
zolgm5zc5i@PyZ10_SDPW4I7lH34$>(%&QnM?~IMP$bZTb(~WCEF5{5}$t4f*{^nBY
z5^pgRugJ>Sc6P*~<%Xu`#7yrXVhWHBI#LrEXKxQ~4QzjUBv#zS22j(pORR)U;e)K>
z>*YUo0ZKMzwe(EqqvT1Y!oeD7kylY-_7io1AtjiuI%Jc_N8AUhQEU#RRJP%qrk@&_
zrL4D&WVneSFa(5dA*XFq3*sy#reJDutTHIM_}Dy@lq%Tp+o!+5Nj2&sNAUD{SHTD(
zo2yZ>_yezgjXEPU`QJ{7wHXamlSqSb!VA>_PANETEjg?2!KIBul()^z|h8e&=fJp-bi+O0G
zvev~uceb^*o9Ahh^nJV)_}fgS4BJ)YueF
z43_t*fhn!zjo{YC@dveFr(y^VY|J7AISDNFE+V50*5=O&W*%>v5R;
ziiRUyoQEsip)W!{h+F}sM0%5`JBJCjJCUF`;c%zc=T
zN(Wgf8+xaLKPtf`b~AVTwg%=#c(LZf^*
zdIs$}W8Ii4EgrU@1RUwLak6AO1t~ci2K);;B|FjCZ_=D`lS4-g2cemWf`D(xB#+-a
z8jkzN5+4%CPAADUu|>v~p*{EIe5PGm2~6IcY~RNya0NI68`e3tyFze}&c$C~jUU5E
zx{)28el1X>$Q#!%#7+N>1=9RYEX8Fn3wch!zyJwA$p}Dd$?}$N%a%RI(GRK34d)L?
zFD%QH4npAL_l~mDjQQNjQu|~nfdPqz-?9M|MrU%`v6P=RtJw4pI&C9LN3WPIJTPzZ
z+i#wsta3%7p%t~PYnD~VJ!cn<^;&;4M9r}46M5{?ve3|Os`I)O=X|9alLIR3Lcw#O
z-Ab+`lDsmNA?U*4AYZlZQ4$*D!BGiL
zmBU-D@*SH=inT~u&lBaIgAB^)Yu!0RbbOXU6}s(k=-n>&e%*b4W4(qou~dA%y0jqK
z34sqyXYQMSsOejIANy|kbv@M44UNJ6yY$R4eoc1^%EsSWC8Xh{>nkos?H{-$eiS@U
z%kZzaS{u^kB;W=AeXH3a@^5+sQs1(##3fRSGaJuy1iT6d*wxdBWJz(^dnD+ID{V|o
zigzoZBn!!LYfXGPK+UvnNl;Xv>`MQ9Z`u@MNgj;yP%4FlTFSny`VsM`s%|TgX>c<5
z5ieW63OgTBI{UFx-4Yst-C(EcbowiHD{1Aq4*t$Dp3OHOsz3~y))FXXm2l*%QMN*+
zZ)Gn>w95GZ4o2|#+Q06@L%&X!&AQi=P7J9$R%v}`!sWsil%
zfGa-R(QP>JsVnvtXZTHc+E=pSh4uc|%e^?5y)8~0P&nspRl_$~=wJIrEUL7|jESrsPhzfn)
zn7<|P634@@Ggz0|Rq+cRf86!2O8xY$as9|uU^*CBE&}hBia8{EApQLr$&KTo+ypKV
zrK~E7-7!k1yD7z1@PZJQDiLRStj3tg9I+J$DHv_o(*%GG2)wk^V-XfB=-a
z1&z6XE)QBtAz#Ee>7=2qjBN$Zm7CO@F`g)Iplp%+c}21kDuf9$TB$8#st$2Ol;-J^
ztFWHAVxogxF)Xqj)4-v28j^-UUWk=xxU;49aQ$fz63Y877JgmA_CD%bO6TqE}_isgurqN+Nx
z$B9ZSLaox3U6rOsO>aZmp1Hc|Ts@1yp~GMV0$rCt(MDnZbSa7+WFd9RR;AvTX2zz#
z#$OR-&LrM#E&feowNgJi(Ay0}2r*HE8#%KDf?&WG=ULnGpcgKObiI`$)&rdt+zTFmx!N^B<9Pyt08
z_J`-#fqfr1!hWE)0!7(5amaCjW&PcwrKG(LX5(p&BpKa8rG+old)brF>zVdB;m+h>
zL2xK`UX
z0zd%60gfq@mGN2Y!;G>!uD_RPdZ1@BC>2tA!v3v
z0(Z%$3Z{ev)%w=iM%TuAjE<^;H1l(-Y<~|3?`c7gZRaI%HrQyl+(Y*_CGf|k!&v30}cM3MkE=W_RHPD
zpCXl`dpSUUT5CjDS$CtrV5Yj9D0mm*(?9O1Txh=>-S`#WA=HK4@-~V=`1~7h+3|6@
zvk`i08ScwG2ZbuZ;Eln0{MyEP)VW}YD>jO;Ep2?hiAjwC+QoB-#}2q)J+x#JWs~Ek
z+6JY_ZiRl9*>1nf^`T78i7TZXDjcR#y>ggkE=JV&j7x&m>Cok?_b4R?1{S0u_=cQ=
zl&8=GH7Mj%8S6y?KwdDW8Yhm8)z$9$nbm#X^GGHeqb!<9vuRNzit&QIZ7&6Svmc-B
z7pa|yKq1AYiLD;el*$_-=}0Ve5OSRY+p2;>r=|_w(0q-q!D$h(XcOuaM4I`0CHq+$sfsL`O!P#+1yps}|9B@{_Eb
z@08#rvzt?3+f^;~W&>05dOi~zUse2iL*Zbq6(2($W1)9QX+q`^Oa(Uwv;Y}4x`Rlm
zV=^XPjVE5xH=CHd0;dSN^`r6-qiRjAO4~6#KKYicT>sr=)iK=c
z>tkzX^k^x%ts$9)jI`Bz!|&XV%iqTSF;KwZBshxtd_1zBg*F%4?)cU)$&Z6R&6^wf
zUU5r|605?Yy{Dw}8`I&0dO@&!J}MpNK>Ncl@l?9$6snm0-6W@dGXX7-;t^74N@n$5
zk}wxfH6LCjm29)h8R)hp;$iy>R$;@b6m;Q=poN+w(3=Ch2^#V!jvwr{&^
z_c0`}B*LH<4E*k9Nd<|;<1mfeR--9y7z<1-A_1=Gh=470j+VKS_OyF!C^bC?_wBSg
z;Ru?57md}|O3wYZcpN2i84uH|?JLtZ=X|CPowhR!`BBCGKzI95P`UFfiOqA0SFi@dQOFp9(XuXszkEk9Od(wig
z5w2_Dqv({}D`j_jsFVmYgh~*S`G%8C$;GbX&s@SPp4^D9sP$ck;vuy1AbvmIRe1ze
z_k>}g^}vN!qepR0<1Tn{b(fei_bK|
zJ7a^xH_v@2t1>!anD!AGn{(LKC;_q6cFkP=hF9@pOC8>P?1i#l(vI6drrf?#IbGR^
z`N7e6_qkd!{+@s)K{%W!B(8O<#KS~f4bk_d<0{PH6-I+i&{7*u(x(*2?|(6LxRKTq
z@xIli?Y08(`p9e$>L5uRqgiJtknAt;G7Rjzq^UDHyrtC?Yy~vQkz3l@Na5T>d&G@=
z!tA$rp7JThi7JA^=;uICFrU5wL0tUZhLnk?oW6sJa~h$^SZxO^{Hi9R5Tt-o(~%$u
z=DBiZb`Pk7jIoa==j8~WURQAsQ76nODeI!0i!X&YJ~Y0DK%f61xj+ToVM?7b`uciP
z6FQL~)z&FSg*m2%*33%L3y+^+l%H{sRy&01%sBnl>KzBaS)NG?fvb=8C5R5v7tS;_
zqE@boe6!>QN`SG4-j;1xK%*Pn6+EDmVIEN&~zTGVQmql37#sH19k{VAKNbY+dE9
zC6$O+eZ&xCtqu>J>F}U=!d$5EDM{g{Oa>^OipKnTif*L5ymsM;V}F`S-H~O?TIY;2
zC?&*v&92QT8qD3@%QDh>U?27W6QM($mAI(upK7%87MoFIcdRf*?bnl=T1Aa(DCkD=
z{$eHlN0=`hdqU{7*Mhj`yY51lT@i#Sx($O0g5Po^uqpTK_F>aZ&Bp%ow@$$qub)eP~rx
zn9eVWebx9{R1kF6qgR2UrBwJsR#h2|I_+@P5vqBw=3cRBJ1NT%n_A~1Ra()c64qoV
zOa1hCl?Xt-N%&N_%R?bugA-oL4Cgk^F+rV4ct=ZNViLXHn@W4rl7(p|3&Koox|mM?
zRuZ%m?j@gMXKH=}!mc&Fc!;v1K()Qp@OY*SRn(t>T6Q&G$iHB6SN(j@c
z>9ARmpRA?Ntf2(0UlPWM3sd*x$H&r-66(i??Kpz{u)Vxt@$TZ6Tqi#c%5xG9C4_tM
zM~tFyRM>rfHSddow(K&HT%}Gfa2KAt8xh9*TKTmIoapAZ)}$WwcRW{Sm<4CkZXL#`jegxXV$
zRlzK*P%;JHV7Jmt!x^eV$%-xyE&3uRWHC=mg<^MA3SoS69#Pq(R~5OI|7RVrLWA)@
zdknIyK5O`;wWN7ZngjldjNgS3Og_sWDDBWzY$g(4QF#k-$*R>cVr
zh}GFymXgjau1JKu1@#}->$bp6tBek!P~5gw{V)C|3|zlV1lI)|Aq)Z@B?)Um0W)A^7@CD9(k3W(c=dB<&w3_MI{)sZzs(5
z)=c6osMgR}SklX45G^`@f#qvFc9F+CCE`MQNBFVA(Rd6jOnQSVQpv)+fqCk&dpXtO
z&bZQ{pDQ16m{xBr$Ew_FX&
zaniglB+Qt)nNo$$UD>g3>eQuT#v)7-T3hP5)3R?ml&zolno`v(CKR6|SYV
z`RCt5BH%hdku|#UHrrYczR78Awy_gEPTNFWZtlAaJRHi_EH20D3Ols`@gs-@gQt^(
z*oMfE3d$dTGp~m>^J^L&w4lrowJrxHoI|D}416Dz3Zz))3|5f}yPLV+b1tR0-~dP_
zCy!E1ul~_<#p=gl$0=dXYnMJVvRAJob;}LX;r{WVg@Sq|ZY7+ZoO*3wWLhU&;(lD{
zfcKWnXo-l+eS7}2gRF9F6^&E!W7ePV%lt{K=;m?Pkq>gw6lsZR%}ZP>9tu(|m}B&N
zFKTW6u2d5=AIB7I1>P?DKL-`le?Vu_l4eEiONOZL+E_k_T1go-=~+ALev&l-`vOd&
zC|^xh&}+X0`Lx*e$aIoe`1mwfX$UG33di;5-G1Zc2`;P7X~-kp&e9_S2x_ED!>hJF
z3%;%znexB1sZMUs1@Ond-%F!z{q?BnPv~xdjYI5T&7cm+Z{UF(CK=dV;;UJA7lgsp
zxI^{dviTO^$b0P5_D4vrU)=zL0v7LxWvt>Sud(SM{DtJSw@
zd4wKrb?}Lm7vkG;*d4fun^mNoCMVIjL8{>)E&GvL_%1M+C`2loyhNu`6A7TaaDu-g
zJXd;v>%?8KD3?%8Ad6XPNX?}Q2yKvUZHX_Xf{+4M=}{^q6;%PTArzt3=i?>$scj;R
z`*1;ily`8JH7GMX(W-a>YkGw3Nc&QN64gJeHOgGH<@cRR;HFWvz|a6~#ev0s7WR~P
z^_+uq4=h@$4{w8xic?RYsxZ(D;Rj@>B!!zUb$z#KX=DgDmlK6dryS{!fL8CgoxS21Q`R$=#
z@cfO)(yq2Is9(>LM%nXk(+|%*5){KzOJ@DDvk|zG5^xIeXNyKrfFTw?|FiNMMz7EN
z443b@8tPU
z2EvX=lP5_?L4SP(*h4lJvZ(;4WnZ}c$qv9>Gs#IIH>*E9T4F}zm`M3Sx|3$sK0
zF10FCoe07S1V&i0fIaPiR}{&u8!t`G)b4HeS3V)i4)b!u8G6uQaKA(
zjTZrohhR{8JL7rjEYw&*?~u?lDL#SqaJH&9AzmcM?X2*Sc1X`~Ep=Mcd0Sx+^*#cqvoB34a%*am{r
zMMLrCg$6FTiPz$8E%6m{%m^7?!rF2LHpz}cE6&MeSTJC#)M?W$5qt7~Y_vLlJ>-5X
zZ%QJ0p&gpK%ZRQr)}RloPR*75-YXrN%^1!M$L#u5bi$
z#T!E)xFlQoBz$7Vg{Q7bh>jpe1F79c_tNH&(8K)}5jzp>rg#4gQYFMk$b;>3J?p#o
zdg%D@0skoXEOei_*r%q2r!!?FEB7dd7s(*^O@HZ!iylqKba%E*WDsFRN=U-GGezgN
z!p<7lzI!|LeWEM9c}N~&Ja}c~kKP`Ob{SfSo5L?M1gt@kw|ut|(cp-b&gYde+!{h&
z_$(Ii4rpGl$6(N6>~-*ac5qy4?~;L9*!1+pF5^NXoX75u)~!B@VvlSPr+e+I+lUbL
z9(uh&y)QHqMV_zwBDRu5tM7lNz(&+o2Xi#opT?#&iCAY4d%m;wO;=r92@g#Blo6u6
zDlyl$m}A-fhBFnA-Ax+%JDYHvY@nP_cN`83e
zbU0?rS#?9hw`W8r<`)kGF6&-?3|RF5Kq5
zK^H6}O?=_Okx3+zgWBg9a>>A0-5I}?U|eGr;czQ~_WJAw2GtTvYSt>HXgSb;3ixuRRQDc5asffZKcN
z2yykZ)1#HGe60RH8P9Yal@%X|$tv|d6_yV-EcG6p!a`~~z!YvmuC;yN%U~PL4kMk6
z6HMx!#B%<0*D>W$9P^6~ub5Kuo^pAP8w~+%DzGXK=-RzLlJ=`q_LP~633scTIriB4
z5r2hVq!^d_M%e2jtCUXl&XbD{;yW8h8fDl!Z)9T>+3Zi#`aMh%v7zc!(bm>2m-x_8
z@q4O9LDsiX1|7*5B7Vh>BgkKwMr&Lq+BUX|v3}9m(QK25H&fBml7~z9ibl;e9BEsOq^W;2%)3@C1Tus5CVo!$3&TAda
zxFoF#N8slS6H0rnGCc}Ojm3Vm_~$}3VdccQ>w1(-WdNGV?dInsV(u-`nP}Am?ia2Z
z{TJ*tRoe`x8iXs+SzthIf71vayd$iA`G1sTZV!}9snKi8Rm;1G-w@QfzaG3;8E
zf!8FL4qSx6eU!&Q1ICeZPo8Y#^vxS|CYjKOoOe--X9WE|bTVm|R@dM@bWxyjSk-Y$
zxH@I!qVuieRrYODDSg%lFS3YVC!lZPMMXV~tHWt|hF?&Am);~OH*&wn@@?`^#+VUl
zF$~JUrJ@R;kG|&)a$^|g$4KLYcXY@
z303k;<%9b2;dxg&-A6>t?A)(9Wq;oi$+Z+a`YU?bg#tr;vbl4=%-QJBGNWT!8J|^|
zS+c(sc)?l{Gb9t52G%E|OEr_?)Vk^Cs%T1LaV5CwivoeIp6$fwO-2gY7~+t1Y^U*lgFQ;%V~ziRv6K2ASWM*Ek~W`VZb;4bV&I!~?5W
z8G)fu24vLz&rriPKew9=CGRL5xBZ5ZdbuAliCtVe!+msW3yL<_&BX=Bc)X}ZnttP+
zC=z5g1VMqHYfc=@*1QGrCkGw6Qy{3bgiP;!u}j*%TeiQ6c7xV?vX1#}ZpPHwlL!_S
zSCaFf(pp&kSGa-|^mq76y61c;9E%lz(DIb%=m2z)q}{>Ayu@6-3g`r}^)Xsa5sAse
zE*#P;m($odw(+j(y+;&w#~g41XL6qP+JW(V8=qG8$4OYzIMxCpBYHJ8CRVSksL`a^rnw1X=E6ETN`6bM(gv}}WjR;AA@P3Q-&B^$agd^r
z@vZ3^5@cOeQYb^?=i;n9o)}+4PzyX-cvUd&X>{?!5N!S56y#OggzCv#3`YVI#m!`2
zS?e^>;AT=p0tiD1_t~o~hxa>&De6*^_bnHLvfB`Ag5~VnTVzrYg^Jfp?qWfoTEZK3
zmwEVQkw*aW#mgfXA&)CeNXac=tBQv}CvIvOJ+zS~C~biZzglc3SQ4xm=1p1^4tCs^
z=LW)OkqqTzrhrrM94HG)W%=qHg0L9!4-B_hvG6!r16jE;J;JBIgv7btG`W+%isE14
zH7i(W+n+a4&_xySkMJt9m2HV8JLmGZrw>QzBeVqHBKTNV@yAf%04*O
zZt{#FpKbr;zwL{%FIyW`$OV{&GIU_$nY)OKLLi5AWmrpXQb_7zn)p?F(O8u68OdsU
z768AGtGQjWl1#8d&^E@QXCgBIa{XNhv)V>GGxYt24k&~^LL@!0|0WkZwGk^`q!kqp
z5H0p{FFjjIAJ2J()OnimUhQ`~-Uzy=Bp0@ntrNTcd+&zHRfT(@{a6)~`1te6hE0-*sY3*1TRFU&xj{0deXF0xdrXQNBZwOx+aB)4f7N<
z(+K^W4|(4+1jT&vD3kpw9hZcZfwS$k|_RFzAKpaMkBVgi{5vhX#-&3f85H^}L6K;fRTo#p>@d0ZZtGYF5Z>v;S5Of>ogVm0~I+U_ylRZO6jgmW_2yFpfg?B15Y-u(KD
zelTO7z*A%Or!BT$6)EP>?geHN-XE9v8~#3fzs_a+MRvH2H-y)Vz$Bz5>f%s51r?U-
z-=c2XKCn1bbjaY;2KOT>6D973RN;MERK^rhvx%f9v%98tBp-=%qnc|tEH}lQgx*XZ
zAFWV+89{0Oo<&aswqpIROuRFhjWo-XQ7#K)^KkrC_9ncGGB&}%rSB~&q-j_DB;sfB
z(sPgZzU%`Hf`e$3^81k&;y@%iA{b^lkWqli0b^^(SYn83UD_amPr5&~yjm3?$f&=(#-8*JDQ
zis4<Q)cWjIYwT9110+Z^#tZvg%%Y-KEa1@|ipAnb;edEj
zCHq?>iAPkl6mRJLOgc82FR*>$!dyu+DFCejS6O67@4igHC^+{pW25wPCwI5f+Pb{_
zoL}j@x6eB~&tdPFW1vg@r%gj03(dg^NQ`Bl9ILcO-F4jP&7?(K`Zwo)`
z>P(RpTAwT6xw(dde>lq~gqjUkbL9VeJNwIrAfNCr<1xWr%^&pae^aa`Z=VSt_Xi8K
z?C|xu24(SPV8ntdxW(HzYVO9AJV9fGCUTI9^3YQ|9a|orjfzU1U^%`y{Dre>deazGhPb|{=Ee}J1rcqw0T7lzK;+61r=$7&(*Lj%{
z53w)#)X+QwrUf%C50rg{2#yT0HAbvf^s(aiapA_FTu&x)C2I?|_J6EhYOI087giQ6
zz^zfdmQ#wHORNq+gcIt`eqT!TX(lj4EB*yr@Ep&4t8pCi?bL;1Yf>`T4IIhM=t7HP
z`$xtR`@f?n_Gj!Xj=L7AXVE0ycRG>e)hjGQI#el#StyYmt*%umXXAAnAf{VfDb0H6
zX4h+HZ%{J%q5kRT;*K@bQds?4XlklQxKAude#$oU(UFTaK
zgra^UIBuQO@?k|C=S<-nNL`sD0|WvFEGG2g?6jVE{KXV(D_*=?Bg6kyHA%w|PBUQ~
zN~@u-b`^3c^val>&S;de5%<-W>HxXw_*?J~n95_Vx=HE@$3`)PgU*+;8`YuElF#7#
z3EZPG>UaXEay&%?lg}a?Wlp642OR1kJR6_wxxGIA)erA_`(KF2$u+xI4=ZexKHumY
zc9DvFq8tqyMf7-6d%0bd(iP=+wD0(0mi9P%IwB!_@GVov|HfI5n4T5
zEL^U(F3=eF{=3rF)`)!RI{G2Uo6MD0xbZci2HP^bwAoSiz$+(`wkBpg-dzGxS=md)
zYd*o99xM_@ZY+@~)1o0b8TdKeh!$0Lx#nPkhZnrU@Bf>>afgAV=747xL4bXjYxMW7
z^Q7lw`-L$bsdd!@g)8R55v_Muw070lVi-1AA&W
zt2+w&m`hN~+yp-Rx^~)GWPTpw4!qgdt3+&dA+Gx;MPCv|D4KcGU_cG_zg86kHH86HmdbWso3E=@mzuT{C)|;
zD@`CBYnRhx>TVw!CL-TwwJE1`FI*AT!m#UMbk9e4$E0JCc%M3vz`j`2IR(Jbyu;`&
zrY9&79n@43hoBQLM^zlCuW_$HD-f*JeRpu^#}#tSIre1lb5Y+Gv=g*~&B9hMyddpR
z&1CwmePraI(_0XtiGg*s{~z)MFi
zxOn0(CE0YtJ{>y^D}$`MI&5X7+b_zH9kWjc=^ZmFn_?d`-^s8e1c@*mIdYGee(&j93P_bYn+Wc=Sm{)&)D$)3pKY
zo2`!RtXpj5MQ(}A5NSxMB~?WhxT$dH9S#!-GB>O0WQ0U#8ByE&^Rp5ErF$+)*WZai
zgTW}*dsi2Z>C2#`?ROv?@we%gwQPMu$?7jawKTIa%`6z#dI1J2G9E)3#7=tn;cp2V
zBv~ZSZIy-0Zm~$Q$3?>!LW-3+Z7%Hrk3Yd*+Y>NS6qj3T$}*+t$>&YNd6inEZjRaj
zc5>dhybs|8Rzp>~T17WyJmhkeY4lSWZ-j8@iUTirju%MQ)-Q>Nd?2eY4A*n9N*Gq+
z3+rTw?>OMTu-S&4{`{uEg;?y*)*Rs3uKk2h9D{LKapG||j3po%OhNNi>q#ofC~UMO
zEJ^x;LVC07pMfj$y>D(>uQh)qSHf$H<^}C4bwIM=e_zI?kQi4Q
zAE{}yDj=OpP(jQb8Ej<1+|I@fr$%Pg$Qf82eNpKW<4C+UCdWd^A2e_;=Bk$dqeOln
z?>z;;nEnl_r6nu;733eYe@ugUzVS`h)K&mf!{swmpj>!NLi3~J|TM$KQotRwQYOF?X`D0)I@5F)X|^b2?q=y7X=~{7DbJQjP{yvSeU^Zc~(FSD%p-5W#x;hj_NxZO6{w)r05ik3C4Um1ZW3KKAxb2gm3KAnE@A
z(LgT0N_UdHi#nyTrLjtILc0~haE>W`IL?d;M)S=ii33lP5xo;ea>50bBs}2=1Mn5b
ztDe%kCFzMIkU9<#H0!$Z=2hL|lB2+sDc~d~QzqHW9?(m!m;~PBV3{9ZWt}&l!o)#l
zW|#z5CHB0&ujXfF?XKk0DY{~h+v#YPXjhwlPRNo%2~gPZ^`g5bZchqj`@zh+oKpn^
zG}-EY+g}#U7%@4|OY)+=5zsjDbT2l3@J6mC)2LlQ5jufLcL(IOUFa8p$
zRse_paR+MFZAN0Wg>L6DoO-1Xqi5sXXt7u+$wh@uazbOxjyF5rIgZQ%Y4WwH0rNyd
z#*2_+Tq>Om4fb=2ufilytz(UUy$@26$~e~Ao7g$b-01!|3QU6n4wy`XXfuC=jKHO`
zQ9vs=O}^eltjSYeFJkI8iT=E9n6V~n{g#@9ldbGFc)vh|=Za~8h*pGF?V@ijVa-!Q
z50r5D8Pp+EA3}or)+9O)qhES1SRSy19J
z5T4>49#GLQ!!TH;IY$>?AEiS`pCp!G91`w(GO^vdzqsU;$MWf(iZqQHDUopou*FgD
za(9`)XdEfQKpYEK$FSLbz7?YIk
zayifBn)+Mg(Mm%m&nb^&vd7$Kx6+V=PwxdmI^Pp(2rKt^?a4$yLgQHDwWIgJycq)X
z-ng2)1b5a^;8GNr94=q#Zg<{MU=C2gdb`+Fo0WK2b(m3|nAzR}5$Vw_xX`@<2Kq(j
ztVx@bt&Rm6##JtAye3=%3j4ouEyCpT>&|QPtmrCD$)S|FIpqT_
zcP_%h>u$!u?>@#|nz!N5Q+?R=!S67VQ8QX@vC&&Ovdj=v*P4SX7FymLEZC68jR1-4
zo)jcfwnrKhY;VCfPPG_+fAmY&r#k-hoJ)=ZmI4l#xREgLC}5eoW_SA$c#utpBpp^d
zVbvCP#V8{`NiQUY&}3>21+_ZNWIVC80EK;3tZIcSAzs&HERbQY<$S1CNcmAe7H_>B
zwT(mA^%q}4dZ-;EshiMvOA{Fu-IDpwiqHi>JPC=qz?;Z*P5?!dy_Kk#9~b=*6Jz_r
z1QM~$TyF1xZ}brIwOe?n+Ba1|!TaSR%<%Nc574$+p~+Pz$7D!^%Ou$yNwXHl$e9F&
zj}D^smLS6UUaY>W6${sIL$vu_7&zID`cp9J#lyCS1=Nl|2k?v449RLx~F?oZr$k81#JABfUOOP
zy~hFw*_H4vla~O5k!0i)^C64Qy{C~_QHxVOalG}@ccOmfdi?lL_MvJCjhxAZB$3fG
zCaSCvi_0}v^^&cNmB{c%5y^sve5{=kjx#i@q|`(nIWINEaDI`svC_Z!rUW
zSXY-Or+~2wBIlN8^~o2GsKg?1WIoZ535-A7NO_e2K$
zokuBc@FN}PxHIIPjWo97Q13!2*~NPD-QaOBRBPS`jW2l?rc(Xp9WomN8t^VqZO2~F
z7{c;V-sh5|z*H#UYv1;{>C9kLDZ}0CC{R%fBoo~%&oXVz{E}!$y`#|Y-lVB>#^&T4
z1NWGN_c~Y!iVSCSd{z<{&+S)WMs_DwtztojHD1+Zu5AZKF>o}Cjqh862R{2=GogQF
z)5lPM(>pmy)6;=<%4G$P1bkRSP5LH{mt8nf#225@3N?9#DxGJodWT`ljlBt1wQ7Iy
ziT6t7H24zh%zc6C?<9kjyXsW)DZa$Hc_;gve!eN%6GdY~=zHw|L(Sih8-M!!+-9f>
zqkRJy8;IkFpL+z|uji47`zbA9uXC(Lo21HPtG%wiiUbG{I7rHIab;wWm1gi+8*52M
zu4%g4YikXGeuX#4WiyP!RBzrZHNi+u*f6~##ujBtW^jAIv=FHh5&{qdbUy$-B$o5h3KdJW&YX8*D`4V8*ehUDj)$3
ziMPqiX$aWZp2a}t7!pG0ank0<}=7#h|$AQWR=82MAigA)^TF=b6W
zcll$sM*u`Bohqty;xEECvX`KOyToxwROd6ZUg>S#LGnuK;Y4su*?8x%X{u
zw0SP{%e7KR2TZP&p1y|KsW9%@4C5{s3?jfF^^5?8k{C5*0ENx@?35{&VBIX~ihzRD
zH;Tf_4HpSu^x+@P#QM*Vz>Jl0m;hGVN!Zx1Sl40(0UcW*x&jRWm8Y`_QAd;%!~Ek>HCI6oZ!0{FVejv2ix
zde>}TbE^5SvR9n~o(%b_SHxAoQJ_>5Fk873M+iJv6^jf?^;^KhPN14F#|>e1pN-5r
zzTbys=9*ls%EmHyP0WhEYR^f8&HU(HBcP*+*%-nK2=ITTrV6!7!)R`AMgQp#I(D4G
zrjNcAn?JN3y$7DOh7f&mJEo
zFj*SG5`^)`6!V#1x94ftoEo6va^I_1uxg0GanItm-}xM>SKNrhFA&50RS;i{y7{K%d}jmf0Bw^zZtOz%LDAXXq=Y9760-aDr;
zBejm=pIqzXXKs#wt6OU)F{wEHwUW?mb!|`fru9M#Wk`~a7%7LzTV;3>5djIsG|}$P
znnd-Hgv9E`$-EZZ1dEPt5OwdGM2bnz=iRAd2I9Zo>S4vJD2t$)|A7Z
ze>s5MXcKPxl`RN{#q9SW+g*=9Yz@WVbw~~~7+a&ff2ek%ipMgvsnCb_vW{{W&R5?6
z-|%*-bzWtI8HqF6%4=3qqBdA(P1JDsh5+(={Ks9}LSpv_Mi)=f`xs3^=wvkl?{YD}p
z@FCElPaqgFdw!YmdTw6R{3c*S;7#DJW0xESra}P+Or}Du8Qp6|nyKUf4atViF~k{V
zIcFahEkZOL)?<9aQbBV_hb#G$ltsAqk3?pI5HbM3H(jtr|a``SNxIe8X6G=ey9fss@Mt;s2m<
z+e2u*ZXM3Hz{$)Z(pta{t6*#FI+VCT0_AUzgfWY$5B6JkDN
z%vumU&l~4@+*k6>A^d
zh}Ii!#?q}DkQ^Ao4}a+kSakDE+|4zFzK#rnRka94=ut$#f1spJ$3bkf<
zuU_bJU{VnO&M?op`nYIOE?^>6nempPj$D!jD?t&Z1TGYwgfQtZ#{bjDn
zkW@&(FmVbWILR6biFEBcYbI_%7(O+Mj+eV|Z2Nwk
z+%b&7qj|L5w+=l=I&uBG??a4{WMBE#-ynQ?7eetkF-08J6l63qc-zp)5rP~Byo%7k
zl6BAJSbute*!rp=Y7@V;W3V<6OrPei!5uusW4QnuX5n$yLc{rlNv56zT`7jTm(;}U
zn=?5B^B$R3-|SmtM&Oqk3<(T$RpHD5y8UXm#0h;k*V}wKU?Sk1VeAqw1igL-2|@xv
z8TDGVj^1C-*jq-fdM74=bJ<{xt99l&wOsuxNdX5;DoAYyOvZ^_PGBOVJx!jHP*^Y_
zqf$m?)vnGmQZy4NnK7A+&g-`YFV?`-p0g4X)i_I$woP`4JU;mw4_^`rl*u
z%_~sVoX6QG{vLJ9dC$wc5n-7zo=Xo?_s>^j88&|
zou7LFX+IzX&L&&>vmIp4P()dJotS-eF4>a#c7bgl*$;p22=_VKfU}3lU;{i?*xP8n
zwD!`#L_jrbJaDY%T7c@9CelD2ZQQCso;g(I$Ro!ljUT=*8hCuCJV|2VqWf|bm@)+%
zFqtyRrvHGLmy?)S`iU0hl9)(@oI4lgRg)(7$SH`TrQ)%YkO}>0__Cdh1uC>C9#`kc
zjzE$V&AD1BDvE5upwD6(p4YSb5Em5GJKk?|@)CcDCxm4fo}AB`I#6puBiUR!i+vAw
zBAKB_NW&sDtXzaJHwGLzy#OPrr0s@wq;~GbJzx42thl!g&;Rw8(AwrhG(vaQ
zUa4*}QvT)tpTd4GT?VEW0ybE*S2irrS=F9XTbNpXPL$bq9D|YZKAHm!DXNN>
z1}1u(2H&(NQLsP`YC(K@j|~~MrLIXuqp@zry8*#^Z#-~r0e(@Ha5p&$OhN%qmOaTO
zcgj&LF@W0%+VwH{M)5YPPl_%^TBe
zH{jTdG%#cs`9>PI`e|es4Dp%NVS3K|UW1SOfI5k!r?7ge4sk2N}x#
zBqaf~UkOpFqUf3ftgUi0h5_f&pkUUDPSf>@)3!aJ*KZoH4yMTZxq#)4A@*0n2?PI
zS!_WJtwtFzV$?J`&)0;<*2vWYo3b^$z~i$ikGcEMu$sVu|FTJWNq7u0fKXkGHY$vv
z!E@DH@4(2YfB^y3f~CY41SC>H$@55U%@L%S52Yuh1@ba#B*4lsRtZDT`_hN$GNiWg
zqfq?1N1(()g6`yhwWH7|9?jmw
zI!ao?pmLd^Hwe;JA)O~!8==~Z@jK>36G}{6PfaPwrnYO)d!>DHWN(#D(OZk-j&F~7
zFG8WZbAUweT88`V=s9m8P~xh&?B+QI959*Z9eiyzPwE#2hvKy91xT|oMy%nhCR{O2
z#aIx>u__uf&+=HC^~t70b|)UN(LFq4T#CyKOeX0tZ-13>P@?93e3F_)s
z5_1WVibm6^=fpNnJZ4_X(=>m!(O_ab{e?Is93MuqC|6;|HKBS(Nzz3D54~r3RPjAy
zU!Ut+zB|SqWj43ZG{gOwr4(?$WR`Y}tJd@>kfmxv>KFkIBaSA+u}obPrN9X1T3+)&
zi-3w<5lW&WwTGUQ%dr57eoHj1Z@n(l;!-NIEXk7y6*yg?4A#*AkMW=8y4q1tSDk;q
z=t2f$GLzcEYoop`y-ZH#G=FD?_>;6z&^f7wxSKL`EO3*i8YH)e*QE-htFfPMMDz^G
zM;LvU2Dru>$Vi|Bh?L=MVP33<>OXdv^<7NWmg2audY4U>Ru@YJtV&NRMI}W|%^Mw^
z&CmcIUEsKr1R|Xh_?wVOe;dwTw7**Pq30U=$`6~lek8VI7>HXffT8Y
z1YqRVGsYc{6OyKo*+<$4QfRMY_T!;+$CKEM=X7m?Yl;jCUq7_J?A#QX%
z^3ml4D70g_Bv_iHoN}Gw81N-oM*`xTjZVzT#4Jfj@Keu4xV=cakN
znBJZE{H2Zs+(V&yB$GY#aAOP?MS3Oh%uvl2rsPX1oJ?kz?@hIRci2{4w*l5Bw(C+Q
zxbuzzm!^ONCYPp-yE3OKz{%05WN7Hh2~&(g)3%j_h)LKm^E&6nDUng1t{Y}UV?O~F
zyOS^(u1m#Hz(_#6TiFK+=Ejir`A7>;wU(gdG`)e2($MyK0)Z7YhuK7cvY7l?Ojn;u
zCvkK3FJrtQ+e2WrlE%HXi81ZC44Y?t<&tjP(hb01y?HJet~O&!V8oL=833a8=|1M0
zO3;P7Fdi*<@zjS>gRmNuz>E!@G62Wk$Gmw#`_&kuZ572&m|Gy!GLrD_QSR-;%^#Q2
zdnlt~cx~oMB~bE1;F#k3<3%@C82W665nTyXBGE;>L#5fag~HmpKuOQKT#yF46AS)(fj5}33ZBRBYewQxY+Er-nYwZRk9kC$BPSB;A
z?yoj&%3r=1mG==jNmFC*Lgvrab-sEH@(p(bvD>INp{w)o)9}UL;VnqneQ
zRWP|MB|37OHr_{8#dlFg1Q;3@;TQwfDSWR1B?Z9C<6ww(7y>rtJjaf}3hhV&RN*K^
zt6LF{()LC`g()1Nii_=5L{%f9E1b7+c+4&4j{(v_dOQ`ROEnDmHkJuu_Zc7Fy`&%a
zH6?L2!}3c;Le-CLWxeDAtm8;*A;Ab^;TTY*Q>2mUY76=Wnq-^hlB2*(Dd2#~OeyG|
zpC$!LHiKSNzKx|IVW1t}5=)z?ul&~P)qc48EHFaP-o(3KldwyrqJZIC%uGz0j7*F-
zK*PwIjmX#DMUsf)_>}4sdsB8yCkb!%FXDYhqM}CSN|_ee+OKl^ngU{FykS
z+;%`}n*+TeG}I2_AAf2$KL6nV!MoO;L9#yx0hXC3t8$VGN8P6PNp@MOq*YG|wCf#`
zSDT{1YU4hEej^#0dEqVxM*%oh&J_niDH$m9DrI(8th=4miny(Uw}=4JyFXs1myL-E;2%z`rY6SDL|z99t|riOF;+
z_=a~-y|df&SH6~pr>1Aj9cBKbaX?9qq$L@~a|zIMC?siy>UW73mE%SLB01Vq7%LOU
zjK)`?Z5&53c$J~3+-xPelcZ&Aq?6CfW6X{-tMXAE)&|OqGmX(g@d5;~swPy?kofhu
z02FY2;~0MT&O^9=^=>Sz=|PmvR!gdx-!Q9Bt=P;{VZ6)EhVP2C8zG4ZjI5ni9w|nF
zl~Hdl7e|_)PM&L$3|&5&?96NCa=11q;DE_x`dO2V-JI$YlhkFNz4(%58(OQIQ0dA~
zzssEz$3yW2W?#~ZvRp}O8s)D^)G`XKXf4x_9f@Xr!x#dpgN*j%Ibp@swqM5kTOdb_
z!~&Oc)%Y6CS7Lt|%hujXaPl(2$s5E8F?z+UB|$3jeu(ZI3sUiAY4{qxj;|=aaR@{;
zUOX_W9dtwC8cIWGhhplb@uRULFu}3Gu@kFVgbdw&6QkYA)I6U`4k9Y42|W7kw<+J87#NeZG^E+$cx!^py_r`{P)Y7z?761XrrvT-B6
z7}VQm-Csw6sZqcibyIulnz-ApU{WcHWo2|`CZw4+T$7!*TAF!om&-vDUjR|+g_SBb
z&0nrY-Zf2a)~2I8wX)I?cfqfzWmN-pTB6J5e<<&&ySY=dFIvUgFrCXor#4kSa*
z836==4=qqMX5=0#)z8H7D=;C0Q2SnMF(3~XMM@1Cj~v~rkJLDkSTj-v;Lzxrc_+sD
z$>+KE3~f-t(MAJofn4WzNq#)cPz*R}-d<1b+-CMXOS_m6>_qr20nqhMt9Q
zbPrvQ>EU6FP|G=5R5rOhW3QNKxj}M$Nd#D=0Eo{=psJr_#%Y2G9ulxLQgtyx>4;Tp
zgu=D7y%;2*=;D3MtFZ7ez?Sodo>dZ~m(i;vZ`Rf%h79AFaG7GBQ3!WGL3F(@i;j^L
z22(sA$>ERR`X=7Kb~lzcbkMMsJ5h7bB)?C<+D7`#=PiKF=P9jFy-jJQ`J?wh!QZ69
zi5OLAkiaCtcPL`^#*vE0GmKS<7J%8nhNd#6Hs$QU(zMg%GW#gtfXQY0#J*Dc>t3$Q
zkZIOkGBzgcGXpd@feV~T+-%OXE3LN;`4QD>-mw}n@mS(&JI!0knI>JW7_YS?P*Tm^
zj}IPF7<%B=Emi1?r^z4UaFN=B-T0HqXEI~jW^0gx=EAx%cg
zlBPkZq$7%cnx<=TAWSL{bxdlPVpns^LYGE(9DpugTp`@OD*e@4VLjRuPmOI+((*{Y)!zB*$0&Y%j2E
z0oE<1mxfRa&uJ56kCKzqhw(MYtc`SvF18`kU#bx+t>P1g`}3QOJ0EL@FV#uoi30>G
zwB!tv;g;1~hn3fUK-#d-*p~>9+Wr@yRRmXo4%v?goP=YI=c2gkcyw61I8zlv;42#u
zf!{O@LuGdoqARdr(nc+)xF;z}P3n0c_n09dl2L0aeg=N3a`tAHp|1W6{$%Su+`Hx_
ztZhC+bqxVQj&!Eb#YdHm3A}p&fpt#wA4yVT!oE`NOO>SJl3L17+Q|RzD>n^s?S&(QSgr89
zA@5D1=MyF;^$#ahO~~hx#PbD^a_-zUa!?l|(-|1V{rB9Dciy!GAOFZ5_~%Dnz@Pq4
zdaT5WK>lRTGKn^)Zp$CbqKZaRPB#{Lnlsr&1Dh6kbKnWn}LMs4iY&aw}VXPCx
z)-vCx;2S$Zl@2!uJjeJZ=2(Jb)m3}gWa-YL%?DLG)=e=)1Te&SH5M#DsWsn(xaPhb
zr9TQOACA=(K&b){Rxgk
ze*#~)>jm7m`Z#S!UIb&N@foQV$=C}jo5@;hsOisOfEuTr-2~SSc_d1POjutUy9z*w
z7E<3RrPGS`DiscQycVD&jciOHyPu9|D9;?7m(j4kU2+tdDh0e*daBR4`yB;lH3h^>
zEOXHYYGO3jW5KFxBq0&=l&y)}IITgfadqSsP55>~)%UfXvPm)M1?YR_DTJz?#H}~|
z8P@F^*oTYhE2A^VB1P1DZY{t6J
zN~Iwn5UO2+EcdO+CtoLrUO~wT4Y=7)>a!~+ukWsWr@e#s=j3PGN=8cQ06*3fk0IG9
zEs-ilDO_Dgsm~~qi9S0tDEZNSRw^gPExRU;B{A)SWI=I2u1Lrs)0dHGivjiuMkr~c0GIm9Q%|WM$2|arj;7iVrfKevqky4+114@D
z%sUDQU<3kOjhmKn@}wGvLU=BY8BUw4;Q+eKXMv9V*xzWa2h{8(}1rXqSRq9gLT#12ZsB%
zWXL$BjyR83GC$8Q!*-8W)`~L|GU^@)G13_tnue(=NK&1{@b%%^cCkQ8Ca$!h9rGrG
z^!l=>hH^~EkQ9DON)&pXhOFl$p#g&2v3QpmPXa5Fh{)ho081(=*;U90Hb~V?Vx)_)
zO3tFHc8QU^NWD{4(@OOfQ2EYL*S*^Ka?K18a9+W&Hs|pIokruD`Th@eLQo
zt*wRV;b}usG>BDq1cM$$1rkz-C)VNRBaPU&FpGhb*q5-$EJ(mANV}V8jG^aQ5z$9L
zo{Ah}5UU!7=*ldY;qj!mP9Lw1<5%2~qd>6~aKNNkO1b;y6a}ngMEl^-jg#VcKUEEO
zb?XJe(!^;2$;Hf!ld@L7Tm{=nc1|_Dw3QW)2~p~lc>wZ+}n8yAK!dCzV|{B|LZ7?Rw)%gAdB`oKRr5fM#Z3gT;vw(t-=K$RMI+I;6p2SF%^lMC)kegy6(6*O?K|$yRL;Z{vag=
z9V4h(|C@|0LNAn&5#%VvNFDtjppyuKYd?Wp%LX)ce+El#U5i^E+KgY@@rQWg@ZI?S
z!9{rNAf*x67}{$hXv+6fNs*?9iKJPgG3if+m!W+Itw70Bt^J11L-q-=HMBK(5}9F2
z6>6?$KMG*Ven@H^n`BS)|FicV0CpVLmFRgnPRud%4ukE!d+uv(TmShD}S(LRa7)Pu+X&Ev;Eoq?JX$Ko+qpD$Y5nUBBJM_Fw3P6CN-?iCL`Zh^@x1Y17(=MQfx#r+y*7b6J5J)R)&D{c
ziM_K5#TWsElCd$$hmCM}BgqtVl+Z1fNt3(6@TYhzkCqm#;wunjFG>2|(fLKX$szr0S%~jP}S|N;olCDj@enWUAtWs$}FxyRr@%z;foa;e_Ej>
z!7z`=&9(T#pM4(p-&%z_&hm3KW{mvs3&@@MD}=ZI3WG)ig3n{h!0;=TCP}B-ntyT1~tQ6`eLPv5`
zDD)5)`^y}KC)Z0L&_GhG4Kvn^Ui~PJhmNs*%Kb+{i4L|!{14Tsq-f)RR~_CGbE}Z(
zS@J|_6<|hFhV!bZK3QMHBQbUJmQZdI!&joqs7MQf?vlz#wTaV_9b*7d9O=vB4XY00
z_is6iYnwlVRvJdq1Z(*s38MmB9_BdV?Wbn4Nf;XPV`x}O(@NxOFv%`TKy($9saM_SpPC^UjzK+;4S$4D-C$^#2P$5NNr3@!TLZEH9;>1a&+~Gsw{$O
zdI&{kWA&zcSpP7&Bf|s|1-dtw2ZiC^j&c;BG)0PE;ot@U&>F%%HfiOlEJfUuE>(Z)
zUeFhT6HyMH%&NPK#-!K
zmy4DpwbNeGM1M=ADA&eG;yZ`$!r$$y!?yY&hAi!5qTOeHRj&e&`0@RMV9M^%7V(ko|GyOJfP8?!%?Mn#l`UJvjb|HKG_aHBd
zH?|(Bv!6%*A)sdS{fKS-aSVLt2^9JcAvN4XAVDBVGf7Q&gs*3?sR8)eT^>Apau@#n
z)E4~h$t;c>45PBqi}o-r73nS#Eg9)ju@1Rp8$1O@N9Q^SyUX?oJgA{xtqdW4fwFG5}!X{EE(RyAoG;50_Ea}uQOJO&LM_3
z7x{gFuQbow*u{$sVqHZLKM_BQwspg}Yts*LUGotHeTrHM(n>-XA(#}6vXi?;uQc5>
zLqk5}vWY|rXllw3nAr2nI+A141xTg_B?L3RB#!n~;dh_ifVxWNkKuK&mQgv({(~G`
z0&_~kFMt@KUMia2zTm9>U?PX9^AiW58l?j
z701r6$3Gmb#J7(!RG8pkbsd#(sFo?+8)*4>m33Up=o#`!M$U2e9yh7p#s3iTHB5r4
zI3QZHh8s{|zL669I7kJn)b9Z;p2wUdCM&BfXyS|TrHGdSdXkD!USpnSj)Xu7XZ)Ta
zrpISuxrPMpXgZJe)e)?U4WceWOF=M$cqGMWqa$c%w$@r24TN`|NaKNc(w|gYjPMS
z>Gd*)PF&%nTPXw9s8C^|67p>LZUDUwuz+5i92EmzjO9_le{Nf0PT>AokrHr$$rRl_
z_L%9D9@eE))aJ1(n6H-077^xYt7NvGr<0Z#M(GTSI%g@!oF#chJ8?Ih7^=FL>C~Cw
zl)RJZU4M&Q_xDkE;%`Vm;kqyB5YG2&!M6Zv$eRdtl2YU7n?j8>x~5JpQQ(AInaZFOhS
zUYWq^$~Yncf+_EDx>vGXJ$H~qQ{ogFOq2vo)(vx?@sEH=hSxxQ2k${{Fo2j(9`Si0wv;&6zM)Al&AOvN+bbYrwd4oAlyH5DghUmOd(GD=;?2HB${I1#rBg1kog8C
z4k$T9Yd3oN;vhcf#knl!U5v>J-o|EO5uQzVbT3;XbVLw>FdL!8M*RG))Dsm&tPXM2
zR5Q@4byUJ;5Y}h6NCL_QqGDVg6tALCe16LDR0>~DJ%nA?-i!3f|HG8=)cK7!qu8<;
z;Zt9RZ{UzpmylPV(l$$HiTHr^2pK^fsQCZ2bS4NvKx;yTmMP^mpgPH(osj&fCHbzht?mMxaIB*!ueg`qDVB@`%mZH0VP3#k}vPS4ZrmGMr^GmarP>K65A#Uz!WxG!r)819_#iL;nn_bT|8sg
zUp~lvb0sh<33%4L<|FQ1HHio{yA#Q4z7_l=&AIZ;96Yqb>3o$S$0~u64{+VJAKjqz
z+ICEv)AMUU)CgaG*CJv09x4~Q4Lr1um$G!>#7TPW?}wMxl46RXpqx!3$wlNU;`qin
zFCOJ<4fSA-_1H*>t1;)C)2p7R%BTWK7>&eROxSHY*+Vf~UQU3Pg$yHC+C2mK^`Gm-
zuBwx$58R6WnjO1E17&O
zlz=~&LuE9JXy62@!^hCT+rNsqMWj-vs6I7I?@pl|fE#$NY^+x7<=w%T4l;Z48q6=w
z?w7plTD5CGT{a`bUWN~o%u6@Rc$dv32Tfyx&MHfZ14`ca)$6gP!iS{Brg4_PX;gK|
zE+Vfw>)KrMqb^4O)rWF@@qE_hsYj2pGQ}oTE6@TK2q>CKvT}vNnQMgBsMG>PS8{nwx%>3r6iA%j6EkdWPr8bWT
z-2@Dc6?n@t0;MW2i08UIcy(h}bv72Id60
zJ$VxEzF`#&WK9V9Pw{mQF}hePc}vLBp?YLIv+v3YLe~z#vgn*vak5bY5iG`CH3~kg
z<;HAhn)m4xox^}Ho>vLcdV7N$FN!3!lJmm6BD&=+242&t*V#u_CKE3Dte6=hH4t3{MOj8@2`Qy`GG7&=HpnX(A&(#!L2e#UiDn#q37NmAIy^
zh)*9`jR*GKgq^pB^*v7h(-0P&-SJsUJ
zc_V4Mg7mCfV53@SaUtl?S~8Y+%T04!5fG5W_Kbf(QXfnwFB0K1Pz#g2I`-zlk$y=9%zD
zm{=>xh>;;Kz(%Fzt*m7LC$$MRpj97JT*M0dNlM+XP+Uf^Xt6x2*`^fDml(JO%8n)=VBlr*iVTL!}XGFqA
zL7LXi%b#K9BI)Z3(rr`VwUFq`^BZdo&wg?Hr`sU8ZphxIWiO2r17ZCA^AF+=o@v2W
zad_EB%J@1T#hH*@9(cHbWIp<1rJX~gyjxreEFB4G_QoVy9=RBl1ypIxll^=rot1ej
ziU}&!#2|_TUGNaRXkP#miArOKUlPIlDQ*`A`+r?xhWrbg@f
z5h$5yk=sUQy#Pr$US9@J9=iem;a?xYAM9zz79~()<+J6KTuIxErO~)44xVA~5uM@tOjVLi{
zqm-ipi&deN7BMn_Iv)qw5<^Yhf%cNlH70xJ;1Fz6n6F9gPf!tLrc-eV#61be8qB&e
z70gfa#YYQNq#Ll~*M0FT2OgydMcMN}rTo|qpN!Jqni#dcYD&P**N
zMOvV^>J7OvT;7mC?pS{VKJ&tz_=CM|#MEAFB@budsz%>TN01l7c-<#1H)}d_bN894
z1YBS;Q^~od#?qm^9Iz90U&HolV_OMXeZ
zvJKx((I(CrDV;xVaU>wq#i(YL5-O3Z!~7)7j+#z(Gw%k$9ajL>D!v7-G%LW8O&74?
z4nO|+uSu3|JBaIV_2Hh>_fhORhFoA6L8fm{dFoN0?Z%d#FA{{vI9o)HYpc`@zii2|
za;WiiI=x;o5@AE3G|qPiapp`2jkH7rnD;{@($VHQZC(WLrrm7EOxk8FMhuUR_%n1y
zCD1w8hVLHTieKHmj!Oqcur6jpsjpU^lFNy2415Trj#m?8?l1$dU8Va%z;d}d-QQga
zOjiQq@3`q685;PMhED;vJes7yK73zYCBYlsN1UgoNhgI}&3Pn2Pcw5AkDfK>+0
z0@I<3kKk)u&-_{$Y~&gvQoL0hY?+YL7N?OHe=q+${!OtPN}1v8I{vq%_gOPvt{cQ8
zWkmuH`M7LIC{vaxAsLH9hSm09NR|_>g~NsOEGf1c>tEA_ul&^r!o6*%+k7*w$?e8>
zH~kLc$s^c){L>hyxDI{QccXgfIRv?uN{}eSL!QV)Mb;57T^X%hRky$2Z(KXg%^5_=
zM;RaPJJGR=eVZIkVh9`86J1eq*_7yxdZniUdr$Ae&+lnPshg_=*Dw)XoB)pZWU!mEFDMU_~B|EuaIJ$BT#On
zvu?a5_liB0ZJi6tnW98)dhzR8u1}J-AMI~T_OI9(T9(?H=b=Z6U&G^(!wYaYHS^ho&6P1~1R(d~s2NyT&_dObOl
zww^&{c>dgGeE-!QTxq!$FP@BI9lenOx`s@8`9*b|<_dTb;~!*a+fwTdx4lb60&aq}
zrP5PNpq())?fpzE@>^ShU%HvO9Y_Wrxf|1!2AIUHx8TF?AIe}Yv&m(b&G>8uA@Cs}A{UNRv@J>(Se}3-%LpBRkPGM$
zpoAjz2Auc^W{N}%VWQCUz1=up5yoF`dkVMQT8XD_e-B=_`~O9K-;>COD%b}}f)R2m
zn6{lAgg_>Z93yu~7UG|=bL!l3lPHU7HtQicQRGVz;&dz}a3ZM&gll<^_49V^Ph5jN
zXKL}KS61O0=b36hegx?4sj4ZB)vmghpY=%R5U4*equ1q^FIdM&`NAEDY((jI^(!fp52>S
zzBHSPwmG}$5WSR+baWdxC^4J12MK0qSuvmcnUqbShi)NRUyx+jY;6P1^pw!{S4CXk
z2Ar3nVc8Vx)=}nH>(;RsCVoLMQfqRXC?$C)Tx%kCLR>UYzsap*0)PoP@`*Fdwp)Rt
zM^m`#{vuxUj%)Bp>K_p&h^!pg$LJhdV_0p0p~XI~7JPj5htPKBOSEJiAz;xjPN$3s
zRn!!vcpn~SdUp~*oo*f5V{v|x>&DNBq25#w&zyKI9(%PGpM9D9CQlH4h8H&z7%L&6
zjC78nZoaQ%I>5xb_Uyee-|chHw{j)mx^h---_QA#OSNP;&~Buqqk@T8FI6*BpHl0T
z$U~Fj@n)tuWd#%b=x8nHU8hrpMjh%ittJXn)%MZC-@Wpgv(QRX7+}s5CTRl~XY&vt
zl@dvkX`|#gq!H{s#|7eQ*f=HgvWG06zmyAQRU*;VCe)(T2o|(9N^1Cw;g(2_BEu>x
zY#>z0-+jb%y#TyGL%+syBFl=73lfcPUhl!P&v5a&zdelHhj(Dxo7dp&z5j;u^*4~{
z=|kiBJ<9$L)6o#{002M$NklI-rkw?ri1t8Tyhqf4Z%ZZnsF1O!er
z-ljP4iidF}FS$liJh+#Yyl*|w6t}p-F@((Q^3@`zaBk197rZzn*Y{G2E}txyqpzr?
z`-W@paHCfVHDt1(MMZFiHUTCWtHdW>02hE;3W~+K?#sUml!#*zpsPlq!ohGgubELe
z1S)=pVC#1iLQFYm7G0uwKc5{YC?P2}TxqNfOuE7_#o2Nq~vd@}&j@b=N7cNo9zjDw@MTo@z&sfFySb
zFm|qR{@=&a%ujG`30x0`TU-e&fCOBN$pQ%7J>?>i0B3_mx@eRW#T#V;wxJ%Tx=%7H
zf>sd`95^_8xjM1Qo1$cGBP7>~SR06$-xkSL%tc
zab
z{POLM=(sh88|fiiQ$2**frH2r$S9+!A=UD)psmu$^F(uUZ3|V2R!&amJ=oR0AMfnA
z7N0rMh*exm$8eh#s!`KDuM0}1D=Bxss~`avm|O*+x>YU}3D8tc#Y#=e1`~uZ!ly*#
zt1PleaV9U<2CXRGiaHbsNC;J^GZ(%6edfX^5Ys=oYbX>0P9%dIEhFJrqv4r&xqxD7
z_^h#-c)28ipCC<~lzfg>B_(C^GVQ$bo={pD5tIoqyaXX@v!~#XhwzEr4CnrfVch+`
z`*Cm60Iua)Jl8evXieu4*CR0L281TCRx9cAO?!gc*d
z0$y5H3dtUFCkkA;sA&|e1cNo)Pdt?@SNJ7K)*Ht)H4I6B*2Ri?4rUS4czd;#yjb~8SGunp_!+DU6^
zgvDOUKvjL{x^)(NXDzSxxxi$3^{0C^S1JKoKx8GL*0gRK=I1C9q?E`j@l`j%r_8Z^
zB!7~Bfl!QjQ+R^B_P^2-E%yCsF`~soa?n!uBDVU1yk=giEFy`44&rl)fvp&?+htbGI`ErE-9vg_G
z*uRnCTqG88zuRj1aZB|++|=yBriN~`R6NB6{A91XFr;{~OH}O>Hz{8Xp^Ds^cWgh7
zPrpE`3FGQT`K4$VabbQN6V|PE5{5FGXtB3=5ew68+7(K`rCeX3z}(%hz63O2oBSt=
z+zVi(9K)
zaVIIFM!b>$R~Igl(CVe-C=zcm))BdHf{{9ck2(a!HxY=Vbwn!gFeI<
zX|%Wt!3ceDUT?^}FS{FlaSMONx6ZYgtoU1C@d&^fs)d%7oUO=dWr`MjtB|gwolZ?t
z97D~dl=dV}nDF2ttpHkI2fhlF>NfDC1SWK06bpPrN=$uES5CNcHIu4w=AI@GrRwS^
zu_;=7)qj#@M0reP*(fj>U54%%aZHp@O^T^hq{WE8350~1$7D1S8%2>$bTJWIy9pzf
z3rNIS6@eI&7lraRQ9ZPnl$erTKuDxn6@i63fA@GoVw5mcynq`T>+wuD!8Kz}VVI}S
z(w!3tG1(i*qXLClEo>*SIL%_gj0YjEy`u^k8O}9;Ea}$IWstSq$NV-K=1fuA__-{)
zQ}VPLiAQtS`os9MS87e8J*ted=GW!+
z$78~U6>%f?i?MD9C@^P<_#|ePVFC>iT}6g9925dOC16u#SaDATI1CVx>ErNP%{MerE1EbA5MA@i8)A00;`bq;GH=W*Za1BlZK)14@wKbOXxYk}K0
z^rOCl3kfpfWjc3<`kAR`OgY^b!ojl@c=nXCu__4~ug{;ymIe=QTc5=0x+MFWwO)k|
z79lv9FW~3D_yK&as{*Tobn{$=^VloB{NKwbSi5{3?jEiLu7(6$i^
zd04-NcI5Yv7^>eSU_S}Rx!z|`NqllRwHY70r4~PT>l6HC1jFexPWFcJ-0>>>(myxh
z!}pC~N4p}YbRFZbVeD%3Nlg60G3ZCep<*qlaXG>u50fI((w?HY*S
zLtm`MPv0EHy<2-|J-M)+x%j4dt_m8G-hab>eC><3!5?BSnB{OY&4qB>I$Q~ilYk3M
z#tFmyxOgNm8{IFRA|)`>Qr6j*96s6hOLkoB8>p>~<$?;msC*