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. Screenshot 2024-10-09 at 8 38 24 AM 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 image #### Stack Management > Data view image #### If User does not have privilege to create a Data View image ### 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 Data view illustration; + return ( + Data view illustration + ); }; 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 ( + ES|QL illustration + ); +}; 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 Screenshot 2024-09-30 at 11 43 16 PM ### 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) => ( +

+ +
+ ), +})); + 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 image image image --- .../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)|Screenshot 2024-10-09 at 12 52 48| --- .../.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 Screenshot 2024-10-07 at 17 01 17 ### User with a valid license (or Trial), but no ML privileges Screenshot 2024-10-07 at 17 03 48 ### User with a valid license (or Trial) and only Read ML privileges Screenshot 2024-10-07 at 17 04 21 ### User with a valid license (or Trial) and All ML privileges, which are the requirements to work with ML Logs features Screenshot 2024-10-07 at 17 04 52 --------- 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 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≧cyL3ADuVDC5UQB;*_fCcrl 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

$JcxZ z^C-3n$GLFxM%sw+V~$V#wsB#V8SFWW?kM^%qd!dlHwDP-=tgO$1^0wE;rhnMaM1Qi ze7)d7*jd!d_75U2_!UE8wkh{f^p!kj7>o7yHK9N7YnDbg!$zG%$^O8o83o4nc1>CJ z(gjT<1q4bGWY33sn>g_@Dr=0&c;uXzoP!}|vWYDG0(k`}U6J@ZiQ=enU z=~w;O_sjuWhR~#O+cE&qnqWjhI2vglHt;3H#TaZQaGu9B*f&BJSk)k*5Y zXunm1PUb<;-tN!Bd+f__%>Pf6S3QIq8vYu|(!23l->-3Oc^j;2ci~6L8ob?2B_=ZO zPPjIp&hs$+owTqWicKRgAIfiRNi=lR--)j}hk%P9SqwsHq3^z?7AnC2`f zIZbdv&z;&Wd&?Mxo|CM$TACI4>{N?#OoCEhK?-+%58hqcLuJR~JS||t)#!prek$n9 ziaHMMIZF|Dt%NgDhop;^29QsZRDz48(-rRh$ zwr8J6N4t?;f(bG;=%g@+vQrCQE6Fvnq><$@D2?ZHwTMkNp&j7TUo`5r&?EVl3_t*x=6$ z;vd!@#wV`+Ph3%Yl6pm4P!pNVH^ln{oz6=d(i{rzA~zRs3U?GNPmLH;Vgm@kGRP^U zH8YZ{{+z&!AeRw{lYyOB6Zj8~l~r)X+OeeLViv+m`GnGLwY7T z{Ai4_f@4fFDVG0{zSw{Z`7yVPwcxYtK7+Glr#_3p*cn_k9kcl=GeHMxvg8?>IjKm) z^q_4i#5cAtLm>nDrG1%UizWQrT{@kB-D;@pqVV<0Uq%gPRRf$ z;g-?mErp$W7$zv0>`Glc{|L=Mz3v3Qvu}~%+T8T~aH=uv{GlpLym|%wZ4lwC-?hG41XjE(&ovwI=tj>K_M6P66%^fl3!`)rrgN; z#q$X@hwnxFr!!7(W&PR>gZ^paDTk@X><ygD05=>#wMlUo)0$5TJ{kn?NIc1W?6h>dy1WwsT1pMYfO*Gb z=@V%ROb`Xq9Fqwm+u+5AI$CN~A}UOvb1jo5kH`jN+Sjf)bfMo-=#t`6)CP zKA&c8OF#C9T)4iZixFg7k)YeojEk_6WvLO&EWq-X29&gY2N72(Ji#|Guk|(R43wZU z+Jfc<3-I#K6DXl)KI*&@j93tAiy{&TV9u3uNF$m=%5ArjzAocW20?)Qjm#mZOwoY@ z7Mm*g~+TP1B!U_L89EmT%5WFAIvMgGpA zf0-isGx5jbc<>*8j=ukU3gs&@aWY5;ZIp&QG^Yb!yJ-{Nv*dXcd%6urUdeufk)b#w z>W5U~+9@3jJ3)+xi>XXr4N=9gC?@sb#lFc=wC2Iu-it)hBPh-7<#^bSTK6iH#eRbJ z%p2h+7iAadIW=<`aw|TLjE;XYB^!z+8I`pTMTka>ry*DOi&en9u6FQozIrtECD0~F zi3b(?hUdnxZ8W$h6q`AHrd#}Xa7|Py$G3E>^d0lR(<`msDT%9Wwk~qB%J72&WhmpO zATZy5KK5^g*Oa=jlnWlVwI&C z(a0c)Mv}F5OI5v9byan(xi2qY&hK~b`!ch#GP9Phtjg;8Zg5x zw!E?w00`N^Atzt?@ClCIeli~a>)#%IBWzRF8CDJP3J<8iIPF=)Y>RQf%cEFLJU;%b zEbDyl&iyQ9c+x3Ki{-EP?AsULKXOm}-{+o+!#KD;%oPY9IeR=lz;2%R4Id=d!T7U< zi+S7dDK6Li=EuGg@sFR6{qG%%&p+RZcOShJKl;Y6#&!N&( zfoACim^1*CH?@5_Cn81<#mr}a0dW8OJA9U5Jn{SlhoZ66qJ(yETx%dN zuq9R#CM8~cQ{}xnJEiR)q=;@TG^vN?=K&Es4)5LVY0!p0WZ9gI7PLF<;R{rGVzIc`i={=(! zk*(Eavl(f*n0;pOp%|N;k1=)|T^N0H?7Q@Q-1Fk!jxRp?^YQJ+e=^?pkN&s#*6ds2%_2;@Nl&AZ2QBJU(;jT)dOR()#>X#C#4{#HErE_Ux+oQhxgbKi{be&`S4$mn=1wDC0o_<8NXhFihcW0hDD{n`@M zkY0TH8)X6V@Bo<%YilMz9U`g?v0FYnDL-uX)gQxy#^zHb!F^eBc1Itoy zzDc9!K1^>fK$mle1N>8>iRI!4aS>zG^40Wa|z;2a6BkiWuD0*N#~bAe@@(|OLxAdas3lx%7#A#Du|C9T=|{ z>H->zogEw`!t^~&955B$_#Fq$eDCR^9#l8y;tqkEgV{>OszxJr?d^>V9IW3c@1hW=9;tf^YIU# z__6qvuOEs>h8IF#usYNn*lG^c7L%>!@a@VUVbmHZmXjvVu2%&mw~Nj-tS_i!S3i`@e{M{X*Sr(&Ts8clXh^f8m2<4lv$@h9RybYehWBuS>0T7?T^Lj|yBe*Sg6}S_=*l_PY7KAGZtaRs&3-T*UwQ)kGKP-D{B?tY ztiyxB+$L-nSa%!j4d%U^42KTgMn*VfZqJ|mSX_Gi7vce|ZaqUE$MJQ7^si$ zxyPyV9wq|2xvuFLKw&>wP~YWC{%(@eHXE%=Fn8o2pc{XXi4z?A&k|%>PvFDVf_$63 zFq?iWe%~l~-pmkZo6JwpMwZ)+>-jP5&Oq!Po{z`(w&PbOJ?@@7t!^D^4s0<8YGAU( z48FbDqgV!aLjq>^n{|rQ7FTG78QB|$7UCn{I1qpHO>d1|`#+0vKs~NK+hP?R82Fj- zj>q`O>oGrb&IbMygLBWuy{BJ{Q}{H@Gr_;K^9SPKU*aU#pPS(Ku9(JRqF1F`Uzr>y zk5*-QSAD%yX(MW_U4x^W7XU_+^PY;d~ux4%`wBtTNu+lJe?8 zZZQW0Bv&l+++rH8x43zw?$SVf^7uWuiqO1)9gJT?Vf#H_BRq22S=g@ zxq4#kXdHXfd*i3x_*6W@(!#-7%3g1?TvPmNLhW{K{6?Aei~71+_L_276N#i=OeeG{ zq551C?`8i~)3ZE<;q3g=FXG$5L353<_?0~$jZ6Ff4vwU6hy&AKi{ZI1M+=Km9QbK8 zHmAZ4JQ|ICd~cti&+@lS=gTH&IfKgYWG8+n{kvJVh0F|_u)<}TMV8y<#{nDe_1y@_ z@La3O;2vyYj)_(A+`{d-D0_Au$H#dtW^h{Gq{uE&^+(Ntm(PJ3n7n)*);Zi{4!Dke z?@&8_`KkM3a_#`ijU6PLibO3RO{^rxqksNH?BrzI*@1^}+I%=(cl=|V3>)#&KkRSG=>jq?IROW;6_M+=Yop)Nz7K81@qQw!d7MfR-Yi?xYHl} z)rbBl-oI~wTco|TakI?ocQprY4hL#ra&!1t=W>%d(9fABU!NR~Pe1!``e7~=yZI)i zuyq)4B0$k!V0ikgIK(~~r}uta9J}YQA;72N_<{d1cHuCMya6C8h zd~OVmBY%NCv_BDz1K$TQVN3fQ3twk`0jH+#jK-rsOT~{u_(h7k$wZo+V1&<+!^7Qs zb^vTzPu44nxbDtz+i;%yBBC(;20^RJ;9l;OUS&1WudB@x*3q%~cyI@PILr@fV6p)N z>&G{h1Fk66q2@qxU_JSEyI7NmnR639|J)nndmjEYWME^@?U|cVq~)X+f0GV}u5ktuTDg3k!_F6BJX<_HGPu%Pfj3=``Mx6I#3;6HV1aH&JNx1ea@ z804NEcPUNice2hoBuN}}wWm9rhtg(u(dMGQvYL31-TdUr4jr)!mkY;cKQ#w#0tYtltv8{7I*YC2fLGJcbL;EP4Cy3WZ16WT+w0Ctaeg1WYd-$sNc^{F zUd^2@@sapPY>EBp z&i8SodQZ$~^S&$qROCT!XFw9e=ak3_<>lwy1l8}vHu+CCopFuR&1X4YH;%8&-{)0W zy?tW!^Xh#yWnR_}tI{sV;Y&I?^OsoGn+KF^K;@MgR8L5d))Fr5>tRPuF5@g`B8^qVE&NpQNbbB^ldS$(bKs_Ppav#4ou#)n!?N4vW=iU-NUvK|J-43j>|Km6o*jrUoqTJ& z;o#HARc4~yy&+d4!`(Rp^JkdAehe!LdZJscKF;a*($SxZ-471O`~Tr*;&=By7Ck$6 z#^P$F%b6B=d?qKIrV{5KnFJ8p#ywV(6Lgn@@3^!s6}kXNP0+)i`T3j6@^DNj>8j5J z3cT;rq~I(FGEjLAI9my}FLAsUSB+h1AoETDW95jGpn^|M|IO!|_M6YGM^%Rw zJ`tA585v2J&nv*Ae}!fWvy%t@UIr>Wz_EB_;9R_C?=Qux z2f43k$<4(#cz%iGG0X4aHe8i((nL9a(OGlJD+Hu-tAfYn_pqdM;bU>;OE`e`PsE5@ zcBpm?yftFqdpV}~U%&%+#BxWnu=mxx6O_CJaQOhAF92Tt5IT30Xninu!{k$UtsMHD zjNUg2fSZb`(_Ii)@kJIv@7%@tSUlLt9Zj40>JUBsOgu5RvclNCJXh{rkDQKy-{#7S z?7KHib)B&0z%_AzQB{XKp97U7y7OzYx&~OG<#cD*H9DCe6_%EV67UNx?`w9MCihXQj{gJnPB!2hle-gXK4#c_1 z3vB;HigQD5f&!Iy3m_yA@ctl`v*M~<;XV8&KW+m~WiA~kwcu`>m^xqRHjSXfPlF(YBkE<UOoqCNLpS<46~u4btr5 z9cSXjn|A+pd}z-va&`%a)-Ab{<_19tc}cdraV`r!w|XpL%(EuP*R2gK>2Q->3{CBh z&a;!TbnxqVg8m2gQ~#@o=l%*jypMMJM8rA3$Jk%yK$je$|R;o^5=`A{07jIS0N z?BqZgUmUqFI)DTHX7^n9V(ggwdLD`L+Nb`0yz@){OWc3|2O|#ei564B2DcO^kWd;6 z00vx7r#ze>(S!w*a6uwd_yP!WDvII~$OvFm@*@BMfrWrUB`?rO>x9-1@)anUmq5cb z9OGW{CcXf`JOn;kMpXX(nUC}sd}lr?I{wdc1Pg=EYPtUPl9zWRX)Q6WWhS737M}~U z1U*?6bV<+fu5Mb&t1LBIfvn@Q0-``^*7}k(@QzPZVAmGV7r!<6J=`rL+lcovcBiOF zwFI;{nq8|v)|DJHKt>O@46o2tz1X5pieGA;i|MC+Gp3(E8I6;_K|5m^+3`QY<7a7O z%I$f$v=vJ|G5ZV&Ao8W32dMl371>7w_`StJONjUE?0jGhOA~#*+vTcaXD@iZ&vIi{ zkJtT_?>z%ps;rx1j{0=k&&4}mbt=xxG{I`ume0DG*AHtBTmuK{+l8-zNp+Ol#{sSQ z=UL+DN6EkY-Y-O7PaE-y;NK)*a0ZYxV)yu0qkrLSbb5BgzVn}ngJ(XUy9y>y#J~6W z&&RJW{<-+_u|3h|vbqJ1>o35B0#hNYO!%H4LnTb$M%`580)tt$i)w{1_^IW>=Ykn6 z4K1uIT1=cl``dR~R?JU;!>zsfYzK-<@Zi#l78JpX0Hm_a2u{*+L%sqH0fnD}Am0fz z%+vhcHes2j^&J=PW%hW;KaRdW%F`8Jg^YNu|4^jXlEJ-{pR}zc%xYp0n7_2XPsK{i zoB569BLLaLxR+$IWq9cs>JK<+-3zFDFIJH+(wxIK{*Y+H2SZu_lQEL}*TrBiKthV+~~=x&|PVvD<^7Fl_G zApX_AI2!jnuo$!Tg?{UCuzq%vIIw!;+@vh)Y-$eNJPv3zc};&i{`ohL#))wbv&z}p zwRBtNhiI`LEC9WWmtqcG{Qv$}ABj_Y-xovk7vjMePsM-!9LgEpH|Gpj%SQTJ@I3`I zN?Y(@$WN4tOCT<(XpImAm_|*hg07yqtm3lHdgMiMp2CmIFnO_QWTSy($Ib zW^#3<5*#tkGQekF`;+mldwwKvDT$rq--v$b}pr1Q}V8 z>kG|Z?qwO_gwsl^u1Vg5mhAPc^lZ2_FSqO`cDg23gqhixcz})#ahyy^}>Vw^3i_1E8i4hKB6QFnwOk&N+`RL5U zw5v#c)AtQ=zZScGMSXQp9AB{YvbYmu2~HsR;iJ_B(VYwq!p0s5bEvsn|ss;c8Tp!9>9lg1iR0(osCwN5C zQj+C4b7EyKvOpzD15ptH-yg+w5Cz|^{R?_%XICTAvpfPg{sZQB!k?64l^X(ED20sc z+tuM95HmY4$$PI73;1rBgW}Mfv`YraZq9CAHQDgibk=IFWlzM|h5dBy-r*iS&cDGm zCirtS;I8Q0wLj@b{4AAU?+beQ7;=5^vo?8geV3o%TM6-S_3S6_wPNTZ=1_W$_B35O z{Yu_j=nb^MULg751L|MM*oO5N-FzT7l#q70GTvUKx6fBM4cGEm~T^32|+bW=vw z29~J1l*4O4)L3Z^X<6N!@CI;S-CnV>s|Fga?OmQf+a?ARW>^RI56@)nbgJ9GZBli5 z7x~vx`rw}B$h)w&a|!}C2k}lr{DeKF7Ow5<)|JOL95cf8whZJmVqC`God|6h)~T$K z6Dr*Frmud%(ok_b9L{negcKH{AJF?69q3MGGrAFQTb`rR*A~jJ!632jGz!>_`?sME8wWYAr7;544Fwxe7)D^9igs6b4vWo zRx{8N$_f&}6%D%nS4A-)MusR7*HhukI1Rm|KV^CtG3Ye);z5B=W(T?UN%s423Fg+} z8I$~7UT2rMa@+~j@t;>q|5jmabvjWjV9Yb{U6x!tW0b?$MTE&Bs449MpX{JQdD7M_ zX$!OVvV}qYMT%Z!cNr0-tDziVBP@Ifd6zMA*syOxzc^tpbgla_Q(ru~c2%$tf?$imK7Wu^o%Vg?BS&MqWD*^+;Kx!XJRX7;W+^?`P6GC|W9z;oD))&$j* zotesGnTnFdC4ZR$j_0)2O4bokLyi!rww3Oawt18R{*<(34891WzuX(Y8M*sY!ufeK zYC}vlO(#7zUDkAgRApU|_zwVfuex6!_1Z5zV`tKHU5onCBv9!A4Dr@l{!|tpq=B>v zv2wP0wN$9twLqe;O(#$IrFpQvUm8d=!XfkoJ!Y zFnS&&wlDST`_voDKDDd8DvLn(rk>p7NCk+@Xwa3ROJQQ9Xlha@U7o>*;`H}FR`fF|kW|q~g7@e!7oT?v3nxx*tR=SCn zw4jba@BuwHa(N2;b$RNcsaD#$iyahTkJ91q<5N%Mv%h){`K9IVw*u4{XW;U8e6Xg#D2TytTOjAXH+u95e4&7#6vo4DiDxA)9D|d|iIUTTSi5rY+f5 zM5-5_6`)~H>y`7QomoCAMzUK>4kzqa&gwu$xUw?24J`=YhXL2FLo>|q%SQBLJSUwWpT@Bnn z=?O8}^1QebEQas>iRv;wAE|2CI_1Ve4urc&iO34CDO-D^rNiK*QvRc-Ur6^T0graV zVW9$>maJ_dKj$XmF!A@e5zSoD-Bk_>^dVu@d!d(cwu6EDa5!-y0}iCQO@<$tL9H6> zC5?JWMY$a^LTTpVB|wcVJV`~$iOE~>_0966@52=!>|qDgJcmF6NMeEsDw~|KzQ4a9 zT>J$h9eFbC1kQ{q*SA&hhsG=F&i6Yl`-o6&!x8cR1zn^~6&&LS{WuUzyJ85X=YPyr$pt(>jngos=5_~7m@dOx0%Tp3`2fy( zEF^(|cYJE9yI`2wA6GO?z+fi-C22J>)7P{1mpLLk`JnPi%P(s4cQ{)0rwCL;L2D1; z@3n9~Dzd+wBu(T0E+IB|aJOkI$PD=IFcb09VaQTM42U}t;azolF8fD%ZEvMbv+|X z?(;T!$F~?O+(OZ%mtjm>Ga%q9G=Uf1EIkM$cN};|>%iC?nauSvZ}yWLjVkavcPhu4Yj$9DUbm zSBV{57HWeqVxKJm>V?6zxp1?TUz~uBw$Y|L5xQ0Kab{0s1zX zm|b*)4q8c4@w$7G2BM1;7V`P{va&7kyPx~4B=)C&9d7xo8K`zrK) zO`JlIsb8^`--xzd%xN)1F$X4YfL}(<-$$EA&`Xb6MotC@?&5gj!~*1YX;EW(Dsb>n z=J~1E{5&4SETx7Kt!b%W1Rfv(3gDPA_bdT&#F_@gz6nSs#i35IR*|<3t;$0tGped( zFR6G!V9b7zma`r`x~(jm<9SOa!S{VgF*J$}5~!9LgU!P6Uddjo!n(b@YX^`2#x30& z_{X$~M{SRPP?noqzHXB4rg&}z?|E(oRmML-tnGifvR0Wq16neL(b(LhPQkk-^yvz7 zMaA<6;|DE772!2EdTMKR=f%60A{qq-&o*sWvo!~t()~u&w*8B4mu+TSz3yXZ%?Huu z4MYD?`iJX)$aXAOR%HBoq}(ThuQjeE)FifTSAAN~HaVwi`fBd8r`MON)~5yYFB|BR zTS=jhTYhOHs_u<~Q3h$LU=mq@#Ysn6VaIcyhywco&rCCh2v#^y^M@+YhP~Ofl3Q3! zc3!r9&RRl2^Myv-D({M*wr|AK;&YlXBLE|Kl8UPGykav?mZu%>7Z@N@`)#iyd-ksz z2e7iL7Cs=c_x#WK3e|US{-K+Tl>`M}rDH-% zM|xz8g=E&-)Klbk1(->9tci!>`cV(w@S&WU$1r>Sz6SE;x?RgouiJQz*=EA)HXBob zRg=N3+d~M_bjsK|XNT?87{qezaintcvhGwpArAG>=LCwiV0mbl;9xkoSkJlR3Z{6kshu@jPgt8SRBv&-L# z@U?`HC(_|}#`L<5DWA3ec<2W*Fj(Pq1XzrDC2o$N;_{0BHF;?ijGin*77Yz-jZydJ zY0;j%R0-Ru!Ti~J=F9C2o%CQ*nty|~svm|a&6rtic4w=2y3pK3*Kl_;uGV9l@bvNX z#Q7Moe;UnMR5&)Vq|NB-0$(G!^d73bz z^)6wnu8rbt2~bH})ng0Xjn6fY5M!@;|6*X32|@OZwzRval!vHkhd8zTRV+42m+RK* zjLKbo|MzH@no3Fwc1fKYYd^o56n%{IVWp>|Ey5dz;|#Dl z^#foV-5BweEjLx5Yr^n2)c~sV+>`!uU_&oIf$cZ>9f3XqgjYT~kd{OXf zzS11eLy+l=#&Z9bnTB#<6-?6%0u48aaJ{oH#w!y~ORRtVwpOVpqigiYwlu8McBvC# zArvWd$ix%^-s>BAio@dy>nut!3pQ>aLQkiLuRagkaNSgMC>|)Y5nP?w7~6(!y)-x4`0CPVe2pWHoy>bv7&S|JMligjOZ1xVh}`Jka<3Mnrzx>iL2t$z z^3Kz2Tlzk;hjSCCJG)GL{ur%S)35}N-To&oq$>XPoDV)Avo{s4zsX)bl|8aMsn_jc zis2*O4>q4yD2j@_RNKH&Wf}(KMx^iewfW;idG+HqE_$Nd%~P+}3Zl={G^8u!J;K-g z+G_=(&SUlqGQcWu(xJZMk6STjk9L+8f3aKblZB^A=nAe78D$aaSeScNSU}{5t*4#RNwVTtanCCT@&OZZQijH&4 zC%^Rl;MbQQti9*xWp|8vcU3*3ZlmpkcX=WtVWDOZ#;;v@ck3inFEGO7bIAK`1p`Wv zgX^AC!!UN2mwSqw-%X0AkE_)<6wk>^&X1eOYg{BT@7uj$mSuO7wkdxs;-kE;2VWrm z#&^b&Dd$xML0iw*Vf{^my$gerFXO2OAwjp3{d_;RMTCTiDLf7l*C;7^>S9LB^rUL~0P+Lo`e!U-X$S(iZTqCaCp{6Bu zDQ2Xv<1OuTnm28Wi_=Mxwx?3D=GSF$PL>!N(&0)9cn)<-yMl0Ru04k25O$YsbF zA>x$9Uv9{cIDU4GF4|^XMK#vyKPT!dLlK{%WkZmj znlj+Vj6ZjDMC3cBs%y?`T;4=|vURg;A79LJ)?jvLh+z)bBUvEca=atfto86W6fohn znjX5@%*IRjxKK7b&i*hB$@j3GD%uZN5a7b;5!A_C<5rC1*UOM(r+7~uy1OL4>?DNu z$mi9FNqUmy*=30RtPm+Ye4!m`eY{KZ7%bjlkQ}y!7ycghewl3foDHkL86@>hvvF6n&?ckF(FM9c)IdN+y6uHnvmdQUX)_YF$ZN)u zc)M;+brCZ`_K`;+TGESRI=oSs@`lN+!|c-SZ@aeN7V+;hc4`{wGwI_&mx_INEk1?{ zeJZplza4Ntt<_@_lt|AiY{VI-a|sOqVB=5A_uzuRI{PCJMKFk-yQ)vi=zleg);7LN zDo;b}%rHVkcHZ4F)Uka8j6x`;HV5fInYS6cKaTvxCjdGJ>5?WoWc!V%qB3&%T(OSCk_Nf;&t0 zwE1@Mso)QpPb>WxGH|>ZbMMVBv@8|Ia^rEz`D&;#I2@JtJ~TqyN)wJ7y<&xUf7mQK zY`a_D>?Z*A`^5Wj<8gk^P{iR&r<(qDae@56|-*8MI@;?*+9-7Kf zY5PnImKYS`G-2x&GXR za)gOhl8yjE(o2}gNyNL~BhwUknlXJm$AC?ezm^*noSZ+{u+!O~C)I&4`5JVVZ& zXrmfr%RFyUvIqQH9sSUUdZnQ@D8eDzs6-^=b<_s{zpN@c{oZQe?qT-&Bf=)|S>k{W zYWPmBQo+ju4yH}`L+v7r$-TCcO@dsA{qM81EZ~lz{PUjiaHFy4DqlM?5@}32w#r_O zRtUkT$SHaEOlR@t1i%L%eE%o7hcM}Xy`KDYqL!9J7~tU8)4LP+qg(qAna28OlV4M} z7hnyYf7!uOimT=p_7@*t;{@stJJ-8L3ZgM1*ze=xK@S1;A7Vx1J!yURnYI+qx)brK zSh2DGkwal_>L81A28;W`9&4CynPyN+1iT(Iw2goi3Av&MgxtaSkr~*k0#zMH_z3(> z`h>#CjMC@KR*?5|Jo`<@pc>zZ^g^}^s?P%ZxCf0icOdwNA}EM1wy_Q_%rRnPxk{|C zPYthscgL3kWXrt{Y$f1|FUeNKVKxVSzEh&?ke$EzLh`jClS52u)Rl$yd1`Xw2Q`4J zl#@#d=nqP8*7cU@{rn;nn;@$1@EFxkJ^P zNXvQvci231=%hYlPBJMj2u`dP8Cv`(C*3;~0Z{9Kj`GDfZf_{I$!e@izmYfZTJb+t zh8VmOZRStKz|{s&^1%U)^GuB4L~zIJ>{(W)TbOY;B7F+W%Hz-evrQ#LiM3yyxEbl%~MY?`}_q+XtL1FoJZAlQ3 z$e_WhKs@8(R~?jv?W`@~1n)teN~OW5T+DWUeKI5IMUFyo@w{i!ci*^qZK zi%{Y+p2d?JtRV)6B5ZBM3Pf}8AL-5aR;>5xQ!wfzO|O0LIf^Aecy6{utTMY4b45kS zp(+3ZuO`K&tbzlk(o`EtHdX7?R>tpSF#KJi$Wh)g3%3(RiO_3JjQlECvjh_B(Temn zHe3hPH1Hv8NTJnS$tn%z`rqEu`=?n+mo)L3@iOfF8+o!j6OTmK2gN5!ToiRiBb9Mm zoBq6SVKNuw5M<_`VBV{;8JojjWyA=`gjsZHeoI?kjJsm`Mlho7T1dcYxJHmcd^zAz zt-vRWq8oLiyVI3xEfm^;1qjz_&YFlcsPUX8fYbYB;;!;)DNsCukcld2(N7Bk_E3f7 z*U}kI-4tNf*iH%Ga6eL}1$JbAtJPg_t&Gbx`Q*KHnwqwxadi3ooVwQSDE`{HYGs^& zciga((f;M?9YIM~y=yRA0s^oR+{W_&#h7i@tY`vP>dKv=hH zPM~RYWT98d$~>h}SBkm``#BdpH4e8xZ+%D!srNoU>hEpnc0e3_@F-ju9d6gyGkt>F zc1Sg!9P~iHJ(E0O$wGPYvL$CcD*Ms zsT*j5A!<^vofdTwT9ZSuSpnI^w4y(z7F${sx)i%frbdU1m{Zwn;Xnh}g0kO*5Hq); zX(V}?Ga|SQ|LC8ih3D!b>EjF45%|K1HXFi9PI(?-|`V#p?^JW>^$|(zh zoA&4%-WIME?djPX7|K}~?Gf(C#G`HdyT)jIDH{-U!Du}i{rTqIQ{mn;5Jvu_%OT`` z5vj5`y%TBj?>4vhCD#b-rFE9=XgX_fuj*HA6K#y0w=7G%Ra}|Zm8tMc9qRXcNQ34} z!KbQtSVSr*REG#JmmT||k+RQQDCji&&B)RCjmfa3Q-TDdjZiEsT1ks)QfXEY?JUxC zdhqf4^0ggmR;K`&9+Ak~fsq^Xbp1hHR{_|*@ArfHhj5sM{2w`X5CQcDp*)@0pV~YI zWtkF>*QU2F{&>!2=Mhv7Pz_Iw=K|?j7H^=E*iT$-4phUgM3^|nObS$SE$ zIWjx5c!*6kD`H;3Yc7^iI&uo{Kx$cfup~pE->L+SKN50{{tebk>;>OCfv^=eG;$jQ zSG2$+7X|!_8OReL*qymCr}`_WMg=ilhlIch?N781dsYGs8R9yQg`VSFSm|}n(H{G& zuossiWBGB(H!y2JeobyJdS}1|4pg(fdl+($=C{%V9`xLWlR=XjE;;~+YW3IpYa(4U z+vgIjfuQm3-$jQ6)jkxtsm3zKbKr4@=YPA86I<(Uc`9wx^C^e}g)? zW!AA46sl1q<5h+&@g_k)Co9*udLMtZ$FD@3y)wq_jCPL!YhHA1K#$AxjDFY9bHgTW zoLr6OW5=nIZq^I{$wm1SH&}!6tL}4oEdUg$+XvMON!p*Q)0ALB4|5v8?*AtLK6jZt zL6$DQ!lVnfxM`+FORJ3pN2C|ouj9rg@gh&I$*6SBNt?&rPtMW#z=uTYIyFZt(l38b za!CeK>i>AUbIv3~^i!AKT1|_yG6aRAr8@U4AJdw@3(YVLTcqlVz5%S@vF(ali{d1u zlSFD^?JUJ_QP|_M1;v!B*9lz)dEnobBmXHdRHiRS2q5VGwV2|+>Q~mOXqe|Kftu?p zs#s7d%BC^Yt6=s64I$u9x3-_rKYTV6&!07{0UAm77GCsYbz=uoV)Wkdt(0y4SQ&Wc zarwFwk|;Z+ipd;4vN(5!%@UeKk~IyST3>c-xVhUyq>@*hbM1K31r_1=rUTD%YZ9B( zFwh2pszVrx0eNgJGBTO52-HXTzvCuiki5@c<7DRl&3r%-A|P^0GWU5*iQ2L%#EXx0tY1MZ1ld4D|3`o{HQVt*bo zXKS;=mBBc3(Nb(Y=jT9*?9X&)%r|Aj_JxS)A##sU9T0v>%8S4CIs>1Hh&hNn-6<*g zhw@GFPlK6%pR>DBTCwj(f1u}Ja6oIpa_v~9=V7%ak*tY(KM$9}{dCv4tMu-&6IEXb zoaP~X>_AYXLY7JH?dz zN3~~fnG6OyLUCK*`#2CMb+rb&8dez^GczQf)k{KD3LO}}3#^j)Dy6|5AaEI%kDF^> z*S#${M{}qoap29kAgg<*6=b>LdrY5M_Mk1TQc7tu_<0S!4D&*Lb?Nd+SLu_slDe3d zyi$}&P-aEZHX@=w(obmicadSW^9SLyw0mpRUR(|Q!jPq>+z)=@gik1+j&quh{AO9q z4px#{1k7`}6BJ`iWj@+kNF!Qzj0qyHo9L=N*8$+l&aA?FUP%IqqaKh!S+S|H!WiPjA9!B8L%_U^|;wY365s zdP&aMlA?3VNC`5oAACPO)SXE~(VrPn?oV-$T#Jgah}1fQTt;3SU>wcPjE&*XL`N@r!7_`mguEZr98 zNG2&vhDv2sf`F1tIm&fez<|uUz`j%>aNzoZ5kE@_A+)B01OD<^+2%{BY4r0^7(J{w z^=gxqvs?E;bfE+R-xrN9A<|(bN${zRPgevlm!x|}g)@}emR-#f$Rh7-GPfWr#HSaM znY5sYI{GyEY!+}L_G662T?1kFpgE;;%`)S@6^q2dw_Q1Q3`Qk`xTkLYm@_n~h@UYw z!7_+1T*7wzC-6eB<6#(i*sEw;w4;_{N#uSDT?V`N&jqI~L6y6^O~`$7OzDI=sh(Q* z+kPhR%ad%g4EJ+YHMu52W?fiO8P45Nk< z27M6a`y86x%kfj9P`NpO9(NBn>MOU;Q<@NOe~miyS-V)#ZM^~kFjH}53x;S`FuJpr5!hNVq)U{#c{ppKUz84T;DDY`VH&cxF{(zX7i>4rI5K1J!mFMTMfkk*%NhO5pGlx z1Y}0|FLV9`mlY#08hK7r?jLv+iKlw#^RBs8?&ZG{DSNq3#CR1EJc?_ylp|Zx#|#(| z_o~v9Bbi%r8JwA&?Hi&(K8b3&6&Ohc%n(Gh7WQB|WYSN~2J00&ey(`+hvq^uh{xMC z@ng=1@A3E1Cy#&9BwQ>z0cX(~x0SO&fK(INAOF^tU0Ue8IOQyfeKax!6L1E8c;5&P zbsQ~MiySs&&vvf73E@53`ICwI4<&nlTo`aroMNE{u^Bx4F{J6@d@iaEfq)PcP56R1 z1EzTM0rt#)_Rqd7_EkKIZTrQOBm|&k$dFy|)oxGW8TY#G*tQ88^n2@qe@*4UAJ|1m z_b1*Lm;_yLM)p1MT@&@?@=3S2lSnoXHGR07XpAHBkkP>>mx{S^ zj}Nz~jyRigVmXHN!6a!&WYT1Wtl(cgO|^esLN|e00L8RhbFVh~{WiCrjB=G(@Gu{Z8((4LF>n zv*@LW<)b1>pX-?rAEn#P07}rsm8o&Q*V}^&`?&Yc9ExU=KcUdOgZJfTEVT}UKx_qd zTf@vsV&T60aHjcbxwbbbUuHLk-wG2M!KY+tuUdZg*s#5!0|iAU#b`h0!!dHC zmzwK~v}28zklxPPwz5s+ujw}u%jBCH>BlMwd^uio z!#!2EUx;+O3jtDDS(*Uwd|nYy-HCjby`J7-npmT<#)XL1Qerte9)|y5cM;x7N?{@w zchwi?(!Vvtp}fDVEuX8GshlSZeM6R|#i_8ZyR#%&R#eS(Kj_E)55~1aEyT8hQcaP= z5@%nX+YmL|+T=aAy%Ufj%OK$i7GT^+i$HzNXzeyAQ{{Y`W80C&_(?D5c--${d;WOs z^V08bszx4KDN!wiT%ZD+su;}~zC-d7D%vc5{mYpayIxD)g44dIwgh$hjxj+Pt>;$$ zSpi0eM)#0lS`nk*sQ6dw!ct<1OweV$dK{38L*^yN0Zu8;K`TFocdSuPC*q71P&Q#$ zec)=U^y)Yav8!vR|Bz?x@Pl`#iN1hQZ=ZxF;`~Xkjnn#8Rk74;bw&A_>@gYY?y`d) z|I2G+ebFL1vk^>PMh^vt(JPb@X+uWFU(7qE#FYI|ADxJpm|kS#)8~-m$?D3tt0iu( zt7o=ahWq0gFb=PKm3fCFPoiz|%I6Az5Kq66q2Ky_x$e!L)R0121Vnt0nY-v)%#Jd7 zQQb0imlKNvy+5q9657-*w4_lnd%m7BZD zrs1$BRJcI@3S18+5QePi9=x3{7Ng83QC%}1FIn(UgI!SS><7?y2lb%Gp|Iri4h4_Op^tvcL-8uS(;RVt6&D$ z?2pG8gmBt1lWOkUZI(XB?4vWe|I^?gcYm)Ns3{ip&lM3IzPwp%}8k<_it+AWVY8q|B@GuyZhdyCYD!u$PVt+;Qca~&D}uV{c$Bv}>{P>Fcf zhvBay&4NpTwtd%gUwdGll~%6L_)MW|gl+STT_{_4og0%a`C4%AXFeGa(*lt)NPP0z zXA{Ao#JJt+EC@*BIGt$bbvcT(aQ&Q2+Vjhm%MO$VpfRn9Dve-^Mu%w6C;3T&)qGO- zXheQP*;E;ZpoUoBuk$tE@G5OCF0Qd$*72jOYnyJ6NEw|!wqHQEy-B>f>9{}cWf4LW zuo}g+OxQ&7vwK55%S7MftXLF<^FlqT=8|^;-ggSss-?LbIOcEl)+_=KW$1-o>CdW^ zot(_NR+@nyMHHUx2E9KidQ1^Y=xVUb7#Tc|EPgaZJTik{R zeSmwM#K4cpDmVpP+MURb5vb9+;uP^eA*zOzR4PefW1N_o>d1AV4OX`r5%zsmC=h^t zc>|?5dL^f}4!e>pySbosJri0MAU_-y<-nr;J8vd`9k@b6HD7%qr|v8FcYPY|6&cIY zq;|o6eJscO4zm98!uJ*0-d{yoA5)%ZcTYwbINg63px&@fqM?*4KdM8Iuo8!gT3b~`^wwxHx{Ei#FRQ5nR2i1c2n?=d2bsH0AlXK`_Z85o7ln4mdf(#$y~ z$WsT+-<_xI+h+e+$!Q;w?Dkxrl}5BUlNNf()kicqkFoxQN858yRA#40C{;&D>`?j5 z{F68!>a+ynO+0j2o12><1;+s+vz0c$|8$-EQp#7E3}0wgh6qRiDl_FZH2VZ5+%azUg#ovQd~=1HSxlQW`a z9~U$(-E~y?R*}rJ1Bi!j9)Wpd_0?0Tc}dggSR34sJ+nQkxu0@KRRJExgf%=FhiZyn zd-HHv&Bv%=ioE^TSfO?w~b%~L{)3yfA?n*~jGBfpH29{8NdW6dbv zpIF^m>mCAGLKg32I0z!|vGAIfFqmNL{4V7tyD`<5I~Uic;nQ(cO&n_v(xZOi5XG@h z&5qMp?|*vo(k9wqICOXZlHqshrbqWevx~%P42QcKZ7UHOh({HYMAS?P?!I_>1!{f) z0@fF2wJdIfmp?i55&7j@`RKJ+Zyy#{`rlG_%U;&r$TROVu)P$@QBb@{kI?{=O7^`H z--F2@FgwmN=GEGR2&79*CBW>zTY|+o?6=PA0l!V0mJ>^`-M(G8g3L{Cxk#{=Jf(6e zJq#QZm!`Ml68b!4nTpK26u7`@x9O(ihRxC|K9cA-DXuJpSmE^%* zW2=$YmCaU<`gL^+>slPc?uy23bDt{}ayj6m8qEn3qT+1b%Q6e8<{I6$&0#1Y(zzeD z(ie0*#dP{c_lrFXbbNNkd~3=lE#y;l%-n&F`U%9p@vu$CDdn`S|J^3Tb0dGpYQ2Cc z!pCUnr_6B||L?$LcvtX$Ew%kTdSBfBci;@E8|=TL>WWsLGE3F}vG|{f6U{gDxE*2L zoP72_7XR~jHuN7&pzm(2Jzw~LzLu;0OFoe;IdO0N=lXx9wjTH2D{8&?S6i<1f4&-x r`QD}_P{04Z+w%WELnqT8_*ao9%mR!{M-p?u+a)a_|EXNe(EtAdb1?86 literal 0 HcmV?d00001 diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/buttons/ask_assistant_button.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/buttons/ask_assistant_button.stories.tsx similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/buttons/ask_assistant_button.stories.tsx rename to x-pack/packages/kbn-ai-assistant/src/buttons/ask_assistant_button.stories.tsx diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/buttons/ask_assistant_button.tsx b/x-pack/packages/kbn-ai-assistant/src/buttons/ask_assistant_button.tsx similarity index 71% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/buttons/ask_assistant_button.tsx rename to x-pack/packages/kbn-ai-assistant/src/buttons/ask_assistant_button.tsx index e13ba34110434..624c3df9a1e84 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/buttons/ask_assistant_button.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/buttons/ask_assistant_button.tsx @@ -46,12 +46,13 @@ export function AskAssistantButton({ variant, onClick, }: AskAssistantButtonProps) { - const buttonLabel = i18n.translate( - 'xpack.observabilityAiAssistant.askAssistantButton.buttonLabel', - { - defaultMessage: 'Ask Assistant', - } - ); + const buttonLabel = i18n.translate('xpack.aiAssistant.askAssistantButton.buttonLabel', { + defaultMessage: 'Ask Assistant', + }); + + const aiAssistantLabel = i18n.translate('xpack.aiAssistant.aiAssistantLabel', { + defaultMessage: 'AI Assistant', + }); switch (variant) { case 'basic': @@ -84,23 +85,13 @@ export function AskAssistantButton({ return ( {props.isExpanded - ? i18n.translate('xpack.observabilityAiAssistant.hideExpandConversationButton.hide', { + ? i18n.translate('xpack.aiAssistant.hideExpandConversationButton.hide', { defaultMessage: 'Hide chats', }) - : i18n.translate('xpack.observabilityAiAssistant.hideExpandConversationButton.show', { + : i18n.translate('xpack.aiAssistant.hideExpandConversationButton.show', { defaultMessage: 'Show chats', })} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/buttons/new_chat_button.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/buttons/new_chat_button.stories.tsx similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/buttons/new_chat_button.stories.tsx rename to x-pack/packages/kbn-ai-assistant/src/buttons/new_chat_button.stories.tsx diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/buttons/new_chat_button.tsx b/x-pack/packages/kbn-ai-assistant/src/buttons/new_chat_button.tsx similarity index 92% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/buttons/new_chat_button.tsx rename to x-pack/packages/kbn-ai-assistant/src/buttons/new_chat_button.tsx index 75cede6344c59..3e515e87c2197 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/buttons/new_chat_button.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/buttons/new_chat_button.tsx @@ -18,7 +18,7 @@ export function NewChatButton( iconType="newChat" {...nextProps} > - {i18n.translate('xpack.observabilityAiAssistant.newChatButton', { + {i18n.translate('xpack.aiAssistant.newChatButton', { defaultMessage: 'New chat', })} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_actions_menu.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_actions_menu.tsx similarity index 65% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_actions_menu.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_actions_menu.tsx index 713a0d2311e3c..ac25fe6c3703a 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_actions_menu.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_actions_menu.tsx @@ -16,10 +16,8 @@ import { EuiToolTip, } from '@elastic/eui'; import { ConnectorSelectorBase } from '@kbn/observability-ai-assistant-plugin/public'; -import { useKibana } from '../../hooks/use_kibana'; -import { getSettingsHref } from '../../utils/get_settings_href'; -import { getSettingsKnowledgeBaseHref } from '../../utils/get_settings_kb_href'; -import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors'; +import type { UseGenAIConnectorsResult } from '../hooks/use_genai_connectors'; +import { useKibana } from '../hooks/use_kibana'; export function ChatActionsMenu({ connectors, @@ -32,14 +30,11 @@ export function ChatActionsMenu({ disabled: boolean; onCopyConversationClick: () => void; }) { - const { - application: { navigateToUrl, navigateToApp }, - http, - } = useKibana().services; + const { application, http } = useKibana().services; const [isOpen, setIsOpen] = useState(false); const handleNavigateToConnectors = () => { - navigateToApp('management', { + application?.navigateToApp('management', { path: '/insightsAndAlerting/triggersActionsConnectors/connectors', }); }; @@ -49,11 +44,17 @@ export function ChatActionsMenu({ }; const handleNavigateToSettings = () => { - navigateToUrl(getSettingsHref(http)); + application?.navigateToUrl( + http!.basePath.prepend(`/app/management/kibana/observabilityAiAssistantManagement`) + ); }; const handleNavigateToSettingsKnowledgeBase = () => { - navigateToUrl(getSettingsKnowledgeBaseHref(http)); + application?.navigateToUrl( + http!.basePath.prepend( + `/app/management/kibana/observabilityAiAssistantManagement?tab=knowledge_base` + ) + ); }; return ( @@ -61,10 +62,9 @@ export function ChatActionsMenu({ isOpen={isOpen} button={ @@ -87,24 +87,21 @@ export function ChatActionsMenu({ panels={[ { id: 0, - title: i18n.translate('xpack.observabilityAiAssistant.chatHeader.actions.title', { + title: i18n.translate('xpack.aiAssistant.chatHeader.actions.title', { defaultMessage: 'Actions', }), items: [ { - name: i18n.translate( - 'xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase', - { - defaultMessage: 'Manage knowledge base', - } - ), + name: i18n.translate('xpack.aiAssistant.chatHeader.actions.knowledgeBase', { + defaultMessage: 'Manage knowledge base', + }), onClick: () => { toggleActionsMenu(); handleNavigateToSettingsKnowledgeBase(); }, }, { - name: i18n.translate('xpack.observabilityAiAssistant.chatHeader.actions.settings', { + name: i18n.translate('xpack.aiAssistant.chatHeader.actions.settings', { defaultMessage: 'AI Assistant Settings', }), onClick: () => { @@ -115,7 +112,7 @@ export function ChatActionsMenu({ { name: (

- {i18n.translate('xpack.observabilityAiAssistant.chatHeader.actions.connector', { + {i18n.translate('xpack.aiAssistant.chatHeader.actions.connector', { defaultMessage: 'Connector', })}{' '} @@ -129,12 +126,9 @@ export function ChatActionsMenu({ panel: 1, }, { - name: i18n.translate( - 'xpack.observabilityAiAssistant.chatHeader.actions.copyConversation', - { - defaultMessage: 'Copy conversation', - } - ), + name: i18n.translate('xpack.aiAssistant.chatHeader.actions.copyConversation', { + defaultMessage: 'Copy conversation', + }), disabled: !conversationId, onClick: () => { toggleActionsMenu(); @@ -146,7 +140,7 @@ export function ChatActionsMenu({ { id: 1, width: 256, - title: i18n.translate('xpack.observabilityAiAssistant.chatHeader.actions.connector', { + title: i18n.translate('xpack.aiAssistant.chatHeader.actions.connector', { defaultMessage: 'Connector', }), content: ( @@ -159,10 +153,9 @@ export function ChatActionsMenu({ data-test-subj="settingsTabGoToConnectorsButton" onClick={handleNavigateToConnectors} > - {i18n.translate( - 'xpack.observabilityAiAssistant.settingsPage.goToConnectorsButtonLabel', - { defaultMessage: 'Manage connectors' } - )} + {i18n.translate('xpack.aiAssistant.settingsPage.goToConnectorsButtonLabel', { + defaultMessage: 'Manage connectors', + })} ), diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_body.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.stories.tsx similarity index 98% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_body.stories.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_body.stories.tsx index 4e71ecdfd2c12..182cb046cba70 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_body.stories.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.stories.tsx @@ -8,9 +8,9 @@ import { ComponentMeta, ComponentStoryObj } from '@storybook/react'; import React from 'react'; import { MessageRole } from '@kbn/observability-ai-assistant-plugin/public'; -import { KibanaReactStorybookDecorator } from '../../utils/storybook_decorator.stories'; +import { buildSystemMessage } from '../utils/builders'; +import { KibanaReactStorybookDecorator } from '../utils/storybook_decorator.stories'; import { ChatBody as Component } from './chat_body'; -import { buildSystemMessage } from '../../utils/builders'; const meta: ComponentMeta = { component: Component, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_body.test.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.test.tsx similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_body.test.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_body.test.tsx diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_body.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.tsx similarity index 93% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_body.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_body.tsx index 0bf5a8009b635..c3989f6971fff 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_body.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_body.tsx @@ -31,20 +31,20 @@ import type { AuthenticatedUser } from '@kbn/security-plugin/common'; import { euiThemeVars } from '@kbn/ui-theme'; import { findLastIndex } from 'lodash'; import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { useConversation } from '../../hooks/use_conversation'; -import { useGenAIConnectors } from '../../hooks/use_genai_connectors'; -import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; -import { useLicense } from '../../hooks/use_license'; -import { useObservabilityAIAssistantChatService } from '../../hooks/use_observability_ai_assistant_chat_service'; -import { useSimulatedFunctionCalling } from '../../hooks/use_simulated_function_calling'; -import { ASSISTANT_SETUP_TITLE, EMPTY_CONVERSATION_TITLE, UPGRADE_LICENSE_TITLE } from '../../i18n'; -import { PromptEditor } from '../prompt_editor/prompt_editor'; +import type { UseKnowledgeBaseResult } from '../hooks/use_knowledge_base'; +import { ASSISTANT_SETUP_TITLE, EMPTY_CONVERSATION_TITLE, UPGRADE_LICENSE_TITLE } from '../i18n'; +import { useAIAssistantChatService } from '../hooks/use_ai_assistant_chat_service'; +import { useSimulatedFunctionCalling } from '../hooks/use_simulated_function_calling'; +import { useGenAIConnectors } from '../hooks/use_genai_connectors'; +import { useConversation } from '../hooks/use_conversation'; import { FlyoutPositionMode } from './chat_flyout'; import { ChatHeader } from './chat_header'; import { ChatTimeline } from './chat_timeline'; import { IncorrectLicensePanel } from './incorrect_license_panel'; import { SimulatedFunctionCallingCallout } from './simulated_function_calling_callout'; import { WelcomeMessage } from './welcome_message'; +import { useLicense } from '../hooks/use_license'; +import { PromptEditor } from '../prompt_editor/prompt_editor'; const fullHeightClassName = css` height: 100%; @@ -110,7 +110,7 @@ export function ChatBody({ showLinkToConversationsApp, onConversationUpdate, onToggleFlyoutPositionMode, - onClose, + navigateToConversation, }: { connectors: ReturnType; currentUser?: Pick; @@ -122,14 +122,14 @@ export function ChatBody({ showLinkToConversationsApp: boolean; onConversationUpdate: (conversation: { conversation: Conversation['conversation'] }) => void; onToggleFlyoutPositionMode?: (flyoutPositionMode: FlyoutPositionMode) => void; - onClose?: () => void; + navigateToConversation: (conversationId?: string) => void; }) { const license = useLicense(); const hasCorrectLicense = license?.hasAtLeast('enterprise'); const euiTheme = useEuiTheme(); const scrollBarStyles = euiScrollBarStyles(euiTheme); - const chatService = useObservabilityAIAssistantChatService(); + const chatService = useAIAssistantChatService(); const { simulatedFunctionCallingEnabled } = useSimulatedFunctionCalling(); @@ -440,12 +440,12 @@ export function ChatBody({ - {i18n.translate('xpack.observabilityAiAssistant.couldNotFindConversationContent', { + {i18n.translate('xpack.aiAssistant.couldNotFindConversationContent', { defaultMessage: 'Could not find a conversation with id {conversationId}. Make sure the conversation exists and you have access to it.', values: { conversationId: initialConversationId }, @@ -470,12 +470,12 @@ export function ChatBody({ {conversation.error ? ( - {i18n.translate('xpack.observabilityAiAssistant.couldNotFindConversationContent', { + {i18n.translate('xpack.aiAssistant.couldNotFindConversationContent', { defaultMessage: 'Could not find a conversation with id {conversationId}. Make sure the conversation exists and you have access to it.', values: { conversationId: initialConversationId }, @@ -500,7 +500,7 @@ export function ChatBody({ saveTitle(newTitle); }} onToggleFlyoutPositionMode={onToggleFlyoutPositionMode} - onClose={onClose} + navigateToConversation={navigateToConversation} /> diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_consolidated_items.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_consolidated_items.tsx similarity index 89% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_consolidated_items.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_consolidated_items.tsx index f31796b8812d2..5771b1fd297d7 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_consolidated_items.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_consolidated_items.tsx @@ -90,11 +90,11 @@ export function ChatConsolidatedItems({ > {!expanded - ? i18n.translate('xpack.observabilityAiAssistant.chatCollapsedItems.showEvents', { + ? i18n.translate('xpack.aiAssistant.chatCollapsedItems.showEvents', { defaultMessage: 'Show {count} events', values: { count: consolidatedItem.length }, }) - : i18n.translate('xpack.observabilityAiAssistant.chatCollapsedItems.hideEvents', { + : i18n.translate('xpack.aiAssistant.chatCollapsedItems.hideEvents', { defaultMessage: 'Hide {count} events', values: { count: consolidatedItem.length }, })} @@ -104,12 +104,9 @@ export function ChatConsolidatedItems({ username="" actions={ {}, + navigateToConversation: () => {}, }; export const ChatFlyout = Template.bind({}); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_flyout.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_flyout.tsx similarity index 88% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_flyout.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_flyout.tsx index 67ac37a88d724..8d636374ac768 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_flyout.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_flyout.tsx @@ -19,16 +19,16 @@ import { i18n } from '@kbn/i18n'; import { Message } from '@kbn/observability-ai-assistant-plugin/common'; import React, { useState } from 'react'; import ReactDOM from 'react-dom'; -import { useConversationKey } from '../../hooks/use_conversation_key'; -import { useConversationList } from '../../hooks/use_conversation_list'; -import { useCurrentUser } from '../../hooks/use_current_user'; -import { useGenAIConnectors } from '../../hooks/use_genai_connectors'; -import { useKibana } from '../../hooks/use_kibana'; -import { useKnowledgeBase } from '../../hooks/use_knowledge_base'; -import { NewChatButton } from '../buttons/new_chat_button'; +import { useConversationKey } from '../hooks/use_conversation_key'; +import { useConversationList } from '../hooks/use_conversation_list'; +import { useCurrentUser } from '../hooks/use_current_user'; +import { useGenAIConnectors } from '../hooks/use_genai_connectors'; import { ChatBody } from './chat_body'; import { ChatInlineEditingContent } from './chat_inline_edit'; import { ConversationList } from './conversation_list'; +import { useKibana } from '../hooks/use_kibana'; +import { useKnowledgeBase } from '../hooks/use_knowledge_base'; +import { NewChatButton } from '../buttons/new_chat_button'; const CONVERSATIONS_SIDEBAR_WIDTH = 260; const CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED = 34; @@ -46,12 +46,14 @@ export function ChatFlyout({ initialFlyoutPositionMode, isOpen, onClose, + navigateToConversation, }: { initialTitle: string; initialMessages: Message[]; initialFlyoutPositionMode?: FlyoutPositionMode; isOpen: boolean; onClose: () => void; + navigateToConversation(conversationId?: string): void; }) { const { euiTheme } = useEuiTheme(); const breakpoint = useCurrentEuiBreakpoint(); @@ -75,11 +77,7 @@ export function ChatFlyout({ const { services: { - plugins: { - start: { - observabilityAIAssistant: { ObservabilityAIAssistantMultipaneFlyoutContext }, - }, - }, + observabilityAIAssistant: { ObservabilityAIAssistantMultipaneFlyoutContext }, }, } = useKibana(); const conversationList = useConversationList(); @@ -148,8 +146,8 @@ export function ChatFlyout({ > { + if (onClose) onClose(); + navigateToConversation(newConversationId); + }} /> diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_header.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_header.stories.tsx similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_header.stories.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_header.stories.tsx diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_header.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_header.tsx similarity index 81% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_header.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_header.tsx index c67596fbafd5e..c9f0588a1c90f 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_header.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_header.tsx @@ -21,8 +21,7 @@ import { i18n } from '@kbn/i18n'; import { css } from '@emotion/css'; import { AssistantAvatar } from '@kbn/observability-ai-assistant-plugin/public'; import { ChatActionsMenu } from './chat_actions_menu'; -import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router'; -import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors'; +import type { UseGenAIConnectorsResult } from '../hooks/use_genai_connectors'; import { FlyoutPositionMode } from './chat_flyout'; // needed to prevent InlineTextEdit component from expanding container @@ -50,7 +49,7 @@ export function ChatHeader({ onCopyConversation, onSaveTitle, onToggleFlyoutPositionMode, - onClose, + navigateToConversation, }: { connectors: UseGenAIConnectorsResult; conversationId?: string; @@ -61,36 +60,17 @@ export function ChatHeader({ onCopyConversation: () => void; onSaveTitle: (title: string) => void; onToggleFlyoutPositionMode?: (newFlyoutPositionMode: FlyoutPositionMode) => void; - onClose?: () => void; + navigateToConversation: (nextConversationId?: string) => void; }) { const theme = useEuiTheme(); const breakpoint = useCurrentEuiBreakpoint(); - const router = useObservabilityAIAssistantRouter(); - const [newTitle, setNewTitle] = useState(title); useEffect(() => { setNewTitle(title); }, [title]); - const handleNavigateToConversations = () => { - if (onClose) { - onClose(); - } - - if (conversationId) { - router.push('/conversations/{conversationId}', { - path: { - conversationId, - }, - query: {}, - }); - } else { - router.push('/conversations/new', { path: {}, query: {} }); - } - }; - const handleToggleFlyoutPositionMode = () => { if (flyoutPositionMode) { onToggleFlyoutPositionMode?.( @@ -126,10 +106,9 @@ export function ChatHeader({ className={css` color: ${!!title ? theme.euiTheme.colors.text : theme.euiTheme.colors.subduedText}; `} - inputAriaLabel={i18n.translate( - 'xpack.observabilityAiAssistant.chatHeader.editConversationInput', - { defaultMessage: 'Edit conversation' } - )} + inputAriaLabel={i18n.translate('xpack.aiAssistant.chatHeader.editConversationInput', { + defaultMessage: 'Edit conversation', + })} isReadOnly={ !conversationId || !connectors.selectedConnector || @@ -162,11 +141,11 @@ export function ChatHeader({ content={ flyoutPositionMode === 'overlay' ? i18n.translate( - 'xpack.observabilityAiAssistant.chatHeader.euiToolTip.flyoutModeLabel.dock', + 'xpack.aiAssistant.chatHeader.euiToolTip.flyoutModeLabel.dock', { defaultMessage: 'Dock chat' } ) : i18n.translate( - 'xpack.observabilityAiAssistant.chatHeader.euiToolTip.flyoutModeLabel.undock', + 'xpack.aiAssistant.chatHeader.euiToolTip.flyoutModeLabel.undock', { defaultMessage: 'Undock chat' } ) } @@ -174,7 +153,7 @@ export function ChatHeader({ > navigateToConversation(conversationId)} /> } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_inline_edit.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_inline_edit.tsx similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_inline_edit.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_inline_edit.tsx diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_item.tsx similarity index 98% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_item.tsx index a1f5d5eb88d2d..23bdbdaea3593 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_item.tsx @@ -22,11 +22,11 @@ import { Feedback, TelemetryEventTypeWithPayload, } from '@kbn/observability-ai-assistant-plugin/public'; +import { getRoleTranslation } from '../utils/get_role_translation'; import { ChatItemActions } from './chat_item_actions'; import { ChatItemAvatar } from './chat_item_avatar'; import { ChatItemContentInlinePromptEditor } from './chat_item_content_inline_prompt_editor'; import { ChatTimelineItem } from './chat_timeline'; -import { getRoleTranslation } from '../../utils/get_role_translation'; export interface ChatItemProps extends Omit { onActionClick: ChatActionClickHandler; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item_actions.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_item_actions.tsx similarity index 73% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item_actions.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_item_actions.tsx index 4995b0163b7be..cb1196baa6bc1 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item_actions.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_item_actions.tsx @@ -46,12 +46,9 @@ export function ChatItemActions({ <> {canEdit ? ( setIsPopoverOpen(undefined)} > - {i18n.translate( - 'xpack.observabilityAiAssistant.chatTimeline.actions.copyMessageSuccessful', - { - defaultMessage: 'Copied message', - } - )} + {i18n.translate('xpack.aiAssistant.chatTimeline.actions.copyMessageSuccessful', { + defaultMessage: 'Copied message', + })} ) : null} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item_avatar.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_item_avatar.tsx similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item_avatar.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_item_avatar.tsx diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item_content_inline_prompt_editor.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_item_content_inline_prompt_editor.tsx similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item_content_inline_prompt_editor.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_item_content_inline_prompt_editor.tsx diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item_title.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_item_title.tsx similarity index 71% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item_title.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_item_title.tsx index 2749ef3635f40..d6a26b0287e46 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_item_title.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_item_title.tsx @@ -7,6 +7,7 @@ import { euiThemeVars } from '@kbn/ui-theme'; import React, { ReactNode } from 'react'; +import { css } from '@emotion/react'; interface ChatItemTitleProps { actionsTrigger?: ReactNode; @@ -14,14 +15,15 @@ interface ChatItemTitleProps { } export function ChatItemTitle({ actionsTrigger, title }: ChatItemTitleProps) { + const containerCSS = css` + position: absolute; + top: 2; + right: ${euiThemeVars.euiSizeS}; + `; return ( <> {title} - {actionsTrigger ? ( -
- {actionsTrigger} -
- ) : null} + {actionsTrigger ?
{actionsTrigger}
: null} ); } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_timeline.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_timeline.stories.tsx similarity index 98% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_timeline.stories.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_timeline.stories.tsx index 88354f41ba293..0afb0c7e79fc0 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_timeline.stories.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_timeline.stories.tsx @@ -18,7 +18,7 @@ import { buildFunctionResponseMessage, buildSystemMessage, buildUserMessage, -} from '../../utils/builders'; +} from '../utils/builders'; import { ChatTimeline as Component, type ChatTimelineProps } from './chat_timeline'; export default { @@ -86,11 +86,11 @@ const defaultProps: ComponentProps = { Mathematical Functions: In mathematics, a function maps input values to corresponding output values based on a specific rule or expression. The general process of how a mathematical function works can be summarized as follows: Step 1: Input - You provide an input value to the function, denoted as 'x' in the notation f(x). This value represents the independent variable. - + Step 2: Processing - The function takes the input value and applies a specific rule or algorithm to it. This rule is defined by the function itself and varies depending on the function's expression. - + Step 3: Output - After processing the input, the function produces an output value, denoted as 'f(x)' or 'y'. This output represents the dependent variable and is the result of applying the function's rule to the input. - + Step 4: Uniqueness - A well-defined mathematical function ensures that each input value corresponds to exactly one output value. In other words, the function should yield the same output for the same input whenever it is called.`, }, }), diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_timeline.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/chat_timeline.tsx similarity index 96% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_timeline.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/chat_timeline.tsx index ec2cf2ca68e7c..9b349f49f3904 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/chat_timeline.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/chat_timeline.tsx @@ -18,10 +18,10 @@ import { type ObservabilityAIAssistantChatService, type TelemetryEventTypeWithPayload, } from '@kbn/observability-ai-assistant-plugin/public'; -import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; +import type { UseKnowledgeBaseResult } from '../hooks/use_knowledge_base'; import { ChatItem } from './chat_item'; import { ChatConsolidatedItems } from './chat_consolidated_items'; -import { getTimelineItemsfromConversation } from '../../utils/get_timeline_items_from_conversation'; +import { getTimelineItemsfromConversation } from '../utils/get_timeline_items_from_conversation'; export interface ChatTimelineItem extends Pick { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/conversation_list.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/conversation_list.stories.tsx similarity index 93% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/conversation_list.stories.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/conversation_list.stories.tsx index b0f72e80c5721..7405b477647fd 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/conversation_list.stories.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/conversation_list.stories.tsx @@ -7,8 +7,8 @@ import { ComponentMeta, ComponentStoryObj } from '@storybook/react'; import React from 'react'; -import { buildConversation } from '../../utils/builders'; -import { KibanaReactStorybookDecorator } from '../../utils/storybook_decorator.stories'; +import { buildConversation } from '../utils/builders'; +import { KibanaReactStorybookDecorator } from '../utils/storybook_decorator.stories'; import { ConversationList as Component } from './conversation_list'; type ConversationListProps = React.ComponentProps; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/conversation_list.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/conversation_list.tsx similarity index 80% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/conversation_list.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/conversation_list.tsx index 1b26922bcaf69..e4a7022edc763 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/conversation_list.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/conversation_list.tsx @@ -21,10 +21,9 @@ import { import { css } from '@emotion/css'; import { i18n } from '@kbn/i18n'; import React, { MouseEvent } from 'react'; -import { useConfirmModal } from '../../hooks/use_confirm_modal'; -import type { UseConversationListResult } from '../../hooks/use_conversation_list'; -import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router'; -import { EMPTY_CONVERSATION_TITLE } from '../../i18n'; +import { useConfirmModal } from '../hooks/use_confirm_modal'; +import type { UseConversationListResult } from '../hooks/use_conversation_list'; +import { EMPTY_CONVERSATION_TITLE } from '../i18n'; import { NewChatButton } from '../buttons/new_chat_button'; const titleClassName = css` @@ -51,15 +50,17 @@ export function ConversationList({ selectedConversationId, onConversationSelect, onConversationDeleteClick, + newConversationHref, + getConversationHref, }: { conversations: UseConversationListResult['conversations']; isLoading: boolean; selectedConversationId?: string; onConversationSelect?: (conversationId?: string) => void; onConversationDeleteClick: (conversationId: string) => void; + newConversationHref?: string; + getConversationHref?: (conversationId: string) => string; }) { - const router = useObservabilityAIAssistantRouter(); - const euiTheme = useEuiTheme(); const scrollBarStyles = euiScrollBarStyles(euiTheme); @@ -70,21 +71,15 @@ export function ConversationList({ `; const { element: confirmDeleteElement, confirm: confirmDeleteCallback } = useConfirmModal({ - title: i18n.translate('xpack.observabilityAiAssistant.flyout.confirmDeleteConversationTitle', { + title: i18n.translate('xpack.aiAssistant.flyout.confirmDeleteConversationTitle', { defaultMessage: 'Delete this conversation?', }), - children: i18n.translate( - 'xpack.observabilityAiAssistant.flyout.confirmDeleteConversationContent', - { - defaultMessage: 'This action cannot be undone.', - } - ), - confirmButtonText: i18n.translate( - 'xpack.observabilityAiAssistant.flyout.confirmDeleteButtonText', - { - defaultMessage: 'Delete conversation', - } - ), + children: i18n.translate('xpack.aiAssistant.flyout.confirmDeleteConversationContent', { + defaultMessage: 'This action cannot be undone.', + }), + confirmButtonText: i18n.translate('xpack.aiAssistant.flyout.confirmDeleteButtonText', { + defaultMessage: 'Delete conversation', + }), }); const displayedConversations = [ @@ -94,7 +89,7 @@ export function ConversationList({ id: '', label: EMPTY_CONVERSATION_TITLE, lastUpdated: '', - href: router.link('/conversations/new'), + href: newConversationHref, }, ] : []), @@ -102,11 +97,7 @@ export function ConversationList({ id: conversation.id, label: conversation.title, lastUpdated: conversation.last_updated, - href: router.link('/conversations/{conversationId}', { - path: { - conversationId: conversation.id, - }, - }), + href: getConversationHref ? getConversationHref(conversation.id) : undefined, })), ]; @@ -123,7 +114,7 @@ export function ConversationList({ - {i18n.translate('xpack.observabilityAiAssistant.conversationList.title', { + {i18n.translate('xpack.aiAssistant.conversationList.title', { defaultMessage: 'Previously', })} @@ -147,12 +138,9 @@ export function ConversationList({ - {i18n.translate( - 'xpack.observabilityAiAssistant.conversationList.errorMessage', - { - defaultMessage: 'Failed to load', - } - )} + {i18n.translate('xpack.aiAssistant.conversationList.errorMessage', { + defaultMessage: 'Failed to load', + })} @@ -185,7 +173,7 @@ export function ConversationList({ ? { iconType: 'trash', 'aria-label': i18n.translate( - 'xpack.observabilityAiAssistant.conversationList.deleteConversationIconLabel', + 'xpack.aiAssistant.conversationList.deleteConversationIconLabel', { defaultMessage: 'Delete', } @@ -211,12 +199,9 @@ export function ConversationList({ {!isLoading && !conversations.error && !displayedConversations?.length ? ( - {i18n.translate( - 'xpack.observabilityAiAssistant.conversationList.noConversations', - { - defaultMessage: 'No conversations', - } - )} + {i18n.translate('xpack.aiAssistant.conversationList.noConversations', { + defaultMessage: 'No conversations', + })} ) : null} @@ -228,7 +213,7 @@ export function ConversationList({ | MouseEvent ) => { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/disclaimer.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/disclaimer.tsx similarity index 91% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/disclaimer.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/disclaimer.tsx index e4eb5176469de..8f9c3abca0e71 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/disclaimer.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/disclaimer.tsx @@ -17,7 +17,7 @@ export function Disclaimer() { textAlign="center" data-test-subj="observabilityAiAssistantDisclaimer" > - {i18n.translate('xpack.observabilityAiAssistant.disclaimer.disclaimerLabel', { + {i18n.translate('xpack.aiAssistant.disclaimer.disclaimerLabel', { defaultMessage: "This chat is powered by an integration with your LLM provider. LLMs are known to sometimes present incorrect information as if it's correct. Elastic supports configuration and connection to the LLM provider and your knowledge base, but is not responsible for the LLM's responses.", })} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/function_list_popover.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/function_list_popover.stories.tsx similarity index 91% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/function_list_popover.stories.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/function_list_popover.stories.tsx index a8f1e23b8173d..62da0b2d14ff8 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/function_list_popover.stories.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/function_list_popover.stories.tsx @@ -7,7 +7,7 @@ import { ComponentStory } from '@storybook/react'; import React from 'react'; -import { KibanaReactStorybookDecorator } from '../../utils/storybook_decorator.stories'; +import { KibanaReactStorybookDecorator } from '../utils/storybook_decorator.stories'; import { FunctionListPopover as Component } from './function_list_popover'; export default { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/function_list_popover.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/function_list_popover.tsx similarity index 85% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/function_list_popover.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/function_list_popover.tsx index 16df72f48c91b..d24aae12fd8c6 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/function_list_popover.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/function_list_popover.tsx @@ -22,7 +22,7 @@ import type { EuiSelectableOptionCheckedType } from '@elastic/eui/src/components import { i18n } from '@kbn/i18n'; import { FunctionVisibility } from '@kbn/observability-ai-assistant-plugin/public'; import type { FunctionDefinition } from '@kbn/observability-ai-assistant-plugin/common'; -import { useObservabilityAIAssistantChatService } from '../../hooks/use_observability_ai_assistant_chat_service'; +import { useAIAssistantChatService } from '../hooks/use_ai_assistant_chat_service'; interface FunctionListOption { label: string; @@ -40,7 +40,7 @@ export function FunctionListPopover({ onSelectFunction: (func: string | undefined) => void; disabled: boolean; }) { - const { getFunctions } = useObservabilityAIAssistantChatService(); + const { getFunctions } = useAIAssistantChatService(); const functions = getFunctions(); const [functionOptions, setFunctionOptions] = useState< @@ -80,21 +80,18 @@ export function FunctionListPopover({ content={ mode === 'prompt' ? i18n.translate( - 'xpack.observabilityAiAssistant.functionListPopover.euiToolTip.selectAFunctionLabel', + 'xpack.aiAssistant.functionListPopover.euiToolTip.selectAFunctionLabel', { defaultMessage: 'Select a function' } ) - : i18n.translate( - 'xpack.observabilityAiAssistant.functionListPopover.euiToolTip.clearFunction', - { - defaultMessage: 'Clear function', - } - ) + : i18n.translate('xpack.aiAssistant.functionListPopover.euiToolTip.clearFunction', { + defaultMessage: 'Clear function', + }) } display="block" > - +

{UPGRADE_LICENSE_TITLE}

- {i18n.translate('xpack.observabilityAiAssistant.incorrectLicense.body', { + {i18n.translate('xpack.aiAssistant.incorrectLicense.body', { defaultMessage: 'You need an Enterprise license to use the Elastic AI Assistant.', })} @@ -57,12 +57,9 @@ export function IncorrectLicensePanel() { href="https://www.elastic.co/subscriptions" target="_blank" > - {i18n.translate( - 'xpack.observabilityAiAssistant.incorrectLicense.subscriptionPlansButton', - { - defaultMessage: 'Subscription plans', - } - )} + {i18n.translate('xpack.aiAssistant.incorrectLicense.subscriptionPlansButton', { + defaultMessage: 'Subscription plans', + })}
@@ -70,7 +67,7 @@ export function IncorrectLicensePanel() { data-test-subj="observabilityAiAssistantIncorrectLicensePanelManageLicenseButton" onClick={handleNavigateToLicenseManagement} > - {i18n.translate('xpack.observabilityAiAssistant.incorrectLicense.manageLicense', { + {i18n.translate('xpack.aiAssistant.incorrectLicense.manageLicense', { defaultMessage: 'Manage license', })} diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/index.ts b/x-pack/packages/kbn-ai-assistant/src/chat/index.ts new file mode 100644 index 0000000000000..4b04d7dec81c1 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/chat/index.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. + */ + +export * from './chat_body'; +export * from './chat_inline_edit'; +export * from './conversation_list'; +export * from './chat_flyout'; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/knowledge_base_callout.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/knowledge_base_callout.stories.tsx similarity index 95% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/knowledge_base_callout.stories.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/knowledge_base_callout.stories.tsx index d66729dc75a3d..e87aa161d80c3 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/knowledge_base_callout.stories.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/knowledge_base_callout.stories.tsx @@ -7,7 +7,7 @@ import { ComponentMeta, ComponentStoryObj } from '@storybook/react'; import { merge } from 'lodash'; -import { KibanaReactStorybookDecorator } from '../../utils/storybook_decorator.stories'; +import { KibanaReactStorybookDecorator } from '../utils/storybook_decorator.stories'; import { KnowledgeBaseCallout as Component } from './knowledge_base_callout'; const meta: ComponentMeta = { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/knowledge_base_callout.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/knowledge_base_callout.tsx similarity index 85% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/knowledge_base_callout.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/knowledge_base_callout.tsx index 36d6842286aa8..abb296713b2d2 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/knowledge_base_callout.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/knowledge_base_callout.tsx @@ -17,7 +17,7 @@ import { EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; +import { UseKnowledgeBaseResult } from '../hooks/use_knowledge_base'; export function KnowledgeBaseCallout({ knowledgeBase }: { knowledgeBase: UseKnowledgeBaseResult }) { let content: React.ReactNode; @@ -32,7 +32,7 @@ export function KnowledgeBaseCallout({ knowledgeBase }: { knowledgeBase: UseKnow - {i18n.translate('xpack.observabilityAiAssistant.checkingKbAvailability', { + {i18n.translate('xpack.aiAssistant.checkingKbAvailability', { defaultMessage: 'Checking availability of knowledge base', })} @@ -43,7 +43,7 @@ export function KnowledgeBaseCallout({ knowledgeBase }: { knowledgeBase: UseKnow color = 'danger'; content = ( - {i18n.translate('xpack.observabilityAiAssistant.failedToGetStatus', { + {i18n.translate('xpack.aiAssistant.failedToGetStatus', { defaultMessage: 'Failed to get model status.', })} @@ -53,7 +53,7 @@ export function KnowledgeBaseCallout({ knowledgeBase }: { knowledgeBase: UseKnow content = ( {' '} - {i18n.translate('xpack.observabilityAiAssistant.poweredByModel', { + {i18n.translate('xpack.aiAssistant.poweredByModel', { defaultMessage: 'Powered by {model}', values: { model: 'ELSER', @@ -70,7 +70,7 @@ export function KnowledgeBaseCallout({ knowledgeBase }: { knowledgeBase: UseKnow - {i18n.translate('xpack.observabilityAiAssistant.installingKb', { + {i18n.translate('xpack.aiAssistant.installingKb', { defaultMessage: 'Setting up the knowledge base', })} @@ -81,7 +81,7 @@ export function KnowledgeBaseCallout({ knowledgeBase }: { knowledgeBase: UseKnow color = 'danger'; content = ( - {i18n.translate('xpack.observabilityAiAssistant.failedToSetupKnowledgeBase', { + {i18n.translate('xpack.aiAssistant.failedToSetupKnowledgeBase', { defaultMessage: 'Failed to set up knowledge base.', })} @@ -96,7 +96,7 @@ export function KnowledgeBaseCallout({ knowledgeBase }: { knowledgeBase: UseKnow > {' '} - {i18n.translate('xpack.observabilityAiAssistant.setupKb', { + {i18n.translate('xpack.aiAssistant.setupKb', { defaultMessage: 'Improve your experience by setting up the knowledge base.', })} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/simulated_function_calling_callout.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/simulated_function_calling_callout.tsx similarity index 90% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/simulated_function_calling_callout.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/simulated_function_calling_callout.tsx index 41b14e683dd64..26eb589b25dfc 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/simulated_function_calling_callout.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/simulated_function_calling_callout.tsx @@ -17,7 +17,7 @@ export function SimulatedFunctionCallingCallout() { - {i18n.translate('xpack.observabilityAiAssistant.simulatedFunctionCallingCalloutLabel', { + {i18n.translate('xpack.aiAssistant.simulatedFunctionCallingCalloutLabel', { defaultMessage: 'Simulated function calling is enabled. You might see degradated performance.', })} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/starter_prompts.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/starter_prompts.tsx similarity index 85% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/starter_prompts.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/starter_prompts.tsx index 1f5402978d41d..faaecc0024135 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/starter_prompts.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/starter_prompts.tsx @@ -16,9 +16,9 @@ import { } from '@elastic/eui'; import { css } from '@emotion/css'; import { uniq } from 'lodash'; -import { useObservabilityAIAssistantAppService } from '../../hooks/use_observability_ai_assistant_app_service'; -import { useGenAIConnectors } from '../../hooks/use_genai_connectors'; -import { nonNullable } from '../../utils/non_nullable'; +import { useAIAssistantAppService } from '../hooks/use_ai_assistant_app_service'; +import { useGenAIConnectors } from '../hooks/use_genai_connectors'; +import { nonNullable } from '../utils/non_nullable'; const starterPromptClassName = css` max-width: 50%; @@ -30,7 +30,7 @@ const starterPromptInnerClassName = css` `; export function StarterPrompts({ onSelectPrompt }: { onSelectPrompt: (prompt: string) => void }) { - const service = useObservabilityAIAssistantAppService(); + const service = useAIAssistantAppService(); const { connectors } = useGenAIConnectors(); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/welcome_message.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message.tsx similarity index 83% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/welcome_message.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/welcome_message.tsx index 18f4c5598c6fd..a449235ba44e6 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/welcome_message.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message.tsx @@ -5,19 +5,19 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { css } from '@emotion/css'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, useCurrentEuiBreakpoint } from '@elastic/eui'; import type { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public'; import { GenerativeAIForObservabilityConnectorFeatureId } from '@kbn/actions-plugin/common'; import { isSupportedConnectorType } from '@kbn/observability-ai-assistant-plugin/public'; -import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; -import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors'; +import type { UseKnowledgeBaseResult } from '../hooks/use_knowledge_base'; +import type { UseGenAIConnectorsResult } from '../hooks/use_genai_connectors'; import { Disclaimer } from './disclaimer'; import { WelcomeMessageConnectors } from './welcome_message_connectors'; import { WelcomeMessageKnowledgeBase } from './welcome_message_knowledge_base'; -import { useKibana } from '../../hooks/use_kibana'; import { StarterPrompts } from './starter_prompts'; +import { useKibana } from '../hooks/use_kibana'; const fullHeightClassName = css` height: 100%; @@ -39,22 +39,15 @@ export function WelcomeMessage({ }) { const breakpoint = useCurrentEuiBreakpoint(); - const { - application: { navigateToApp, capabilities }, - plugins: { - start: { - triggersActionsUi: { getAddConnectorFlyout: ConnectorFlyout }, - }, - }, - } = useKibana().services; + const { application, triggersActionsUi } = useKibana().services; const [connectorFlyoutOpen, setConnectorFlyoutOpen] = useState(false); const handleConnectorClick = () => { - if (capabilities.management?.insightsAndAlerting?.triggersActions) { + if (application?.capabilities.management?.insightsAndAlerting?.triggersActions) { setConnectorFlyoutOpen(true); } else { - navigateToApp('management', { + application?.navigateToApp('management', { path: '/insightsAndAlerting/triggersActionsConnectors/connectors', }); } @@ -72,6 +65,11 @@ export function WelcomeMessage({ } }; + const ConnectorFlyout = useMemo( + () => triggersActionsUi.getAddConnectorFlyout, + [triggersActionsUi] + ); + return ( <> {isForbiddenError ? i18n.translate( - 'xpack.observabilityAiAssistant.welcomeMessageConnectors.connectorsForbiddenTextLabel', + 'xpack.aiAssistant.welcomeMessageConnectors.connectorsForbiddenTextLabel', { defaultMessage: 'Required privileges to get connectors are missing' } ) : i18n.translate( - 'xpack.observabilityAiAssistant.welcomeMessageConnectors.connectorsErrorTextLabel', + 'xpack.aiAssistant.welcomeMessageConnectors.connectorsErrorTextLabel', { defaultMessage: 'Could not load connectors' } )} @@ -72,21 +72,15 @@ export function WelcomeMessageConnectors({ return !connectors.loading && connectors.connectors?.length === 0 && onSetupConnectorClick ? (
- {i18n.translate( - 'xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.description2', - { - defaultMessage: - 'Start working with the Elastic AI Assistant by setting up a connector for your AI provider. The model needs to support function calls. When using OpenAI or Azure, we recommend using GPT4.', - } - )} + {i18n.translate('xpack.aiAssistant.initialSetupPanel.setupConnector.description2', { + defaultMessage: + 'Start working with the Elastic AI Assistant by setting up a connector for your AI provider. The model needs to support function calls. When using OpenAI or Azure, we recommend using GPT4.', + })} - {i18n.translate( - 'xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.buttonLabel', - { - defaultMessage: 'Set up GenAI connector', - } - )} + {i18n.translate('xpack.aiAssistant.initialSetupPanel.setupConnector.buttonLabel', { + defaultMessage: 'Set up GenAI connector', + })}
diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/welcome_message_knowledge_base.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message_knowledge_base.tsx similarity index 81% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/welcome_message_knowledge_base.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/welcome_message_knowledge_base.tsx index afdbed9ed4c43..72653473c41ae 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/welcome_message_knowledge_base.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message_knowledge_base.tsx @@ -22,8 +22,8 @@ import usePrevious from 'react-use/lib/usePrevious'; import useTimeoutFn from 'react-use/lib/useTimeoutFn'; import useInterval from 'react-use/lib/useInterval'; import { WelcomeMessageKnowledgeBaseSetupErrorPanel } from './welcome_message_knowledge_base_setup_error_panel'; -import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; -import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors'; +import type { UseKnowledgeBaseResult } from '../hooks/use_knowledge_base'; +import type { UseGenAIConnectorsResult } from '../hooks/use_genai_connectors'; export function WelcomeMessageKnowledgeBase({ connectors, @@ -80,13 +80,10 @@ export function WelcomeMessageKnowledgeBase({ {knowledgeBase.isInstalling ? ( <> - {i18n.translate( - 'xpack.observabilityAiAssistant.welcomeMessage.weAreSettingUpTextLabel', - { - defaultMessage: - 'We are setting up your knowledge base. This may take a few minutes. You can continue to use the Assistant while this process is underway.', - } - )} + {i18n.translate('xpack.aiAssistant.welcomeMessage.weAreSettingUpTextLabel', { + defaultMessage: + 'We are setting up your knowledge base. This may take a few minutes. You can continue to use the Assistant while this process is underway.', + })} @@ -96,10 +93,9 @@ export function WelcomeMessageKnowledgeBase({ isLoading onClick={noop} > - {i18n.translate( - 'xpack.observabilityAiAssistant.welcomeMessage.div.settingUpKnowledgeBaseLabel', - { defaultMessage: 'Setting up Knowledge base' } - )} + {i18n.translate('xpack.aiAssistant.welcomeMessage.div.settingUpKnowledgeBaseLabel', { + defaultMessage: 'Setting up Knowledge base', + })} ) : null} @@ -112,7 +108,7 @@ export function WelcomeMessageKnowledgeBase({ <> {i18n.translate( - 'xpack.observabilityAiAssistant.welcomeMessageKnowledgeBase.yourKnowledgeBaseIsNotSetUpCorrectlyLabel', + 'xpack.aiAssistant.welcomeMessageKnowledgeBase.yourKnowledgeBaseIsNotSetUpCorrectlyLabel', { defaultMessage: `Your Knowledge base hasn't been set up.` } )} @@ -130,12 +126,9 @@ export function WelcomeMessageKnowledgeBase({ iconType="importAction" onClick={handleRetryInstall} > - {i18n.translate( - 'xpack.observabilityAiAssistant.welcomeMessage.retryButtonLabel', - { - defaultMessage: 'Install Knowledge base', - } - )} + {i18n.translate('xpack.aiAssistant.welcomeMessage.retryButtonLabel', { + defaultMessage: 'Install Knowledge base', + })}
@@ -149,7 +142,7 @@ export function WelcomeMessageKnowledgeBase({ onClick={() => setIsPopoverOpen(!isPopoverOpen)} > {i18n.translate( - 'xpack.observabilityAiAssistant.welcomeMessage.inspectErrorsButtonEmptyLabel', + 'xpack.aiAssistant.welcomeMessage.inspectErrorsButtonEmptyLabel', { defaultMessage: 'Inspect issues' } )} @@ -180,7 +173,7 @@ export function WelcomeMessageKnowledgeBase({ {i18n.translate( - 'xpack.observabilityAiAssistant.welcomeMessage.knowledgeBaseSuccessfullyInstalledLabel', + 'xpack.aiAssistant.welcomeMessage.knowledgeBaseSuccessfullyInstalledLabel', { defaultMessage: 'Knowledge base successfully installed' } )} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/welcome_message_knowledge_base_setup_error_panel.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message_knowledge_base_setup_error_panel.tsx similarity index 80% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/welcome_message_knowledge_base_setup_error_panel.tsx rename to x-pack/packages/kbn-ai-assistant/src/chat/welcome_message_knowledge_base_setup_error_panel.tsx index a9a6fcff85240..eeff9c8afd7f3 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/chat/welcome_message_knowledge_base_setup_error_panel.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/welcome_message_knowledge_base_setup_error_panel.tsx @@ -21,8 +21,8 @@ import { EuiPanel, } from '@elastic/eui'; import { css } from '@emotion/css'; -import { useKibana } from '../../hooks/use_kibana'; -import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; +import { useKibana } from '../hooks/use_kibana'; +import type { UseKnowledgeBaseResult } from '../hooks/use_knowledge_base'; const panelContainerClassName = css` width: 330px; @@ -47,10 +47,9 @@ export function WelcomeMessageKnowledgeBaseSetupErrorPanel({ - {i18n.translate( - 'xpack.observabilityAiAssistant.welcomeMessage.issuesDescriptionListTitleLabel', - { defaultMessage: 'Issues' } - )} + {i18n.translate('xpack.aiAssistant.welcomeMessage.issuesDescriptionListTitleLabel', { + defaultMessage: 'Issues', + })} @@ -61,7 +60,7 @@ export function WelcomeMessageKnowledgeBaseSetupErrorPanel({
  • {' '} {modelName}, @@ -75,7 +74,7 @@ export function WelcomeMessageKnowledgeBaseSetupErrorPanel({
  • {' '} {modelName}, @@ -92,7 +91,7 @@ export function WelcomeMessageKnowledgeBaseSetupErrorPanel({
  • {' '} {modelName}, @@ -113,7 +112,7 @@ export function WelcomeMessageKnowledgeBaseSetupErrorPanel({ {i18n.translate( - 'xpack.observabilityAiAssistant.welcomeMessageKnowledgeBaseSetupErrorPanel.retryInstallingLinkLabel', + 'xpack.aiAssistant.welcomeMessageKnowledgeBaseSetupErrorPanel.retryInstallingLinkLabel', { defaultMessage: 'Retry install' } )} @@ -133,13 +132,12 @@ export function WelcomeMessageKnowledgeBaseSetupErrorPanel({ - {i18n.translate( - 'xpack.observabilityAiAssistant.welcomeMessage.trainedModelsLinkLabel', - { defaultMessage: 'Trained Models' } - )} + {i18n.translate('xpack.aiAssistant.welcomeMessage.trainedModelsLinkLabel', { + defaultMessage: 'Trained Models', + })} ), }} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/routes/conversations/conversation_view.tsx b/x-pack/packages/kbn-ai-assistant/src/conversation/conversation_view.tsx similarity index 71% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/routes/conversations/conversation_view.tsx rename to x-pack/packages/kbn-ai-assistant/src/conversation/conversation_view.tsx index da34c98b86fbc..260a7cb5c10ed 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/routes/conversations/conversation_view.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/conversation/conversation_view.tsx @@ -9,44 +9,44 @@ import { css } from '@emotion/css'; import { euiThemeVars } from '@kbn/ui-theme'; import React, { useEffect, useState } from 'react'; import ReactDOM from 'react-dom'; -import { useAbortableAsync } from '@kbn/observability-ai-assistant-plugin/public'; -import { ChatBody } from '../../components/chat/chat_body'; -import { ChatInlineEditingContent } from '../../components/chat/chat_inline_edit'; -import { ConversationList } from '../../components/chat/conversation_list'; -import { useCurrentUser } from '../../hooks/use_current_user'; -import { useGenAIConnectors } from '../../hooks/use_genai_connectors'; -import { useKnowledgeBase } from '../../hooks/use_knowledge_base'; -import { useObservabilityAIAssistantParams } from '../../hooks/use_observability_ai_assistant_params'; -import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router'; -import { useObservabilityAIAssistantAppService } from '../../hooks/use_observability_ai_assistant_app_service'; -import { useKibana } from '../../hooks/use_kibana'; -import { useConversationKey } from '../../hooks/use_conversation_key'; -import { useConversationList } from '../../hooks/use_conversation_list'; +import { useKibana } from '../hooks/use_kibana'; +import { ConversationList, ChatBody, ChatInlineEditingContent } from '../chat'; +import { useConversationKey } from '../hooks/use_conversation_key'; +import { useCurrentUser } from '../hooks/use_current_user'; +import { useGenAIConnectors } from '../hooks/use_genai_connectors'; +import { useKnowledgeBase } from '../hooks/use_knowledge_base'; +import { useAIAssistantAppService } from '../hooks/use_ai_assistant_app_service'; +import { useAbortableAsync } from '../hooks/use_abortable_async'; +import { useConversationList } from '../hooks/use_conversation_list'; const SECOND_SLOT_CONTAINER_WIDTH = 400; -export function ConversationView() { +interface ConversationViewProps { + conversationId?: string; + navigateToConversation: (nextConversationId?: string) => void; + getConversationHref?: (conversationId: string) => string; + newConversationHref?: string; +} + +export const ConversationView: React.FC = ({ + conversationId, + navigateToConversation, + getConversationHref, + newConversationHref, +}) => { const { euiTheme } = useEuiTheme(); const currentUser = useCurrentUser(); - const service = useObservabilityAIAssistantAppService(); + const service = useAIAssistantAppService(); const connectors = useGenAIConnectors(); const knowledgeBase = useKnowledgeBase(); - const observabilityAIAssistantRouter = useObservabilityAIAssistantRouter(); - - const { path } = useObservabilityAIAssistantParams('/conversations/*'); - const { services: { - plugins: { - start: { - observabilityAIAssistant: { ObservabilityAIAssistantChatServiceContext }, - }, - }, + observabilityAIAssistant: { ObservabilityAIAssistantChatServiceContext }, }, } = useKibana(); @@ -57,8 +57,6 @@ export function ConversationView() { [service] ); - const conversationId = 'conversationId' in path ? path.conversationId : undefined; - const { key: bodyKey, updateConversationIdInPlace } = useConversationKey(conversationId); const [secondSlotContainer, setSecondSlotContainer] = useState(null); @@ -66,19 +64,6 @@ export function ConversationView() { const conversationList = useConversationList(); - function navigateToConversation(nextConversationId?: string) { - if (nextConversationId) { - observabilityAIAssistantRouter.push('/conversations/{conversationId}', { - path: { - conversationId: nextConversationId, - }, - query: {}, - }); - } else { - observabilityAIAssistantRouter.push('/conversations/new', { path: {}, query: {} }); - } - } - function handleRefreshConversations() { conversationList.conversations.refresh(); } @@ -153,6 +138,9 @@ export function ConversationView() { } }); }} + newConversationHref={newConversationHref} + onConversationSelect={navigateToConversation} + getConversationHref={getConversationHref} /> @@ -176,6 +164,7 @@ export function ConversationView() { knowledgeBase={knowledgeBase} showLinkToConversationsApp={false} onConversationUpdate={handleConversationUpdate} + navigateToConversation={navigateToConversation} />
    @@ -189,4 +178,4 @@ export function ConversationView() { )} ); -} +}; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_chat.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_chat.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_chat.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_chat.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_conversation.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_conversation.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_conversation.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_conversation.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_conversation_list.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_conversation_list.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_conversation_list.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_conversation_list.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_conversations.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_conversations.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_conversations.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_conversations.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_current_user.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_current_user.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_current_user.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_current_user.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_genai_connectors.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_genai_connectors.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_genai_connectors.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_genai_connectors.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_knowledge_base.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_knowledge_base.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_knowledge_base.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/__storybook_mocks__/use_knowledge_base.ts diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/index.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/index.ts new file mode 100644 index 0000000000000..ee630d1caec82 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/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 './use_ai_assistant_app_service'; +export * from './use_ai_assistant_chat_service'; +export * from './use_knowledge_base'; diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/use_abortable_async.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_abortable_async.ts new file mode 100644 index 0000000000000..433ca877b0f62 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_abortable_async.ts @@ -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 { isPromise } from '@kbn/std'; +import { useEffect, useMemo, useRef, useState } from 'react'; + +interface State { + error?: Error; + value?: T; + loading: boolean; +} + +export type AbortableAsyncState = (T extends Promise + ? State + : State) & { refresh: () => void }; + +export function useAbortableAsync( + fn: ({}: { signal: AbortSignal }) => T | Promise, + deps: any[], + options?: { clearValueOnNext?: boolean; defaultValue?: () => T } +): AbortableAsyncState { + const clearValueOnNext = options?.clearValueOnNext; + + const controllerRef = useRef(new AbortController()); + + const [refreshId, setRefreshId] = useState(0); + + const [error, setError] = useState(); + const [loading, setLoading] = useState(false); + const [value, setValue] = useState(options?.defaultValue); + + useEffect(() => { + controllerRef.current.abort(); + + const controller = new AbortController(); + controllerRef.current = controller; + + if (clearValueOnNext) { + setValue(undefined); + setError(undefined); + } + + try { + const response = fn({ signal: controller.signal }); + if (isPromise(response)) { + setLoading(true); + response + .then((nextValue) => { + setError(undefined); + setValue(nextValue); + }) + .catch((err) => { + setValue(undefined); + setError(err); + }) + .finally(() => setLoading(false)); + } else { + setError(undefined); + setValue(response); + setLoading(false); + } + } catch (err) { + setValue(undefined); + setError(err); + setLoading(false); + } + + return () => { + controller.abort(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, deps.concat(refreshId, clearValueOnNext)); + + return useMemo>(() => { + return { + error, + loading, + value, + refresh: () => { + setRefreshId((id) => id + 1); + }, + } as unknown as AbortableAsyncState; + }, [error, value, loading]); +} diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/use_ai_assistant_app_service.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_ai_assistant_app_service.ts new file mode 100644 index 0000000000000..bb1f93079eb09 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_ai_assistant_app_service.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 { useKibana } from './use_kibana'; + +export function useAIAssistantAppService() { + const { services } = useKibana(); + + if (!services.observabilityAIAssistant?.service) { + throw new Error( + 'AI Assistant Service is not available. Did you provide this service in your plugin contract?' + ); + } + + return services.observabilityAIAssistant.service; +} diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/use_ai_assistant_chat_service.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_ai_assistant_chat_service.ts new file mode 100644 index 0000000000000..a3eefef196901 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_ai_assistant_chat_service.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useKibana } from './use_kibana'; + +export function useAIAssistantChatService() { + const { + services: { observabilityAIAssistant }, + } = useKibana(); + + return observabilityAIAssistant.useObservabilityAIAssistantChatService(); +} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_confirm_modal.tsx b/x-pack/packages/kbn-ai-assistant/src/hooks/use_confirm_modal.tsx similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_confirm_modal.tsx rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_confirm_modal.tsx diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_conversation.test.tsx b/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.test.tsx similarity index 94% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_conversation.test.tsx rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.test.tsx index 150847a011207..4c4ced36c8796 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_conversation.test.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.test.tsx @@ -19,9 +19,8 @@ import { StreamingChatResponseEventType, StreamingChatResponseEventWithoutError, } from '@kbn/observability-ai-assistant-plugin/common'; -import { ObservabilityAIAssistantAppServiceProvider } from '../context/observability_ai_assistant_app_service_provider'; import { EMPTY_CONVERSATION_TITLE } from '../i18n'; -import type { ObservabilityAIAssistantAppService } from '../service/create_app_service'; +import type { AIAssistantAppService } from '../service/create_app_service'; import { useConversation, type UseConversationProps, @@ -35,9 +34,9 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; let hookResult: RenderHookResult; -type MockedService = DeeplyMockedKeys> & { +type MockedService = DeeplyMockedKeys> & { conversations: DeeplyMockedKeys< - Omit + Omit > & { predefinedConversation$: Observable; }; @@ -66,18 +65,15 @@ const useKibanaMockServices = { uiSettings: { get: jest.fn(), }, - plugins: { - start: { - observabilityAIAssistant: { - useChat: createUseChat({ - notifications: { - toasts: { - addError: addErrorMock, - }, - } as unknown as NotificationsStart, - }), - }, - }, + observabilityAIAssistant: { + useChat: createUseChat({ + notifications: { + toasts: { + addError: addErrorMock, + }, + } as unknown as NotificationsStart, + }), + service: mockService, }, }; @@ -87,11 +83,7 @@ describe('useConversation', () => { beforeEach(() => { jest.clearAllMocks(); wrapper = ({ children }: PropsWithChildren) => ( - - - {children} - - + {children} ); }); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_conversation.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.ts similarity index 91% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_conversation.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.ts index 617b1b302473f..744e071d5b1ba 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_conversation.ts +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.ts @@ -12,16 +12,14 @@ import type { ConversationCreateRequest, Message, } from '@kbn/observability-ai-assistant-plugin/common'; -import { - ObservabilityAIAssistantChatService, - useAbortableAsync, -} from '@kbn/observability-ai-assistant-plugin/public'; +import type { ObservabilityAIAssistantChatService } from '@kbn/observability-ai-assistant-plugin/public'; import type { AbortableAsyncState } from '@kbn/observability-ai-assistant-plugin/public'; import type { UseChatResult } from '@kbn/observability-ai-assistant-plugin/public'; import { EMPTY_CONVERSATION_TITLE } from '../i18n'; +import { useAIAssistantAppService } from './use_ai_assistant_app_service'; import { useKibana } from './use_kibana'; import { useOnce } from './use_once'; -import { useObservabilityAIAssistantAppService } from './use_observability_ai_assistant_app_service'; +import { useAbortableAsync } from './use_abortable_async'; function createNewConversation({ title = EMPTY_CONVERSATION_TITLE, @@ -62,17 +60,13 @@ export function useConversation({ connectorId, onConversationUpdate, }: UseConversationProps): UseConversationResult { - const service = useObservabilityAIAssistantAppService(); + const service = useAIAssistantAppService(); const { scope } = service; const { services: { notifications, - plugins: { - start: { - observabilityAIAssistant: { useChat }, - }, - }, + observabilityAIAssistant: { useChat }, }, } = useKibana(); @@ -106,8 +100,8 @@ export function useConversation({ }, }) .catch((err) => { - notifications.toasts.addError(err, { - title: i18n.translate('xpack.observabilityAiAssistant.errorUpdatingConversation', { + notifications!.toasts.addError(err, { + title: i18n.translate('xpack.aiAssistant.errorUpdatingConversation', { defaultMessage: 'Could not update conversation', }), }); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_conversation_key.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation_key.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_conversation_key.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation_key.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_conversation_list.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation_list.ts similarity index 86% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_conversation_list.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation_list.ts index 6fa6bc02e7b35..d0db7665a30b6 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_conversation_list.ts +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation_list.ts @@ -12,9 +12,8 @@ import { type Conversation, useAbortableAsync, } from '@kbn/observability-ai-assistant-plugin/public'; +import { useAIAssistantAppService } from './use_ai_assistant_app_service'; import { useKibana } from './use_kibana'; -import { useObservabilityAIAssistantAppService } from './use_observability_ai_assistant_app_service'; - export interface UseConversationListResult { isLoading: boolean; conversations: AbortableAsyncState<{ conversations: Conversation[] }>; @@ -22,7 +21,7 @@ export interface UseConversationListResult { } export function useConversationList(): UseConversationListResult { - const service = useObservabilityAIAssistantAppService(); + const service = useAIAssistantAppService(); const [isUpdatingList, setIsUpdatingList] = useState(false); @@ -62,8 +61,8 @@ export function useConversationList(): UseConversationListResult { conversations.refresh(); } catch (err) { - notifications.toasts.addError(err, { - title: i18n.translate('xpack.observabilityAiAssistant.flyout.failedToDeleteConversation', { + notifications!.toasts.addError(err, { + title: i18n.translate('xpack.aiAssistant.flyout.failedToDeleteConversation', { defaultMessage: 'Could not delete conversation', }), }); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_current_user.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_current_user.ts similarity index 84% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_current_user.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_current_user.ts index 82c13eb876117..c169358653a49 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_current_user.ts +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_current_user.ts @@ -5,9 +5,9 @@ * 2.0. */ +import { useKibana } from '@kbn/kibana-react-plugin/public'; import { AuthenticatedUser } from '@kbn/security-plugin/common'; import { useEffect, useState } from 'react'; -import { useKibana } from './use_kibana'; export function useCurrentUser() { const { @@ -19,7 +19,7 @@ export function useCurrentUser() { useEffect(() => { const getCurrentUser = async () => { try { - const authenticatedUser = await security.authc.getCurrentUser(); + const authenticatedUser = await security!.authc.getCurrentUser(); setUser(authenticatedUser); } catch { setUser(undefined); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_genai_connectors.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_genai_connectors.ts similarity index 66% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_genai_connectors.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_genai_connectors.ts index 1b105513a2323..642bf9488f186 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_genai_connectors.ts +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_genai_connectors.ts @@ -5,16 +5,13 @@ * 2.0. */ -import { useKibana } from './use_kibana'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { AIAssistantPluginStartDependencies } from '../types'; export function useGenAIConnectors() { const { - services: { - plugins: { - start: { observabilityAIAssistant }, - }, - }, - } = useKibana(); + services: { observabilityAIAssistant }, + } = useKibana(); return observabilityAIAssistant.useGenAIConnectors(); } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_json_editor_model.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_json_editor_model.ts similarity index 92% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_json_editor_model.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_json_editor_model.ts index 6f4535d84acef..1b14c504d935d 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_json_editor_model.ts +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_json_editor_model.ts @@ -7,7 +7,7 @@ import { useEffect, useMemo, useState } from 'react'; import { monaco } from '@kbn/monaco'; import { createInitializedObject } from '../utils/create_initialized_object'; -import { useObservabilityAIAssistantChatService } from './use_observability_ai_assistant_chat_service'; +import { useAIAssistantChatService } from './use_ai_assistant_chat_service'; import { safeJsonParse } from '../utils/safe_json_parse'; const { editor, languages, Uri } = monaco; @@ -19,7 +19,7 @@ export const useJsonEditorModel = ({ functionName: string | undefined; initialJson?: string | undefined; }) => { - const chatService = useObservabilityAIAssistantChatService(); + const chatService = useAIAssistantChatService(); const functionDefinition = chatService.getFunctions().find((func) => func.name === functionName); diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/use_kibana.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_kibana.ts new file mode 100644 index 0000000000000..44aec48a06467 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_kibana.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. + */ + +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { AIAssistantPluginStartDependencies } from '../types'; + +const useTypedKibana = () => useKibana(); + +export { useTypedKibana as useKibana }; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_knowledge_base.tsx b/x-pack/packages/kbn-ai-assistant/src/hooks/use_knowledge_base.tsx similarity index 82% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_knowledge_base.tsx rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_knowledge_base.tsx index bca9b38485695..0b949fcdbff0e 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_knowledge_base.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_knowledge_base.tsx @@ -15,7 +15,7 @@ import { useAbortableAsync, } from '@kbn/observability-ai-assistant-plugin/public'; import { useKibana } from './use_kibana'; -import { useObservabilityAIAssistantAppService } from './use_observability_ai_assistant_app_service'; +import { useAIAssistantAppService } from './use_ai_assistant_app_service'; export interface UseKnowledgeBaseResult { status: AbortableAsyncState<{ @@ -31,13 +31,8 @@ export interface UseKnowledgeBaseResult { } export function useKnowledgeBase(): UseKnowledgeBaseResult { - const { - notifications: { toasts }, - plugins: { - start: { ml }, - }, - } = useKibana().services; - const service = useObservabilityAIAssistantAppService(); + const { notifications, ml } = useKibana().services; + const service = useAIAssistantAppService(); const status = useAbortableAsync( ({ signal }) => { @@ -75,8 +70,8 @@ export function useKnowledgeBase(): UseKnowledgeBaseResult { return install(); } setInstallError(error); - toasts.addError(error, { - title: i18n.translate('xpack.observabilityAiAssistant.errorSettingUpKnowledgeBase', { + notifications!.toasts.addError(error, { + title: i18n.translate('xpack.aiAssistant.errorSettingUpKnowledgeBase', { defaultMessage: 'Could not set up Knowledge Base', }), }); @@ -92,5 +87,5 @@ export function useKnowledgeBase(): UseKnowledgeBaseResult { isInstalling, installError, }; - }, [status, isInstalling, installError, service, ml.mlApi?.savedObjects, toasts]); + }, [status, isInstalling, installError, service, ml, notifications]); } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_last_used_prompts.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_last_used_prompts.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_last_used_prompts.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_last_used_prompts.ts diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/use_license.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_license.ts new file mode 100644 index 0000000000000..6d146274c7f4d --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_license.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ILicense, LicenseType } from '@kbn/licensing-plugin/public'; +import { useCallback } from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { useKibana } from './use_kibana'; + +interface UseLicenseReturnValue { + getLicense: () => ILicense | null; + hasAtLeast: (level: LicenseType) => boolean | undefined; +} + +export const useLicense = (): UseLicenseReturnValue => { + const { + services: { licensing }, + } = useKibana(); + + const license = useObservable(licensing.license$); + + return { + getLicense: () => license ?? null, + hasAtLeast: useCallback( + (level: LicenseType) => { + if (!license) return; + + return !!license && license.isAvailable && license.isActive && license.hasAtLeast(level); + }, + [license] + ), + }; +}; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_license_management_locator.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_license_management_locator.ts similarity index 89% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_license_management_locator.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_license_management_locator.ts index 1d5dd04203352..7e650affa2ca5 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_license_management_locator.ts +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_license_management_locator.ts @@ -11,11 +11,7 @@ const LICENSE_MANAGEMENT_LOCATOR = 'LICENSE_MANAGEMENT_LOCATOR'; export const useLicenseManagementLocator = () => { const { - services: { - plugins: { - start: { share }, - }, - }, + services: { share }, } = useKibana(); const locator = share.url.locators.get(LICENSE_MANAGEMENT_LOCATOR); diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/use_local_storage.test.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_local_storage.test.ts new file mode 100644 index 0000000000000..ab1d00392fdb9 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_local_storage.test.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { useLocalStorage } from './use_local_storage'; + +describe('useLocalStorage', () => { + const key = 'testKey'; + const defaultValue = 'defaultValue'; + + beforeEach(() => { + localStorage.clear(); + }); + + it('should return the default value when local storage is empty', () => { + const { result } = renderHook(() => useLocalStorage(key, defaultValue)); + const [item] = result.current; + + expect(item).toBe(defaultValue); + }); + + it('should return the stored value when local storage has a value', () => { + const storedValue = 'storedValue'; + localStorage.setItem(key, JSON.stringify(storedValue)); + const { result } = renderHook(() => useLocalStorage(key, defaultValue)); + const [item] = result.current; + + expect(item).toBe(storedValue); + }); + + it('should save the value to local storage', () => { + const { result } = renderHook(() => useLocalStorage(key, defaultValue)); + const [, saveToStorage] = result.current; + const newValue = 'newValue'; + + act(() => { + saveToStorage(newValue); + }); + + expect(JSON.parse(localStorage.getItem(key) || '')).toBe(newValue); + }); + + it('should remove the value from local storage when the value is undefined', () => { + const { result } = renderHook(() => useLocalStorage(key, defaultValue)); + const [, saveToStorage] = result.current; + + act(() => { + saveToStorage(undefined as unknown as string); + }); + + expect(localStorage.getItem(key)).toBe(null); + }); + + it('should listen for storage events to window, and remove the listener upon unmount', () => { + const addEventListenerSpy = jest.spyOn(window, 'addEventListener'); + const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); + + const { unmount } = renderHook(() => useLocalStorage(key, defaultValue)); + + expect(addEventListenerSpy).toHaveBeenCalled(); + + const eventTypes = addEventListenerSpy.mock.calls; + + expect(eventTypes).toContainEqual(['storage', expect.any(Function)]); + + unmount(); + + expect(removeEventListenerSpy).toHaveBeenCalled(); + + addEventListenerSpy.mockRestore(); + removeEventListenerSpy.mockRestore(); + }); +}); diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/use_local_storage.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_local_storage.ts new file mode 100644 index 0000000000000..ea9e13163e4b0 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_local_storage.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 { useState, useEffect, useMemo, useCallback } from 'react'; + +export function useLocalStorage(key: string, defaultValue: T) { + // This is necessary to fix a race condition issue. + // It guarantees that the latest value will be always returned after the value is updated + const [storageUpdate, setStorageUpdate] = useState(0); + + const item = useMemo(() => { + return getFromStorage(key, defaultValue); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [key, storageUpdate, defaultValue]); + + const saveToStorage = useCallback( + (value: T) => { + if (value === undefined) { + window.localStorage.removeItem(key); + } else { + window.localStorage.setItem(key, JSON.stringify(value)); + setStorageUpdate(storageUpdate + 1); + } + }, + [key, storageUpdate] + ); + + useEffect(() => { + function onUpdate(event: StorageEvent) { + if (event.key === key) { + setStorageUpdate(storageUpdate + 1); + } + } + window.addEventListener('storage', onUpdate); + return () => { + window.removeEventListener('storage', onUpdate); + }; + }, [key, setStorageUpdate, storageUpdate]); + + return useMemo(() => [item, saveToStorage] as const, [item, saveToStorage]); +} + +function getFromStorage(keyName: string, defaultValue: T) { + const storedItem = window.localStorage.getItem(keyName); + + if (storedItem !== null) { + try { + return JSON.parse(storedItem) as T; + } catch (err) { + window.localStorage.removeItem(keyName); + // eslint-disable-next-line no-console + console.log(`Unable to decode: ${keyName}`); + } + } + return defaultValue; +} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_once.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_once.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_once.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_once.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_simulated_function_calling.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_simulated_function_calling.ts similarity index 89% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_simulated_function_calling.ts rename to x-pack/packages/kbn-ai-assistant/src/hooks/use_simulated_function_calling.ts index 4d441b03a3ddc..4515f2126dbfd 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_simulated_function_calling.ts +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_simulated_function_calling.ts @@ -13,7 +13,7 @@ export function useSimulatedFunctionCalling() { services: { uiSettings }, } = useKibana(); - const simulatedFunctionCallingEnabled = uiSettings.get( + const simulatedFunctionCallingEnabled = uiSettings!.get( aiAssistantSimulatedFunctionCalling, false ); diff --git a/x-pack/packages/kbn-ai-assistant/src/i18n.ts b/x-pack/packages/kbn-ai-assistant/src/i18n.ts new file mode 100644 index 0000000000000..5c5be1633a07a --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/i18n.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 { i18n } from '@kbn/i18n'; + +export const ASSISTANT_SETUP_TITLE = i18n.translate('xpack.aiAssistant.assistantSetup.title', { + defaultMessage: 'Welcome to the Elastic AI Assistant', +}); + +export const EMPTY_CONVERSATION_TITLE = i18n.translate('xpack.aiAssistant.emptyConversationTitle', { + defaultMessage: 'New conversation', +}); + +export const UPGRADE_LICENSE_TITLE = i18n.translate('xpack.aiAssistant.incorrectLicense.title', { + defaultMessage: 'Upgrade your license', +}); diff --git a/x-pack/packages/kbn-ai-assistant/src/index.ts b/x-pack/packages/kbn-ai-assistant/src/index.ts new file mode 100644 index 0000000000000..ba2265e88715f --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/index.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. + */ + +export * from './conversation/conversation_view'; +export * from './service/create_app_service'; +export * from './hooks'; +export * from './chat'; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/prompt_editor/prompt_editor.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/prompt_editor/prompt_editor.stories.tsx similarity index 96% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/prompt_editor/prompt_editor.stories.tsx rename to x-pack/packages/kbn-ai-assistant/src/prompt_editor/prompt_editor.stories.tsx index f951653b152cc..ed2948e50f15e 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/prompt_editor/prompt_editor.stories.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/prompt_editor/prompt_editor.stories.tsx @@ -8,13 +8,13 @@ import React from 'react'; import { ComponentStory, ComponentStoryObj } from '@storybook/react'; import { MessageRole } from '@kbn/observability-ai-assistant-plugin/public'; +import { KibanaReactStorybookDecorator } from '../utils/storybook_decorator.stories'; import { PromptEditor as Component, PromptEditorProps } from './prompt_editor'; -import { KibanaReactStorybookDecorator } from '../../utils/storybook_decorator.stories'; /* JSON Schema validation in the PromptEditor compponent does not work when rendering the component from within Storybook. - + */ export default { component: Component, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/prompt_editor/prompt_editor.tsx b/x-pack/packages/kbn-ai-assistant/src/prompt_editor/prompt_editor.tsx similarity index 97% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/prompt_editor/prompt_editor.tsx rename to x-pack/packages/kbn-ai-assistant/src/prompt_editor/prompt_editor.tsx index db7f3a8f11888..cc2fe761d6176 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/prompt_editor/prompt_editor.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/prompt_editor/prompt_editor.tsx @@ -14,10 +14,10 @@ import { type TelemetryEventTypeWithPayload, ObservabilityAIAssistantTelemetryEventType, } from '@kbn/observability-ai-assistant-plugin/public'; +import { useLastUsedPrompts } from '../hooks/use_last_used_prompts'; import { FunctionListPopover } from '../chat/function_list_popover'; import { PromptEditorFunction } from './prompt_editor_function'; import { PromptEditorNaturalLanguage } from './prompt_editor_natural_language'; -import { useLastUsedPrompts } from '../../hooks/use_last_used_prompts'; export interface PromptEditorProps { disabled: boolean; @@ -194,7 +194,7 @@ export function PromptEditor({ {functionName} {chatService.renderFunction(props.name, props.arguments, props.response, props.onActionClick)} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/service/create_app_service.ts b/x-pack/packages/kbn-ai-assistant/src/service/create_app_service.ts similarity index 63% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/service/create_app_service.ts rename to x-pack/packages/kbn-ai-assistant/src/service/create_app_service.ts index dfb9b703bc4ed..bd01ab39a6d5c 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/service/create_app_service.ts +++ b/x-pack/packages/kbn-ai-assistant/src/service/create_app_service.ts @@ -6,15 +6,15 @@ */ import type { ObservabilityAIAssistantService } from '@kbn/observability-ai-assistant-plugin/public'; -import type { ObservabilityAIAssistantAppPluginStartDependencies } from '../types'; +import { AIAssistantPluginStartDependencies } from '../types'; -export type ObservabilityAIAssistantAppService = ObservabilityAIAssistantService; +export type AIAssistantAppService = ObservabilityAIAssistantService; export function createAppService({ pluginsStart, }: { - pluginsStart: ObservabilityAIAssistantAppPluginStartDependencies; -}): ObservabilityAIAssistantAppService { + pluginsStart: AIAssistantPluginStartDependencies; +}): AIAssistantAppService { return { ...pluginsStart.observabilityAIAssistant.service, }; diff --git a/x-pack/packages/kbn-ai-assistant/src/types/index.ts b/x-pack/packages/kbn-ai-assistant/src/types/index.ts new file mode 100644 index 0000000000000..afebbafd7e643 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/types/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. + */ + +import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; +import type { MlPluginStart } from '@kbn/ml-plugin/public'; +import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; +import type { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public'; + +export interface AIAssistantPluginStartDependencies { + licensing: LicensingPluginStart; + ml: MlPluginStart; + observabilityAIAssistant: ObservabilityAIAssistantPublicStart; + share: SharePluginStart; + triggersActionsUi: TriggersAndActionsUIPublicPluginStart; +} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/builders.ts b/x-pack/packages/kbn-ai-assistant/src/utils/builders.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/builders.ts rename to x-pack/packages/kbn-ai-assistant/src/utils/builders.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/create_initialized_object.test.ts b/x-pack/packages/kbn-ai-assistant/src/utils/create_initialized_object.test.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/create_initialized_object.test.ts rename to x-pack/packages/kbn-ai-assistant/src/utils/create_initialized_object.test.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/create_initialized_object.ts b/x-pack/packages/kbn-ai-assistant/src/utils/create_initialized_object.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/create_initialized_object.ts rename to x-pack/packages/kbn-ai-assistant/src/utils/create_initialized_object.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/create_mock_chat_service.ts b/x-pack/packages/kbn-ai-assistant/src/utils/create_mock_chat_service.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/create_mock_chat_service.ts rename to x-pack/packages/kbn-ai-assistant/src/utils/create_mock_chat_service.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_role_translation.ts b/x-pack/packages/kbn-ai-assistant/src/utils/get_role_translation.ts similarity index 61% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_role_translation.ts rename to x-pack/packages/kbn-ai-assistant/src/utils/get_role_translation.ts index f74c9f842e402..95421a089dea0 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_role_translation.ts +++ b/x-pack/packages/kbn-ai-assistant/src/utils/get_role_translation.ts @@ -10,21 +10,18 @@ import { MessageRole } from '@kbn/observability-ai-assistant-plugin/public'; export function getRoleTranslation(role: MessageRole) { if (role === MessageRole.User) { - return i18n.translate('xpack.observabilityAiAssistant.chatTimeline.messages.user.label', { + return i18n.translate('xpack.aiAssistant.chatTimeline.messages.user.label', { defaultMessage: 'You', }); } if (role === MessageRole.System) { - return i18n.translate('xpack.observabilityAiAssistant.chatTimeline.messages.system.label', { + return i18n.translate('xpack.aiAssistant.chatTimeline.messages.system.label', { defaultMessage: 'System', }); } - return i18n.translate( - 'xpack.observabilityAiAssistant.chatTimeline.messages.elasticAssistant.label', - { - defaultMessage: 'Elastic Assistant', - } - ); + return i18n.translate('xpack.aiAssistant.chatTimeline.messages.elasticAssistant.label', { + defaultMessage: 'Elastic Assistant', + }); } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_timeline_items_from_conversation.test.tsx b/x-pack/packages/kbn-ai-assistant/src/utils/get_timeline_items_from_conversation.test.tsx similarity index 98% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_timeline_items_from_conversation.test.tsx rename to x-pack/packages/kbn-ai-assistant/src/utils/get_timeline_items_from_conversation.test.tsx index 6fb7e1a323d08..337c11419209e 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_timeline_items_from_conversation.test.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/utils/get_timeline_items_from_conversation.test.tsx @@ -23,12 +23,8 @@ function Providers({ children }: { children: React.ReactElement }) { mockChatService, - }, - }, + observabilityAIAssistant: { + useObservabilityAIAssistantChatService: () => mockChatService, }, }} > diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_timeline_items_from_conversation.tsx b/x-pack/packages/kbn-ai-assistant/src/utils/get_timeline_items_from_conversation.tsx similarity index 94% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_timeline_items_from_conversation.tsx rename to x-pack/packages/kbn-ai-assistant/src/utils/get_timeline_items_from_conversation.tsx index 9a3fed770b944..999ac4f095025 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_timeline_items_from_conversation.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/utils/get_timeline_items_from_conversation.tsx @@ -18,9 +18,9 @@ import { ObservabilityAIAssistantChatService, } from '@kbn/observability-ai-assistant-plugin/public'; import type { ChatActionClickPayload } from '@kbn/observability-ai-assistant-plugin/public'; -import type { ChatTimelineItem } from '../components/chat/chat_timeline'; -import { RenderFunction } from '../components/render_function'; +import { RenderFunction } from '../render_function'; import { safeJsonParse } from './safe_json_parse'; +import type { ChatTimelineItem } from '../chat/chat_timeline'; function convertMessageToMarkdownCodeBlock(message: Message['message']) { let value: object; @@ -95,7 +95,7 @@ export function getTimelineItemsfromConversation({ '@timestamp': new Date().toISOString(), message: { role: MessageRole.User }, }, - title: i18n.translate('xpack.observabilityAiAssistant.conversationStartTitle', { + title: i18n.translate('xpack.aiAssistant.conversationStartTitle', { defaultMessage: 'started a conversation', }), role: MessageRole.User, @@ -149,7 +149,7 @@ export function getTimelineItemsfromConversation({ title = !isError ? ( , @@ -157,7 +157,7 @@ export function getTimelineItemsfromConversation({ /> ) : ( , @@ -189,7 +189,7 @@ export function getTimelineItemsfromConversation({ // User suggested a function title = ( , @@ -222,7 +222,7 @@ export function getTimelineItemsfromConversation({ if (message.message.function_call?.name) { title = ( , diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_settings_href.ts b/x-pack/packages/kbn-ai-assistant/src/utils/non_nullable.ts similarity index 57% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_settings_href.ts rename to x-pack/packages/kbn-ai-assistant/src/utils/non_nullable.ts index 45a3083d66327..8618e44dbb823 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_settings_href.ts +++ b/x-pack/packages/kbn-ai-assistant/src/utils/non_nullable.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { HttpStart } from '@kbn/core/public'; - -export function getSettingsHref(http: HttpStart) { - return http!.basePath.prepend(`/app/management/kibana/observabilityAiAssistantManagement`); +export function nonNullable(v: T): v is NonNullable { + return v !== null && v !== undefined; } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_settings_kb_href.ts b/x-pack/packages/kbn-ai-assistant/src/utils/safe_json_parse.ts similarity index 52% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_settings_kb_href.ts rename to x-pack/packages/kbn-ai-assistant/src/utils/safe_json_parse.ts index 2aa625da08d17..a4f2dfa5c2503 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/get_settings_kb_href.ts +++ b/x-pack/packages/kbn-ai-assistant/src/utils/safe_json_parse.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { HttpStart } from '@kbn/core/public'; - -export function getSettingsKnowledgeBaseHref(http: HttpStart) { - return http!.basePath.prepend( - `/app/management/kibana/observabilityAiAssistantManagement?tab=knowledge_base` - ); +export function safeJsonParse(jsonStr: string) { + try { + return JSON.parse(jsonStr); + } catch (err) { + return jsonStr; + } } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/storybook_decorator.stories.tsx b/x-pack/packages/kbn-ai-assistant/src/utils/storybook_decorator.stories.tsx similarity index 57% rename from x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/storybook_decorator.stories.tsx rename to x-pack/packages/kbn-ai-assistant/src/utils/storybook_decorator.stories.tsx index 9dc2e7057b951..d6292803b42af 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/storybook_decorator.stories.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/utils/storybook_decorator.stories.tsx @@ -13,10 +13,9 @@ import { } from '@kbn/observability-ai-assistant-plugin/public'; import { Subject } from 'rxjs'; import { coreMock } from '@kbn/core/public/mocks'; -import { ObservabilityAIAssistantAppService } from '../service/create_app_service'; -import { ObservabilityAIAssistantAppServiceProvider } from '../context/observability_ai_assistant_app_service_provider'; +import { AIAssistantAppService } from '../service/create_app_service'; -const mockService: ObservabilityAIAssistantAppService = { +const mockService: AIAssistantAppService = { ...createStorybookService(), }; @@ -38,25 +37,17 @@ export function KibanaReactStorybookDecorator(Story: ComponentType) { licensing: { license$: new Subject(), }, - // observabilityAIAssistant: { - // ObservabilityAIAssistantChatServiceContext, - // ObservabilityAIAssistantMultipaneFlyoutContext, - // }, - plugins: { - start: { - observabilityAIAssistant: { - ObservabilityAIAssistantMultipaneFlyoutContext, - }, - triggersActionsUi: { getAddRuleFlyout: {}, getAddConnectorFlyout: {} }, - }, + observabilityAIAssistant: { + ObservabilityAIAssistantChatServiceContext, + ObservabilityAIAssistantMultipaneFlyoutContext, + service: mockService, }, + triggersActionsUi: { getAddRuleFlyout: {}, getAddConnectorFlyout: {} }, }} > - - - - - + + + ); } diff --git a/x-pack/packages/kbn-ai-assistant/tsconfig.json b/x-pack/packages/kbn-ai-assistant/tsconfig.json new file mode 100644 index 0000000000000..c8d91c9d37450 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/tsconfig.json @@ -0,0 +1,39 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react", + "@emotion/react/types/css-prop" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/core-http-browser", + "@kbn/i18n", + "@kbn/triggers-actions-ui-plugin", + "@kbn/actions-plugin", + "@kbn/i18n-react", + "@kbn/ui-theme", + "@kbn/core", + "@kbn/observability-ai-assistant-plugin", + "@kbn/security-plugin", + "@kbn/user-profile-components", + "@kbn/std", + "@kbn/utility-types-jest", + "@kbn/kibana-react-plugin", + "@kbn/monaco", + "@kbn/licensing-plugin", + "@kbn/code-editor", + "@kbn/ml-plugin", + "@kbn/share-plugin", + ] +} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/assets/elastic_ai_assistant.png b/x-pack/plugins/observability_solution/observability_ai_assistant/public/assets/elastic_ai_assistant.png new file mode 100644 index 0000000000000000000000000000000000000000..af1064557968369b9d426fde8eb9891240436f55 GIT binary patch literal 95099 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≧cyL3ADuVDC5UQB;*_fCcrl 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%0D-oDwiUwxJNTS2z?D=``O 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 Iol`;+08M<;jsO4v literal 0 HcmV?d00001 diff --git a/dev_docs/shared_ux/shared_ux_landing.mdx b/dev_docs/shared_ux/shared_ux_landing.mdx index 4be8ad134be15..d96798eefa61f 100644 --- a/dev_docs/shared_ux/shared_ux_landing.mdx +++ b/dev_docs/shared_ux/shared_ux_landing.mdx @@ -66,5 +66,10 @@ layout: landing title: 'Kibana React Contexts', description: 'Learn how to use common React contexts in Kibana', }, + { + pageId: 'kibDevDocsChromeRecentlyAccessed', + title: 'Chrome Recently Viewed', + description: 'Learn how to add recently viewed items to the side navigation', + }, ]} /> From bf72e414206e7eafedb92b127df7f318604fc78e Mon Sep 17 00:00:00 2001 From: Yngrid Coello Date: Thu, 10 Oct 2024 16:19:52 +0200 Subject: [PATCH 52/87] [Dataset quality] Failure store support in synthtrace (#195726) This PR enables the creation of scenarios using failure store in synthtrace. #### How to test? 1. Run the scenario `node scripts/synthtrace failed_logs` 2. Go to dev console - For getting documents ingested `GET logs-*-*/_search`. This is equivalent to `GET logs-*-*/_search?failure_store=exclude` and will only include the documents that were properly ingested. - For getting documents in failure store `GET logs-*-*/_search?failure_store=only` https://github.com/user-attachments/assets/5013a0af-fdfc-453a-b70c-fb2c452ad4d8 --- .../lib/logs/custom_logsdb_index_templates.ts | 1 + .../src/lib/logs/logs_synthtrace_es_client.ts | 49 ++++- .../src/scenarios/degraded_logs.ts | 4 +- .../scenarios/degraded_synthetics_monitors.ts | 5 +- .../src/scenarios/failed_logs.ts | 195 ++++++++++++++++++ .../src/scenarios/helpers/logs_mock_data.ts | 3 + .../src/scenarios/logs_traces_hosts.ts | 18 +- .../src/scenarios/simple_logs.ts | 16 +- 8 files changed, 263 insertions(+), 28 deletions(-) create mode 100644 packages/kbn-apm-synthtrace/src/scenarios/failed_logs.ts diff --git a/packages/kbn-apm-synthtrace/src/lib/logs/custom_logsdb_index_templates.ts b/packages/kbn-apm-synthtrace/src/lib/logs/custom_logsdb_index_templates.ts index a0b155444919e..3eadd3f3941de 100644 --- a/packages/kbn-apm-synthtrace/src/lib/logs/custom_logsdb_index_templates.ts +++ b/packages/kbn-apm-synthtrace/src/lib/logs/custom_logsdb_index_templates.ts @@ -25,6 +25,7 @@ export const indexTemplates: { template: { settings: { mode: 'logsdb', + default_pipeline: 'logs@default-pipeline', }, }, priority: 500, diff --git a/packages/kbn-apm-synthtrace/src/lib/logs/logs_synthtrace_es_client.ts b/packages/kbn-apm-synthtrace/src/lib/logs/logs_synthtrace_es_client.ts index a6a64429f9b86..9673d1678132b 100644 --- a/packages/kbn-apm-synthtrace/src/lib/logs/logs_synthtrace_es_client.ts +++ b/packages/kbn-apm-synthtrace/src/lib/logs/logs_synthtrace_es_client.ts @@ -7,16 +7,20 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { Client } from '@elastic/elasticsearch'; +import { Client, estypes } from '@elastic/elasticsearch'; import { pipeline, Readable } from 'stream'; import { LogDocument } from '@kbn/apm-synthtrace-client/src/lib/logs'; -import { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; +import { IngestProcessorContainer, MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; +import { ValuesType } from 'utility-types'; import { SynthtraceEsClient, SynthtraceEsClientOptions } from '../shared/base_client'; import { getSerializeTransform } from '../shared/get_serialize_transform'; import { Logger } from '../utils/create_logger'; import { indexTemplates, IndexTemplateName } from './custom_logsdb_index_templates'; import { getRoutingTransform } from '../shared/data_stream_get_routing_transform'; +export const LogsIndex = 'logs'; +export const LogsCustom = 'logs@custom'; + export type LogsSynthtraceEsClientOptions = Omit; export class LogsSynthtraceEsClient extends SynthtraceEsClient { @@ -60,6 +64,47 @@ export class LogsSynthtraceEsClient extends SynthtraceEsClient { this.logger.error(`Index creation failed: ${index} - ${err.message}`); } } + + async updateIndexTemplate( + indexName: string, + modify: ( + template: ValuesType< + estypes.IndicesGetIndexTemplateResponse['index_templates'] + >['index_template'] + ) => estypes.IndicesPutIndexTemplateRequest + ) { + try { + const response = await this.client.indices.getIndexTemplate({ + name: indexName, + }); + + await Promise.all( + response.index_templates.map((template) => { + return this.client.indices.putIndexTemplate({ + ...modify(template.index_template), + name: template.name, + }); + }) + ); + + this.logger.info(`Updated ${indexName} index template`); + } catch (err) { + this.logger.error(`Update index template failed: ${indexName} - ${err.message}`); + } + } + + async createCustomPipeline(processors: IngestProcessorContainer[]) { + try { + this.client.ingest.putPipeline({ + id: LogsCustom, + processors, + version: 1, + }); + this.logger.info(`Custom pipeline created: ${LogsCustom}`); + } catch (err) { + this.logger.error(`Custom pipeline creation failed: ${LogsCustom} - ${err.message}`); + } + } } function logsPipeline() { diff --git a/packages/kbn-apm-synthtrace/src/scenarios/degraded_logs.ts b/packages/kbn-apm-synthtrace/src/scenarios/degraded_logs.ts index 47dd4ffd2652f..b3e41bbdd4e28 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/degraded_logs.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/degraded_logs.ts @@ -16,12 +16,10 @@ import { getCluster, getCloudRegion, getCloudProvider, + MORE_THAN_1024_CHARS, } from './helpers/logs_mock_data'; import { parseLogsScenarioOpts } from './helpers/logs_scenario_opts_parser'; -const MORE_THAN_1024_CHARS = - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?'; - // Logs Data logic const MESSAGE_LOG_LEVELS = [ { message: 'A simple log', level: 'info' }, diff --git a/packages/kbn-apm-synthtrace/src/scenarios/degraded_synthetics_monitors.ts b/packages/kbn-apm-synthtrace/src/scenarios/degraded_synthetics_monitors.ts index c61fecd8b7109..6e00bfd0abf15 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/degraded_synthetics_monitors.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/degraded_synthetics_monitors.ts @@ -14,12 +14,9 @@ import { } from '@kbn/apm-synthtrace-client'; import { Scenario } from '../cli/scenario'; import { withClient } from '../lib/utils/with_client'; -import { getIpAddress } from './helpers/logs_mock_data'; +import { MORE_THAN_1024_CHARS, getIpAddress } from './helpers/logs_mock_data'; import { getAtIndexOrRandom } from './helpers/get_at_index_or_random'; -const MORE_THAN_1024_CHARS = - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?'; - const MONITOR_NAMES = Array(4) .fill(null) .map((_, idx) => `synth-monitor-${idx}`); diff --git a/packages/kbn-apm-synthtrace/src/scenarios/failed_logs.ts b/packages/kbn-apm-synthtrace/src/scenarios/failed_logs.ts new file mode 100644 index 0000000000000..91ddedac270b5 --- /dev/null +++ b/packages/kbn-apm-synthtrace/src/scenarios/failed_logs.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", 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 { LogDocument, log, generateShortId, generateLongId } from '@kbn/apm-synthtrace-client'; +import { merge } from 'lodash'; +import { Scenario } from '../cli/scenario'; +import { IndexTemplateName } from '../lib/logs/custom_logsdb_index_templates'; +import { withClient } from '../lib/utils/with_client'; +import { + getServiceName, + getCluster, + getCloudRegion, + getCloudProvider, + MORE_THAN_1024_CHARS, +} from './helpers/logs_mock_data'; +import { parseLogsScenarioOpts } from './helpers/logs_scenario_opts_parser'; +import { LogsIndex } from '../lib/logs/logs_synthtrace_es_client'; + +const processors = [ + { + script: { + tag: 'normalize log level', + lang: 'painless', + source: ` + String level = ctx['log.level']; + if ('0'.equals(level)) { + ctx['log.level'] = 'info'; + } else if ('1'.equals(level)) { + ctx['log.level'] = 'debug'; + } else if ('2'.equals(level)) { + ctx['log.level'] = 'warning'; + } else if ('3'.equals(level)) { + ctx['log.level'] = 'error'; + } else { + throw new Exception("Not a valid log level"); + } + `, + }, + }, +]; + +// Logs Data logic +const MESSAGE_LOG_LEVELS = [ + { message: 'A simple log', level: '0' }, + { + message: 'Another log message', + level: '1', + }, + { + message: 'A log message generated from a warning', + level: '2', + }, + { message: 'Error with certificate: "ca_trusted_fingerprint"', level: '3' }, +]; + +const scenario: Scenario = async (runOptions) => { + const { isLogsDb } = parseLogsScenarioOpts(runOptions.scenarioOpts); + return { + bootstrap: async ({ logsEsClient }) => { + await logsEsClient.createCustomPipeline(processors); + if (isLogsDb) await logsEsClient.createIndexTemplate(IndexTemplateName.LogsDb); + + await logsEsClient.updateIndexTemplate( + isLogsDb ? IndexTemplateName.LogsDb : LogsIndex, + (template) => { + const next = { + name: LogsIndex, + data_stream: { + failure_store: true, + }, + }; + + return merge({}, template, next); + } + ); + }, + generate: ({ range, clients: { logsEsClient } }) => { + const { logger } = runOptions; + + const constructLogsCommonData = () => { + const index = Math.floor(Math.random() * 3); + const serviceName = getServiceName(index); + const logMessage = MESSAGE_LOG_LEVELS[index]; + const { clusterId, clusterName } = getCluster(index); + const cloudRegion = getCloudRegion(index); + + const commonLongEntryFields: LogDocument = { + 'trace.id': generateShortId(), + 'agent.name': 'synth-agent', + 'orchestrator.cluster.name': clusterName, + 'orchestrator.cluster.id': clusterId, + 'orchestrator.resource.id': generateShortId(), + 'cloud.provider': getCloudProvider(), + 'cloud.region': cloudRegion, + 'cloud.availability_zone': `${cloudRegion}a`, + 'cloud.project.id': generateShortId(), + 'cloud.instance.id': generateShortId(), + 'log.file.path': `/logs/${generateLongId()}/error.txt`, + }; + + return { + index, + serviceName, + logMessage, + cloudRegion, + commonLongEntryFields, + }; + }; + + const datasetSynth1Logs = (timestamp: number) => { + const { + serviceName, + logMessage: { level, message }, + commonLongEntryFields, + } = constructLogsCommonData(); + + return log + .create({ isLogsDb }) + .dataset('synth.1') + .message(message) + .logLevel(level) + .service(serviceName) + .defaults(commonLongEntryFields) + .timestamp(timestamp); + }; + + const datasetSynth2Logs = (i: number, timestamp: number) => { + const { + serviceName, + logMessage: { level, message }, + commonLongEntryFields, + } = constructLogsCommonData(); + const isFailed = i % 60 === 0; + return log + .create({ isLogsDb }) + .dataset('synth.2') + .message(message) + .logLevel(isFailed ? '4' : level) // "script_exception": Not a valid log level + .service(serviceName) + .defaults(commonLongEntryFields) + .timestamp(timestamp); + }; + + const datasetSynth3Logs = (i: number, timestamp: number) => { + const { + serviceName, + logMessage: { level, message }, + cloudRegion, + commonLongEntryFields, + } = constructLogsCommonData(); + const isMalformed = i % 10 === 0; + const isFailed = i % 80 === 0; + return log + .create({ isLogsDb }) + .dataset('synth.3') + .message(message) + .logLevel(isFailed ? '5' : level) // "script_exception": Not a valid log level + .service(serviceName) + .defaults({ + ...commonLongEntryFields, + 'cloud.availability_zone': isMalformed + ? MORE_THAN_1024_CHARS // "ignore_above": 1024 in mapping + : `${cloudRegion}a`, + }) + .timestamp(timestamp); + }; + + const logs = range + .interval('1m') + .rate(1) + .generator((timestamp) => { + return Array(200) + .fill(0) + .flatMap((_, index) => [ + datasetSynth1Logs(timestamp), + datasetSynth2Logs(index, timestamp), + datasetSynth3Logs(index, timestamp), + ]); + }); + + return withClient( + logsEsClient, + logger.perf('generating_logs', () => logs) + ); + }, + }; +}; + +export default scenario; diff --git a/packages/kbn-apm-synthtrace/src/scenarios/helpers/logs_mock_data.ts b/packages/kbn-apm-synthtrace/src/scenarios/helpers/logs_mock_data.ts index e974528f16a80..5f3cbd5f054dd 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/helpers/logs_mock_data.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/helpers/logs_mock_data.ts @@ -59,6 +59,9 @@ const SERVICE_NAMES = Array(3) .fill(null) .map((_, idx) => `synth-service-${idx}`); +export const MORE_THAN_1024_CHARS = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?'; + // Functions to get random elements export const getCluster = (index?: number) => getAtIndexOrRandom(CLUSTER, index); export const getIpAddress = (index?: number) => getAtIndexOrRandom(IP_ADDRESSES, index); diff --git a/packages/kbn-apm-synthtrace/src/scenarios/logs_traces_hosts.ts b/packages/kbn-apm-synthtrace/src/scenarios/logs_traces_hosts.ts index 8a6bdf409a573..6dac3fc9f3226 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/logs_traces_hosts.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/logs_traces_hosts.ts @@ -8,21 +8,22 @@ */ import { - log, - LogDocument, + ApmFields, InfraDocument, - apm, Instance, - infra, - ApmFields, + LogDocument, + apm, generateShortId, + infra, + log, } from '@kbn/apm-synthtrace-client'; import { Scenario } from '../cli/scenario'; +import { IndexTemplateName } from '../lib/logs/custom_logsdb_index_templates'; import { Logger } from '../lib/utils/create_logger'; -import { withClient } from '../lib/utils/with_client'; import { getSynthtraceEnvironment } from '../lib/utils/get_synthtrace_environment'; +import { withClient } from '../lib/utils/with_client'; +import { MORE_THAN_1024_CHARS } from './helpers/logs_mock_data'; import { parseLogsScenarioOpts, parseStringToBoolean } from './helpers/logs_scenario_opts_parser'; -import { IndexTemplateName } from '../lib/logs/custom_logsdb_index_templates'; const ENVIRONMENT = getSynthtraceEnvironment(__filename); @@ -475,6 +476,3 @@ const DATASETS = [ ]; const LOG_LEVELS = ['info', 'error', 'warn', 'debug']; - -const MORE_THAN_1024_CHARS = - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?'; diff --git a/packages/kbn-apm-synthtrace/src/scenarios/simple_logs.ts b/packages/kbn-apm-synthtrace/src/scenarios/simple_logs.ts index 3c1fdc5131395..08d914c1017dd 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/simple_logs.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/simple_logs.ts @@ -7,19 +7,20 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { LogDocument, log, generateShortId, generateLongId } from '@kbn/apm-synthtrace-client'; +import { LogDocument, generateLongId, generateShortId, log } from '@kbn/apm-synthtrace-client'; import moment from 'moment'; import { Scenario } from '../cli/scenario'; import { IndexTemplateName } from '../lib/logs/custom_logsdb_index_templates'; import { withClient } from '../lib/utils/with_client'; import { - getServiceName, - getGeoCoordinate, - getIpAddress, - getCluster, + MORE_THAN_1024_CHARS, + getAgentName, getCloudProvider, getCloudRegion, - getAgentName, + getCluster, + getGeoCoordinate, + getIpAddress, + getServiceName, } from './helpers/logs_mock_data'; import { parseLogsScenarioOpts } from './helpers/logs_scenario_opts_parser'; @@ -30,9 +31,6 @@ const MESSAGE_LOG_LEVELS = [ { message: 'Error with certificate: "ca_trusted_fingerprint"', level: 'error' }, ]; -const MORE_THAN_1024_CHARS = - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?'; - const scenario: Scenario = async (runOptions) => { const { isLogsDb } = parseLogsScenarioOpts(runOptions.scenarioOpts); From 6f1449b1f589b05a0919753dbb1afa51dac02185 Mon Sep 17 00:00:00 2001 From: Khristinin Nikita Date: Thu, 10 Oct 2024 16:28:48 +0200 Subject: [PATCH 53/87] Skip Backfill groups in the periodic pipeline (#195760) ## Summary This test shouldn't run in periodic pipeline. We expect only FTR and Cypress running in periodic and 2nd quality gate pipelines enabled in https://github.com/elastic/kibana/pull/193666. --- .../rule_management/rule_details/backfill_group.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f747e6be43e5a..6466c20dfde21 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,7 +34,7 @@ import { describe( 'Backfill groups', { - tags: ['@ess', '@serverless'], + tags: ['@ess', '@serverless', '@skipInServerlessMKI'], }, function () { before(() => { From e53e54550f9ab9ce2db83ec56a5c704a96f37355 Mon Sep 17 00:00:00 2001 From: Paulo Silva Date: Thu, 10 Oct 2024 07:52:49 -0700 Subject: [PATCH 54/87] [Cloud Security] [CDR] Handle grouping fields with missing mapping (#195702) ## Summary This PR fixes https://github.com/elastic/security-team/issues/10632 by adding runtime mapping support for fields that are missing in mapping, this is useful when querying a DataView that points to multiple indices where the mapping is not guaranteed to exist as it's the case with CDR that adds supports to Third Party data. Also added runtime mapping to sorted fields, as it's not guaranteed that all fields shown on the table have mapped fields. --- .../latest_findings/use_latest_findings.ts | 16 +++++ .../use_latest_findings_grouping.tsx | 72 +++++++++++++++++++ .../hooks/use_latest_vulnerabilities.tsx | 21 ++++++ .../use_latest_vulnerabilities_grouping.tsx | 46 ++++++++++++ .../utils/custom_sort_script.ts | 8 ++- 5 files changed, 162 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings.ts index f6f27e15ee7a4..955f5a45a9743 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings.ts @@ -21,6 +21,7 @@ import type { CspFinding } from '@kbn/cloud-security-posture-common'; import type { CspBenchmarkRulesStates } from '@kbn/cloud-security-posture-common/schema/rules/latest'; import type { FindingsBaseEsQuery } from '@kbn/cloud-security-posture'; import { useGetCspBenchmarkRulesStatesApi } from '@kbn/cloud-security-posture/src/hooks/use_get_benchmark_rules_state_api'; +import type { RuntimePrimitiveTypes } from '@kbn/data-views-plugin/common'; import { useKibana } from '../../../common/hooks/use_kibana'; import { getAggregationCount, getFindingsCountAggQuery } from '../utils/utils'; @@ -39,6 +40,20 @@ interface FindingsAggs { count: estypes.AggregationsMultiBucketAggregateBase; } +const getRuntimeMappingsFromSort = (sort: string[][]) => { + return sort.reduce((acc, [field]) => { + // TODO: Add proper type for all fields available in the field selector + const type: RuntimePrimitiveTypes = field === '@timestamp' ? 'date' : 'keyword'; + + return { + ...acc, + [field]: { + type, + }, + }; + }, {}); +}; + export const getFindingsQuery = ( { query, sort }: UseFindingsOptions, rulesStates: CspBenchmarkRulesStates, @@ -49,6 +64,7 @@ export const getFindingsQuery = ( return { index: CDR_MISCONFIGURATIONS_INDEX_PATTERN, sort: getMultiFieldsSort(sort), + runtime_mappings: getRuntimeMappingsFromSort(sort), size: MAX_FINDINGS_TO_LOAD, aggs: getFindingsCountAggQuery(), ignore_unavailable: true, diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx index cc409fb95024d..6482d864347a1 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx @@ -114,6 +114,72 @@ const getAggregationsByGroupField = (field: string): NamedAggregation[] => { return aggMetrics; }; +/** + * Get runtime mappings for the given group field + * Some fields require additional runtime mappings to aggregate additional information + * Fallback to keyword type to support custom fields grouping + */ +const getRuntimeMappingsByGroupField = ( + field: string +): Record | undefined => { + switch (field) { + case FINDINGS_GROUPING_OPTIONS.RESOURCE_NAME: + return { + [FINDINGS_GROUPING_OPTIONS.RESOURCE_NAME]: { + type: 'keyword', + }, + 'resource.id': { + type: 'keyword', + }, + 'resource.sub_type': { + type: 'keyword', + }, + 'resource.type': { + type: 'keyword', + }, + }; + case FINDINGS_GROUPING_OPTIONS.RULE_NAME: + return { + [FINDINGS_GROUPING_OPTIONS.RULE_NAME]: { + type: 'keyword', + }, + 'rule.benchmark.version': { + type: 'keyword', + }, + }; + case FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME: + return { + [FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME]: { + type: 'keyword', + }, + 'rule.benchmark.name': { + type: 'keyword', + }, + 'rule.benchmark.id': { + type: 'keyword', + }, + }; + case FINDINGS_GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME: + return { + [FINDINGS_GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME]: { + type: 'keyword', + }, + 'rule.benchmark.name': { + type: 'keyword', + }, + 'rule.benchmark.id': { + type: 'keyword', + }, + }; + default: + return { + [field]: { + type: 'keyword', + }, + }; + } +}; + /** * Type Guard for checking if the given source is a FindingsRootGroupingAggregation */ @@ -189,6 +255,12 @@ export const useLatestFindingsGrouping = ({ size: pageSize, sort: [{ groupByField: { order: 'desc' } }, { complianceScore: { order: 'asc' } }], statsAggregations: getAggregationsByGroupField(currentSelectedGroup), + runtimeMappings: { + ...getRuntimeMappingsByGroupField(currentSelectedGroup), + 'result.evaluation': { + type: 'keyword', + }, + }, rootAggregations: [ { failedFindings: { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities.tsx index 0d0ea9ba5a22f..0b9cf6978c258 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities.tsx @@ -23,6 +23,7 @@ import { } from '@kbn/cloud-security-posture-common'; import { FindingsBaseEsQuery, showErrorToast } from '@kbn/cloud-security-posture'; import type { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/latest'; +import type { RuntimePrimitiveTypes } from '@kbn/data-views-plugin/common'; import { VULNERABILITY_FIELDS } from '../../../common/constants'; import { useKibana } from '../../../common/hooks/use_kibana'; import { getCaseInsensitiveSortScript } from '../utils/custom_sort_script'; @@ -52,6 +53,25 @@ const getMultiFieldsSort = (sort: string[][]) => { }); }; +const getRuntimeMappingsFromSort = (sort: string[][]) => { + return sort.reduce((acc, [field]) => { + // TODO: Add proper type for all fields available in the field selector + const type: RuntimePrimitiveTypes = + field === VULNERABILITY_FIELDS.SCORE_BASE + ? 'double' + : field === '@timestamp' + ? 'date' + : 'keyword'; + + return { + ...acc, + [field]: { + type, + }, + }; + }, {}); +}; + export const getVulnerabilitiesQuery = ( { query, sort }: VulnerabilitiesQuery, pageParam: number @@ -59,6 +79,7 @@ export const getVulnerabilitiesQuery = ( index: CDR_VULNERABILITIES_INDEX_PATTERN, ignore_unavailable: true, sort: getMultiFieldsSort(sort), + runtime_mappings: getRuntimeMappingsFromSort(sort), size: MAX_FINDINGS_TO_LOAD, query: { ...query, diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities_grouping.tsx index 516cbed0c3975..3c52590f8fd80 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities_grouping.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities_grouping.tsx @@ -94,6 +94,51 @@ const getAggregationsByGroupField = (field: string): NamedAggregation[] => { return aggMetrics; }; +/** + * Get runtime mappings for the given group field + * Some fields require additional runtime mappings to aggregate additional information + * Fallback to keyword type to support custom fields grouping + */ +const getRuntimeMappingsByGroupField = ( + field: string +): Record | undefined => { + switch (field) { + case VULNERABILITY_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME: + return { + [VULNERABILITY_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME]: { + type: 'keyword', + }, + [VULNERABILITY_FIELDS.CLOUD_PROVIDER]: { + type: 'keyword', + }, + }; + case VULNERABILITY_GROUPING_OPTIONS.RESOURCE_NAME: + return { + [VULNERABILITY_GROUPING_OPTIONS.RESOURCE_NAME]: { + type: 'keyword', + }, + [VULNERABILITY_FIELDS.RESOURCE_ID]: { + type: 'keyword', + }, + }; + case VULNERABILITY_GROUPING_OPTIONS.CVE: + return { + [VULNERABILITY_GROUPING_OPTIONS.CVE]: { + type: 'keyword', + }, + [VULNERABILITY_FIELDS.DESCRIPTION]: { + type: 'keyword', + }, + }; + default: + return { + [field]: { + type: 'keyword', + }, + }; + } +}; + /** * Type Guard for checking if the given source is a VulnerabilitiesRootGroupingAggregation */ @@ -163,6 +208,7 @@ export const useLatestVulnerabilitiesGrouping = ({ size: pageSize, sort: [{ groupByField: { order: 'desc' } }], statsAggregations: getAggregationsByGroupField(currentSelectedGroup), + runtimeMappings: getRuntimeMappingsByGroupField(currentSelectedGroup), }); const { data, isFetching } = useGroupedVulnerabilities({ diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/custom_sort_script.ts b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/custom_sort_script.ts index 780cd539305b3..e517d622e71c5 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/custom_sort_script.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/custom_sort_script.ts @@ -14,7 +14,13 @@ export const getCaseInsensitiveSortScript = (field: string, direction: string) = type: 'string', order: direction, script: { - source: `doc["${field}"].value.toLowerCase()`, + source: ` + if (doc.containsKey('${field}') && !doc['${field}'].empty) { + return doc['${field}'].value.toLowerCase(); + } else { + return ""; + } + `, lang: 'painless', }, }, From 745fb6225f499d2a30c51683556a79afc04dc6da Mon Sep 17 00:00:00 2001 From: Brad White Date: Thu, 10 Oct 2024 08:57:35 -0600 Subject: [PATCH 55/87] skip failing test suite (#195602) --- .../apis/ml/anomaly_detectors/forecast_with_spaces.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/ml/anomaly_detectors/forecast_with_spaces.ts b/x-pack/test/api_integration/apis/ml/anomaly_detectors/forecast_with_spaces.ts index a330edd9a41d7..32e82c67e348d 100644 --- a/x-pack/test/api_integration/apis/ml/anomaly_detectors/forecast_with_spaces.ts +++ b/x-pack/test/api_integration/apis/ml/anomaly_detectors/forecast_with_spaces.ts @@ -58,7 +58,8 @@ export default ({ getService }: FtrProviderContext) => { return body; } - describe('POST anomaly_detectors _forecast with spaces', function () { + // Failing see: https://github.com/elastic/kibana/issues/195602 + describe.skip('POST anomaly_detectors _forecast with spaces', function () { let forecastId: string; before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); From bc75d03f5ca01889a4a7813f8abbeeee239921c3 Mon Sep 17 00:00:00 2001 From: Karen Grigoryan Date: Thu, 10 Oct 2024 17:29:49 +0200 Subject: [PATCH 56/87] ci(deploy): dead deploy fix script (#195753) This fixes edge case with dead deploys failing current server deploy jobs in https://github.com/elastic/kibana/pull/191898 --- .buildkite/scripts/steps/cloud/build_and_deploy.sh | 2 +- .buildkite/scripts/steps/serverless/deploy.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.buildkite/scripts/steps/cloud/build_and_deploy.sh b/.buildkite/scripts/steps/cloud/build_and_deploy.sh index 25e7d8fc631c9..220ab497aaf7b 100755 --- a/.buildkite/scripts/steps/cloud/build_and_deploy.sh +++ b/.buildkite/scripts/steps/cloud/build_and_deploy.sh @@ -51,7 +51,7 @@ fi if is_pr_with_label "ci:cloud-redeploy"; then echo "--- Shutdown Previous Deployment" CLOUD_DEPLOYMENT_ID=$(ecctl deployment list --output json | jq -r '.deployments[] | select(.name == "'$CLOUD_DEPLOYMENT_NAME'") | .id') - if [ -z "${CLOUD_DEPLOYMENT_ID}" ]; then + if [ -z "${CLOUD_DEPLOYMENT_ID}" ] || [ "${CLOUD_DEPLOYMENT_ID}" == "null" ]; then echo "No deployment to remove" else echo "Shutting down previous deployment..." diff --git a/.buildkite/scripts/steps/serverless/deploy.sh b/.buildkite/scripts/steps/serverless/deploy.sh index d30723393dacd..cbbc6c6c664dd 100644 --- a/.buildkite/scripts/steps/serverless/deploy.sh +++ b/.buildkite/scripts/steps/serverless/deploy.sh @@ -56,7 +56,7 @@ deploy() { PROJECT_ID=$(jq -r '[.items[] | select(.name == "'$PROJECT_NAME'")] | .[0].id' $PROJECT_EXISTS_LOGS) if is_pr_with_label "ci:project-redeploy"; then - if [ -z "${PROJECT_ID}" ]; then + if [ -z "${PROJECT_ID}" ] || [ "${PROJECT_ID}" == "null" ]; then echo "No project to remove" else echo "Shutting down previous project..." From ed75ef67ca34795e88cc81e739daa3a6ec655d6a Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Thu, 10 Oct 2024 17:33:23 +0200 Subject: [PATCH 57/87] [data.search] Disable bfetch by default (#192789) ## Summary Part of https://github.com/elastic/kibana/issues/186139. Disables `bfetch` by default. ### Checklist Delete any items that are not applicable to this PR. - [ ] 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 - [ ] [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 - [ ] 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)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] 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)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### 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) --------- Co-authored-by: Peter Pisljar --- src/plugins/bfetch/server/ui_settings.ts | 2 +- .../tests/browser.ts | 165 +++++++----------- 2 files changed, 62 insertions(+), 105 deletions(-) diff --git a/src/plugins/bfetch/server/ui_settings.ts b/src/plugins/bfetch/server/ui_settings.ts index aee4903d226c0..132dd19ef8b9c 100644 --- a/src/plugins/bfetch/server/ui_settings.ts +++ b/src/plugins/bfetch/server/ui_settings.ts @@ -18,7 +18,7 @@ export function getUiSettings(): Record> { name: i18n.translate('bfetch.disableBfetch', { defaultMessage: 'Disable request batching', }), - value: false, + value: true, description: i18n.translate('bfetch.disableBfetchDesc', { defaultMessage: 'Disables requests batching. This increases number of HTTP requests from Kibana, but allows to debug requests individually.', diff --git a/x-pack/test/functional_execution_context/tests/browser.ts b/x-pack/test/functional_execution_context/tests/browser.ts index e1d7ba6a3b965..c7228528ee756 100644 --- a/x-pack/test/functional_execution_context/tests/browser.ts +++ b/x-pack/test/functional_execution_context/tests/browser.ts @@ -85,14 +85,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { type: 'application', name: 'discover', url: '/app/discover', - child: { - name: 'discover', - url: '/app/discover', - type: 'application', - page: 'app', - id: 'new', - description: 'fetch documents', - }, + page: 'app', + id: 'new', + description: 'fetch documents', }), }); }); @@ -105,20 +100,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { type: 'application', name: 'discover', url: '/app/discover', + page: 'app', + id: 'new', + description: 'fetch chart data and total hits', child: { - name: 'discover', - url: '/app/discover', - type: 'application', - page: 'app', - id: 'new', - description: 'fetch chart data and total hits', - child: { - type: 'lens', - name: 'lnsXY', - id: 'unifiedHistogramLensComponent', - description: 'Edit visualization', - url: '/app/lens#/edit_by_value', - }, + type: 'lens', + name: 'lnsXY', + id: 'unifiedHistogramLensComponent', + description: 'Edit visualization', + url: '/app/lens#/edit_by_value', }, }), }); @@ -185,9 +175,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('propagates to Elasticsearch via "x-opaque-id" header', async () => { await logContains({ description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', - predicate: checkHttpRequestId( - 'dashboard:dashboards:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsXY:086ac2e9-dd16-4b45-92b8-1e43ff7e3f65' - ), + predicate: checkHttpRequestId('lens:lnsXY:086ac2e9-dd16-4b45-92b8-1e43ff7e3f65'), }); }); @@ -195,23 +183,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await logContains({ description: 'execution context propagates to Kibana logs', predicate: checkExecutionContextEntry({ - type: 'application', + type: 'dashboard', name: 'dashboards', url: '/app/dashboards', + page: 'app', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', child: { - name: 'dashboards', - url: '/app/dashboards', - type: 'dashboard', - page: 'app', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - child: { - type: 'lens', - name: 'lnsXY', - id: '086ac2e9-dd16-4b45-92b8-1e43ff7e3f65', - description: '[Flights] Flight count', - url: '/app/lens#/edit_by_value', - }, + type: 'lens', + name: 'lnsXY', + id: '086ac2e9-dd16-4b45-92b8-1e43ff7e3f65', + description: '[Flights] Flight count', + url: '/app/lens#/edit_by_value', }, }), }); @@ -222,9 +205,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('propagates to Elasticsearch via "x-opaque-id" header', async () => { await logContains({ description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', - predicate: checkHttpRequestId( - 'dashboard:dashboards:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsMetric:b766e3b8-4544-46ed-99e6-9ecc4847e2a2' - ), + predicate: checkHttpRequestId('lens:lnsMetric:b766e3b8-4544-46ed-99e6-9ecc4847e2a2'), }); }); @@ -232,23 +213,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await logContains({ description: 'execution context propagates to Kibana logs', predicate: checkExecutionContextEntry({ + type: 'dashboard', name: 'dashboards', url: '/app/dashboards', - type: 'application', + page: 'app', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', child: { - name: 'dashboards', - url: '/app/dashboards', - type: 'dashboard', - page: 'app', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - child: { - type: 'lens', - name: 'lnsMetric', - id: 'b766e3b8-4544-46ed-99e6-9ecc4847e2a2', - description: '', - url: '/app/lens#/edit_by_value', - }, + type: 'lens', + name: 'lnsMetric', + id: 'b766e3b8-4544-46ed-99e6-9ecc4847e2a2', + description: '', + url: '/app/lens#/edit_by_value', }, }), }); @@ -260,7 +236,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await logContains({ description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', predicate: checkHttpRequestId( - 'dashboard:dashboards:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsDatatable:fb86b32f-fb7a-45cf-9511-f366fef51bbd' + 'lens:lnsDatatable:fb86b32f-fb7a-45cf-9511-f366fef51bbd' ), }); }); @@ -269,23 +245,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await logContains({ description: 'execution context propagates to Kibana logs', predicate: checkExecutionContextEntry({ + type: 'dashboard', name: 'dashboards', url: '/app/dashboards', - type: 'application', + page: 'app', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', child: { - name: 'dashboards', - url: '/app/dashboards', - type: 'dashboard', - page: 'app', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - child: { - type: 'lens', - name: 'lnsDatatable', - id: 'fb86b32f-fb7a-45cf-9511-f366fef51bbd', - description: 'Cities by delay, cancellation', - url: '/app/lens#/edit_by_value', - }, + type: 'lens', + name: 'lnsDatatable', + id: 'fb86b32f-fb7a-45cf-9511-f366fef51bbd', + description: 'Cities by delay, cancellation', + url: '/app/lens#/edit_by_value', }, }), }); @@ -296,9 +267,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('propagates to Elasticsearch via "x-opaque-id" header', async () => { await logContains({ description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', - predicate: checkHttpRequestId( - 'dashboard:dashboards:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsPie:5d53db36-2d5a-4adc-af7b-cec4c1a294e0' - ), + predicate: checkHttpRequestId('lens:lnsPie:5d53db36-2d5a-4adc-af7b-cec4c1a294e0'), }); }); @@ -306,23 +275,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await logContains({ description: 'execution context propagates to Kibana logs', predicate: checkExecutionContextEntry({ + type: 'dashboard', name: 'dashboards', url: '/app/dashboards', - type: 'application', + page: 'app', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', child: { - name: 'dashboards', - url: '/app/dashboards', - type: 'dashboard', - page: 'app', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - child: { - type: 'lens', - name: 'lnsPie', - id: '5d53db36-2d5a-4adc-af7b-cec4c1a294e0', - description: '[Flights] Delay Type', - url: '/app/lens#/edit_by_value', - }, + type: 'lens', + name: 'lnsPie', + id: '5d53db36-2d5a-4adc-af7b-cec4c1a294e0', + description: '[Flights] Delay Type', + url: '/app/lens#/edit_by_value', }, }), }); @@ -334,9 +298,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('propagates to Elasticsearch via "x-opaque-id" header', async () => { await logContains({ description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', - predicate: checkHttpRequestId( - 'dashboard:dashboards:7adfa750-4c81-11e8-b3d7-01146121b73d;search:discover:571aaf70-4c88-11e8-b3d7-01146121b73d' - ), + predicate: checkHttpRequestId('search:discover:571aaf70-4c88-11e8-b3d7-01146121b73d'), }); }); @@ -344,23 +306,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await logContains({ description: 'execution context propagates to Kibana logs', predicate: checkExecutionContextEntry({ - type: 'application', + type: 'dashboard', name: 'dashboards', url: '/app/dashboards', + page: 'app', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', child: { - type: 'dashboard', - name: 'dashboards', - url: '/app/dashboards', - page: 'app', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - child: { - type: 'search', - name: 'discover', - id: '571aaf70-4c88-11e8-b3d7-01146121b73d', - description: '[Flights] Flight Log', - url: '/app/discover#/view/571aaf70-4c88-11e8-b3d7-01146121b73d', - }, + type: 'search', + name: 'discover', + id: '571aaf70-4c88-11e8-b3d7-01146121b73d', + description: '[Flights] Flight Log', + url: '/app/discover#/view/571aaf70-4c88-11e8-b3d7-01146121b73d', }, }), }); From dffe0b571899b2ed0c71ee9f090095311d4d2b55 Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Thu, 10 Oct 2024 09:33:55 -0600 Subject: [PATCH 58/87] [ES|QL] only suggest pipe at the end of the field list (#195679) ## Summary Close https://github.com/elastic/kibana/issues/191100 ### Improvements 1. You no longer get a comma suggestion when you're out of fields... https://github.com/user-attachments/assets/3ed3617b-99e2-44a5-917e-294b98f16ef4 2. Fixed https://github.com/elastic/kibana/issues/191100 ### 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: Elastic Machine --- .../src/autocomplete/autocomplete.test.ts | 83 +++++++++++++++---- .../src/autocomplete/autocomplete.ts | 38 +++++---- 2 files changed, 88 insertions(+), 33 deletions(-) diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts index 84779f1dd36b5..a0a4a359c5ff6 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts @@ -387,6 +387,23 @@ describe('autocomplete', () => { '```````````````````````````````round(doubleField) + 1```````````````` + 1```````` + 1```` + 1`` + 1`', ] ); + + it('should not suggest already-used fields and variables', async () => { + const { suggest: suggestTest } = await setup(); + const getSuggestions = async (query: string) => + (await suggestTest(query)).map((value) => value.text); + + expect(await getSuggestions('from a_index | EVAL foo = 1 | KEEP /')).toContain('foo'); + expect(await getSuggestions('from a_index | EVAL foo = 1 | KEEP foo, /')).not.toContain( + 'foo' + ); + expect(await getSuggestions('from a_index | EVAL foo = 1 | KEEP /')).toContain( + 'doubleField' + ); + expect( + await getSuggestions('from a_index | EVAL foo = 1 | KEEP doubleField, /') + ).not.toContain('doubleField'); + }); }); } @@ -1111,11 +1128,14 @@ describe('autocomplete', () => { ]); }); - describe('KEEP ', () => { + describe.each(['KEEP', 'DROP'])('%s ', (commandName) => { // KEEP field - testSuggestions('FROM a | KEEP /', getFieldNamesByType('any').map(attachTriggerCommand)); testSuggestions( - 'FROM a | KEEP d/', + `FROM a | ${commandName} /`, + getFieldNamesByType('any').map(attachTriggerCommand) + ); + testSuggestions( + `FROM a | ${commandName} d/`, getFieldNamesByType('any') .map((text) => ({ text, @@ -1124,11 +1144,11 @@ describe('autocomplete', () => { .map(attachTriggerCommand) ); testSuggestions( - 'FROM a | KEEP doubleFiel/', + `FROM a | ${commandName} doubleFiel/`, getFieldNamesByType('any').map(attachTriggerCommand) ); testSuggestions( - 'FROM a | KEEP doubleField/', + `FROM a | ${commandName} doubleField/`, ['doubleField, ', 'doubleField | '] .map((text) => ({ text, @@ -1141,7 +1161,7 @@ describe('autocomplete', () => { // Let's get funky with the field names testSuggestions( - 'FROM a | KEEP @timestamp/', + `FROM a | ${commandName} @timestamp/`, ['@timestamp, ', '@timestamp | '] .map((text) => ({ text, @@ -1150,10 +1170,15 @@ describe('autocomplete', () => { })) .map(attachTriggerCommand), undefined, - [[{ name: '@timestamp', type: 'date' }]] + [ + [ + { name: '@timestamp', type: 'date' }, + { name: 'utc_stamp', type: 'date' }, + ], + ] ); testSuggestions( - 'FROM a | KEEP foo.bar/', + `FROM a | ${commandName} foo.bar/`, ['foo.bar, ', 'foo.bar | '] .map((text) => ({ text, @@ -1162,26 +1187,34 @@ describe('autocomplete', () => { })) .map(attachTriggerCommand), undefined, - [[{ name: 'foo.bar', type: 'double' }]] + [ + [ + { name: 'foo.bar', type: 'double' }, + { name: 'baz', type: 'date' }, + ], + ] ); describe('escaped field names', () => { // This isn't actually the behavior we want, but this test is here // to make sure no weird suggestions start cropping up in this case. - testSuggestions('FROM a | KEEP `foo.bar`/', ['foo.bar'], undefined, [ + testSuggestions(`FROM a | ${commandName} \`foo.bar\`/`, ['foo.bar'], undefined, [ [{ name: 'foo.bar', type: 'double' }], ]); // @todo re-enable these tests when we can use AST to support this case - testSuggestions.skip('FROM a | KEEP `foo.bar`/', ['foo.bar, ', 'foo.bar | '], undefined, [ - [{ name: 'foo.bar', type: 'double' }], - ]); testSuggestions.skip( - 'FROM a | KEEP `foo`.`bar`/', + `FROM a | ${commandName} \`foo.bar\`/`, ['foo.bar, ', 'foo.bar | '], undefined, [[{ name: 'foo.bar', type: 'double' }]] ); - testSuggestions.skip('FROM a | KEEP `any#Char$Field`/', [ + testSuggestions.skip( + `FROM a | ${commandName} \`foo\`.\`bar\`/`, + ['foo.bar, ', 'foo.bar | '], + undefined, + [[{ name: 'foo.bar', type: 'double' }]] + ); + testSuggestions.skip(`FROM a | ${commandName} \`any#Char$Field\`/`, [ '`any#Char$Field`, ', '`any#Char$Field` | ', ]); @@ -1189,12 +1222,28 @@ describe('autocomplete', () => { // Subsequent fields testSuggestions( - 'FROM a | KEEP doubleField, dateFiel/', + `FROM a | ${commandName} doubleField, dateFiel/`, getFieldNamesByType('any') .filter((s) => s !== 'doubleField') .map(attachTriggerCommand) ); - testSuggestions('FROM a | KEEP doubleField, dateField/', ['dateField, ', 'dateField | ']); + testSuggestions(`FROM a | ${commandName} doubleField, dateField/`, [ + 'dateField, ', + 'dateField | ', + ]); + + // out of fields + testSuggestions( + `FROM a | ${commandName} doubleField, dateField/`, + ['dateField | '], + undefined, + [ + [ + { name: 'doubleField', type: 'double' }, + { name: 'dateField', type: 'date' }, + ], + ] + ); }); }); }); diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts index 2433f5d496521..6f9fb66a8c715 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts @@ -627,7 +627,7 @@ async function getExpressionSuggestionsByType( literals: argDef.constantOnly, }, { - ignoreFields: isNewExpression + ignoreColumns: isNewExpression ? command.args.filter(isColumnItem).map(({ name }) => name) : [], } @@ -656,10 +656,15 @@ async function getExpressionSuggestionsByType( })); } - return [ - { ...pipeCompleteItem, text: ' | ' }, - { ...commaCompleteItem, text: ', ' }, - ].map((s) => ({ + const finalSuggestions = [{ ...pipeCompleteItem, text: ' | ' }]; + if (fieldSuggestions.length > 1) + // when we fix the editor marker, this should probably be checked against 0 instead of 1 + // this is because the last field in the AST is currently getting removed (because it contains + // the editor marker) so it is not included in the ignored list which is used to filter out + // existing fields above. + finalSuggestions.push({ ...commaCompleteItem, text: ', ' }); + + return finalSuggestions.map((s) => ({ ...s, filterText: fragment, text: fragment + s.text, @@ -1176,15 +1181,15 @@ async function getFieldsOrFunctionsSuggestions( }, { ignoreFn = [], - ignoreFields = [], + ignoreColumns = [], }: { ignoreFn?: string[]; - ignoreFields?: string[]; + ignoreColumns?: string[]; } = {} ): Promise { const filteredFieldsByType = pushItUpInTheList( (await (fields - ? getFieldsByType(types, ignoreFields, { + ? getFieldsByType(types, ignoreColumns, { advanceCursor: commandName === 'sort', openSuggestions: commandName === 'sort', }) @@ -1195,7 +1200,10 @@ async function getFieldsOrFunctionsSuggestions( const filteredVariablesByType: string[] = []; if (variables) { for (const variable of variables.values()) { - if (types.includes('any') || types.includes(variable[0].type)) { + if ( + (types.includes('any') || types.includes(variable[0].type)) && + !ignoreColumns.includes(variable[0].name) + ) { filteredVariablesByType.push(variable[0].name); } } @@ -1515,7 +1523,7 @@ async function getListArgsSuggestions( fields: true, variables: anyVariables, }, - { ignoreFields: [firstArg.name, ...otherArgs.map(({ name }) => name)] } + { ignoreColumns: [firstArg.name, ...otherArgs.map(({ name }) => name)] } )) ); } @@ -1875,18 +1883,16 @@ async function getOptionArgsSuggestions( * for a given fragment of text in a generic way. A good example is * a field name. * - * When typing a field name, there are three scenarios + * When typing a field name, there are 2 scenarios * - * 1. user hasn't begun typing + * 1. field name is incomplete (includes the empty string) * KEEP / - * - * 2. user is typing a partial field name * KEEP fie/ * - * 3. user has typed a complete field name + * 2. field name is complete * KEEP field/ * - * This function provides a framework for handling all three scenarios in a clean way. + * This function provides a framework for detecting and handling both scenarios in a clean way. * * @param innerText - the query text before the current cursor position * @param isFragmentComplete — return true if the fragment is complete From 52148775b1ff4b4379f0049cc2332fb27e405a07 Mon Sep 17 00:00:00 2001 From: Jusheng Huang <117657272+viajes7@users.noreply.github.com> Date: Fri, 11 Oct 2024 00:05:23 +0800 Subject: [PATCH 59/87] [Index Management] Fix filter index list by lifecycle status (#195350) ## Summary Fixes #180970 In `indexLifecycleDataEnricher`, add `only_managed: true` query parameter to fetch lifecycle data. It causes the `ilm` property to be empty in the response. And `EuiSearchBar` `field_value_selection` doesn't support filtering a `undefined` filed value. So, maybe `only_managed: true` should be removed. Before: image After: image Co-authored-by: Elastic Machine Co-authored-by: Elena Stoeva <59341489+ElenaStoeva@users.noreply.github.com> --- .../server/plugin.ts | 1 - .../index_management/data_enrichers/ilm.ts | 22 +++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/index_lifecycle_management/server/plugin.ts b/x-pack/plugins/index_lifecycle_management/server/plugin.ts index 0d88acbaaa4ff..a5002cd36da44 100644 --- a/x-pack/plugins/index_lifecycle_management/server/plugin.ts +++ b/x-pack/plugins/index_lifecycle_management/server/plugin.ts @@ -27,7 +27,6 @@ const indexLifecycleDataEnricher = async ( const { indices: ilmIndicesData } = await client.asCurrentUser.ilm.explainLifecycle({ index: '*,.*', - only_managed: true, }); return indicesList.map((index: Index) => { return { diff --git a/x-pack/test/api_integration/apis/management/index_management/data_enrichers/ilm.ts b/x-pack/test/api_integration/apis/management/index_management/data_enrichers/ilm.ts index 234a1518a9c59..3ae9b554bf3ee 100644 --- a/x-pack/test/api_integration/apis/management/index_management/data_enrichers/ilm.ts +++ b/x-pack/test/api_integration/apis/management/index_management/data_enrichers/ilm.ts @@ -55,16 +55,17 @@ export default function ({ getService }: FtrProviderContext) { const testAlias = 'test_alias'; const testIlmPolicy = 'test_policy'; describe('GET indices with data enrichers', () => { - before(async () => { + beforeEach(async () => { await createIndex(testIndex); - await createIlmPolicy('test_policy'); - await addPolicyToIndex(testIlmPolicy, testIndex, testAlias); }); - after(async () => { + afterEach(async () => { await esDeleteAllIndices([testIndex]); }); it(`ILM data is fetched by the ILM data enricher`, async () => { + await createIlmPolicy('test_policy'); + await addPolicyToIndex(testIlmPolicy, testIndex, testAlias); + const { body: indices } = await supertest .get(`${API_BASE_PATH}/indices`) .set('kbn-xsrf', 'xxx') @@ -75,5 +76,18 @@ export default function ({ getService }: FtrProviderContext) { const { ilm } = index; expect(ilm.policy).to.eql(testIlmPolicy); }); + + it(`ILM data is not empty even if the index unmanaged`, async () => { + const { body: indices } = await supertest + .get(`${API_BASE_PATH}/indices`) + .set('kbn-xsrf', 'xxx') + .expect(200); + + const index = indices.find((item: Index) => item.name === testIndex); + + const { ilm } = index; + expect(ilm.index).to.eql(testIndex); + expect(ilm.managed).to.eql(false); + }); }); } From 8a3832188ac0f9660f243fdf0181e007af623dc3 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 11 Oct 2024 03:58:15 +1100 Subject: [PATCH 60/87] skip failing test suite (#184558) --- .../detection_engine/rule_creation/esql_rule.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule.cy.ts index 0045a79ff4394..64423a921e595 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule.cy.ts @@ -67,7 +67,8 @@ const workaroundForResizeObserver = () => } }); -describe( +// Failing: See https://github.com/elastic/kibana/issues/184558 +describe.skip( 'Detection ES|QL rules, creation', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'], From 35bbb49176c3a933ca6d68e770602bdb2931e71f Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 11 Oct 2024 03:58:44 +1100 Subject: [PATCH 61/87] skip failing test suite (#195804) --- .../e2e/investigations/threat_intelligence/indicators.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/threat_intelligence/indicators.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/threat_intelligence/indicators.cy.ts index ebfc5d4e9a0cb..f485ead495949 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/threat_intelligence/indicators.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/threat_intelligence/indicators.cy.ts @@ -67,7 +67,8 @@ const URL = '/app/security/threat_intelligence/indicators'; const URL_WITH_CONTRADICTORY_FILTERS = '/app/security/threat_intelligence/indicators?indicators=(filterQuery:(language:kuery,query:%27%27),filters:!((%27$state%27:(store:appState),meta:(alias:!n,disabled:!f,index:%27%27,key:threat.indicator.type,negate:!f,params:(query:file),type:phrase),query:(match_phrase:(threat.indicator.type:file))),(%27$state%27:(store:appState),meta:(alias:!n,disabled:!f,index:%27%27,key:threat.indicator.type,negate:!f,params:(query:url),type:phrase),query:(match_phrase:(threat.indicator.type:url)))),timeRange:(from:now/d,to:now/d))'; -describe('Single indicator', { tags: ['@ess'] }, () => { +// Failing: See https://github.com/elastic/kibana/issues/195804 +describe.skip('Single indicator', { tags: ['@ess'] }, () => { before(() => cy.task('esArchiverLoad', { archiveName: 'ti_indicators_data_single' })); after(() => cy.task('esArchiverUnload', { archiveName: 'ti_indicators_data_single' })); From 2bb9c3cc92957faea5985169371e75197f86e407 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Thu, 10 Oct 2024 19:12:29 +0200 Subject: [PATCH 62/87] [ES|QL] Omits sorting non sortable fields on Discover histogram (#195531) ## Summary Closes https://github.com/elastic/kibana/issues/195510 Sorting by `geo_point`, tsdb counter fields and _source is not supported in ES|QL. This PR is omitting the sorting for these types and now the breakdown works fine. image Note: This behavior is unreleased. ### 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 --- packages/kbn-esql-utils/index.ts | 1 + packages/kbn-esql-utils/src/index.ts | 1 + .../src/utils/esql_fields_utils.test.ts | 66 +++++++++++++++++++ .../src/utils/esql_fields_utils.ts | 40 +++++++++++ .../lens_vis_service.suggestions.test.ts | 58 +++++++++++++++- .../public/services/lens_vis_service.ts | 19 ++++-- 6 files changed, 179 insertions(+), 6 deletions(-) create mode 100644 packages/kbn-esql-utils/src/utils/esql_fields_utils.test.ts create mode 100644 packages/kbn-esql-utils/src/utils/esql_fields_utils.ts diff --git a/packages/kbn-esql-utils/index.ts b/packages/kbn-esql-utils/index.ts index 223181f2bd154..333557964d873 100644 --- a/packages/kbn-esql-utils/index.ts +++ b/packages/kbn-esql-utils/index.ts @@ -29,6 +29,7 @@ export { isQueryWrappedByPipes, retrieveMetadataColumns, getQueryColumnsFromESQLQuery, + isESQLColumnSortable, TextBasedLanguages, } from './src'; diff --git a/packages/kbn-esql-utils/src/index.ts b/packages/kbn-esql-utils/src/index.ts index e36283c7a9238..3b3228e7a2a4a 100644 --- a/packages/kbn-esql-utils/src/index.ts +++ b/packages/kbn-esql-utils/src/index.ts @@ -31,3 +31,4 @@ export { getStartEndParams, hasStartEndParams, } from './utils/run_query'; +export { isESQLColumnSortable } from './utils/esql_fields_utils'; diff --git a/packages/kbn-esql-utils/src/utils/esql_fields_utils.test.ts b/packages/kbn-esql-utils/src/utils/esql_fields_utils.test.ts new file mode 100644 index 0000000000000..ef8a24e686bd6 --- /dev/null +++ b/packages/kbn-esql-utils/src/utils/esql_fields_utils.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright 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 { DatatableColumn } from '@kbn/expressions-plugin/common'; +import { isESQLColumnSortable } from './esql_fields_utils'; + +describe('esql fields helpers', () => { + describe('isESQLColumnSortable', () => { + it('returns false for geo fields', () => { + const geoField = { + id: 'geo.coordinates', + name: 'geo.coordinates', + meta: { + type: 'geo_point', + esType: 'geo_point', + }, + isNull: false, + } as DatatableColumn; + expect(isESQLColumnSortable(geoField)).toBeFalsy(); + }); + + it('returns false for source fields', () => { + const sourceField = { + id: '_source', + name: '_source', + meta: { + type: '_source', + esType: '_source', + }, + isNull: false, + } as DatatableColumn; + expect(isESQLColumnSortable(sourceField)).toBeFalsy(); + }); + + it('returns false for counter fields', () => { + const tsdbField = { + id: 'tsbd_counter', + name: 'tsbd_counter', + meta: { + type: 'number', + esType: 'counter_long', + }, + isNull: false, + } as DatatableColumn; + expect(isESQLColumnSortable(tsdbField)).toBeFalsy(); + }); + + it('returns true for everything else', () => { + const keywordField = { + id: 'sortable', + name: 'sortable', + meta: { + type: 'string', + esType: 'keyword', + }, + isNull: false, + } as DatatableColumn; + expect(isESQLColumnSortable(keywordField)).toBeTruthy(); + }); + }); +}); diff --git a/packages/kbn-esql-utils/src/utils/esql_fields_utils.ts b/packages/kbn-esql-utils/src/utils/esql_fields_utils.ts new file mode 100644 index 0000000000000..f5a0fe7b81340 --- /dev/null +++ b/packages/kbn-esql-utils/src/utils/esql_fields_utils.ts @@ -0,0 +1,40 @@ +/* + * Copyright 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 { DatatableColumn } from '@kbn/expressions-plugin/common'; + +const SPATIAL_FIELDS = ['geo_point', 'geo_shape', 'point', 'shape']; +const SOURCE_FIELD = '_source'; +const TSDB_COUNTER_FIELDS_PREFIX = 'counter_'; + +/** + * Check if a column is sortable. + * + * @param column The DatatableColumn of the field. + * @returns True if the column is sortable, false otherwise. + */ + +export const isESQLColumnSortable = (column: DatatableColumn): boolean => { + // We don't allow sorting on spatial fields + if (SPATIAL_FIELDS.includes(column.meta?.type)) { + return false; + } + + // we don't allow sorting on the _source field + if (column.meta?.type === SOURCE_FIELD) { + return false; + } + + // we don't allow sorting on tsdb counter fields + if (column.meta?.esType && column.meta?.esType?.indexOf(TSDB_COUNTER_FIELDS_PREFIX) !== -1) { + return false; + } + + return true; +}; diff --git a/src/plugins/unified_histogram/public/services/lens_vis_service.suggestions.test.ts b/src/plugins/unified_histogram/public/services/lens_vis_service.suggestions.test.ts index 28819f7a5c54b..1719adebe7a49 100644 --- a/src/plugins/unified_histogram/public/services/lens_vis_service.suggestions.test.ts +++ b/src/plugins/unified_histogram/public/services/lens_vis_service.suggestions.test.ts @@ -12,6 +12,7 @@ import { DataViewField } from '@kbn/data-views-plugin/common'; import { deepMockedFields, buildDataViewMock } from '@kbn/discover-utils/src/__mocks__'; import { allSuggestionsMock } from '../__mocks__/suggestions'; import { getLensVisMock } from '../__mocks__/lens_vis'; +import { convertDatatableColumnToDataViewFieldSpec } from '@kbn/data-view-utils'; import { UnifiedHistogramSuggestionType } from '../types'; describe('LensVisService suggestions', () => { @@ -198,6 +199,11 @@ describe('LensVisService suggestions', () => { }); test('should return histogramSuggestion if no suggestions returned by the api with the breakdown field if it is given', async () => { + const breakdown = convertDatatableColumnToDataViewFieldSpec({ + name: 'var0', + id: 'var0', + meta: { type: 'number' }, + }); const lensVis = await getLensVisMock({ filters: [], query: { esql: 'from the-data-view | limit 100' }, @@ -207,7 +213,7 @@ describe('LensVisService suggestions', () => { from: '2023-09-03T08:00:00.000Z', to: '2023-09-04T08:56:28.274Z', }, - breakdownField: { name: 'var0' } as DataViewField, + breakdownField: breakdown as DataViewField, columns: [ { id: 'var0', @@ -247,4 +253,54 @@ describe('LensVisService suggestions', () => { expect(lensVis.visContext?.attributes.state.query).toStrictEqual(histogramQuery); }); + + test('should return histogramSuggestion if no suggestions returned by the api with a geo point breakdown field correctly', async () => { + const lensVis = await getLensVisMock({ + filters: [], + query: { esql: 'from the-data-view | limit 100' }, + dataView: dataViewMock, + timeInterval: 'auto', + timeRange: { + from: '2023-09-03T08:00:00.000Z', + to: '2023-09-04T08:56:28.274Z', + }, + breakdownField: { name: 'coordinates' } as DataViewField, + columns: [ + { + id: 'coordinates', + name: 'coordinates', + meta: { + type: 'geo_point', + }, + }, + ], + isPlainRecord: true, + allSuggestions: [], + hasHistogramSuggestionForESQL: true, + }); + + expect(lensVis.currentSuggestionContext?.type).toBe( + UnifiedHistogramSuggestionType.histogramForESQL + ); + expect(lensVis.currentSuggestionContext?.suggestion).toBeDefined(); + expect(lensVis.currentSuggestionContext?.suggestion?.visualizationState).toHaveProperty( + 'layers', + [ + { + layerId: '662552df-2cdc-4539-bf3b-73b9f827252c', + seriesType: 'bar_stacked', + xAccessor: '@timestamp every 30 second', + accessors: ['results'], + layerType: 'data', + }, + ] + ); + + const histogramQuery = { + esql: `from the-data-view | limit 100 +| EVAL timestamp=DATE_TRUNC(30 minute, @timestamp) | stats results = count(*) by timestamp, \`coordinates\` | rename timestamp as \`@timestamp every 30 minute\``, + }; + + expect(lensVis.visContext?.attributes.state.query).toStrictEqual(histogramQuery); + }); }); diff --git a/src/plugins/unified_histogram/public/services/lens_vis_service.ts b/src/plugins/unified_histogram/public/services/lens_vis_service.ts index eccfd663b2557..25bb8be6f6242 100644 --- a/src/plugins/unified_histogram/public/services/lens_vis_service.ts +++ b/src/plugins/unified_histogram/public/services/lens_vis_service.ts @@ -9,7 +9,11 @@ import { BehaviorSubject, distinctUntilChanged, map, Observable } from 'rxjs'; import { isEqual } from 'lodash'; -import { removeDropCommandsFromESQLQuery, appendToESQLQuery } from '@kbn/esql-utils'; +import { + removeDropCommandsFromESQLQuery, + appendToESQLQuery, + isESQLColumnSortable, +} from '@kbn/esql-utils'; import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import type { CountIndexPatternColumn, @@ -553,12 +557,17 @@ export class LensVisService { const queryInterval = interval ?? computeInterval(timeRange, this.services.data); const language = getAggregateQueryMode(query); const safeQuery = removeDropCommandsFromESQLQuery(query[language]); - const breakdown = breakdownColumn - ? `, \`${breakdownColumn.name}\` | sort \`${breakdownColumn.name}\` asc` - : ''; + const breakdown = breakdownColumn ? `, \`${breakdownColumn.name}\`` : ''; + + // sort by breakdown column if it's sortable + const sortBy = + breakdownColumn && isESQLColumnSortable(breakdownColumn) + ? ` | sort \`${breakdownColumn.name}\` asc` + : ''; + return appendToESQLQuery( safeQuery, - `| EVAL timestamp=DATE_TRUNC(${queryInterval}, ${dataView.timeFieldName}) | stats results = count(*) by timestamp${breakdown} | rename timestamp as \`${dataView.timeFieldName} every ${queryInterval}\`` + `| EVAL timestamp=DATE_TRUNC(${queryInterval}, ${dataView.timeFieldName}) | stats results = count(*) by timestamp${breakdown}${sortBy} | rename timestamp as \`${dataView.timeFieldName} every ${queryInterval}\`` ); }; From 8d1bc50335892cc2eecf9fadce569e6facc282e5 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 10 Oct 2024 12:42:05 -0500 Subject: [PATCH 63/87] skip failing configs (#195811) --- .buildkite/ftr_oblt_serverless_configs.yml | 6 ++++-- .buildkite/ftr_search_serverless_configs.yml | 6 ++++-- .buildkite/ftr_security_serverless_configs.yml | 6 ++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.buildkite/ftr_oblt_serverless_configs.yml b/.buildkite/ftr_oblt_serverless_configs.yml index fbf0406f37be4..75909e7c21c46 100644 --- a/.buildkite/ftr_oblt_serverless_configs.yml +++ b/.buildkite/ftr_oblt_serverless_configs.yml @@ -6,6 +6,10 @@ disabled: - x-pack/test_serverless/functional/test_suites/observability/cypress/config_headless.ts - x-pack/test_serverless/functional/test_suites/observability/cypress/config_runner.ts + # serverless config files that run deployment-agnostic tests + # Failing https://github.com/elastic/kibana/issues/195811 + - x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts + defaultQueue: 'n2-4-spot' enabled: - x-pack/test_serverless/api_integration/test_suites/observability/config.ts @@ -25,5 +29,3 @@ enabled: - x-pack/test_serverless/functional/test_suites/observability/common_configs/config.group5.ts - x-pack/test_serverless/functional/test_suites/observability/common_configs/config.group6.ts - x-pack/test_serverless/functional/test_suites/observability/config.screenshots.ts - # serverless config files that run deployment-agnostic tests - - x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts diff --git a/.buildkite/ftr_search_serverless_configs.yml b/.buildkite/ftr_search_serverless_configs.yml index e6efee5860806..413558bffa0fe 100644 --- a/.buildkite/ftr_search_serverless_configs.yml +++ b/.buildkite/ftr_search_serverless_configs.yml @@ -1,6 +1,10 @@ disabled: # Base config files, only necessary to inform config finding script + # serverless config files that run deployment-agnostic tests + # Failing https://github.com/elastic/kibana/issues/195811 + - x-pack/test/api_integration/deployment_agnostic/configs/serverless/search.serverless.config.ts + defaultQueue: 'n2-4-spot' enabled: - x-pack/test_serverless/api_integration/test_suites/search/config.ts @@ -18,5 +22,3 @@ enabled: - x-pack/test_serverless/functional/test_suites/search/common_configs/config.group4.ts - x-pack/test_serverless/functional/test_suites/search/common_configs/config.group5.ts - x-pack/test_serverless/functional/test_suites/search/common_configs/config.group6.ts - # serverless config files that run deployment-agnostic tests - - x-pack/test/api_integration/deployment_agnostic/configs/serverless/search.serverless.config.ts diff --git a/.buildkite/ftr_security_serverless_configs.yml b/.buildkite/ftr_security_serverless_configs.yml index 6d42c030b2d4f..caf9fcc5ac92a 100644 --- a/.buildkite/ftr_security_serverless_configs.yml +++ b/.buildkite/ftr_security_serverless_configs.yml @@ -20,6 +20,10 @@ disabled: - x-pack/test_serverless/functional/config.base.ts - x-pack/test_serverless/shared/config.base.ts + # serverless config files that run deployment-agnostic tests + # Failing https://github.com/elastic/kibana/issues/195811 + - x-pack/test/api_integration/deployment_agnostic/configs/serverless/security.serverless.config.ts + defaultQueue: 'n2-4-spot' enabled: - x-pack/test_serverless/api_integration/test_suites/security/config.ts @@ -100,5 +104,3 @@ enabled: - x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/configs/serverless.config.ts - x-pack/test/security_solution_endpoint/configs/serverless.endpoint.config.ts - x-pack/test/security_solution_endpoint/configs/serverless.integrations.config.ts - # serverless config files that run deployment-agnostic tests - - x-pack/test/api_integration/deployment_agnostic/configs/serverless/security.serverless.config.ts From 84ebcb07c9aa83a59629e0591cdf560855c4a13c Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 11 Oct 2024 05:17:18 +1100 Subject: [PATCH 64/87] skip failing test suite (#184557) --- .../detection_engine/rule_edit/esql_rule.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 9fa45987407f0..cc8acadc412b1 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 @@ -55,7 +55,8 @@ const expectedValidEsqlQuery = 'from auditbeat* | stats _count=count(event.category) by event.category'; // Skipping in MKI due to flake -describe( +// Failing: See https://github.com/elastic/kibana/issues/184557 +describe.skip( 'Detection ES|QL rules, edit', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'], From 7a53d8104602dcb510621b31f85a7a74c0fe7809 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 11 Oct 2024 05:22:10 +1100 Subject: [PATCH 65/87] skip failing test suite (#184556) --- .../detection_engine/rule_edit/esql_rule.cy.ts | 1 + 1 file changed, 1 insertion(+) 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 cc8acadc412b1..34f301602b692 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 @@ -56,6 +56,7 @@ const expectedValidEsqlQuery = // Skipping in MKI due to flake // Failing: See https://github.com/elastic/kibana/issues/184557 +// Failing: See https://github.com/elastic/kibana/issues/184556 describe.skip( 'Detection ES|QL rules, edit', { From ad8cec13b6ebb8270dd91fc4012ed8ff2b531353 Mon Sep 17 00:00:00 2001 From: Jorge Sanz Date: Thu, 10 Oct 2024 21:02:23 +0200 Subject: [PATCH 66/87] [Docs][Maps] Update EMS Server instructions (#195419) ## Summary Small improvements to the Elastic Maps Service documentation: * fixes the reference to the Docker image to pull * adds details about using `cosign` to verify the image pulled * updates the screenshot to a more recent UI. --- docs/maps/connect-to-ems.asciidoc | 45 ++++++++++++------ .../elastic-maps-server-instructions.png | Bin 48671 -> 107478 bytes 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/docs/maps/connect-to-ems.asciidoc b/docs/maps/connect-to-ems.asciidoc index e41d544d64e4d..1ccdedb1da2a9 100644 --- a/docs/maps/connect-to-ems.asciidoc +++ b/docs/maps/connect-to-ems.asciidoc @@ -1,6 +1,6 @@ :ems: Elastic Maps Service :ems-docker-repo: docker.elastic.co/elastic-maps-service/elastic-maps-server -:ems-docker-image: {ems-docker-repo}:{version}-amd64 +:ems-docker-image: {ems-docker-repo}:{version} :ems-headers-url: https://deployment-host [[maps-connect-to-ems]] @@ -81,34 +81,53 @@ If you cannot connect to {ems} from the {kib} server or browser clients, and you {hosted-ems} is a self-managed version of {ems} offered as a Docker image that provides both the EMS basemaps and EMS boundaries. The image is bundled with basemaps up to zoom level 8. After connecting it to your {es} cluster for license validation, you have the option to download and configure a more detailed basemaps database. -You can use +docker pull+ to download the {hosted-ems} image from the Elastic Docker registry. - +. Pull the {hosted-ems} Docker image. ++ ifeval::["{release-state}"=="unreleased"] -Version {version} of {hosted-ems} has not yet been released, so no Docker image is currently available for this version. +WARNING: Version {version} of {hosted-ems} has not yet been released. +No Docker image is currently available for this version. endif::[] - -ifeval::["{release-state}"!="unreleased"] - ++ ["source","bash",subs="attributes"] ---------------------------------- docker pull {ems-docker-image} ---------------------------------- -Start {hosted-ems} and expose the default port `8080`: +. Optional: Install +https://docs.sigstore.dev/system_config/installation/[Cosign] for your +environment. Then use Cosign to verify the {es} image's signature. ++ +[source,sh,subs="attributes"] +---- +wget https://artifacts.elastic.co/cosign.pub +cosign verify --key cosign.pub {ems-docker-image} +---- ++ +The `cosign` command prints the check results and the signature payload in JSON format: ++ +[source,sh,subs="attributes"] +-------------------------------------------- +Verification for {ems-docker-image} -- +The following checks were performed on each of these signatures: + - The cosign claims were validated + - Existence of the claims in the transparency log was verified offline + - The signatures were verified against the specified public key +-------------------------------------------- + +. Start {hosted-ems} and expose the default port `8080`: ++ ["source","bash",subs="attributes"] ---------------------------------- docker run --rm --init --publish 8080:8080 \ {ems-docker-image} ---------------------------------- - ++ Once {hosted-ems} is running, follow instructions from the webpage at `localhost:8080` to define a configuration file and optionally download a more detailed basemaps database. - ++ [role="screenshot"] image::images/elastic-maps-server-instructions.png[Set-up instructions] -endif::[] - [float] [[elastic-maps-server-configuration]] ==== Configuration @@ -193,7 +212,6 @@ One way to configure {hosted-ems} is to provide `elastic-maps-server.yml` via bi ["source","yaml",subs="attributes"] -------------------------------------------- -version: '2' services: ems-server: image: {ems-docker-image} @@ -212,7 +230,6 @@ These variables can be set with +docker-compose+ like this: ["source","yaml",subs="attributes"] ---------------------------------------------------------- -version: '2' services: ems-server: image: {ems-docker-image} diff --git a/docs/maps/images/elastic-maps-server-instructions.png b/docs/maps/images/elastic-maps-server-instructions.png index 5c0b47ce8f49f34278e6f5515b940585ee74cb52..524ae2192b5e57a91d538fefea302e6f5b1421a8 100644 GIT binary patch literal 107478 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

    WRf>n&7Y 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�c@_+`AqvL}#)jNh5|2F0;pEc_zHjN)n!-xwcG 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�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 literal 48671 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

    !XY%h{+U)}( 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 Date: Thu, 10 Oct 2024 14:59:20 -0500 Subject: [PATCH 67/87] Skip only Risk Engine initializing test with a FIPS issue (#195651) ## Summary More investigation is needed to ensure that the `remove legacy risk score transform` test is passing in promotion pipelines. However, that particular feature that the test asserts (Legacy Entity Risk Scoring) was never available in Serverless. Therefore, we're enabling the broader tests, and just skipping the one containing the FIPS issue. --- .../trial_license_complete_tier/init_and_status_apis.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts index bd3493b82d348..19a9bb85326fa 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts @@ -26,8 +26,7 @@ export default ({ getService }: FtrProviderContext) => { const riskEngineRoutes = riskEngineRouteHelpersFactory(supertest); const log = getService('log'); - // Failing: See https://github.com/elastic/kibana/issues/191637 - describe.skip('@ess @serverless @serverlessQA init_and_status_apis', () => { + describe('@ess @serverless @serverlessQA init_and_status_apis', () => { before(async () => { await riskEngineRoutes.cleanUp(); }); @@ -298,8 +297,8 @@ export default ({ getService }: FtrProviderContext) => { firstResponse?.saved_objects?.[0]?.id ); }); - - describe('remove legacy risk score transform', function () { + // Failing: See https://github.com/elastic/kibana/issues/191637 + describe.skip('remove legacy risk score transform', function () { this.tags('skipFIPS'); it('should remove legacy risk score transform if it exists', async () => { await installLegacyRiskScore({ supertest }); From 3ec190823fa39520dc50f5c4631eb81cd223ed3e Mon Sep 17 00:00:00 2001 From: Sandra G Date: Thu, 10 Oct 2024 15:59:48 -0400 Subject: [PATCH 68/87] [Data Usage] process autoops mock data (#195640) - validates autoOps response data using mock data and new type - processes autoOps data to return an object of {x,y} values from our API instead of array of [timestamp, value]. updates UI accordingly --- .../common/rest_types/usage_metrics.test.ts | 79 +++++++-------- .../common/rest_types/usage_metrics.ts | 99 ++++++++++--------- .../public/app/components/chart_panel.tsx | 13 +-- .../public/app/components/charts.tsx | 4 +- .../data_usage/public/app/data_usage.tsx | 29 +++--- x-pack/plugins/data_usage/public/app/types.ts | 24 ----- .../public/hooks/use_get_usage_metrics.ts | 18 ++-- .../server/routes/internal/usage_metrics.ts | 6 +- .../routes/internal/usage_metrics_handler.ts | 59 +++++------ 9 files changed, 153 insertions(+), 178 deletions(-) delete mode 100644 x-pack/plugins/data_usage/public/app/types.ts diff --git a/x-pack/plugins/data_usage/common/rest_types/usage_metrics.test.ts b/x-pack/plugins/data_usage/common/rest_types/usage_metrics.test.ts index f6c08e2caddc0..473e64c6b03d9 100644 --- a/x-pack/plugins/data_usage/common/rest_types/usage_metrics.test.ts +++ b/x-pack/plugins/data_usage/common/rest_types/usage_metrics.test.ts @@ -10,48 +10,29 @@ import { UsageMetricsRequestSchema } from './usage_metrics'; describe('usage_metrics schemas', () => { it('should accept valid request query', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: new Date().toISOString(), metricTypes: ['storage_retained'], - }) - ).not.toThrow(); - }); - - it('should accept a single `metricTypes` in request query', () => { - expect(() => - UsageMetricsRequestSchema.query.validate({ - from: new Date().toISOString(), - to: new Date().toISOString(), - metricTypes: 'ingest_rate', + dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], }) ).not.toThrow(); }); it('should accept multiple `metricTypes` in request query', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: new Date().toISOString(), metricTypes: ['ingest_rate', 'storage_retained', 'index_rate'], - }) - ).not.toThrow(); - }); - - it('should accept a single string as `dataStreams` in request query', () => { - expect(() => - UsageMetricsRequestSchema.query.validate({ - from: new Date().toISOString(), - to: new Date().toISOString(), - metricTypes: 'storage_retained', - dataStreams: 'data_stream_1', + dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], }) ).not.toThrow(); }); it('should accept `dataStream` list', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: new Date().toISOString(), metricTypes: ['storage_retained'], @@ -62,74 +43,76 @@ describe('usage_metrics schemas', () => { it('should error if `dataStream` list is empty', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: new Date().toISOString(), metricTypes: ['storage_retained'], dataStreams: [], }) - ).toThrowError('expected value of type [string] but got [Array]'); + ).toThrowError('[dataStreams]: array size is [0], but cannot be smaller than [1]'); }); - it('should error if `dataStream` is given an empty string', () => { + it('should error if `dataStream` is given type not array', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: new Date().toISOString(), metricTypes: ['storage_retained'], dataStreams: ' ', }) - ).toThrow('[dataStreams] must have at least one value'); + ).toThrow('[dataStreams]: could not parse array value from json input'); }); it('should error if `dataStream` is given an empty item in the list', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: new Date().toISOString(), metricTypes: ['storage_retained'], dataStreams: ['ds_1', ' '], }) - ).toThrow('[dataStreams] list can not contain empty values'); + ).toThrow('[dataStreams]: [dataStreams] list cannot contain empty values'); }); it('should error if `metricTypes` is empty string', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: new Date().toISOString(), + dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], metricTypes: ' ', }) ).toThrow(); }); - it('should error if `metricTypes` is empty item', () => { + it('should error if `metricTypes` contains an empty item', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: new Date().toISOString(), - metricTypes: [' ', 'storage_retained'], + dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], + metricTypes: [' ', 'storage_retained'], // First item is invalid }) - ).toThrow('[metricTypes] list can not contain empty values'); + ).toThrowError(/list cannot contain empty values/); }); - it('should error if `metricTypes` is not a valid value', () => { + it('should error if `metricTypes` is not a valid type', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: new Date().toISOString(), + dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], metricTypes: 'foo', }) - ).toThrow( - '[metricTypes] must be one of storage_retained, ingest_rate, search_vcu, ingest_vcu, ml_vcu, index_latency, index_rate, search_latency, search_rate' - ); + ).toThrow('[metricTypes]: could not parse array value from json input'); }); it('should error if `metricTypes` is not a valid list', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: new Date().toISOString(), + dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], metricTypes: ['storage_retained', 'foo'], }) ).toThrow( @@ -139,9 +122,10 @@ describe('usage_metrics schemas', () => { it('should error if `from` is not a valid input', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: 1010, to: new Date().toISOString(), + dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], metricTypes: ['storage_retained', 'foo'], }) ).toThrow('[from]: expected value of type [string] but got [number]'); @@ -149,9 +133,10 @@ describe('usage_metrics schemas', () => { it('should error if `to` is not a valid input', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: 1010, + dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], metricTypes: ['storage_retained', 'foo'], }) ).toThrow('[to]: expected value of type [string] but got [number]'); @@ -159,9 +144,10 @@ describe('usage_metrics schemas', () => { it('should error if `from` is empty string', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: ' ', to: new Date().toISOString(), + dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], metricTypes: ['storage_retained', 'foo'], }) ).toThrow('[from]: Date ISO string must not be empty'); @@ -169,9 +155,10 @@ describe('usage_metrics schemas', () => { it('should error if `to` is empty string', () => { expect(() => - UsageMetricsRequestSchema.query.validate({ + UsageMetricsRequestSchema.validate({ from: new Date().toISOString(), to: ' ', + dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'], metricTypes: ['storage_retained', 'foo'], }) ).toThrow('[to]: Date ISO string must not be empty'); diff --git a/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts b/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts index f2bbdb616fc79..3dceeadc198b0 100644 --- a/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts +++ b/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts @@ -37,51 +37,31 @@ const metricTypesSchema = schema.oneOf( // @ts-expect-error TS2769: No overload matches this call METRIC_TYPE_VALUES.map((metricType) => schema.literal(metricType)) // Create a oneOf schema for the keys ); -export const UsageMetricsRequestSchema = { - query: schema.object({ - from: DateSchema, - to: DateSchema, - metricTypes: schema.oneOf([ - schema.arrayOf(schema.string(), { - minSize: 1, - validate: (values) => { - if (values.map((v) => v.trim()).some((v) => !v.length)) { - return '[metricTypes] list can not contain empty values'; - } else if (values.map((v) => v.trim()).some((v) => !isValidMetricType(v))) { - return `[metricTypes] must be one of ${METRIC_TYPE_VALUES.join(', ')}`; - } - }, - }), - schema.string({ - validate: (v) => { - if (!v.trim().length) { - return '[metricTypes] must have at least one value'; - } else if (!isValidMetricType(v)) { - return `[metricTypes] must be one of ${METRIC_TYPE_VALUES.join(', ')}`; - } - }, - }), - ]), - dataStreams: schema.maybe( - schema.oneOf([ - schema.arrayOf(schema.string(), { - minSize: 1, - validate: (values) => { - if (values.map((v) => v.trim()).some((v) => !v.length)) { - return '[dataStreams] list can not contain empty values'; - } - }, - }), - schema.string({ - validate: (v) => - v.trim().length ? undefined : '[dataStreams] must have at least one value', - }), - ]) - ), +export const UsageMetricsRequestSchema = schema.object({ + from: DateSchema, + to: DateSchema, + metricTypes: schema.arrayOf(schema.string(), { + minSize: 1, + validate: (values) => { + const trimmedValues = values.map((v) => v.trim()); + if (trimmedValues.some((v) => !v.length)) { + return '[metricTypes] list cannot contain empty values'; + } else if (trimmedValues.some((v) => !isValidMetricType(v))) { + return `[metricTypes] must be one of ${METRIC_TYPE_VALUES.join(', ')}`; + } + }, }), -}; + dataStreams: schema.arrayOf(schema.string(), { + minSize: 1, + validate: (values) => { + if (values.map((v) => v.trim()).some((v) => !v.length)) { + return '[dataStreams] list cannot contain empty values'; + } + }, + }), +}); -export type UsageMetricsRequestSchemaQueryParams = TypeOf; +export type UsageMetricsRequestSchemaQueryParams = TypeOf; export const UsageMetricsResponseSchema = { body: () => @@ -92,11 +72,40 @@ export const UsageMetricsResponseSchema = { schema.object({ name: schema.string(), data: schema.arrayOf( - schema.arrayOf(schema.number(), { minSize: 2, maxSize: 2 }) // Each data point is an array of 2 numbers + schema.object({ + x: schema.number(), + y: schema.number(), + }) ), }) ) ), }), }; -export type UsageMetricsResponseSchemaBody = TypeOf; +export type UsageMetricsResponseSchemaBody = Omit< + TypeOf, + 'metrics' +> & { + metrics: Partial>; +}; +export type MetricSeries = TypeOf< + typeof UsageMetricsResponseSchema.body +>['metrics'][MetricTypes][number]; + +export const UsageMetricsAutoOpsResponseSchema = { + body: () => + schema.object({ + metrics: schema.recordOf( + metricTypesSchema, + schema.arrayOf( + schema.object({ + name: schema.string(), + data: schema.arrayOf(schema.arrayOf(schema.number(), { minSize: 2, maxSize: 2 })), + }) + ) + ), + }), +}; +export type UsageMetricsAutoOpsResponseSchemaBody = TypeOf< + typeof UsageMetricsAutoOpsResponseSchema.body +>; diff --git a/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx b/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx index c7937ae149de9..1ba3f0fe3f454 100644 --- a/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx +++ b/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx @@ -19,8 +19,7 @@ import { } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import { LegendAction } from './legend_action'; -import { MetricTypes } from '../../../common/rest_types'; -import { MetricSeries } from '../types'; +import { MetricTypes, MetricSeries } from '../../../common/rest_types'; // TODO: Remove this when we have a title for each metric type type ChartKey = Extract; @@ -50,7 +49,7 @@ export const ChartPanel: React.FC = ({ }) => { const theme = useEuiTheme(); - const chartTimestamps = series.flatMap((stream) => stream.data.map((d) => d[0])); + const chartTimestamps = series.flatMap((stream) => stream.data.map((d) => d.x)); const [minTimestamp, maxTimestamp] = [Math.min(...chartTimestamps), Math.max(...chartTimestamps)]; @@ -72,6 +71,7 @@ export const ChartPanel: React.FC = ({ }, [idx, popoverOpen, togglePopover] ); + return ( @@ -94,9 +94,9 @@ export const ChartPanel: React.FC = ({ data={stream.data} xScaleType={ScaleType.Time} yScaleType={ScaleType.Linear} - xAccessor={0} // x is the first element in the tuple - yAccessors={[1]} // y is the second element in the tuple - stackAccessors={[0]} + xAccessor="x" + yAccessors={['y']} + stackAccessors={['x']} /> ))} @@ -118,6 +118,7 @@ export const ChartPanel: React.FC = ({ ); }; + const formatBytes = (bytes: number) => { return numeral(bytes).format('0.0 b'); }; diff --git a/x-pack/plugins/data_usage/public/app/components/charts.tsx b/x-pack/plugins/data_usage/public/app/components/charts.tsx index 6549f7e03830a..8d04324fb2246 100644 --- a/x-pack/plugins/data_usage/public/app/components/charts.tsx +++ b/x-pack/plugins/data_usage/public/app/components/charts.tsx @@ -6,11 +6,11 @@ */ import React, { useCallback, useState } from 'react'; import { EuiFlexGroup } from '@elastic/eui'; -import { MetricsResponse } from '../types'; import { MetricTypes } from '../../../common/rest_types'; import { ChartPanel } from './chart_panel'; +import { UsageMetricsResponseSchemaBody } from '../../../common/rest_types'; interface ChartsProps { - data: MetricsResponse; + data: UsageMetricsResponseSchemaBody; } export const Charts: React.FC = ({ data }) => { diff --git a/x-pack/plugins/data_usage/public/app/data_usage.tsx b/x-pack/plugins/data_usage/public/app/data_usage.tsx index c32f86d68b5bf..bea9f2b511a77 100644 --- a/x-pack/plugins/data_usage/public/app/data_usage.tsx +++ b/x-pack/plugins/data_usage/public/app/data_usage.tsx @@ -26,7 +26,6 @@ import { PLUGIN_NAME } from '../../common'; import { useGetDataUsageMetrics } from '../hooks/use_get_usage_metrics'; import { DEFAULT_DATE_RANGE_OPTIONS, useDateRangePicker } from './hooks/use_date_picker'; import { useDataUsageMetricsUrlParams } from './hooks/use_charts_url_params'; -import { MetricsResponse } from './types'; export const DataUsage = () => { const { @@ -42,37 +41,37 @@ export const DataUsage = () => { setUrlDateRangeFilter, } = useDataUsageMetricsUrlParams(); - const [queryParams, setQueryParams] = useState({ + const [metricsFilters, setMetricsFilters] = useState({ metricTypes: ['storage_retained', 'ingest_rate'], - dataStreams: [], + // TODO: Replace with data streams from /data_streams api + dataStreams: [ + '.alerts-ml.anomaly-detection-health.alerts-default', + '.alerts-stack.alerts-default', + ], from: DEFAULT_DATE_RANGE_OPTIONS.startDate, to: DEFAULT_DATE_RANGE_OPTIONS.endDate, }); useEffect(() => { if (!metricTypesFromUrl) { - setUrlMetricTypesFilter( - typeof queryParams.metricTypes !== 'string' - ? queryParams.metricTypes.join(',') - : queryParams.metricTypes - ); + setUrlMetricTypesFilter(metricsFilters.metricTypes.join(',')); } if (!startDateFromUrl || !endDateFromUrl) { - setUrlDateRangeFilter({ startDate: queryParams.from, endDate: queryParams.to }); + setUrlDateRangeFilter({ startDate: metricsFilters.from, endDate: metricsFilters.to }); } }, [ endDateFromUrl, metricTypesFromUrl, - queryParams.from, - queryParams.metricTypes, - queryParams.to, + metricsFilters.from, + metricsFilters.metricTypes, + metricsFilters.to, setUrlDateRangeFilter, setUrlMetricTypesFilter, startDateFromUrl, ]); useEffect(() => { - setQueryParams((prevState) => ({ + setMetricsFilters((prevState) => ({ ...prevState, metricTypes: metricTypesFromUrl?.length ? metricTypesFromUrl : prevState.metricTypes, dataStreams: dataStreamsFromUrl?.length ? dataStreamsFromUrl : prevState.dataStreams, @@ -89,7 +88,7 @@ export const DataUsage = () => { refetch: refetchDataUsageMetrics, } = useGetDataUsageMetrics( { - ...queryParams, + ...metricsFilters, from: dateRangePickerState.startDate, to: dateRangePickerState.endDate, }, @@ -140,7 +139,7 @@ export const DataUsage = () => { - {isFetched && data ? : } + {isFetched && data ? : } ); diff --git a/x-pack/plugins/data_usage/public/app/types.ts b/x-pack/plugins/data_usage/public/app/types.ts deleted file mode 100644 index 13f53bc2ea6dd..0000000000000 --- a/x-pack/plugins/data_usage/public/app/types.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 { MetricTypes } from '../../common/rest_types'; - -export type DataPoint = [number, number]; // [timestamp, value] - -export interface MetricSeries { - name: string; // Name of the data stream - data: DataPoint[]; // Array of data points in tuple format [timestamp, value] -} -// Use MetricTypes dynamically as keys for the Metrics interface -export type Metrics = Partial>; - -export interface MetricsResponse { - metrics: Metrics; -} -export interface MetricsResponse { - metrics: Metrics; -} diff --git a/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts b/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts index 6b9860e997c12..3d648eb183f07 100644 --- a/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts +++ b/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts @@ -21,24 +21,24 @@ interface ErrorType { } export const useGetDataUsageMetrics = ( - query: UsageMetricsRequestSchemaQueryParams, + body: UsageMetricsRequestSchemaQueryParams, options: UseQueryOptions> = {} ): UseQueryResult> => { const http = useKibanaContextForPlugin().services.http; return useQuery>({ - queryKey: ['get-data-usage-metrics', query], + queryKey: ['get-data-usage-metrics', body], ...options, keepPreviousData: true, queryFn: async () => { - return http.get(DATA_USAGE_METRICS_API_ROUTE, { + return http.post(DATA_USAGE_METRICS_API_ROUTE, { version: '1', - query: { - from: query.from, - to: query.to, - metricTypes: query.metricTypes, - dataStreams: query.dataStreams, - }, + body: JSON.stringify({ + from: body.from, + to: body.to, + metricTypes: body.metricTypes, + dataStreams: body.dataStreams, + }), }); }, }); diff --git a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.ts b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.ts index 5bf3008ef668a..0013102f697fb 100644 --- a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.ts +++ b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.ts @@ -17,7 +17,7 @@ export const registerUsageMetricsRoute = ( ) => { if (dataUsageContext.serverConfig.enabled) { router.versioned - .get({ + .post({ access: 'internal', path: DATA_USAGE_METRICS_API_ROUTE, }) @@ -25,7 +25,9 @@ export const registerUsageMetricsRoute = ( { version: '1', validate: { - request: UsageMetricsRequestSchema, + request: { + body: UsageMetricsRequestSchema, + }, response: { 200: UsageMetricsResponseSchema, }, diff --git a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts index 6f992c9fb2a38..09e9f88721c63 100644 --- a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts +++ b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts @@ -9,8 +9,10 @@ import { RequestHandler } from '@kbn/core/server'; import { IndicesGetDataStreamResponse } from '@elastic/elasticsearch/lib/api/types'; import { MetricTypes, + UsageMetricsAutoOpsResponseSchema, + UsageMetricsAutoOpsResponseSchemaBody, UsageMetricsRequestSchemaQueryParams, - UsageMetricsResponseSchema, + UsageMetricsResponseSchemaBody, } from '../../../common/rest_types'; import { DataUsageContext, DataUsageRequestHandlerContext } from '../../types'; @@ -34,45 +36,26 @@ export const getUsageMetricsHandler = ( const core = await context.core; const esClient = core.elasticsearch.client.asCurrentUser; - // @ts-ignore - const { from, to, metricTypes, dataStreams: dsNames, size } = request.query; + const { from, to, metricTypes, dataStreams: requestDsNames } = request.query; logger.debug(`Retrieving usage metrics`); const { data_streams: dataStreamsResponse }: IndicesGetDataStreamResponse = await esClient.indices.getDataStream({ - name: '*', + name: requestDsNames, expand_wildcards: 'all', }); - const hasDataStreams = dataStreamsResponse.length > 0; - let userDsNames: string[] = []; - - if (dsNames?.length) { - userDsNames = typeof dsNames === 'string' ? [dsNames] : dsNames; - } else if (!userDsNames.length && hasDataStreams) { - userDsNames = dataStreamsResponse.map((ds) => ds.name); - } - - // If no data streams are found, return an empty response - if (!userDsNames.length) { - return response.ok({ - body: { - metrics: {}, - }, - }); - } - const metrics = await fetchMetricsFromAutoOps({ from, to, metricTypes: formatStringParams(metricTypes) as MetricTypes[], - dataStreams: formatStringParams(userDsNames), + dataStreams: formatStringParams(dataStreamsResponse.map((ds) => ds.name)), }); + const processedMetrics = transformMetricsData(metrics); + return response.ok({ - body: { - metrics, - }, + body: processedMetrics, }); } catch (error) { logger.error(`Error retrieving usage metrics: ${error.message}`); @@ -94,7 +77,7 @@ const fetchMetricsFromAutoOps = async ({ }) => { // TODO: fetch data from autoOps using userDsNames /* - const response = await axios.post('https://api.auto-ops.{region}.{csp}.cloud.elastic.co/monitoring/serverless/v1/projects/{project_id}/metrics', { + const response = await axios.post({AUTOOPS_URL}, { from: Date.parse(from), to: Date.parse(to), metric_types: metricTypes, @@ -231,7 +214,25 @@ const fetchMetricsFromAutoOps = async ({ }, }; // Make sure data is what we expect - const validatedData = UsageMetricsResponseSchema.body().validate(mockData); + const validatedData = UsageMetricsAutoOpsResponseSchema.body().validate(mockData); - return validatedData.metrics; + return validatedData; }; +function transformMetricsData( + data: UsageMetricsAutoOpsResponseSchemaBody +): UsageMetricsResponseSchemaBody { + return { + metrics: Object.fromEntries( + Object.entries(data.metrics).map(([metricType, series]) => [ + metricType, + series.map((metricSeries) => ({ + name: metricSeries.name, + data: (metricSeries.data as Array<[number, number]>).map(([timestamp, value]) => ({ + x: timestamp, + y: value, + })), + })), + ]) + ), + }; +} From eefabb0f534234d6c2c0e3468bbdc65a16009e93 Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Thu, 10 Oct 2024 22:00:38 +0200 Subject: [PATCH 69/87] ci(bump automation): bump ubi9 for ironbank (#191660) --- .github/updatecli/values.d/ironbank.yml | 2 + .github/updatecli/values.d/scm.yml | 11 ++++++ .../updatecli/values.d/updatecli-compose.yml | 3 ++ .github/workflows/updatecli-compose.yml | 38 +++++++++++++++++++ src/dev/precommit_hook/casing_check_config.js | 3 ++ updatecli-compose.yaml | 14 +++++++ 6 files changed, 71 insertions(+) create mode 100644 .github/updatecli/values.d/ironbank.yml create mode 100644 .github/updatecli/values.d/scm.yml create mode 100644 .github/updatecli/values.d/updatecli-compose.yml create mode 100644 .github/workflows/updatecli-compose.yml create mode 100644 updatecli-compose.yaml diff --git a/.github/updatecli/values.d/ironbank.yml b/.github/updatecli/values.d/ironbank.yml new file mode 100644 index 0000000000000..fd1134eda376a --- /dev/null +++ b/.github/updatecli/values.d/ironbank.yml @@ -0,0 +1,2 @@ +config: + - path: src/dev/build/tasks/os_packages/docker_generator/templates/ironbank \ No newline at end of file diff --git a/.github/updatecli/values.d/scm.yml b/.github/updatecli/values.d/scm.yml new file mode 100644 index 0000000000000..34d902fb389d5 --- /dev/null +++ b/.github/updatecli/values.d/scm.yml @@ -0,0 +1,11 @@ +scm: + enabled: true + owner: elastic + repository: kibana + branch: main + commitusingapi: true + # begin updatecli-compose policy values + user: kibanamachine + email: 42973632+kibanamachine@users.noreply.github.com + # end updatecli-compose policy values + diff --git a/.github/updatecli/values.d/updatecli-compose.yml b/.github/updatecli/values.d/updatecli-compose.yml new file mode 100644 index 0000000000000..02df609f2a30c --- /dev/null +++ b/.github/updatecli/values.d/updatecli-compose.yml @@ -0,0 +1,3 @@ +spec: + files: + - "updatecli-compose.yaml" \ No newline at end of file diff --git a/.github/workflows/updatecli-compose.yml b/.github/workflows/updatecli-compose.yml new file mode 100644 index 0000000000000..cbab42d3a63b1 --- /dev/null +++ b/.github/workflows/updatecli-compose.yml @@ -0,0 +1,38 @@ +--- +name: updatecli-compose + +on: + workflow_dispatch: + schedule: + - cron: '0 6 * * *' + +permissions: + contents: read + +jobs: + compose: + runs-on: ubuntu-latest + permissions: + contents: write + packages: read + pull-requests: write + steps: + - uses: actions/checkout@v4 + + - uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: elastic/oblt-actions/updatecli/run@v1 + with: + command: --experimental compose diff + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - uses: elastic/oblt-actions/updatecli/run@v1 + with: + command: --experimental compose apply + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 2eaeb64f8be5f..3572781c4b262 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -87,6 +87,9 @@ export const IGNORE_FILE_GLOBS = [ // Support for including http-client.env.json configurations '**/http-client.env.json', + + // updatecli configuration for driving the UBI/Ironbank image updates + 'updatecli-compose.yaml', ]; /** diff --git a/updatecli-compose.yaml b/updatecli-compose.yaml new file mode 100644 index 0000000000000..8ad9bd6df8afb --- /dev/null +++ b/updatecli-compose.yaml @@ -0,0 +1,14 @@ +# Config file for `updatecli compose ...`. +# https://www.updatecli.io/docs/core/compose/ +policies: + - name: Handle ironbank bumps + policy: ghcr.io/elastic/oblt-updatecli-policies/ironbank/templates:0.3.0@sha256:b0c841d8fb294e6b58359462afbc83070dca375ac5dd0c5216c8926872a98bb1 + values: + - .github/updatecli/values.d/scm.yml + - .github/updatecli/values.d/ironbank.yml + + - name: Update Updatecli policies + policy: ghcr.io/updatecli/policies/autodiscovery/updatecli:0.4.0@sha256:254367f5b1454fd6032b88b314450cd3b6d5e8d5b6c953eb242a6464105eb869 + values: + - .github/updatecli/values.d/scm.yml + - .github/updatecli/values.d/updatecli-compose.yml \ No newline at end of file From 84d6899a4f2f97e0d015e733cc20064b43636154 Mon Sep 17 00:00:00 2001 From: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com> Date: Thu, 10 Oct 2024 21:20:50 +0100 Subject: [PATCH 70/87] [Security Solution][Detection Engine] removes feature flag for logged requests for preview (#195569) ## Summary - removes feature flag for logged requests for preview --- .../common/experimental_features.ts | 5 ----- .../components/rule_preview/index.test.tsx | 21 ------------------- .../components/rule_preview/index.tsx | 6 +----- .../config/ess/config.base.ts | 1 - .../configs/serverless.config.ts | 1 - .../execution_logic/eql.ts | 3 +-- .../execution_logic/esql.ts | 3 +-- .../test/security_solution_cypress/config.ts | 1 - .../detection_engine/rule_edit/preview.cy.ts | 5 ----- .../serverless_config.ts | 1 - 10 files changed, 3 insertions(+), 44 deletions(-) diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 1ae20af759611..1e5ffee50afc7 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -138,11 +138,6 @@ export const allowedExperimentalValues = Object.freeze({ */ esqlRulesDisabled: false, - /** - * enables logging requests during rule preview - */ - loggingRequestsEnabled: false, - /** * Enables Protection Updates tab in the Endpoint Policy Details page */ diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.test.tsx index 4ebb460177476..25d5b90d5408a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.test.tsx @@ -23,7 +23,6 @@ import { stepDefineDefaultValue, } from '../../../../detections/pages/detection_engine/rules/utils'; import { usePreviewInvocationCount } from './use_preview_invocation_count'; -import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; jest.mock('../../../../common/lib/kibana'); jest.mock('./use_preview_route'); @@ -40,7 +39,6 @@ jest.mock('../../../../common/hooks/use_experimental_features', () => ({ useIsExperimentalFeatureEnabled: jest.fn(), })); -const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; // rule types that do not support logged requests const doNotSupportLoggedRequests: Type[] = [ 'threshold', @@ -114,8 +112,6 @@ describe('PreviewQuery', () => { }); (usePreviewInvocationCount as jest.Mock).mockReturnValue({ invocationCount: 500 }); - - useIsExperimentalFeatureEnabledMock.mockReturnValue(true); }); afterEach(() => { @@ -172,23 +168,6 @@ describe('PreviewQuery', () => { }); }); - supportLoggedRequests.forEach((ruleType) => { - test(`does not render "Show Elasticsearch requests" for ${ruleType} rule type when feature is disabled`, () => { - useIsExperimentalFeatureEnabledMock.mockReturnValue(false); - - render( - - - - ); - - expect(screen.queryByTestId('show-elasticsearch-requests')).toBeNull(); - }); - }); - doNotSupportLoggedRequests.forEach((ruleType) => { test(`does not render "Show Elasticsearch requests" for ${ruleType} rule type`, () => { render( diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.tsx index 2a86600d94e7a..f941cad91d3a4 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.tsx @@ -40,7 +40,6 @@ import type { TimeframePreviewOptions, } from '../../../../detections/pages/detection_engine/rules/types'; import { usePreviewInvocationCount } from './use_preview_invocation_count'; -import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; export const REASONABLE_INVOCATION_COUNT = 200; @@ -90,8 +89,6 @@ const RulePreviewComponent: React.FC = ({ const { indexPattern, ruleType } = defineRuleData; const { spaces } = useKibana().services; - const isLoggingRequestsFeatureEnabled = useIsExperimentalFeatureEnabled('loggingRequestsEnabled'); - const [spaceId, setSpaceId] = useState(''); useEffect(() => { if (spaces) { @@ -282,8 +279,7 @@ const RulePreviewComponent: React.FC = ({ - {isLoggingRequestsFeatureEnabled && - RULE_TYPES_SUPPORTING_LOGGED_REQUESTS.includes(ruleType) ? ( + {RULE_TYPES_SUPPORTING_LOGGED_REQUESTS.includes(ruleType) ? ( 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 3ab6d5059fd07..a0d2ee79a7b46 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 @@ -82,7 +82,6 @@ export function createTestConfig(options: CreateTestConfigOptions, testFiles?: s '--xpack.ruleRegistry.unsafe.legacyMultiTenancy.enabled=true', `--xpack.securitySolution.enableExperimental=${JSON.stringify([ 'previewTelemetryUrlEnabled', - 'loggingRequestsEnabled', 'riskScoringPersistence', 'riskScoringRoutesEnabled', ])}`, 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 ce949d5cc23fc..137ee1f67b9b3 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,6 +17,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(['loggingRequestsEnabled'])}`, ], }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts index aff2ccc6bccb3..9077873274fa5 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts @@ -1190,8 +1190,7 @@ export default ({ getService }: FtrProviderContext) => { }); }); - // skipped on MKI since feature flags are not supported there - describe('@skipInServerlessMKI preview logged requests', () => { + describe('preview logged requests', () => { it('should not return requests property when not enabled', async () => { const { logs } = await previewRule({ supertest, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts index 166a62b9b08ad..ee976de14186d 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts @@ -1409,8 +1409,7 @@ export default ({ getService }: FtrProviderContext) => { }); }); - // skipped on MKI since feature flags are not supported there - describe('@skipInServerlessMKI preview logged requests', () => { + describe('preview logged requests', () => { let rule: EsqlRuleCreateProps; let id: string; beforeEach(async () => { diff --git a/x-pack/test/security_solution_cypress/config.ts b/x-pack/test/security_solution_cypress/config.ts index 05bc2e381527a..f02968945087d 100644 --- a/x-pack/test/security_solution_cypress/config.ts +++ b/x-pack/test/security_solution_cypress/config.ts @@ -44,7 +44,6 @@ 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(['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 c2e41c9d4680c..268968c76ecc0 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 @@ -33,11 +33,6 @@ describe( 'Detection rules, preview', { tags: ['@ess', '@serverless'], - env: { - kbnServerArgs: [ - `--xpack.securitySolution.enableExperimental=${JSON.stringify(['loggingRequestsEnabled'])}`, - ], - }, }, () => { beforeEach(() => { diff --git a/x-pack/test/security_solution_cypress/serverless_config.ts b/x-pack/test/security_solution_cypress/serverless_config.ts index 71a63b697187f..f3f04dda79dbb 100644 --- a/x-pack/test/security_solution_cypress/serverless_config.ts +++ b/x-pack/test/security_solution_cypress/serverless_config.ts @@ -34,7 +34,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { { product_line: 'endpoint', product_tier: 'complete' }, { product_line: 'cloud', product_tier: 'complete' }, ])}`, - `--xpack.securitySolution.enableExperimental=${JSON.stringify(['loggingRequestsEnabled'])}`, '--csp.strict=false', '--csp.warnLegacyBrowsers=false', ], From 4df2d9f068445d3606a0cea58be6c32e00721d3f Mon Sep 17 00:00:00 2001 From: Vadim Kibana <82822460+vadimkibana@users.noreply.github.com> Date: Thu, 10 Oct 2024 22:22:23 +0200 Subject: [PATCH 71/87] [ES|QL] Add pretty-printing support for list literals (#195383) ## Summary Closes https://github.com/elastic/kibana/issues/194840 This PR add pretty-printing support for list literal expressions. For example, this query: ``` ROW ["..............................................", "..............................................", ".............................................."] ``` will be formatted as so: ``` ROW [ "..............................................", "..............................................", ".............................................."] ``` ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) Co-authored-by: Stratoula Kalafateli --- .../__tests__/wrapping_pretty_printer.test.ts | 79 +++++++++++++++++++ .../pretty_print/wrapping_pretty_printer.ts | 23 +++--- packages/kbn-esql-ast/src/types.ts | 1 + packages/kbn-esql-ast/src/visitor/contexts.ts | 12 ++- packages/kbn-esql-ast/src/visitor/utils.ts | 31 +++++++- 5 files changed, 132 insertions(+), 14 deletions(-) diff --git a/packages/kbn-esql-ast/src/pretty_print/__tests__/wrapping_pretty_printer.test.ts b/packages/kbn-esql-ast/src/pretty_print/__tests__/wrapping_pretty_printer.test.ts index 21330d0fea3b1..2dfe239ce5b88 100644 --- a/packages/kbn-esql-ast/src/pretty_print/__tests__/wrapping_pretty_printer.test.ts +++ b/packages/kbn-esql-ast/src/pretty_print/__tests__/wrapping_pretty_printer.test.ts @@ -593,6 +593,85 @@ ROW (asdf + asdf)::string, 1.2::string, "1234"::integer, (12321342134 + 23412341 - "aaaaaaaaaaa")::boolean`); }); }); + + describe('list literals', () => { + describe('numeric', () => { + test('wraps long list literals one line', () => { + const query = + 'ROW [1234567890, 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, 1234567890]'; + const text = reprint(query).text; + + expect('\n' + text).toBe(` +ROW + [1234567890, 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, + 1234567890, 1234567890, 1234567890]`); + }); + + test('wraps long list literals to multiple lines one line', () => { + const query = `ROW [1234567890, 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, + 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, + 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, + 1234567890, 1234567890, 1234567890]`; + const text = reprint(query).text; + + expect('\n' + text).toBe(` +ROW + [1234567890, 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, + 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, + 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, 1234567890, + 1234567890, 1234567890, 1234567890]`); + }); + + test('breaks very long values one-per-line', () => { + const query = `ROW fn1(fn2(fn3(fn4(fn5(fn6(fn7(fn8([1234567890, 1234567890, 1234567890, 1234567890, 1234567890]))))))))`; + const text = reprint(query, { wrap: 40 }).text; + + expect('\n' + text).toBe(` +ROW + FN1( + FN2( + FN3( + FN4( + FN5( + FN6( + FN7( + FN8( + [ + 1234567890, + 1234567890, + 1234567890, + 1234567890, + 1234567890]))))))))`); + }); + }); + + describe('string', () => { + test('wraps long list literals one line', () => { + const query = + 'ROW ["some text", "another text", "one more text literal", "and another one", "and one more", "and one more", "and one more", "and one more", "and one more"]'; + const text = reprint(query).text; + + expect('\n' + text).toBe(` +ROW + ["some text", "another text", "one more text literal", "and another one", + "and one more", "and one more", "and one more", "and one more", + "and one more"]`); + }); + + test('can break very long strings per line', () => { + const query = + 'ROW ["..............................................", "..............................................", ".............................................."]'; + const text = reprint(query).text; + + expect('\n' + text).toBe(` +ROW + [ + "..............................................", + "..............................................", + ".............................................."]`); + }); + }); + }); }); test.todo('Idempotence on multiple times pretty printing'); diff --git a/packages/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts b/packages/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts index fde7f60a1dba5..91f65a389f0c3 100644 --- a/packages/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts +++ b/packages/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts @@ -15,9 +15,10 @@ import { CommandVisitorContext, ExpressionVisitorContext, FunctionCallExpressionVisitorContext, + ListLiteralExpressionVisitorContext, Visitor, } from '../visitor'; -import { singleItems } from '../visitor/utils'; +import { children, singleItems } from '../visitor/utils'; import { BasicPrettyPrinter, BasicPrettyPrinterOptions } from './basic_pretty_printer'; import { getPrettyPrintStats } from './helpers'; import { LeafPrinter } from './leaf_printer'; @@ -235,7 +236,11 @@ export class WrappingPrettyPrinter { } private printArguments( - ctx: CommandVisitorContext | CommandOptionVisitorContext | FunctionCallExpressionVisitorContext, + ctx: + | CommandVisitorContext + | CommandOptionVisitorContext + | FunctionCallExpressionVisitorContext + | ListLiteralExpressionVisitorContext, inp: Input ) { let txt = ''; @@ -247,7 +252,7 @@ export class WrappingPrettyPrinter { let remainingCurrentLine = inp.remaining; let oneArgumentPerLine = false; - for (const child of singleItems(ctx.node.args)) { + for (const child of children(ctx.node)) { if (getPrettyPrintStats(child).hasLineBreakingDecorations) { oneArgumentPerLine = true; break; @@ -489,13 +494,11 @@ export class WrappingPrettyPrinter { }) .on('visitListLiteralExpression', (ctx, inp: Input): Output => { - let elements = ''; - - for (const out of ctx.visitElements(inp)) { - elements += (elements ? ', ' : '') + out.txt; - } - - const formatted = `[${elements}]${inp.suffix ?? ''}`; + const args = this.printArguments(ctx, { + indent: inp.indent, + remaining: inp.remaining - 1, + }); + const formatted = `[${args.txt}]${inp.suffix ?? ''}`; const { txt, indented } = this.decorateWithComments(inp.indent, ctx.node, formatted); return { txt, indented }; diff --git a/packages/kbn-esql-ast/src/types.ts b/packages/kbn-esql-ast/src/types.ts index 0ca48b2326f7d..1bac6e0cff5b3 100644 --- a/packages/kbn-esql-ast/src/types.ts +++ b/packages/kbn-esql-ast/src/types.ts @@ -40,6 +40,7 @@ export type ESQLAstField = ESQLFunction | ESQLColumn; export type ESQLAstItem = ESQLSingleAstItem | ESQLAstItem[]; export type ESQLAstNodeWithArgs = ESQLCommand | ESQLCommandOption | ESQLFunction; +export type ESQLAstNodeWithChildren = ESQLAstNodeWithArgs | ESQLList; /** * *Proper* are nodes which are objects with `type` property, once we get rid diff --git a/packages/kbn-esql-ast/src/visitor/contexts.ts b/packages/kbn-esql-ast/src/visitor/contexts.ts index 0f637962b7ddd..4b4f04fdca4bb 100644 --- a/packages/kbn-esql-ast/src/visitor/contexts.ts +++ b/packages/kbn-esql-ast/src/visitor/contexts.ts @@ -12,11 +12,12 @@ // and makes it harder to understand the code structure. import { type GlobalVisitorContext, SharedData } from './global_visitor_context'; -import { firstItem, singleItems } from './utils'; +import { children, firstItem, singleItems } from './utils'; import type { ESQLAstCommand, ESQLAstItem, ESQLAstNodeWithArgs, + ESQLAstNodeWithChildren, ESQLAstRenameExpression, ESQLColumn, ESQLCommandOption, @@ -47,6 +48,11 @@ import { Builder } from '../builder'; const isNodeWithArgs = (x: unknown): x is ESQLAstNodeWithArgs => !!x && typeof x === 'object' && Array.isArray((x as any).args); +const isNodeWithChildren = (x: unknown): x is ESQLAstNodeWithChildren => + !!x && + typeof x === 'object' && + (Array.isArray((x as any).args) || Array.isArray((x as any).values)); + export class VisitorContext< Methods extends VisitorMethods = VisitorMethods, Data extends SharedData = SharedData, @@ -99,13 +105,13 @@ export class VisitorContext< public arguments(): ESQLAstExpressionNode[] { const node = this.node; - if (!isNodeWithArgs(node)) { + if (!isNodeWithChildren(node)) { return []; } const args: ESQLAstExpressionNode[] = []; - for (const arg of singleItems(node.args)) { + for (const arg of children(node)) { args.push(arg); } diff --git a/packages/kbn-esql-ast/src/visitor/utils.ts b/packages/kbn-esql-ast/src/visitor/utils.ts index 2e54a89c2bf52..0dc95b73cf9d7 100644 --- a/packages/kbn-esql-ast/src/visitor/utils.ts +++ b/packages/kbn-esql-ast/src/visitor/utils.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { ESQLAstItem, ESQLSingleAstItem } from '../types'; +import { ESQLAstItem, ESQLProperNode, ESQLSingleAstItem } from '../types'; /** * Normalizes AST "item" list to only contain *single* items. @@ -48,3 +48,32 @@ export const lastItem = (items: ESQLAstItem[]): ESQLSingleAstItem | undefined => if (Array.isArray(last)) return lastItem(last as ESQLAstItem[]); return last as ESQLSingleAstItem; }; + +export function* children(node: ESQLProperNode): Iterable { + switch (node.type) { + case 'function': + case 'command': + case 'option': { + for (const arg of singleItems(node.args)) { + yield arg; + } + break; + } + case 'list': { + for (const item of singleItems(node.values)) { + yield item; + } + break; + } + case 'inlineCast': { + if (Array.isArray(node.value)) { + for (const item of singleItems(node.value)) { + yield item; + } + } else { + yield node.value; + } + break; + } + } +} From 3974845d24c16d6d9da91d00ad3d2a226ac457bf Mon Sep 17 00:00:00 2001 From: Philippe Oberti Date: Thu, 10 Oct 2024 22:29:36 +0200 Subject: [PATCH 72/87] [Security Solution] - skipping CSP Cypress test failing on MKI (#195794) ## Summary This PR is skipping a CSP test that is failing on MKI (see failing [build](https://buildkite.com/elastic/kibana-serverless-security-solution-quality-gate-investigations/builds/1390#01927579-caed-41bc-9440-3cf29629a263)) The CSP tests are currently under the `expandable_flyout` folder own by the @elastic/security-threat-hunting-investigations team. This is temporary until the CSP has the time to create their own folder and all the associated scripts for CI to run. --- .../expandable_flyout/vulnerabilities_contextual_flyout.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/vulnerabilities_contextual_flyout.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/vulnerabilities_contextual_flyout.cy.ts index 591d458af56c1..fb83df1c79141 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/vulnerabilities_contextual_flyout.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/vulnerabilities_contextual_flyout.cy.ts @@ -138,7 +138,8 @@ const deleteDataStream = () => { }); }; -describe('Alert Host details expandable flyout', { tags: ['@ess', '@serverless'] }, () => { +// skipping because failure on MKI environment (https://buildkite.com/elastic/kibana-serverless-security-solution-quality-gate-investigations/builds/1390#01927579-caed-41bc-9440-3cf29629a263) +describe.skip('Alert Host details expandable flyout', { tags: ['@ess', '@serverless'] }, () => { beforeEach(() => { deleteAlertsAndRules(); login(); From cd217c072fc786cb76ee47d885501688507c2dde Mon Sep 17 00:00:00 2001 From: christineweng <18648970+christineweng@users.noreply.github.com> Date: Thu, 10 Oct 2024 15:46:51 -0500 Subject: [PATCH 73/87] [Security Solution] Add alert and cloud insights to document flyout (#195509) ## Summary This PR adds alert count, misconfiguration and vulnerabilities insights to alert/event flyout. If data is not available, the insights are hidden. [Mocks](https://www.figma.com/design/ubvhBGHee58diJNvSiy0GZ/%5B8.%2B%5D-%5BAlerts%5D-Expandable-Event-Flyout?node-id=8017-179782&node-type=canvas&t=0YjHfPi9zOUFUScc-0) ![image](https://github.com/user-attachments/assets/ba706ab8-448a-4286-8229-c4c398136638) ### Checklist - [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 --- .../src/distribution_bar.stories.tsx | 8 ++ .../src/distribution_bar.test.tsx | 62 ++++++++++++ .../distribution_bar/src/distribution_bar.tsx | 9 +- .../misconfiguration_preview.tsx | 2 +- .../left/components/host_details.test.tsx | 52 ++++++++++ .../left/components/host_details.tsx | 30 ++++++ .../left/components/test_ids.ts | 8 ++ .../left/components/user_details.test.tsx | 38 +++++++ .../left/components/user_details.tsx | 22 +++++ .../components/host_entity_overview.test.tsx | 65 ++++++++++++ .../right/components/host_entity_overview.tsx | 24 ++++- .../right/components/test_ids.ts | 10 ++ .../components/user_entity_overview.test.tsx | 52 ++++++++++ .../right/components/user_entity_overview.tsx | 18 +++- .../components/alert_count_insight.test.tsx | 64 ++++++++++++ .../shared/components/alert_count_insight.tsx | 99 +++++++++++++++++++ .../insight_distribution_bar.test.tsx | 41 ++++++++ .../components/insight_distribution_bar.tsx | 88 +++++++++++++++++ .../misconfiguration_insight.test.tsx | 43 ++++++++ .../components/misconfiguration_insight.tsx | 80 +++++++++++++++ .../shared/components/test_ids.ts | 3 + .../vulnerabilities_insight.test.tsx | 44 +++++++++ .../components/vulnerabilities_insight.tsx | 91 +++++++++++++++++ 23 files changed, 946 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx diff --git a/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.stories.tsx b/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.stories.tsx index 90b6887636c8a..c1b292c3f08cc 100644 --- a/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.stories.tsx +++ b/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.stories.tsx @@ -70,6 +70,14 @@ export const DistributionBar = () => { , + + +

    {'Hide last tooltip'}

    + + + + + ,

    {'Empty state'}

    diff --git a/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.test.tsx b/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.test.tsx index d4bdf4c20f133..e83b66e5e01e7 100644 --- a/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.test.tsx +++ b/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.test.tsx @@ -79,5 +79,67 @@ describe('DistributionBar', () => { }); }); + it('should render last tooltip by default', () => { + const stats = [ + { + key: 'low', + count: 9, + color: 'green', + }, + { + key: 'medium', + count: 90, + color: 'red', + }, + { + key: 'high', + count: 900, + color: 'red', + }, + ]; + + const { container } = render( + + ); + expect(container).toBeInTheDocument(); + const parts = container.querySelectorAll(`[classname*="distribution_bar--tooltip"]`); + parts.forEach((part, index) => { + if (index < parts.length - 1) { + expect(part).toHaveStyle({ opacity: 0 }); + } else { + expect(part).toHaveStyle({ opacity: 1 }); + } + }); + }); + + it('should not render last tooltip when hideLastTooltip is true', () => { + const stats = [ + { + key: 'low', + count: 9, + color: 'green', + }, + { + key: 'medium', + count: 90, + color: 'red', + }, + { + key: 'high', + count: 900, + color: 'red', + }, + ]; + + const { container } = render( + + ); + expect(container).toBeInTheDocument(); + const parts = container.querySelectorAll(`[classname*="distribution_bar--tooltip"]`); + parts.forEach((part) => { + expect(part).toHaveStyle({ opacity: 0 }); + }); + }); + // todo: test tooltip visibility logic }); diff --git a/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.tsx b/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.tsx index 28d8ca4a8a148..5b06292813ccd 100644 --- a/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.tsx +++ b/x-pack/packages/security-solution/distribution_bar/src/distribution_bar.tsx @@ -13,6 +13,8 @@ import { css } from '@emotion/react'; export interface DistributionBarProps { /** distribution data points */ stats: Array<{ key: string; count: number; color: string; label?: React.ReactNode }>; + /** hide the label above the bar at first render */ + hideLastTooltip?: boolean; /** data-test-subj used for querying the component in tests */ ['data-test-subj']?: string; } @@ -136,18 +138,21 @@ export const DistributionBar: React.FC = React.memo(functi props ) { const styles = useStyles(); - const { stats, 'data-test-subj': dataTestSubj } = props; + const { stats, 'data-test-subj': dataTestSubj, hideLastTooltip } = props; const parts = stats.map((stat) => { const partStyle = [ styles.part.base, styles.part.tick, styles.part.hover, - styles.part.lastTooltip, css` background-color: ${stat.color}; flex: ${stat.count}; `, ]; + if (!hideLastTooltip) { + partStyle.push(styles.part.lastTooltip); + } + const prettyNumber = numeral(stat.count).format('0,0a'); return ( diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx index a13a77a3562ff..a372ca4755fd8 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx @@ -35,7 +35,7 @@ const FIRST_RECORD_PAGINATION = { querySize: 1, }; -const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: number) => { +export const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: number) => { if (passedFindingsStats === 0 && failedFindingsStats === 0) return []; return [ { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx index 46288434f48bb..23f6969c36778 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx @@ -7,6 +7,8 @@ import React from 'react'; import { render } from '@testing-library/react'; +import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; +import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; import type { Anomalies } from '../../../../common/components/ml/types'; import { DocumentDetailsContext } from '../../shared/context'; import { TestProviders } from '../../../../common/mock'; @@ -24,6 +26,9 @@ import { HOST_DETAILS_LINK_TEST_ID, HOST_DETAILS_RELATED_USERS_LINK_TEST_ID, HOST_DETAILS_RELATED_USERS_IP_LINK_TEST_ID, + HOST_DETAILS_MISCONFIGURATIONS_TEST_ID, + HOST_DETAILS_VULNERABILITIES_TEST_ID, + HOST_DETAILS_ALERT_COUNT_TEST_ID, } from './test_ids'; import { EXPANDABLE_PANEL_CONTENT_TEST_ID } from '@kbn/security-solution-common'; import { useRiskScore } from '../../../../entity_analytics/api/hooks/use_risk_score'; @@ -35,8 +40,11 @@ import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview import { UserPreviewPanelKey } from '../../../entity_details/user_right'; import { USER_PREVIEW_BANNER } from '../../right/components/user_entity_overview'; import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../../network_details'; +import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'; jest.mock('@kbn/expandable-flyout'); +jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'); +jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'); jest.mock('../../../../common/hooks/use_experimental_features'); const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock; @@ -104,6 +112,10 @@ const mockUseHostsRelatedUsers = useHostRelatedUsers as jest.Mock; jest.mock('../../../../entity_analytics/api/hooks/use_risk_score'); const mockUseRiskScore = useRiskScore as jest.Mock; +jest.mock( + '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data' +); + const timestamp = '2022-07-25T08:20:18.966Z'; const defaultProps = { @@ -158,6 +170,9 @@ describe('', () => { mockUseRiskScore.mockReturnValue(mockRiskScoreResponse); mockUseHostsRelatedUsers.mockReturnValue(mockRelatedUsersResponse); mockUseIsExperimentalFeatureEnabled.mockReturnValue(true); + (useMisconfigurationPreview as jest.Mock).mockReturnValue({}); + (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({}); + (useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] }); }); it('should render host details correctly', () => { @@ -296,4 +311,41 @@ describe('', () => { }); }); }); + + describe('distribution bar insights', () => { + it('should not render if no data is available', () => { + const { queryByTestId } = renderHostDetails(mockContextValue); + expect(queryByTestId(HOST_DETAILS_MISCONFIGURATIONS_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(HOST_DETAILS_VULNERABILITIES_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(HOST_DETAILS_ALERT_COUNT_TEST_ID)).not.toBeInTheDocument(); + }); + + it('should render alert count when data is available', () => { + (useSummaryChartData as jest.Mock).mockReturnValue({ + isLoading: false, + items: [{ key: 'high', value: 78, label: 'High' }], + }); + + const { getByTestId } = renderHostDetails(mockContextValue); + expect(getByTestId(HOST_DETAILS_ALERT_COUNT_TEST_ID)).toBeInTheDocument(); + }); + + it('should render misconfiguration when data is available', () => { + (useMisconfigurationPreview as jest.Mock).mockReturnValue({ + data: { count: { passed: 1, failed: 2 } }, + }); + + const { getByTestId } = renderHostDetails(mockContextValue); + expect(getByTestId(HOST_DETAILS_MISCONFIGURATIONS_TEST_ID)).toBeInTheDocument(); + }); + + it('should render vulnerabilities when data is available', () => { + (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({ + data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } }, + }); + + const { getByTestId } = renderHostDetails(mockContextValue); + expect(getByTestId(HOST_DETAILS_VULNERABILITIES_TEST_ID)).toBeInTheDocument(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx index 33b8bb22fce53..122caa657b039 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx @@ -18,6 +18,8 @@ import { EuiToolTip, EuiIcon, EuiPanel, + EuiHorizontalRule, + EuiFlexGrid, } from '@elastic/eui'; import type { EuiBasicTableColumn } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -51,6 +53,9 @@ import { HOST_DETAILS_RELATED_USERS_TABLE_TEST_ID, HOST_DETAILS_RELATED_USERS_LINK_TEST_ID, HOST_DETAILS_RELATED_USERS_IP_LINK_TEST_ID, + HOST_DETAILS_ALERT_COUNT_TEST_ID, + HOST_DETAILS_MISCONFIGURATIONS_TEST_ID, + HOST_DETAILS_VULNERABILITIES_TEST_ID, } from './test_ids'; import { USER_NAME_FIELD_NAME, @@ -63,6 +68,9 @@ import { PreviewLink } from '../../../shared/components/preview_link'; import { HostPreviewPanelKey } from '../../../entity_details/host_right'; import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview'; import type { NarrowDateRange } from '../../../../common/components/ml/types'; +import { MisconfigurationsInsight } from '../../shared/components/misconfiguration_insight'; +import { VulnerabilitiesInsight } from '../../shared/components/vulnerabilities_insight'; +import { AlertCountInsight } from '../../shared/components/alert_count_insight'; const HOST_DETAILS_ID = 'entities-hosts-details'; const RELATED_USERS_ID = 'entities-hosts-related-users'; @@ -337,6 +345,28 @@ export const HostDetails: React.FC = ({ hostName, timestamp, s )} + + + + + + + + diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts index 0779f3c135b2d..8669b504f6861 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts @@ -43,6 +43,9 @@ export const PREVALENCE_DETAILS_TABLE_UPSELL_CELL_TEST_ID = export const ENTITIES_DETAILS_TEST_ID = `${PREFIX}EntitiesDetails` as const; export const USER_DETAILS_TEST_ID = `${PREFIX}UsersDetails` as const; export const USER_DETAILS_LINK_TEST_ID = `${USER_DETAILS_TEST_ID}TitleLink` as const; +export const USER_DETAILS_ALERT_COUNT_TEST_ID = `${USER_DETAILS_TEST_ID}AlertCount` as const; +export const USER_DETAILS_MISCONFIGURATIONS_TEST_ID = + `${USER_DETAILS_TEST_ID}Misconfigurations` as const; export const USER_DETAILS_RELATED_HOSTS_TABLE_TEST_ID = `${USER_DETAILS_TEST_ID}RelatedHostsTable` as const; export const USER_DETAILS_RELATED_HOSTS_LINK_TEST_ID = @@ -53,6 +56,11 @@ export const USER_DETAILS_INFO_TEST_ID = 'user-overview' as const; export const HOST_DETAILS_TEST_ID = `${PREFIX}HostsDetails` as const; export const HOST_DETAILS_LINK_TEST_ID = `${HOST_DETAILS_TEST_ID}TitleLink` as const; +export const HOST_DETAILS_ALERT_COUNT_TEST_ID = `${HOST_DETAILS_TEST_ID}AlertCount` as const; +export const HOST_DETAILS_MISCONFIGURATIONS_TEST_ID = + `${HOST_DETAILS_TEST_ID}Misconfigurations` as const; +export const HOST_DETAILS_VULNERABILITIES_TEST_ID = + `${HOST_DETAILS_TEST_ID}Vulnerabilities` as const; export const HOST_DETAILS_RELATED_USERS_TABLE_TEST_ID = `${HOST_DETAILS_TEST_ID}RelatedUsersTable` as const; export const HOST_DETAILS_RELATED_USERS_LINK_TEST_ID = diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx index c1ed881e80a95..a2c53afb8c3f3 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { render } from '@testing-library/react'; +import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; import type { Anomalies } from '../../../../common/components/ml/types'; import { TestProviders } from '../../../../common/mock'; import { DocumentDetailsContext } from '../../shared/context'; @@ -24,6 +25,8 @@ import { USER_DETAILS_RELATED_HOSTS_TABLE_TEST_ID, USER_DETAILS_RELATED_HOSTS_LINK_TEST_ID, USER_DETAILS_RELATED_HOSTS_IP_LINK_TEST_ID, + USER_DETAILS_MISCONFIGURATIONS_TEST_ID, + USER_DETAILS_ALERT_COUNT_TEST_ID, } from './test_ids'; import { EXPANDABLE_PANEL_CONTENT_TEST_ID } from '@kbn/security-solution-common'; import { useRiskScore } from '../../../../entity_analytics/api/hooks/use_risk_score'; @@ -35,8 +38,10 @@ import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview import { UserPreviewPanelKey } from '../../../entity_details/user_right'; import { USER_PREVIEW_BANNER } from '../../right/components/user_entity_overview'; import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../../network_details'; +import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'; jest.mock('@kbn/expandable-flyout'); +jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'); jest.mock('../../../../common/hooks/use_experimental_features'); const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock; @@ -101,6 +106,10 @@ const mockUseUsersRelatedHosts = useUserRelatedHosts as jest.Mock; jest.mock('../../../../entity_analytics/api/hooks/use_risk_score'); const mockUseRiskScore = useRiskScore as jest.Mock; +jest.mock( + '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data' +); + const timestamp = '2022-07-25T08:20:18.966Z'; const defaultProps = { @@ -155,6 +164,8 @@ describe('', () => { mockUseRiskScore.mockReturnValue(mockRiskScoreResponse); mockUseUsersRelatedHosts.mockReturnValue(mockRelatedHostsResponse); mockUseIsExperimentalFeatureEnabled.mockReturnValue(true); + (useMisconfigurationPreview as jest.Mock).mockReturnValue({}); + (useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] }); }); it('should render user details correctly', () => { @@ -278,4 +289,31 @@ describe('', () => { }); }); }); + + describe('distribution bar insights', () => { + it('should not render if no data is available', () => { + const { queryByTestId } = renderUserDetails(mockContextValue); + expect(queryByTestId(USER_DETAILS_MISCONFIGURATIONS_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(USER_DETAILS_ALERT_COUNT_TEST_ID)).not.toBeInTheDocument(); + }); + + it('should render alert count when data is available', () => { + (useSummaryChartData as jest.Mock).mockReturnValue({ + isLoading: false, + items: [{ key: 'high', value: 78, label: 'High' }], + }); + + const { getByTestId } = renderUserDetails(mockContextValue); + expect(getByTestId(USER_DETAILS_ALERT_COUNT_TEST_ID)).toBeInTheDocument(); + }); + + it('should render misconfiguration when data is available', () => { + (useMisconfigurationPreview as jest.Mock).mockReturnValue({ + data: { count: { passed: 1, failed: 2 } }, + }); + + const { getByTestId } = renderUserDetails(mockContextValue); + expect(getByTestId(USER_DETAILS_MISCONFIGURATIONS_TEST_ID)).toBeInTheDocument(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx index 13d3e825053ba..c90d11f4b8bc2 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx @@ -18,6 +18,8 @@ import { EuiFlexItem, EuiToolTip, EuiPanel, + EuiHorizontalRule, + EuiFlexGrid, } from '@elastic/eui'; import type { EuiBasicTableColumn } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -51,6 +53,8 @@ import { USER_DETAILS_TEST_ID, USER_DETAILS_RELATED_HOSTS_LINK_TEST_ID, USER_DETAILS_RELATED_HOSTS_IP_LINK_TEST_ID, + USER_DETAILS_MISCONFIGURATIONS_TEST_ID, + USER_DETAILS_ALERT_COUNT_TEST_ID, } from './test_ids'; import { HOST_NAME_FIELD_NAME, @@ -63,6 +67,8 @@ import { UserPreviewPanelKey } from '../../../entity_details/user_right'; import { USER_PREVIEW_BANNER } from '../../right/components/user_entity_overview'; import { PreviewLink } from '../../../shared/components/preview_link'; import type { NarrowDateRange } from '../../../../common/components/ml/types'; +import { MisconfigurationsInsight } from '../../shared/components/misconfiguration_insight'; +import { AlertCountInsight } from '../../shared/components/alert_count_insight'; const USER_DETAILS_ID = 'entities-users-details'; const RELATED_HOSTS_ID = 'entities-users-related-hosts'; @@ -340,6 +346,22 @@ export const UserDetails: React.FC = ({ userName, timestamp, s )} + + + + + + diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx index b710df84e1a13..6ad90adb28997 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.test.tsx @@ -6,6 +6,8 @@ */ import React from 'react'; import { render } from '@testing-library/react'; +import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; +import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; import { TestProviders } from '../../../../common/mock'; import { HostEntityOverview, HOST_PREVIEW_BANNER } from './host_entity_overview'; import { useHostDetails } from '../../../../explore/hosts/containers/hosts/details'; @@ -16,6 +18,9 @@ import { ENTITIES_HOST_OVERVIEW_LINK_TEST_ID, ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID, ENTITIES_HOST_OVERVIEW_LOADING_TEST_ID, + ENTITIES_HOST_OVERVIEW_MISCONFIGURATIONS_TEST_ID, + ENTITIES_HOST_OVERVIEW_VULNERABILITIES_TEST_ID, + ENTITIES_HOST_OVERVIEW_ALERT_COUNT_TEST_ID, } from './test_ids'; import { DocumentDetailsContext } from '../../shared/context'; import { mockContextValue } from '../../shared/mocks/mock_context'; @@ -29,6 +34,7 @@ import { ENTITIES_TAB_ID } from '../../left/components/entities_details'; import { useRiskScore } from '../../../../entity_analytics/api/hooks/use_risk_score'; import { mockFlyoutApi } from '../../shared/mocks/mock_flyout_context'; import { createTelemetryServiceMock } from '../../../../common/lib/telemetry/telemetry_service.mock'; +import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'; const hostName = 'host'; const osFamily = 'Windows'; @@ -46,6 +52,17 @@ const panelContextValue = { }; jest.mock('@kbn/expandable-flyout'); +jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'); +jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'); + +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) }; +}); + +jest.mock( + '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data' +); const mockedTelemetry = createTelemetryServiceMock(); jest.mock('../../../../common/lib/kibana', () => { @@ -99,6 +116,9 @@ describe('', () => { beforeAll(() => { jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi); mockUseIsExperimentalFeatureEnabled.mockReturnValue(true); + (useMisconfigurationPreview as jest.Mock).mockReturnValue({}); + (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({}); + (useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] }); }); describe('license is valid', () => { @@ -150,6 +170,7 @@ describe('', () => { ); expect(getByTestId(ENTITIES_HOST_OVERVIEW_LOADING_TEST_ID)).toBeInTheDocument(); }); + describe('license is not valid', () => { it('should render os family and last seen', () => { mockUseHostDetails.mockReturnValue([false, { hostDetails: hostData }]); @@ -210,4 +231,48 @@ describe('', () => { }); }); }); + + describe('distribution bar insights', () => { + beforeEach(() => { + mockUseHostDetails.mockReturnValue([false, { hostDetails: hostData }]); + mockUseRiskScore.mockReturnValue({ data: riskLevel, isAuthorized: true }); + }); + + it('should not render if no data is available', () => { + const { queryByTestId } = renderHostEntityContent(); + expect( + queryByTestId(ENTITIES_HOST_OVERVIEW_MISCONFIGURATIONS_TEST_ID) + ).not.toBeInTheDocument(); + expect(queryByTestId(ENTITIES_HOST_OVERVIEW_VULNERABILITIES_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(ENTITIES_HOST_OVERVIEW_ALERT_COUNT_TEST_ID)).not.toBeInTheDocument(); + }); + + it('should render alert count when data is available', () => { + (useSummaryChartData as jest.Mock).mockReturnValue({ + isLoading: false, + items: [{ key: 'high', value: 78, label: 'High' }], + }); + + const { getByTestId } = renderHostEntityContent(); + expect(getByTestId(ENTITIES_HOST_OVERVIEW_ALERT_COUNT_TEST_ID)).toBeInTheDocument(); + }); + + it('should render misconfiguration when data is available', () => { + (useMisconfigurationPreview as jest.Mock).mockReturnValue({ + data: { count: { passed: 1, failed: 2 } }, + }); + + const { getByTestId } = renderHostEntityContent(); + expect(getByTestId(ENTITIES_HOST_OVERVIEW_MISCONFIGURATIONS_TEST_ID)).toBeInTheDocument(); + }); + + it('should render vulnerabilities when data is available', () => { + (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({ + data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } }, + }); + + const { getByTestId } = renderHostEntityContent(); + expect(getByTestId(ENTITIES_HOST_OVERVIEW_VULNERABILITIES_TEST_ID)).toBeInTheDocument(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.tsx index ca6a68eb23be8..90405286b004c 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/host_entity_overview.tsx @@ -52,11 +52,17 @@ import { ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID, ENTITIES_HOST_OVERVIEW_LINK_TEST_ID, ENTITIES_HOST_OVERVIEW_LOADING_TEST_ID, + ENTITIES_HOST_OVERVIEW_ALERT_COUNT_TEST_ID, + ENTITIES_HOST_OVERVIEW_MISCONFIGURATIONS_TEST_ID, + ENTITIES_HOST_OVERVIEW_VULNERABILITIES_TEST_ID, } from './test_ids'; import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys'; import { LeftPanelInsightsTab } from '../../left'; import { RiskScoreDocTooltip } from '../../../../overview/components/common'; import { PreviewLink } from '../../../shared/components/preview_link'; +import { MisconfigurationsInsight } from '../../shared/components/misconfiguration_insight'; +import { VulnerabilitiesInsight } from '../../shared/components/vulnerabilities_insight'; +import { AlertCountInsight } from '../../shared/components/alert_count_insight'; const HOST_ICON = 'storage'; @@ -196,12 +202,12 @@ export const HostEntityOverview: React.FC = ({ hostName return ( - + @@ -270,6 +276,20 @@ export const HostEntityOverview: React.FC = ({ hostName )} + + + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts index 40670ddc7110a..e0d8bc6db0f5c 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts @@ -121,6 +121,10 @@ export const ENTITIES_USER_OVERVIEW_LAST_SEEN_TEST_ID = `${ENTITIES_USER_OVERVIEW_TEST_ID}LastSeen` as const; export const ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID = `${ENTITIES_USER_OVERVIEW_TEST_ID}RiskLevel` as const; +export const ENTITIES_USER_OVERVIEW_ALERT_COUNT_TEST_ID = + `${ENTITIES_USER_OVERVIEW_TEST_ID}AlertCount` as const; +export const ENTITIES_USER_OVERVIEW_MISCONFIGURATIONS_TEST_ID = + `${ENTITIES_USER_OVERVIEW_TEST_ID}Misconfigurations` as const; export const ENTITIES_HOST_OVERVIEW_TEST_ID = `${INSIGHTS_ENTITIES_TEST_ID}HostOverview` as const; export const ENTITIES_HOST_OVERVIEW_LOADING_TEST_ID = @@ -132,6 +136,12 @@ export const ENTITIES_HOST_OVERVIEW_LAST_SEEN_TEST_ID = `${ENTITIES_HOST_OVERVIEW_TEST_ID}LastSeen` as const; export const ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID = `${ENTITIES_HOST_OVERVIEW_TEST_ID}RiskLevel` as const; +export const ENTITIES_HOST_OVERVIEW_ALERT_COUNT_TEST_ID = + `${ENTITIES_HOST_OVERVIEW_TEST_ID}AlertCount` as const; +export const ENTITIES_HOST_OVERVIEW_MISCONFIGURATIONS_TEST_ID = + `${ENTITIES_HOST_OVERVIEW_TEST_ID}Misconfigurations` as const; +export const ENTITIES_HOST_OVERVIEW_VULNERABILITIES_TEST_ID = + `${ENTITIES_HOST_OVERVIEW_TEST_ID}Vulnerabilities` as const; /* Threat intelligence */ diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.test.tsx index 000da8946ff61..95c399ca4362e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { TestProviders } from '../../../../common/mock'; +import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; import { UserEntityOverview, USER_PREVIEW_BANNER } from './user_entity_overview'; import { useFirstLastSeen } from '../../../../common/containers/use_first_last_seen'; import { @@ -15,6 +16,8 @@ import { ENTITIES_USER_OVERVIEW_LINK_TEST_ID, ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID, ENTITIES_USER_OVERVIEW_LOADING_TEST_ID, + ENTITIES_USER_OVERVIEW_MISCONFIGURATIONS_TEST_ID, + ENTITIES_USER_OVERVIEW_ALERT_COUNT_TEST_ID, } from './test_ids'; import { useObservedUserDetails } from '../../../../explore/users/containers/users/observed_details'; import { mockContextValue } from '../../shared/mocks/mock_context'; @@ -28,6 +31,7 @@ import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { mockFlyoutApi } from '../../shared/mocks/mock_flyout_context'; import { UserPreviewPanelKey } from '../../../entity_details/user_right'; +import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'; const userName = 'user'; const domain = 'n54bg2lfc7'; @@ -45,6 +49,18 @@ const panelContextValue = { }; jest.mock('@kbn/expandable-flyout'); +jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'); + +jest.mock('../../../../common/lib/kibana'); + +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) }; +}); + +jest.mock( + '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data' +); jest.mock('../../../../common/hooks/use_experimental_features'); const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock; @@ -85,6 +101,8 @@ describe('', () => { beforeAll(() => { jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi); mockUseIsExperimentalFeatureEnabled.mockReturnValue(true); + (useMisconfigurationPreview as jest.Mock).mockReturnValue({}); + (useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] }); }); describe('license is valid', () => { @@ -211,4 +229,38 @@ describe('', () => { }); }); }); + + describe('distribution bar insights', () => { + beforeEach(() => { + mockUseUserDetails.mockReturnValue([false, { userDetails: userData }]); + mockUseRiskScore.mockReturnValue({ data: riskLevel, isAuthorized: true }); + }); + + it('should not render if no data is available', () => { + const { queryByTestId } = renderUserEntityOverview(); + expect( + queryByTestId(ENTITIES_USER_OVERVIEW_MISCONFIGURATIONS_TEST_ID) + ).not.toBeInTheDocument(); + expect(queryByTestId(ENTITIES_USER_OVERVIEW_ALERT_COUNT_TEST_ID)).not.toBeInTheDocument(); + }); + + it('should render alert count when data is available', () => { + (useSummaryChartData as jest.Mock).mockReturnValue({ + isLoading: false, + items: [{ key: 'high', value: 78, label: 'High' }], + }); + + const { getByTestId } = renderUserEntityOverview(); + expect(getByTestId(ENTITIES_USER_OVERVIEW_ALERT_COUNT_TEST_ID)).toBeInTheDocument(); + }); + + it('should render misconfiguration when data is available', () => { + (useMisconfigurationPreview as jest.Mock).mockReturnValue({ + data: { count: { passed: 1, failed: 2 } }, + }); + + const { getByTestId } = renderUserEntityOverview(); + expect(getByTestId(ENTITIES_USER_OVERVIEW_MISCONFIGURATIONS_TEST_ID)).toBeInTheDocument(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.tsx index 624b9e816c9e5..0019228d656cd 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/user_entity_overview.tsx @@ -53,10 +53,14 @@ import { ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID, ENTITIES_USER_OVERVIEW_LINK_TEST_ID, ENTITIES_USER_OVERVIEW_LOADING_TEST_ID, + ENTITIES_USER_OVERVIEW_MISCONFIGURATIONS_TEST_ID, + ENTITIES_USER_OVERVIEW_ALERT_COUNT_TEST_ID, } from './test_ids'; import { useObservedUserDetails } from '../../../../explore/users/containers/users/observed_details'; import { RiskScoreDocTooltip } from '../../../../overview/components/common'; import { PreviewLink } from '../../../shared/components/preview_link'; +import { MisconfigurationsInsight } from '../../shared/components/misconfiguration_insight'; +import { AlertCountInsight } from '../../shared/components/alert_count_insight'; const USER_ICON = 'user'; @@ -196,12 +200,12 @@ export const UserEntityOverview: React.FC = ({ userName return ( - + @@ -270,6 +274,16 @@ export const UserEntityOverview: React.FC = ({ userName )} + + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx new file mode 100644 index 0000000000000..f0d16a418f2b2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.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 React from 'react'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import { AlertCountInsight } from './alert_count_insight'; +import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'; + +jest.mock('../../../../common/lib/kibana'); + +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) }; +}); +jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'); +jest.mock( + '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data' +); + +const fieldName = 'host.name'; +const name = 'test host'; +const testId = 'test'; + +const renderAlertCountInsight = () => { + return render( + + + + ); +}; + +describe('AlertCountInsight', () => { + it('renders', () => { + (useSummaryChartData as jest.Mock).mockReturnValue({ + isLoading: false, + items: [ + { key: 'high', value: 78, label: 'High' }, + { key: 'low', value: 46, label: 'Low' }, + { key: 'medium', value: 32, label: 'Medium' }, + { key: 'critical', value: 21, label: 'Critical' }, + ], + }); + const { getByTestId } = renderAlertCountInsight(); + expect(getByTestId(testId)).toBeInTheDocument(); + expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument(); + }); + + it('renders loading spinner if data is being fetched', () => { + (useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: true, items: [] }); + const { getByTestId } = renderAlertCountInsight(); + expect(getByTestId(`${testId}-loading-spinner`)).toBeInTheDocument(); + }); + + it('renders null if no misconfiguration data found', () => { + (useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] }); + const { container } = renderAlertCountInsight(); + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx new file mode 100644 index 0000000000000..566b77b5739a9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.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 React, { useMemo } from 'react'; +import { v4 as uuid } from 'uuid'; +import { EuiLoadingSpinner, EuiFlexItem, type EuiFlexGroupProps } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { InsightDistributionBar } from './insight_distribution_bar'; +import { severityAggregations } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/aggregations'; +import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'; +import { + getIsAlertsBySeverityData, + getSeverityColor, +} from '../../../../detections/components/alerts_kpis/severity_level_panel/helpers'; + +const ENTITY_ALERT_COUNT_ID = 'entity-alert-count'; + +interface AlertCountInsightProps { + /** + * The name of the entity to filter the alerts by. + */ + name: string; + /** + * The field name to filter the alerts by. + */ + fieldName: 'host.name' | 'user.name'; + /** + * The direction of the flex group. + */ + direction?: EuiFlexGroupProps['direction']; + /** + * The data-test-subj to use for the component. + */ + ['data-test-subj']?: string; +} + +/* + * Displays a distribution bar with the count of critical alerts for a given entity + */ +export const AlertCountInsight: React.FC = ({ + name, + fieldName, + direction, + 'data-test-subj': dataTestSubj, +}) => { + const uniqueQueryId = useMemo(() => `${ENTITY_ALERT_COUNT_ID}-${uuid()}`, []); + const entityFilter = useMemo(() => ({ field: fieldName, value: name }), [fieldName, name]); + + const { items, isLoading } = useSummaryChartData({ + aggregations: severityAggregations, + entityFilter, + uniqueQueryId, + signalIndexName: null, + }); + + const data = useMemo(() => (getIsAlertsBySeverityData(items) ? items : []), [items]); + + const alertStats = useMemo(() => { + return data.map((item) => ({ + key: item.key, + count: item.value, + color: getSeverityColor(item.key), + })); + }, [data]); + + const count = useMemo( + () => data.filter((item) => item.key === 'critical')[0]?.value ?? 0, + [data] + ); + + if (!isLoading && items.length === 0) return null; + + return ( + + {isLoading ? ( + + ) : ( + + } + stats={alertStats} + count={count} + direction={direction} + data-test-subj={`${dataTestSubj}-distribution-bar`} + /> + )} + + ); +}; + +AlertCountInsight.displayName = 'AlertCountInsight'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.test.tsx new file mode 100644 index 0000000000000..a775da8a7f73a --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.test.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 React from 'react'; +import { render } from '@testing-library/react'; +import { InsightDistributionBar } from './insight_distribution_bar'; +import { TestProviders } from '../../../../common/mock'; + +const title = 'test title'; +const count = 10; +const testId = 'test-id'; +const stats = [ + { + key: 'passed', + count: 90, + color: 'green', + }, + { + key: 'failed', + count: 10, + color: 'red', + }, +]; + +describe('', () => { + it('should render', () => { + const { getByTestId, getByText } = render( + + + + ); + expect(getByTestId(testId)).toBeInTheDocument(); + expect(getByText(title)).toBeInTheDocument(); + expect(getByTestId(`${testId}-badge`)).toHaveTextContent(`${count}`); + expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.tsx new file mode 100644 index 0000000000000..006ec8c5dad4f --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.tsx @@ -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 React from 'react'; +import { css } from '@emotion/css'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiBadge, + useEuiTheme, + useEuiFontSize, + type EuiFlexGroupProps, +} from '@elastic/eui'; +import { DistributionBar } from '@kbn/security-solution-distribution-bar'; +import { FormattedCount } from '../../../../common/components/formatted_number'; + +export interface InsightDistributionBarProps { + /** + * Title of the insight + */ + title: string | React.ReactNode; + /** + * Distribution stats to display + */ + stats: Array<{ key: string; count: number; color: string; label?: React.ReactNode }>; + /** + * Count to be displayed in the badge + */ + count: number; + /** + * Flex direction of the component + */ + direction?: EuiFlexGroupProps['direction']; + /** + * Optional test id + */ + ['data-test-subj']?: string; +} + +// Displays a distribution bar with a count badge +export const InsightDistributionBar: React.FC = ({ + title, + stats, + count, + direction = 'row', + 'data-test-subj': dataTestSubj, +}) => { + const { euiTheme } = useEuiTheme(); + const xsFontSize = useEuiFontSize('xs').fontSize; + + return ( + + + + {title} + + + + + + + + + + + + + + + + ); +}; + +InsightDistributionBar.displayName = 'InsightDistributionBar'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.test.tsx new file mode 100644 index 0000000000000..296a61f444a17 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.test.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 { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import { MisconfigurationsInsight } from './misconfiguration_insight'; +import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; + +jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'); + +const fieldName = 'host.name'; +const name = 'test host'; +const testId = 'test'; + +const renderMisconfigurationsInsight = () => { + return render( + + + + ); +}; + +describe('MisconfigurationsInsight', () => { + it('renders', () => { + (useMisconfigurationPreview as jest.Mock).mockReturnValue({ + data: { count: { passed: 1, failed: 2 } }, + }); + const { getByTestId } = renderMisconfigurationsInsight(); + expect(getByTestId(testId)).toBeInTheDocument(); + expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument(); + }); + + it('renders null if no misconfiguration data found', () => { + (useMisconfigurationPreview as jest.Mock).mockReturnValue({}); + const { container } = renderMisconfigurationsInsight(); + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx new file mode 100644 index 0000000000000..552a242c84893 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { EuiFlexItem, type EuiFlexGroupProps } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; +import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common'; +import { InsightDistributionBar } from './insight_distribution_bar'; +import { getFindingsStats } from '../../../../cloud_security_posture/components/misconfiguration/misconfiguration_preview'; + +interface MisconfigurationsInsightProps { + /** + * Entity name to retrieve misconfigurations for + */ + name: string; + /** + * Indicator whether the entity is host or user + */ + fieldName: 'host.name' | 'user.name'; + /** + * The direction of the flex group + */ + direction?: EuiFlexGroupProps['direction']; + /** + * The data-test-subj to use for the component + */ + ['data-test-subj']?: string; +} + +/* + * Displays a distribution bar with the count of failed misconfigurations for a given entity + */ +export const MisconfigurationsInsight: React.FC = ({ + name, + fieldName, + direction, + 'data-test-subj': dataTestSubj, +}) => { + const { data } = useMisconfigurationPreview({ + query: buildEntityFlyoutPreviewQuery(fieldName, name), + sort: [], + enabled: true, + pageSize: 1, + }); + + const passedFindings = data?.count.passed || 0; + const failedFindings = data?.count.failed || 0; + const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0; + + const misconfigurationsStats = useMemo( + () => getFindingsStats(passedFindings, failedFindings), + [passedFindings, failedFindings] + ); + + if (!hasMisconfigurationFindings) return null; + + return ( + + + } + stats={misconfigurationsStats} + count={failedFindings} + direction={direction} + data-test-subj={`${dataTestSubj}-distribution-bar`} + /> + + ); +}; + +MisconfigurationsInsight.displayName = 'MisconfigurationsInsight'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/test_ids.ts index 8561df63d7199..7c2ce2ff5870b 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/test_ids.ts @@ -12,3 +12,6 @@ export const FLYOUT_PREVIEW_LINK_TEST_ID = `${PREFIX}PreviewLink` as const; export const SESSION_VIEW_UPSELL_TEST_ID = `${PREFIX}SessionViewUpsell` as const; export const SESSION_VIEW_NO_DATA_TEST_ID = `${PREFIX}SessionViewNoData` as const; + +export const MISCONFIGURATIONS_TEST_ID = `${PREFIX}Misconfigurations` as const; +export const VULNERABILITIES_TEST_ID = `${PREFIX}Vulnerabilities` as const; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.test.tsx new file mode 100644 index 0000000000000..77c6737266b89 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.test.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 { TestProviders } from '../../../../common/mock'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { VulnerabilitiesInsight } from './vulnerabilities_insight'; +import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; + +jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'); + +const hostName = 'test host'; +const testId = 'test'; + +const renderVulnerabilitiesInsight = () => { + return render( + + + + ); +}; + +describe('VulnerabilitiesInsight', () => { + it('renders', () => { + (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({ + data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } }, + }); + + const { getByTestId } = renderVulnerabilitiesInsight(); + expect(getByTestId(testId)).toBeInTheDocument(); + expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument(); + }); + + it('renders null when data is not available', () => { + (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({}); + + const { container } = renderVulnerabilitiesInsight(); + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx new file mode 100644 index 0000000000000..4c581b6db57d0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { EuiFlexItem, type EuiFlexGroupProps } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; +import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common'; +import { getVulnerabilityStats, hasVulnerabilitiesData } from '@kbn/cloud-security-posture'; +import { InsightDistributionBar } from './insight_distribution_bar'; + +interface VulnerabilitiesInsightProps { + /** + * Host name to retrieve vulnerabilities for + */ + hostName: string; + /** + * The direction of the flex group + */ + direction?: EuiFlexGroupProps['direction']; + /** + * The data-test-subj to use for the component + */ + ['data-test-subj']?: string; +} + +/* + * Displays a distribution bar with the count of critical vulnerabilities for a given host + */ +export const VulnerabilitiesInsight: React.FC = ({ + hostName, + direction, + 'data-test-subj': dataTestSubj, +}) => { + const { data } = useVulnerabilitiesPreview({ + query: buildEntityFlyoutPreviewQuery('host.name', hostName), + sort: [], + enabled: true, + pageSize: 1, + }); + + const { CRITICAL = 0, HIGH = 0, MEDIUM = 0, LOW = 0, NONE = 0 } = data?.count || {}; + const hasVulnerabilitiesFindings = useMemo( + () => + hasVulnerabilitiesData({ + critical: CRITICAL, + high: HIGH, + medium: MEDIUM, + low: LOW, + none: NONE, + }), + [CRITICAL, HIGH, MEDIUM, LOW, NONE] + ); + + const vulnerabilitiesStats = useMemo( + () => + getVulnerabilityStats({ + critical: CRITICAL, + high: HIGH, + medium: MEDIUM, + low: LOW, + none: NONE, + }), + [CRITICAL, HIGH, MEDIUM, LOW, NONE] + ); + + if (!hasVulnerabilitiesFindings) return null; + + return ( + + + } + stats={vulnerabilitiesStats} + count={CRITICAL} + direction={direction} + data-test-subj={`${dataTestSubj}-distribution-bar`} + /> + + ); +}; + +VulnerabilitiesInsight.displayName = 'VulnerabilitiesInsight'; From 6ff2d87b5c8ed48ccfaa66f9cc8d712ae161a076 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 10 Oct 2024 15:59:10 -0600 Subject: [PATCH 74/87] [Security GenAI] Fix `VertexChatAI` tool calling (#195689) --- .../chat_vertex/chat_vertex.test.ts | 33 ++++++++++++++++++- .../language_models/chat_vertex/connection.ts | 17 ++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/chat_vertex.test.ts b/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/chat_vertex.test.ts index 37506922ff69b..07fe252bd5074 100644 --- a/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/chat_vertex.test.ts +++ b/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/chat_vertex.test.ts @@ -12,6 +12,7 @@ import { actionsClientMock } from '@kbn/actions-plugin/server/actions_client/act import { BaseMessage, HumanMessage, SystemMessage } from '@langchain/core/messages'; import { ActionsClientChatVertexAI } from './chat_vertex'; import { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager'; +import { GeminiContent } from '@langchain/google-common'; const connectorId = 'mock-connector-id'; @@ -54,8 +55,10 @@ const mockStreamExecute = jest.fn().mockImplementation(() => { }; }); +const systemInstruction = 'Answer the following questions truthfully and as best you can.'; + const callMessages = [ - new SystemMessage('Answer the following questions truthfully and as best you can.'), + new SystemMessage(systemInstruction), new HumanMessage('Question: Do you know my name?\n\n'), ] as unknown as BaseMessage[]; @@ -196,4 +199,32 @@ describe('ActionsClientChatVertexAI', () => { expect(handleLLMNewToken).toHaveBeenCalledWith('token3'); }); }); + + describe('message formatting', () => { + it('Properly sorts out the system role', async () => { + const actionsClientChatVertexAI = new ActionsClientChatVertexAI(defaultArgs); + + await actionsClientChatVertexAI._generate(callMessages, callOptions, callRunManager); + const params = actionsClient.execute.mock.calls[0][0].params.subActionParams as unknown as { + messages: GeminiContent[]; + systemInstruction: string; + }; + expect(params.messages.length).toEqual(1); + expect(params.messages[0].parts.length).toEqual(1); + expect(params.systemInstruction).toEqual(systemInstruction); + }); + it('Handles 2 messages in a row from the same role', async () => { + const actionsClientChatVertexAI = new ActionsClientChatVertexAI(defaultArgs); + + await actionsClientChatVertexAI._generate( + [...callMessages, new HumanMessage('Oh boy, another')], + callOptions, + callRunManager + ); + const { messages } = actionsClient.execute.mock.calls[0][0].params + .subActionParams as unknown as { messages: GeminiContent[] }; + expect(messages.length).toEqual(1); + expect(messages[0].parts.length).toEqual(2); + }); + }); }); diff --git a/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/connection.ts b/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/connection.ts index 0340d71b438db..dd3c1e1abdda0 100644 --- a/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/connection.ts +++ b/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/connection.ts @@ -7,6 +7,7 @@ import { ChatConnection, + GeminiContent, GoogleAbstractedClient, GoogleAIBaseLLMInput, GoogleLLMResponse, @@ -39,6 +40,22 @@ export class ActionsClientChatConnection extends ChatConnection { this.caller = caller; this.#model = fields.model; this.temperature = fields.temperature ?? 0; + const nativeFormatData = this.formatData.bind(this); + this.formatData = async (data, options) => { + const result = await nativeFormatData(data, options); + if (result?.contents != null && result?.contents.length) { + // ensure there are not 2 messages in a row from the same role, + // if there are combine them + result.contents = result.contents.reduce((acc: GeminiContent[], currentEntry) => { + if (currentEntry.role === acc[acc.length - 1]?.role) { + acc[acc.length - 1].parts = acc[acc.length - 1].parts.concat(currentEntry.parts); + return acc; + } + return [...acc, currentEntry]; + }, []); + } + return result; + }; } async _request( From a397bb72d52e865d0f44c6983bf01c85875251e8 Mon Sep 17 00:00:00 2001 From: christineweng <18648970+christineweng@users.noreply.github.com> Date: Thu, 10 Oct 2024 16:59:54 -0500 Subject: [PATCH 75/87] [Security Solution] Update footer link in rule preview to go to rule details page (#195806) ## Summary Currently, the rule preview footer will open the rule flyout. Although this behavior is consistent with other previews (host, user, alert etc.), the rule flyout does not provide additional information for users. This PR updates the footer go to rule details page instead. https://github.com/user-attachments/assets/6de03775-b1a4-41b9-b233-7817d6cca8ec ### 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: Jan Monschke --- .../rule_details/preview/footer.test.tsx | 31 +++++++++---------- .../flyout/rule_details/preview/footer.tsx | 25 +++++---------- .../flyout/rule_details/right/index.test.tsx | 7 +++-- 3 files changed, 25 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.test.tsx b/x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.test.tsx index f1e276011ca26..0f2a7dc74662f 100644 --- a/x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.test.tsx @@ -9,20 +9,21 @@ import { render } from '@testing-library/react'; import React from 'react'; import { RULE_PREVIEW_FOOTER_TEST_ID, RULE_PREVIEW_OPEN_RULE_FLYOUT_TEST_ID } from './test_ids'; import { PreviewFooter } from './footer'; -import { mockFlyoutApi } from '../../document_details/shared/mocks/mock_flyout_context'; -import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; -import { RulePanelKey } from '../right'; +import { useRuleDetailsLink } from '../../document_details/shared/hooks/use_rule_details_link'; +import { TestProviders } from '../../../common/mock'; -jest.mock('@kbn/expandable-flyout'); +jest.mock('../../document_details/shared/hooks/use_rule_details_link'); -const renderRulePreviewFooter = () => render(); +const renderRulePreviewFooter = () => + render( + + + + ); describe('', () => { - beforeAll(() => { - jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi); - }); - it('should render rule details link correctly when ruleId is available', () => { + (useRuleDetailsLink as jest.Mock).mockReturnValue('rule_details_link'); const { getByTestId } = renderRulePreviewFooter(); expect(getByTestId(RULE_PREVIEW_FOOTER_TEST_ID)).toBeInTheDocument(); @@ -32,13 +33,9 @@ describe('', () => { ); }); - it('should open rule flyout when clicked', () => { - const { getByTestId } = renderRulePreviewFooter(); - - getByTestId(RULE_PREVIEW_OPEN_RULE_FLYOUT_TEST_ID).click(); - - expect(mockFlyoutApi.openFlyout).toHaveBeenCalledWith({ - right: { id: RulePanelKey, params: { ruleId: 'ruleid' } }, - }); + it('should not render the footer if rule link is not available', () => { + (useRuleDetailsLink as jest.Mock).mockReturnValue(null); + const { container } = renderRulePreviewFooter(); + expect(container).toBeEmptyDOMElement(); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.tsx b/x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.tsx index 1774c37d9e535..42c8c1a6d85b9 100644 --- a/x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.tsx +++ b/x-pack/plugins/security_solution/public/flyout/rule_details/preview/footer.tsx @@ -5,38 +5,27 @@ * 2.0. */ -import React, { memo, useCallback } from 'react'; +import React, { memo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FlyoutFooter } from '@kbn/security-solution-common'; -import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { RULE_PREVIEW_FOOTER_TEST_ID, RULE_PREVIEW_OPEN_RULE_FLYOUT_TEST_ID } from './test_ids'; -import { RulePanelKey } from '../right'; +import { useRuleDetailsLink } from '../../document_details/shared/hooks/use_rule_details_link'; /** * Footer in rule preview panel */ export const PreviewFooter = memo(({ ruleId }: { ruleId: string }) => { - const { openFlyout } = useExpandableFlyoutApi(); + const href = useRuleDetailsLink({ ruleId }); - const openRuleFlyout = useCallback(() => { - openFlyout({ - right: { - id: RulePanelKey, - params: { - ruleId, - }, - }, - }); - }, [openFlyout, ruleId]); - - return ( + return href ? ( {i18n.translate('xpack.securitySolution.flyout.preview.rule.viewDetailsLabel', { @@ -46,7 +35,7 @@ export const PreviewFooter = memo(({ ruleId }: { ruleId: string }) => { - ); + ) : null; }); PreviewFooter.displayName = 'PreviewFooter'; diff --git a/x-pack/plugins/security_solution/public/flyout/rule_details/right/index.test.tsx b/x-pack/plugins/security_solution/public/flyout/rule_details/right/index.test.tsx index 1ce755575450c..146da2be34346 100644 --- a/x-pack/plugins/security_solution/public/flyout/rule_details/right/index.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/rule_details/right/index.test.tsx @@ -10,7 +10,7 @@ import { render } from '@testing-library/react'; import { ThemeProvider } from 'styled-components'; import { getMockTheme } from '../../../common/lib/kibana/kibana_react.mock'; import { TestProviders } from '../../../common/mock'; -// import { TestProvider } from '@kbn/expandable-flyout/src/test/provider'; +import { useRuleDetailsLink } from '../../document_details/shared/hooks/use_rule_details_link'; import { RulePanel } from '.'; import { getStepsData } from '../../../detections/pages/detection_engine/rules/helpers'; import { useRuleDetails } from '../hooks/use_rule_details'; @@ -23,6 +23,8 @@ import type { RuleResponse } from '../../../../common/api/detection_engine'; import { BODY_TEST_ID, LOADING_TEST_ID } from './test_ids'; import { RULE_PREVIEW_FOOTER_TEST_ID } from '../preview/test_ids'; +jest.mock('../../document_details/shared/hooks/use_rule_details_link'); + const mockUseRuleDetails = useRuleDetails as jest.Mock; jest.mock('../hooks/use_rule_details'); @@ -89,6 +91,7 @@ describe('', () => { }); it('should render preview footer when isPreviewMode is true', () => { + (useRuleDetailsLink as jest.Mock).mockReturnValue('rule_details_link'); mockUseRuleDetails.mockReturnValue({ rule, loading: false, @@ -97,8 +100,6 @@ describe('', () => { mockGetStepsData.mockReturnValue({}); const { getByTestId } = renderRulePanel(true); - // await act(async () => { expect(getByTestId(RULE_PREVIEW_FOOTER_TEST_ID)).toBeInTheDocument(); - // }); }); }); From db54cb1054cfb83f0efef6a2b087cc914c6694a0 Mon Sep 17 00:00:00 2001 From: Nick Partridge Date: Thu, 10 Oct 2024 17:23:30 -0500 Subject: [PATCH 76/87] [Lens][Datatable] Fix non-numeric default cell text alignment (#193886) Fixes #191258 where the default alignment of the cell was different between the dimension editor and the table vis. - Assigns default alignment of `'right'` for all number values excluding `ranges`, `multi_terms`, `filters` and `filtered_metric`. Otherwise assigns `'left'`. - The default alignment is never save until the user changes it themselves. --- .../common/expressions/datatable/utils.ts | 31 ++++++++-- .../shared_components/coloring/utils.test.ts | 2 +- .../shared_components/coloring/utils.ts | 21 ++++--- .../datatable/components/cell_value.test.tsx | 6 +- .../datatable/components/cell_value.tsx | 2 +- .../datatable/components/columns.test.tsx | 2 +- .../datatable/components/columns.tsx | 4 +- .../components/dimension_editor.test.tsx | 60 +++++++++---------- .../datatable/components/dimension_editor.tsx | 27 +++++---- .../datatable/components/table_basic.test.tsx | 46 ++++++++++++-- .../datatable/components/table_basic.tsx | 57 ++++++++---------- .../datatable/components/types.ts | 8 +-- .../visualizations/datatable/expression.tsx | 8 ++- .../public/visualizations/datatable/index.ts | 6 +- .../public/visualizations/datatable/utils.ts | 14 +++++ .../datatable/visualization.tsx | 6 +- .../public/visualizations/heatmap/utils.ts | 5 +- 17 files changed, 196 insertions(+), 109 deletions(-) create mode 100644 x-pack/plugins/lens/public/visualizations/datatable/utils.ts diff --git a/x-pack/plugins/lens/common/expressions/datatable/utils.ts b/x-pack/plugins/lens/common/expressions/datatable/utils.ts index 71c3d92126b33..bc617d931f500 100644 --- a/x-pack/plugins/lens/common/expressions/datatable/utils.ts +++ b/x-pack/plugins/lens/common/expressions/datatable/utils.ts @@ -5,14 +5,37 @@ * 2.0. */ -import type { Datatable } from '@kbn/expressions-plugin/common'; +import { type Datatable, type DatatableColumnMeta } from '@kbn/expressions-plugin/common'; import { getOriginalId } from './transpose_helpers'; +/** + * Returns true for numerical fields + * + * Excludes the following types: + * - `range` - Stringified range + * - `multi_terms` - Multiple values + * - `filters` - Arbitrary label + * - `filtered_metric` - Array of values + */ +export function isNumericField(meta?: DatatableColumnMeta): boolean { + return ( + meta?.type === 'number' && + meta.params?.id !== 'range' && + meta.params?.id !== 'multi_terms' && + meta.sourceParams?.type !== 'filters' && + meta.sourceParams?.type !== 'filtered_metric' + ); +} + +/** + * Returns true for numerical fields, excluding ranges + */ export function isNumericFieldForDatatable(table: Datatable | undefined, accessor: string) { - return getFieldTypeFromDatatable(table, accessor) === 'number'; + const meta = getFieldMetaFromDatatable(table, accessor); + return isNumericField(meta); } -export function getFieldTypeFromDatatable(table: Datatable | undefined, accessor: string) { +export function getFieldMetaFromDatatable(table: Datatable | undefined, accessor: string) { return table?.columns.find((col) => col.id === accessor || getOriginalId(col.id) === accessor) - ?.meta.type; + ?.meta; } diff --git a/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts b/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts index 5a126565c251f..cc6044fc0f624 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts +++ b/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts @@ -110,7 +110,7 @@ describe('findMinMaxByColumnId', () => { { a: 'shoes', b: 53 }, ], }) - ).toEqual({ b: { min: 2, max: 53 } }); + ).toEqual(new Map([['b', { min: 2, max: 53 }]])); }); }); diff --git a/x-pack/plugins/lens/public/shared_components/coloring/utils.ts b/x-pack/plugins/lens/public/shared_components/coloring/utils.ts index 211628a096189..c58fec1ddb03e 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/utils.ts +++ b/x-pack/plugins/lens/public/shared_components/coloring/utils.ts @@ -95,12 +95,12 @@ export const findMinMaxByColumnId = ( table: Datatable | undefined, getOriginalId: (id: string) => string = (id: string) => id ) => { - const minMax: Record = {}; + const minMaxMap = new Map(); if (table != null) { for (const columnId of columnIds) { const originalId = getOriginalId(columnId); - minMax[originalId] = minMax[originalId] || { + const minMax = minMaxMap.get(originalId) ?? { max: Number.NEGATIVE_INFINITY, min: Number.POSITIVE_INFINITY, }; @@ -108,19 +108,22 @@ export const findMinMaxByColumnId = ( const rowValue = row[columnId]; const numericValue = getNumericValue(rowValue); if (numericValue != null) { - if (minMax[originalId].min > numericValue) { - minMax[originalId].min = numericValue; + if (minMax.min > numericValue) { + minMax.min = numericValue; } - if (minMax[originalId].max < numericValue) { - minMax[originalId].max = numericValue; + if (minMax.max < numericValue) { + minMax.max = numericValue; } } }); + // what happens when there's no data in the table? Fallback to a percent range - if (minMax[originalId].max === Number.NEGATIVE_INFINITY) { - minMax[originalId] = getFallbackDataBounds(); + if (minMax.max === Number.NEGATIVE_INFINITY) { + minMaxMap.set(originalId, getFallbackDataBounds()); + } else { + minMaxMap.set(originalId, minMax); } } } - return minMax; + return minMaxMap; }; diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/cell_value.test.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/cell_value.test.tsx index 76b8fc7b61740..e9f3caba9ec05 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/cell_value.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/cell_value.test.tsx @@ -54,9 +54,7 @@ describe('datatable cell renderer', () => { @@ -217,7 +215,7 @@ describe('datatable cell renderer', () => { { wrapper: DataContextProviderWrapper({ table, - minMaxByColumnId: { a: { min: 12, max: 155 } }, + minMaxByColumnId: new Map([['a', { min: 12, max: 155 }]]), ...context, }), } diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/cell_value.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/cell_value.tsx index 0761c7904e75f..97e7e755ac36e 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/cell_value.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/cell_value.tsx @@ -53,7 +53,7 @@ export const createGridCell = ( } = columnConfig.columns[colIndex] ?? {}; const filterOnClick = oneClickFilter && handleFilterClick; const content = formatters[columnId]?.convert(rawRowValue, filterOnClick ? 'text' : 'html'); - const currentAlignment = alignments && alignments[columnId]; + const currentAlignment = alignments?.get(columnId); useEffect(() => { let colorSet = false; diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/columns.test.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/columns.test.tsx index 3612317f7a565..76437743c5723 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/columns.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/columns.test.tsx @@ -72,7 +72,7 @@ const callCreateGridColumns = ( params.formatFactory ?? (((x: unknown) => ({ convert: () => x })) as unknown as FormatFactory), params.onColumnResize ?? jest.fn(), params.onColumnHide ?? jest.fn(), - params.alignments ?? {}, + params.alignments ?? new Map(), params.headerRowHeight ?? RowHeightMode.auto, params.headerRowLines ?? 1, params.columnCellValueActions ?? [], diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/columns.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/columns.tsx index 6cd8c32db4b6d..8d2fcc9fac0c0 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/columns.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/columns.tsx @@ -51,7 +51,7 @@ export const createGridColumns = ( formatFactory: FormatFactory, onColumnResize: (eventData: { columnId: string; width: number | undefined }) => void, onColumnHide: ((eventData: { columnId: string }) => void) | undefined, - alignments: Record, + alignments: Map, headerRowHeight: RowHeightMode, headerRowLines: number, columnCellValueActions: LensCellValueAction[][] | undefined, @@ -261,7 +261,7 @@ export const createGridColumns = ( }); } } - const currentAlignment = alignments && alignments[field]; + const currentAlignment = alignments && alignments.get(field); const hasMultipleRows = [RowHeightMode.auto, RowHeightMode.custom, undefined].includes( headerRowHeight ); diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.test.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.test.tsx index 09c7d95b309e7..738f7edab2a6e 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.test.tsx @@ -6,25 +6,20 @@ */ import React from 'react'; -import { DEFAULT_COLOR_MAPPING_CONFIG, type PaletteRegistry } from '@kbn/coloring'; +import { DEFAULT_COLOR_MAPPING_CONFIG } from '@kbn/coloring'; import { act, render, screen } from '@testing-library/react'; import userEvent, { type UserEvent } from '@testing-library/user-event'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import { EuiButtonGroupTestHarness } from '@kbn/test-eui-helpers'; -import { - FramePublicAPI, - OperationDescriptor, - VisualizationDimensionEditorProps, - DatasourcePublicAPI, - DataType, -} from '../../../types'; +import { FramePublicAPI, DatasourcePublicAPI, OperationDescriptor } from '../../../types'; import { DatatableVisualizationState } from '../visualization'; import { createMockDatasource, createMockFramePublicAPI } from '../../../mocks'; -import { TableDimensionEditor } from './dimension_editor'; +import { TableDimensionEditor, TableDimensionEditorProps } from './dimension_editor'; import { ColumnState } from '../../../../common/expressions'; import { capitalize } from 'lodash'; import { I18nProvider } from '@kbn/i18n-react'; +import { DatatableColumnType } from '@kbn/expressions-plugin/common'; describe('data table dimension editor', () => { let user: UserEvent; @@ -35,10 +30,8 @@ describe('data table dimension editor', () => { alignment: EuiButtonGroupTestHarness; }; let mockOperationForFirstColumn: (overrides?: Partial) => void; - let props: VisualizationDimensionEditorProps & { - paletteService: PaletteRegistry; - isDarkMode: boolean; - }; + + let props: TableDimensionEditorProps; function testState(): DatatableVisualizationState { return { @@ -80,6 +73,7 @@ describe('data table dimension editor', () => { name: 'foo', meta: { type: 'string', + params: {}, }, }, ], @@ -114,13 +108,7 @@ describe('data table dimension editor', () => { mockOperationForFirstColumn(); }); - const renderTableDimensionEditor = ( - overrideProps?: Partial< - VisualizationDimensionEditorProps & { - paletteService: PaletteRegistry; - } - > - ) => { + const renderTableDimensionEditor = (overrideProps?: Partial) => { return render(, { wrapper: ({ children }) => ( @@ -137,11 +125,18 @@ describe('data table dimension editor', () => { }); it('should render default alignment for number', () => { - mockOperationForFirstColumn({ dataType: 'number' }); + frame.activeData!.first.columns[0].meta.type = 'number'; renderTableDimensionEditor(); expect(btnGroups.alignment.selected).toHaveTextContent('Right'); }); + it('should render default alignment for ranges', () => { + frame.activeData!.first.columns[0].meta.type = 'number'; + frame.activeData!.first.columns[0].meta.params = { id: 'range' }; + renderTableDimensionEditor(); + expect(btnGroups.alignment.selected).toHaveTextContent('Left'); + }); + it('should render specific alignment', () => { state.columns[0].alignment = 'center'; renderTableDimensionEditor(); @@ -181,10 +176,11 @@ describe('data table dimension editor', () => { expect(screen.queryByTestId('lns_dynamicColoring_edit')).not.toBeInTheDocument(); }); - it.each(['date'])( + it.each(['date'])( 'should not show the dynamic coloring option for "%s" columns', - (dataType) => { - mockOperationForFirstColumn({ dataType }); + (type) => { + frame.activeData!.first.columns[0].meta.type = type; + renderTableDimensionEditor(); expect(screen.queryByTestId('lnsDatatable_dynamicColoring_groups')).not.toBeInTheDocument(); expect(screen.queryByTestId('lns_dynamicColoring_edit')).not.toBeInTheDocument(); @@ -231,15 +227,16 @@ describe('data table dimension editor', () => { }); }); - it.each<{ flyout: 'terms' | 'values'; isBucketed: boolean; dataType: DataType }>([ - { flyout: 'terms', isBucketed: true, dataType: 'number' }, - { flyout: 'terms', isBucketed: false, dataType: 'string' }, - { flyout: 'values', isBucketed: false, dataType: 'number' }, + it.each<{ flyout: 'terms' | 'values'; isBucketed: boolean; type: DatatableColumnType }>([ + { flyout: 'terms', isBucketed: true, type: 'number' }, + { flyout: 'terms', isBucketed: false, type: 'string' }, + { flyout: 'values', isBucketed: false, type: 'number' }, ])( - 'should show color by $flyout flyout when bucketing is $isBucketed with $dataType column', - async ({ flyout, isBucketed, dataType }) => { + 'should show color by $flyout flyout when bucketing is $isBucketed with $type column', + async ({ flyout, isBucketed, type }) => { state.columns[0].colorMode = 'cell'; - mockOperationForFirstColumn({ isBucketed, dataType }); + frame.activeData!.first.columns[0].meta.type = type; + mockOperationForFirstColumn({ isBucketed }); renderTableDimensionEditor(); await user.click(screen.getByLabelText('Edit colors')); @@ -251,6 +248,7 @@ describe('data table dimension editor', () => { it('should show the dynamic coloring option for a bucketed operation', () => { state.columns[0].colorMode = 'cell'; + frame.activeData!.first.columns[0].meta.type = 'string'; mockOperationForFirstColumn({ isBucketed: true }); renderTableDimensionEditor(); diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx index c1e097276cf3d..99fe3cc1c164e 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx @@ -8,7 +8,7 @@ import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiSwitch, EuiButtonGroup, htmlIdGenerator } from '@elastic/eui'; -import { PaletteRegistry } from '@kbn/coloring'; +import { PaletteRegistry, getFallbackDataBounds } from '@kbn/coloring'; import { getColorCategories } from '@kbn/chart-expressions-common'; import { useDebouncedValue } from '@kbn/visualization-utils'; import type { VisualizationDimensionEditorProps } from '../../../types'; @@ -26,6 +26,11 @@ import './dimension_editor.scss'; import { CollapseSetting } from '../../../shared_components/collapse_setting'; import { ColorMappingByValues } from '../../../shared_components/coloring/color_mapping_by_values'; import { ColorMappingByTerms } from '../../../shared_components/coloring/color_mapping_by_terms'; +import { getColumnAlignment } from '../utils'; +import { + getFieldMetaFromDatatable, + isNumericField, +} from '../../../../common/expressions/datatable/utils'; const idPrefix = htmlIdGenerator()(); @@ -45,12 +50,13 @@ function updateColumn( }); } -export function TableDimensionEditor( - props: VisualizationDimensionEditorProps & { +export type TableDimensionEditorProps = + VisualizationDimensionEditorProps & { paletteService: PaletteRegistry; isDarkMode: boolean; - } -) { + }; + +export function TableDimensionEditor(props: TableDimensionEditorProps) { const { frame, accessor, isInlineEditing, isDarkMode } = props; const column = props.state.columns.find(({ columnId }) => accessor === columnId); const { inputValue: localState, handleInputChange: setLocalState } = @@ -74,12 +80,13 @@ export function TableDimensionEditor( const currentData = frame.activeData?.[localState.layerId]; const datasource = frame.datasourceLayers?.[localState.layerId]; - const { dataType, isBucketed } = datasource?.getOperationForColumnId(accessor) ?? {}; - const showColorByTerms = shouldColorByTerms(dataType, isBucketed); - const currentAlignment = column?.alignment || (dataType === 'number' ? 'right' : 'left'); + const { isBucketed } = datasource?.getOperationForColumnId(accessor) ?? {}; + const meta = getFieldMetaFromDatatable(currentData, accessor); + const showColorByTerms = shouldColorByTerms(meta?.type, isBucketed); + const currentAlignment = getColumnAlignment(column, isNumericField(meta)); const currentColorMode = column?.colorMode || 'none'; const hasDynamicColoring = currentColorMode !== 'none'; - const showDynamicColoringFeature = dataType !== 'date'; + const showDynamicColoringFeature = meta?.type !== 'date'; const visibleColumnsCount = localState.columns.filter((c) => !c.hidden).length; const hasTransposedColumn = localState.columns.some(({ isTransposed }) => isTransposed); @@ -88,7 +95,7 @@ export function TableDimensionEditor( [] : [accessor]; const minMaxByColumnId = findMinMaxByColumnId(columnsToCheck, currentData, getOriginalId); - const currentMinMax = minMaxByColumnId[accessor]; + const currentMinMax = minMaxByColumnId.get(accessor) ?? getFallbackDataBounds(); const activePalette = column?.palette ?? { type: 'palette', diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.test.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.test.tsx index 21361f874e83e..2358b9ec5b563 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.test.tsx @@ -11,7 +11,6 @@ import userEvent from '@testing-library/user-event'; import { I18nProvider } from '@kbn/i18n-react'; import faker from 'faker'; import { act } from 'react-dom/test-utils'; -import { IAggType } from '@kbn/data-plugin/public'; import { IFieldFormat } from '@kbn/field-formats-plugin/common'; import { coreMock } from '@kbn/core/public/mocks'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; @@ -73,6 +72,17 @@ function sampleArgs() { sourceParams: { indexPatternId, type: 'count' }, }, }, + { + id: 'd', + name: 'd', + meta: { + type: 'number', + source: 'esaggs', + field: 'd', + params: { id: 'range' }, + sourceParams: { indexPatternId, type: 'range' }, + }, + }, ], rows: [{ a: 'shoes', b: 1588024800000, c: 3 }], }; @@ -119,7 +129,9 @@ describe('DatatableComponent', () => { args, formatFactory: () => ({ convert: (x) => x } as IFieldFormat), dispatchEvent: onDispatchEvent, - getType: jest.fn(() => ({ type: 'buckets' } as IAggType)), + getType: jest.fn().mockReturnValue({ + type: 'buckets', + }), paletteService: chartPluginMock.createPaletteRegistry(), theme: setUpMockTheme, renderMode: 'edit' as const, @@ -357,14 +369,39 @@ describe('DatatableComponent', () => { ]); }); - test('it adds alignment data to context', () => { + test('it adds explicit alignment to context', () => { renderDatatableComponent({ args: { ...args, columns: [ { columnId: 'a', alignment: 'center', type: 'lens_datatable_column', colorMode: 'none' }, + { columnId: 'b', alignment: 'center', type: 'lens_datatable_column', colorMode: 'none' }, + { columnId: 'c', alignment: 'center', type: 'lens_datatable_column', colorMode: 'none' }, + { columnId: 'd', alignment: 'center', type: 'lens_datatable_column', colorMode: 'none' }, + ], + }, + }); + const alignmentsClassNames = screen + .getAllByTestId('lnsTableCellContent') + .map((cell) => cell.className); + + expect(alignmentsClassNames).toEqual([ + 'lnsTableCell--center', // set via args + 'lnsTableCell--center', // set via args + 'lnsTableCell--center', // set via args + 'lnsTableCell--center', // set via args + ]); + }); + + test('it adds default alignment data to context', () => { + renderDatatableComponent({ + args: { + ...args, + columns: [ + { columnId: 'a', type: 'lens_datatable_column', colorMode: 'none' }, { columnId: 'b', type: 'lens_datatable_column', colorMode: 'none' }, { columnId: 'c', type: 'lens_datatable_column', colorMode: 'none' }, + { columnId: 'd', type: 'lens_datatable_column', colorMode: 'none' }, ], sortingColumnId: 'b', sortingDirection: 'desc', @@ -375,9 +412,10 @@ describe('DatatableComponent', () => { .map((cell) => cell.className); expect(alignmentsClassNames).toEqual([ - 'lnsTableCell--center', // set via args + 'lnsTableCell--left', // default for string 'lnsTableCell--left', // default for date 'lnsTableCell--right', // default for number + 'lnsTableCell--left', // default for range ]); }); diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx index 83249f86ffa79..55e198b943e81 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx @@ -6,7 +6,7 @@ */ import './table_basic.scss'; -import { ColorMappingInputData, PaletteOutput } from '@kbn/coloring'; +import { ColorMappingInputData, PaletteOutput, getFallbackDataBounds } from '@kbn/coloring'; import React, { useLayoutEffect, useCallback, @@ -58,8 +58,12 @@ import { } from './table_actions'; import { getFinalSummaryConfiguration } from '../../../../common/expressions/datatable/summary'; import { DEFAULT_HEADER_ROW_HEIGHT, DEFAULT_HEADER_ROW_HEIGHT_LINES } from './constants'; -import { getFieldTypeFromDatatable } from '../../../../common/expressions/datatable/utils'; +import { + getFieldMetaFromDatatable, + isNumericField, +} from '../../../../common/expressions/datatable/utils'; import { CellColorFn, getCellColorFn } from '../../../shared_components/coloring/get_cell_color_fn'; +import { getColumnAlignment } from '../utils'; export const DataContext = React.createContext({}); @@ -229,10 +233,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => { columnConfig.columns .filter((_col, index) => { const col = firstTableRef.current.columns[index]; - return ( - col?.meta?.sourceParams?.type && - getType(col.meta.sourceParams.type as string)?.type === 'buckets' - ); + return getType(col?.meta)?.type === 'buckets'; }) .map((col) => col.columnId), [firstTableRef, columnConfig, getType] @@ -240,7 +241,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => { const isEmpty = firstLocalTable.rows.length === 0 || - (bucketedColumns.length && + (bucketedColumns.length > 0 && props.data.rows.every((row) => bucketedColumns.every((col) => row[col] == null))); const visibleColumns = useMemo( @@ -266,34 +267,26 @@ export const DatatableComponent = (props: DatatableRenderProps) => { [onEditAction, setColumnConfig, columnConfig, isInteractive] ); - const isNumericMap: Record = useMemo( + const isNumericMap: Map = useMemo( () => - firstLocalTable.columns.reduce>( - (map, column) => ({ - ...map, - [column.id]: column.meta.type === 'number', - }), - {} - ), - [firstLocalTable] + firstLocalTable.columns.reduce((acc, column) => { + acc.set(column.id, isNumericField(column.meta)); + return acc; + }, new Map()), + [firstLocalTable.columns] ); - const alignments: Record = useMemo(() => { - const alignmentMap: Record = {}; - columnConfig.columns.forEach((column) => { - if (column.alignment) { - alignmentMap[column.columnId] = column.alignment; - } else { - alignmentMap[column.columnId] = isNumericMap[column.columnId] ? 'right' : 'left'; - } - }); - return alignmentMap; - }, [columnConfig, isNumericMap]); + const alignments: Map = useMemo(() => { + return columnConfig.columns.reduce((acc, column) => { + acc.set(column.columnId, getColumnAlignment(column, isNumericMap.get(column.columnId))); + return acc; + }, new Map()); + }, [columnConfig.columns, isNumericMap]); - const minMaxByColumnId: Record = useMemo(() => { + const minMaxByColumnId: Map = useMemo(() => { return findMinMaxByColumnId( columnConfig.columns - .filter(({ columnId }) => isNumericMap[columnId]) + .filter(({ columnId }) => isNumericMap.get(columnId)) .map(({ columnId }) => columnId), props.data, getOriginalId @@ -402,7 +395,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => { return cellColorFnMap.get(originalId)!; } - const dataType = getFieldTypeFromDatatable(firstLocalTable, originalId); + const dataType = getFieldMetaFromDatatable(firstLocalTable, originalId)?.type; const isBucketed = bucketedColumns.some((id) => id === columnId); const colorByTerms = shouldColorByTerms(dataType, isBucketed); @@ -419,7 +412,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => { : { type: 'ranges', bins: 0, - ...minMaxByColumnId[originalId], + ...(minMaxByColumnId.get(originalId) ?? getFallbackDataBounds()), }; const colorFn = getCellColorFn( props.paletteService, @@ -491,7 +484,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => { ]) ); return ({ columnId }: { columnId: string }) => { - const currentAlignment = alignments && alignments[columnId]; + const currentAlignment = alignments.get(columnId); const alignmentClassName = `lnsTableCell--${currentAlignment}`; const columnName = columns.find(({ id }) => id === columnId)?.displayAsText?.replace(/ /g, '-') || columnId; diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/types.ts b/x-pack/plugins/lens/public/visualizations/datatable/components/types.ts index b884a2c716be9..00d916bf956ae 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/types.ts +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/types.ts @@ -8,7 +8,7 @@ import { CoreSetup } from '@kbn/core/public'; import type { PaletteRegistry } from '@kbn/coloring'; import type { IAggType } from '@kbn/data-plugin/public'; -import type { Datatable, RenderMode } from '@kbn/expressions-plugin/common'; +import type { Datatable, DatatableColumnMeta, RenderMode } from '@kbn/expressions-plugin/common'; import type { ILensInterpreterRenderHandlers, LensCellValueAction, @@ -49,7 +49,7 @@ export type LensPagesizeAction = LensEditEvent export type DatatableRenderProps = DatatableProps & { formatFactory: FormatFactory; dispatchEvent: ILensInterpreterRenderHandlers['event']; - getType: (name: string) => IAggType | undefined; + getType: (meta?: DatatableColumnMeta) => IAggType | undefined; renderMode: RenderMode; paletteService: PaletteRegistry; theme: CoreSetup['theme']; @@ -72,8 +72,8 @@ export type DatatableRenderProps = DatatableProps & { export interface DataContextType { table?: Datatable; rowHasRowClickTriggerActions?: boolean[]; - alignments?: Record; - minMaxByColumnId?: Record; + alignments?: Map; + minMaxByColumnId?: Map; handleFilterClick?: ( field: string, value: unknown, diff --git a/x-pack/plugins/lens/public/visualizations/datatable/expression.tsx b/x-pack/plugins/lens/public/visualizations/datatable/expression.tsx index 652abec75695e..a5927dd9183bf 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/expression.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/expression.tsx @@ -13,6 +13,7 @@ import type { IAggType } from '@kbn/data-plugin/public'; import { CoreSetup, IUiSettingsClient } from '@kbn/core/public'; import type { Datatable, + DatatableColumnMeta, ExpressionRenderDefinition, IInterpreterRenderHandlers, } from '@kbn/expressions-plugin/common'; @@ -102,6 +103,11 @@ export const getDatatableRenderer = (dependencies: { handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); const resolvedGetType = await dependencies.getType; + const getType = (meta?: DatatableColumnMeta): IAggType | undefined => { + if (meta?.sourceParams?.type === undefined) return; + return resolvedGetType(String(meta.sourceParams.type)); + }; + const { hasCompatibleActions, isInteractive, getCompatibleCellValueActions } = handlers; const renderComplete = () => { @@ -161,7 +167,7 @@ export const getDatatableRenderer = (dependencies: { dispatchEvent={handlers.event} renderMode={handlers.getRenderMode()} paletteService={dependencies.paletteService} - getType={resolvedGetType} + getType={getType} rowHasRowClickTriggerActions={rowHasRowClickTriggerActions} columnCellValueActions={columnCellValueActions} columnFilterable={columnsFilterable} diff --git a/x-pack/plugins/lens/public/visualizations/datatable/index.ts b/x-pack/plugins/lens/public/visualizations/datatable/index.ts index f68f167ea5f02..93e5e38e03c3c 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/index.ts +++ b/x-pack/plugins/lens/public/visualizations/datatable/index.ts @@ -32,6 +32,7 @@ export class DatatableVisualization { '../../async_services' ); const palettes = await charts.palettes.getPalettes(); + expressions.registerRenderer(() => getDatatableRenderer({ formatFactory, @@ -44,7 +45,10 @@ export class DatatableVisualization { }) ); - return getDatatableVisualization({ paletteService: palettes, kibanaTheme: core.theme }); + return getDatatableVisualization({ + paletteService: palettes, + kibanaTheme: core.theme, + }); }); } } diff --git a/x-pack/plugins/lens/public/visualizations/datatable/utils.ts b/x-pack/plugins/lens/public/visualizations/datatable/utils.ts new file mode 100644 index 0000000000000..ab4d8f05f8d44 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/datatable/utils.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 function getColumnAlignment( + { alignment }: C, + isNumeric = false +): 'left' | 'right' | 'center' { + if (alignment) return alignment; + return isNumeric ? 'right' : 'left'; +} diff --git a/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx b/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx index 0187776985a30..d2d23b2033f90 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx @@ -147,8 +147,8 @@ export const getDatatableVisualization = ({ .map(({ id }) => id) || [] : [accessor]; const minMaxByColumnId = findMinMaxByColumnId(columnsToCheck, currentData, getOriginalId); - - if (palette && !showColorByTerms && !palette?.canDynamicColoring) { + const dataBounds = minMaxByColumnId.get(accessor); + if (palette && !showColorByTerms && !palette?.canDynamicColoring && dataBounds) { const newPalette: PaletteOutput = { type: 'palette', name: showColorByTerms ? 'default' : defaultPaletteParams.name, @@ -158,7 +158,7 @@ export const getDatatableVisualization = ({ palette: { ...newPalette, params: { - stops: applyPaletteParams(paletteService, newPalette, minMaxByColumnId[accessor]), + stops: applyPaletteParams(paletteService, newPalette, dataBounds), }, }, }; diff --git a/x-pack/plugins/lens/public/visualizations/heatmap/utils.ts b/x-pack/plugins/lens/public/visualizations/heatmap/utils.ts index 5e09ce2987bae..fe942dd40427c 100644 --- a/x-pack/plugins/lens/public/visualizations/heatmap/utils.ts +++ b/x-pack/plugins/lens/public/visualizations/heatmap/utils.ts @@ -26,7 +26,10 @@ export function getSafePaletteParams( accessor, }; const minMaxByColumnId = findMinMaxByColumnId([accessor], currentData); - const currentMinMax = minMaxByColumnId[accessor]; + const currentMinMax = minMaxByColumnId.get(accessor) ?? { + max: Number.NEGATIVE_INFINITY, + min: Number.POSITIVE_INFINITY, + }; // need to tell the helper that the colorStops are required to display return { From 872d9da30e74f64dd33e25c6fed2d55e6aa4af47 Mon Sep 17 00:00:00 2001 From: Amir Ben Nun <34831306+amirbenun@users.noreply.github.com> Date: Fri, 11 Oct 2024 01:44:25 +0300 Subject: [PATCH 77/87] Agentless api fix delete path (#195762) ## Summary Agentless API delete path should have the `ess` or `serverless` mark based on the environment. This PR build the request URL based on that. --- .../services/agents/agentless_agent.test.ts | 76 +++++++++++++++++++ .../server/services/agents/agentless_agent.ts | 11 ++- .../fleet/server/services/utils/agentless.ts | 7 -- 3 files changed, 81 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts b/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts index e55b883e80029..e7db96812749b 100644 --- a/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts @@ -370,6 +370,82 @@ describe('Agentless Agent service', () => { ); }); + it('should delete agentless agent for ESS', async () => { + const returnValue = { + id: 'mocked', + }; + + (axios as jest.MockedFunction).mockResolvedValueOnce(returnValue); + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + ca: '/path/to/ca', + }, + }, + }, + } as any); + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + + const deleteAgentlessAgentReturnValue = await agentlessAgentService.deleteAgentlessAgent( + 'mocked-agentless-agent-policy-id' + ); + + expect(axios).toHaveBeenCalledTimes(1); + expect(deleteAgentlessAgentReturnValue).toEqual(returnValue); + expect(axios).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.anything(), + httpsAgent: expect.anything(), + method: 'DELETE', + url: 'http://api.agentless.com/api/v1/ess/deployments/mocked-agentless-agent-policy-id', + }) + ); + }); + + it('should delete agentless agent for serverless', async () => { + const returnValue = { + id: 'mocked', + }; + + (axios as jest.MockedFunction).mockResolvedValueOnce(returnValue); + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + ca: '/path/to/ca', + }, + }, + }, + } as any); + jest + .spyOn(appContextService, 'getCloud') + .mockReturnValue({ isCloudEnabled: true, isServerlessEnabled: true } as any); + + const deleteAgentlessAgentReturnValue = await agentlessAgentService.deleteAgentlessAgent( + 'mocked-agentless-agent-policy-id' + ); + + expect(axios).toHaveBeenCalledTimes(1); + expect(deleteAgentlessAgentReturnValue).toEqual(returnValue); + expect(axios).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.anything(), + httpsAgent: expect.anything(), + method: 'DELETE', + url: 'http://api.agentless.com/api/v1/serverless/deployments/mocked-agentless-agent-policy-id', + }) + ); + }); + it('should redact sensitive information from debug logs', async () => { const returnValue = { id: 'mocked', diff --git a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts index 3bf21c3bec0d1..617f3db7849f4 100644 --- a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts +++ b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts @@ -25,11 +25,7 @@ import { appContextService } from '../app_context'; import { listEnrollmentApiKeys } from '../api_keys'; import { listFleetServerHosts } from '../fleet_server_host'; import type { AgentlessConfig } from '../utils/agentless'; -import { - prependAgentlessApiBasePathToEndpoint, - isAgentlessApiEnabled, - getDeletionEndpointPath, -} from '../utils/agentless'; +import { prependAgentlessApiBasePathToEndpoint, isAgentlessApiEnabled } from '../utils/agentless'; class AgentlessAgentService { public async createAgentlessAgent( @@ -188,7 +184,10 @@ class AgentlessAgentService { const agentlessConfig = appContextService.getConfig()?.agentless; const tlsConfig = this.createTlsConfig(agentlessConfig); const requestConfig = { - url: getDeletionEndpointPath(agentlessConfig, `/deployments/${agentlessPolicyId}`), + url: prependAgentlessApiBasePathToEndpoint( + agentlessConfig, + `/deployments/${agentlessPolicyId}` + ), method: 'DELETE', headers: { 'Content-type': 'application/json', diff --git a/x-pack/plugins/fleet/server/services/utils/agentless.ts b/x-pack/plugins/fleet/server/services/utils/agentless.ts index c85e9cc991a6c..4c27d583d9a79 100644 --- a/x-pack/plugins/fleet/server/services/utils/agentless.ts +++ b/x-pack/plugins/fleet/server/services/utils/agentless.ts @@ -50,10 +50,3 @@ export const prependAgentlessApiBasePathToEndpoint = ( : AGENTLESS_ESS_API_BASE_PATH; return `${agentlessConfig.api.url}${endpointPrefix}${endpoint}`; }; - -export const getDeletionEndpointPath = ( - agentlessConfig: FleetConfigType['agentless'], - endpoint: AgentlessApiEndpoints -) => { - return `${agentlessConfig.api.url}${AGENTLESS_ESS_API_BASE_PATH}${endpoint}`; -}; From 00042177a8e976d379b5e40db3664db1e333999d Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Thu, 10 Oct 2024 18:48:47 -0400 Subject: [PATCH 78/87] [Security Solution] Prevent non-customizable fields from updating for Prebuilt rule types (#195318) ## Summary Addresses https://github.com/elastic/kibana/issues/180273 Adds validation in the `detectionRulesClient` to prevent the updating of non-customizable fields in Prebuilt rule types (i.e. external `rule_source`). Returns a `400` error if `author` or `license` fields are updated via `PUT` and `PATCH` endpoints for external rules. Also updates related test utils to reflect this new logic ### Checklist Delete any items that are not applicable to this PR. - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [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 - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed ### 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) --------- Co-authored-by: Elastic Machine --- .../rule_assets/prebuilt_rule_asset.mock.ts | 2 + .../detection_rules_client.patch_rule.test.ts | 21 ++++++++++ ...detection_rules_client.update_rule.test.ts | 21 ++++++++++ .../methods/patch_rule.ts | 3 ++ .../methods/update_rule.ts | 3 ++ .../rule_management/utils/validate.ts | 30 +++++++++++++++ .../patch_rules.ts | 22 +++++++++++ .../patch_rules_bulk.ts | 38 +++++++++++++++++++ .../update_rules.ts | 30 +++++++++++++++ .../update_rules_bulk.ts | 27 +++++++++++++ .../usage_collector/detection_rules.ts | 20 ++++++---- .../detection_rules_legacy_action.ts | 13 ++++--- .../get_custom_query_rule_params.ts | 1 + 13 files changed, 218 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.mock.ts index c73203c2871ab..8f9c1a6a32357 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.mock.ts @@ -18,6 +18,7 @@ export const getPrebuiltRuleMock = (rewrites?: Partial): Preb language: 'kuery', rule_id: 'rule-1', version: 1, + author: [], ...rewrites, } as PrebuiltRuleAsset); @@ -51,6 +52,7 @@ export const getPrebuiltThreatMatchRuleMock = (): PrebuiltRuleAsset => ({ language: 'kuery', rule_id: 'rule-1', version: 1, + author: [], threat_query: '*:*', threat_index: ['list-index'], threat_mapping: [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.patch_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.patch_rule.test.ts index e460581c02a1c..448df6b581a3b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.patch_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.patch_rule.test.ts @@ -277,6 +277,27 @@ describe('DetectionRulesClient.patchRule', () => { expect(rulesClient.create).not.toHaveBeenCalled(); }); + it('throws an error if rule has external rule source and non-customizable fields are changed', async () => { + // Mock the existing rule + const existingRule = { + ...getRulesSchemaMock(), + rule_source: { type: 'external', is_customized: true }, + }; + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); + + // Mock the rule update + const rulePatch = getCreateRulesSchemaMock('query-rule-id'); + rulePatch.license = 'new license'; + + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw + rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); + + await expect(detectionRulesClient.patchRule({ rulePatch })).rejects.toThrow( + 'Cannot update "license" field for prebuilt rules' + ); + }); + describe('actions', () => { it("updates the rule's actions if provided", async () => { // Mock the existing rule diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.update_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.update_rule.test.ts index a660e5c5e8747..cbd0fb1fe3680 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.update_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.update_rule.test.ts @@ -498,5 +498,26 @@ describe('DetectionRulesClient.updateRule', () => { }) ); }); + + it('throws an error if rule has external rule source and non-customizable fields are changed', async () => { + // Mock the existing rule + const existingRule = { + ...getRulesSchemaMock(), + rule_source: { type: 'external', is_customized: true }, + }; + + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); + + // Mock the rule update + const ruleUpdate = { ...getCreateRulesSchemaMock(), author: ['new user'] }; + + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw + rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); + + await expect(detectionRulesClient.updateRule({ ruleUpdate })).rejects.toThrow( + 'Cannot update "author" field for prebuilt rules' + ); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/patch_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/patch_rule.ts index 1218991bf388e..113576e8d02e2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/patch_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/patch_rule.ts @@ -16,6 +16,7 @@ import type { MlAuthz } from '../../../../../machine_learning/authz'; import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; import { applyRulePatch } from '../mergers/apply_rule_patch'; import { getIdError } from '../../../utils/utils'; +import { validateNonCustomizablePatchFields } from '../../../utils/validate'; import { convertAlertingRuleToRuleResponse } from '../converters/convert_alerting_rule_to_rule_response'; import { convertRuleResponseToAlertingRule } from '../converters/convert_rule_response_to_alerting_rule'; import { ClientError, toggleRuleEnabledOnUpdate, validateMlAuth } from '../utils'; @@ -51,6 +52,8 @@ export const patchRule = async ({ await validateMlAuth(mlAuthz, rulePatch.type ?? existingRule.type); + validateNonCustomizablePatchFields(rulePatch, existingRule); + const patchedRule = await applyRulePatch({ prebuiltRuleAssetClient, existingRule, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/update_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/update_rule.ts index cd84788026870..8fd7f7a89dcb7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/update_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/update_rule.ts @@ -11,6 +11,7 @@ import type { RuleResponse } from '../../../../../../../common/api/detection_eng import type { MlAuthz } from '../../../../../machine_learning/authz'; import { applyRuleUpdate } from '../mergers/apply_rule_update'; import { getIdError } from '../../../utils/utils'; +import { validateNonCustomizableUpdateFields } from '../../../utils/validate'; import { convertRuleResponseToAlertingRule } from '../converters/convert_rule_response_to_alerting_rule'; import { ClientError, toggleRuleEnabledOnUpdate, validateMlAuth } from '../utils'; @@ -50,6 +51,8 @@ export const updateRule = async ({ throw new ClientError(error.message, error.statusCode); } + validateNonCustomizableUpdateFields(ruleUpdate, existingRule); + const ruleWithUpdates = await applyRuleUpdate({ prebuiltRuleAssetClient, existingRule, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.ts index 3d07f935deb7b..5ff9d2d97f2b0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.ts @@ -15,6 +15,7 @@ import { RuleResponse, type RuleResponseAction, type RuleUpdateProps, + type RulePatchProps, } from '../../../../../common/api/detection_engine'; import { RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP, @@ -25,6 +26,7 @@ import { CustomHttpRequestError } from '../../../../utils/custom_http_request_er import { hasValidRuleType, type RuleAlertType, type RuleParams } from '../../rule_schema'; import { type BulkError, createBulkErrorObject } from '../../routes/utils'; import { internalRuleToAPIResponse } from '../logic/detection_rules_client/converters/internal_rule_to_api_response'; +import { ClientError } from '../logic/detection_rules_client/utils'; export const transformValidateBulkError = ( ruleId: string, @@ -117,3 +119,31 @@ function rulePayloadContainsResponseActions(rule: RuleCreateProps | RuleUpdatePr function ruleObjectContainsResponseActions(rule?: RuleAlertType) { return rule != null && 'params' in rule && 'responseActions' in rule?.params; } + +export const validateNonCustomizableUpdateFields = ( + ruleUpdate: RuleUpdateProps, + existingRule: RuleResponse +) => { + // We don't allow non-customizable fields to be changed for prebuilt rules + if (existingRule.rule_source && existingRule.rule_source.type === 'external') { + if (!isEqual(ruleUpdate.author, existingRule.author)) { + throw new ClientError(`Cannot update "author" field for prebuilt rules`, 400); + } else if (ruleUpdate.license !== existingRule.license) { + throw new ClientError(`Cannot update "license" field for prebuilt rules`, 400); + } + } +}; + +export const validateNonCustomizablePatchFields = ( + rulePatch: RulePatchProps, + existingRule: RuleResponse +) => { + // We don't allow non-customizable fields to be changed for prebuilt rules + if (existingRule.rule_source && existingRule.rule_source.type === 'external') { + if (rulePatch.author && !isEqual(rulePatch.author, existingRule.author)) { + throw new ClientError(`Cannot update "author" field for prebuilt rules`, 400); + } else if (rulePatch.license != null && rulePatch.license !== existingRule.license) { + throw new ClientError(`Cannot update "license" field for prebuilt rules`, 400); + } + } +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts index a567eb78a776d..41f207c90f319 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts @@ -16,6 +16,9 @@ import { removeServerGeneratedPropertiesIncludingRuleId, getSimpleRuleOutputWithoutRuleId, updateUsername, + createHistoricalPrebuiltRuleAssetSavedObjects, + installPrebuiltRules, + createRuleAssetSavedObject, } from '../../../utils'; import { createAlertsIndex, @@ -238,6 +241,25 @@ export default ({ getService }: FtrProviderContext) => { }); }); + it('throws an error if rule has external rule source and non-customizable fields are changed', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ rule_id: 'rule-1', author: ['elastic'] }), + ]); + await installPrebuiltRules(es, supertest); + + const { body } = await securitySolutionApi + .patchRule({ + body: { + rule_id: 'rule-1', + author: ['new user'], + }, + }) + .expect(400); + + expect(body.message).toEqual('Cannot update "author" field for prebuilt rules'); + }); + describe('max signals', () => { afterEach(async () => { await deleteAllRules(supertest, log); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts index 086909fc4945b..7929b912768ff 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts @@ -16,6 +16,9 @@ import { getSimpleRuleOutputWithoutRuleId, removeServerGeneratedPropertiesIncludingRuleId, updateUsername, + createHistoricalPrebuiltRuleAssetSavedObjects, + installPrebuiltRules, + createRuleAssetSavedObject, } from '../../../utils'; import { createAlertsIndex, @@ -347,6 +350,41 @@ export default ({ getService }: FtrProviderContext) => { }, ]); }); + + it('throws an error if rule has external rule source and non-customizable fields are changed', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ rule_id: 'rule-1', author: ['elastic'] }), + createRuleAssetSavedObject({ rule_id: 'rule-2', license: 'basic' }), + ]); + await installPrebuiltRules(es, supertest); + + const { body } = await securitySolutionApi + .bulkPatchRules({ + body: [ + { rule_id: 'rule-1', author: ['new user'] }, + { rule_id: 'rule-2', license: 'new license' }, + ], + }) + .expect(200); + + expect([body[0], body[1]]).toEqual([ + { + error: { + message: 'Cannot update "author" field for prebuilt rules', + status_code: 400, + }, + rule_id: 'rule-1', + }, + { + error: { + message: 'Cannot update "license" field for prebuilt rules', + status_code: 400, + }, + rule_id: 'rule-2', + }, + ]); + }); }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts index 60e7bfe3ff88f..c84236a14eb37 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts @@ -18,6 +18,9 @@ import { getSimpleMlRuleUpdate, getSimpleRule, updateUsername, + createHistoricalPrebuiltRuleAssetSavedObjects, + installPrebuiltRules, + createRuleAssetSavedObject, } from '../../../utils'; import { createAlertsIndex, @@ -309,6 +312,33 @@ export default ({ getService }: FtrProviderContext) => { expect(updatedRuleResponse).toMatchObject(expectedRule); }); }); + + it('throws an error if rule has external rule source and non-customizable fields are changed', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ rule_id: 'rule-1', license: 'elastic' }), + ]); + await installPrebuiltRules(es, supertest); + + const { body: existingRule } = await securitySolutionApi + .readRule({ + query: { rule_id: 'rule-1' }, + }) + .expect(200); + + const { body } = await securitySolutionApi + .updateRule({ + body: getCustomQueryRuleParams({ + ...existingRule, + rule_id: 'rule-1', + id: undefined, + license: 'new license', + }), + }) + .expect(400); + + expect(body.message).toEqual('Cannot update "license" field for prebuilt rules'); + }); }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts index f9faee0481bf6..cdca9e3ca6e1a 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts @@ -17,6 +17,9 @@ import { getSimpleRuleUpdate, getSimpleRule, updateUsername, + createHistoricalPrebuiltRuleAssetSavedObjects, + installPrebuiltRules, + createRuleAssetSavedObject, } from '../../../utils'; import { createAlertsIndex, @@ -370,6 +373,30 @@ export default ({ getService }: FtrProviderContext) => { }, ]); }); + + it('throws an error if rule has external rule source and non-customizable fields are changed', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ rule_id: 'rule-1', author: ['elastic'] }), + ]); + await installPrebuiltRules(es, supertest); + + const { body } = await securitySolutionApi + .bulkUpdateRules({ + body: [getCustomQueryRuleParams({ rule_id: 'rule-1', author: ['new user'] })], + }) + .expect(200); + + expect([body[0]]).toEqual([ + { + error: { + message: 'Cannot update "author" field for prebuilt rules', + status_code: 400, + }, + rule_id: 'rule-1', + }, + ]); + }); }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules.ts index b3b58ac7880f8..c43d08a805ca8 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules.ts @@ -31,6 +31,7 @@ import { getRuleSavedObjectWithLegacyInvestigationFields, getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray, createRuleThroughAlertingEndpoint, + getCustomQueryRuleParams, } from '../../../utils'; import { createRule, @@ -1140,7 +1141,7 @@ export default ({ getService }: FtrProviderContext) => { await installMockPrebuiltRules(supertest, es); const immutableRule = await fetchRule(supertest, { ruleId: ELASTIC_SECURITY_RULE_ID }); const hookAction = await createWebHookRuleAction(supertest); - const newRuleToUpdate = getSimpleRule(immutableRule.rule_id); + const newRuleToUpdate = getCustomQueryRuleParams({ rule_id: immutableRule.rule_id }); const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, false, newRuleToUpdate); await updateRule(supertest, ruleToUpdate); @@ -1161,7 +1162,7 @@ export default ({ getService }: FtrProviderContext) => { ...omittedFields } = foundRule; expect(omittedFields).to.eql({ - rule_name: 'Simple Rule Query', + rule_name: 'Custom query rule', rule_type: 'query', enabled: false, elastic_rule: true, @@ -1197,7 +1198,7 @@ export default ({ getService }: FtrProviderContext) => { await installMockPrebuiltRules(supertest, es); const immutableRule = await fetchRule(supertest, { ruleId: ELASTIC_SECURITY_RULE_ID }); const hookAction = await createWebHookRuleAction(supertest); - const newRuleToUpdate = getSimpleRule(immutableRule.rule_id); + const newRuleToUpdate = getCustomQueryRuleParams({ rule_id: immutableRule.rule_id }); const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, true, newRuleToUpdate); await updateRule(supertest, ruleToUpdate); @@ -1218,7 +1219,7 @@ export default ({ getService }: FtrProviderContext) => { ...omittedFields } = foundRule; expect(omittedFields).to.eql({ - rule_name: 'Simple Rule Query', + rule_name: 'Custom query rule', rule_type: 'query', enabled: true, elastic_rule: true, @@ -1254,7 +1255,7 @@ export default ({ getService }: FtrProviderContext) => { await installMockPrebuiltRules(supertest, es); const immutableRule = await fetchRule(supertest, { ruleId: ELASTIC_SECURITY_RULE_ID }); const hookAction = await createWebHookRuleAction(supertest); - const newRuleToUpdate = getSimpleRule(immutableRule.rule_id, false); + const newRuleToUpdate = getCustomQueryRuleParams({ rule_id: immutableRule.rule_id }); await updateRule(supertest, newRuleToUpdate); await createLegacyRuleAction(supertest, immutableRule.id, hookAction.id); @@ -1275,7 +1276,7 @@ export default ({ getService }: FtrProviderContext) => { ...omittedFields } = foundRule; expect(omittedFields).to.eql({ - rule_name: 'Simple Rule Query', + rule_name: 'Custom query rule', rule_type: 'query', enabled: false, elastic_rule: true, @@ -1311,7 +1312,10 @@ export default ({ getService }: FtrProviderContext) => { await installMockPrebuiltRules(supertest, es); const immutableRule = await fetchRule(supertest, { ruleId: ELASTIC_SECURITY_RULE_ID }); const hookAction = await createWebHookRuleAction(supertest); - const newRuleToUpdate = getSimpleRule(immutableRule.rule_id, true); + const newRuleToUpdate = getCustomQueryRuleParams({ + rule_id: immutableRule.rule_id, + enabled: true, + }); await updateRule(supertest, newRuleToUpdate); await createLegacyRuleAction(supertest, immutableRule.id, hookAction.id); @@ -1332,7 +1336,7 @@ export default ({ getService }: FtrProviderContext) => { ...omittedFields } = foundRule; expect(omittedFields).to.eql({ - rule_name: 'Simple Rule Query', + rule_name: 'Custom query rule', rule_type: 'query', enabled: true, elastic_rule: true, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules_legacy_action.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules_legacy_action.ts index e3754d9a09b60..f85f317e2da07 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules_legacy_action.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/usage_collector/detection_rules_legacy_action.ts @@ -21,13 +21,13 @@ import { fetchRule, getRuleWithWebHookAction, getSimpleMlRule, - getSimpleRule, getSimpleThreatMatch, getStats, getThresholdRuleForAlertTesting, installMockPrebuiltRules, updateRule, deleteAllEventLogExecutionEvents, + getCustomQueryRuleParams, } from '../../../utils'; import { createRule, @@ -408,7 +408,7 @@ export default ({ getService }: FtrProviderContext) => { await installMockPrebuiltRules(supertest, es); const immutableRule = await fetchRule(supertest, { ruleId: ELASTIC_SECURITY_RULE_ID }); const hookAction = await createWebHookRuleAction(supertest); - const newRuleToUpdate = getSimpleRule(immutableRule.rule_id, false); + const newRuleToUpdate = getCustomQueryRuleParams({ rule_id: immutableRule.rule_id }); await updateRule(supertest, newRuleToUpdate); await createLegacyRuleAction(supertest, immutableRule.id, hookAction.id); @@ -429,7 +429,7 @@ export default ({ getService }: FtrProviderContext) => { ...omittedFields } = foundRule; expect(omittedFields).to.eql({ - rule_name: 'Simple Rule Query', + rule_name: 'Custom query rule', rule_type: 'query', enabled: false, elastic_rule: true, @@ -465,7 +465,10 @@ export default ({ getService }: FtrProviderContext) => { await installMockPrebuiltRules(supertest, es); const immutableRule = await fetchRule(supertest, { ruleId: ELASTIC_SECURITY_RULE_ID }); const hookAction = await createWebHookRuleAction(supertest); - const newRuleToUpdate = getSimpleRule(immutableRule.rule_id, true); + const newRuleToUpdate = getCustomQueryRuleParams({ + rule_id: immutableRule.rule_id, + enabled: true, + }); await updateRule(supertest, newRuleToUpdate); await createLegacyRuleAction(supertest, immutableRule.id, hookAction.id); @@ -486,7 +489,7 @@ export default ({ getService }: FtrProviderContext) => { ...omittedFields } = foundRule; expect(omittedFields).to.eql({ - rule_name: 'Simple Rule Query', + rule_name: 'Custom query rule', rule_type: 'query', enabled: true, elastic_rule: true, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_params/get_custom_query_rule_params.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_params/get_custom_query_rule_params.ts index b561d3e8dc023..a5c5fe00ed700 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_params/get_custom_query_rule_params.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_params/get_custom_query_rule_params.ts @@ -29,6 +29,7 @@ export function getCustomQueryRuleParams( index: ['logs-*'], interval: '100m', from: 'now-6m', + author: [], enabled: false, ...rewrites, }; From 2b995fa86eb44a2bd54c44a74eb47a2a26ec0ed2 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 10 Oct 2024 16:49:22 -0600 Subject: [PATCH 79/87] [Security Assistant] Fix ESQL tool availability (#195827) --- .../graphs/default_assistant_graph/index.ts | 1 - .../routes/attack_discovery/helpers.test.ts | 2 - .../server/routes/attack_discovery/helpers.ts | 1 - .../server/routes/evaluate/post_evaluate.ts | 1 - .../plugins/elastic_assistant/server/types.ts | 1 - .../alert_counts/alert_counts_tool.test.ts | 2 - .../attack_discovery_tool.test.ts | 1 - .../tools/esql/nl_to_esql_tool.test.ts | 65 ++----------------- .../assistant/tools/esql/nl_to_esql_tool.ts | 5 +- .../knowledge_base_retrieval_tool.ts | 4 +- .../knowledge_base_write_tool.ts | 4 +- .../open_and_acknowledged_alerts_tool.test.ts | 2 - .../tools/security_labs/security_labs_tool.ts | 4 +- 13 files changed, 13 insertions(+), 80 deletions(-) diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts index ada5b8a421441..4f043c681f8df 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts @@ -103,7 +103,6 @@ export const callAssistantGraph: AgentExecutor = async ({ isEnabledKnowledgeBase, kbDataClient: dataClients?.kbDataClient, logger, - modelExists: isEnabledKnowledgeBase, onNewReplacements, replacements, request, diff --git a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers.test.ts b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers.test.ts index 15877e6727715..d5eaf7d159618 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers.test.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers.test.ts @@ -196,7 +196,6 @@ describe('helpers', () => { langChainTimeout, llm, logger: mockLogger, - modelExists: false, onNewReplacements, replacements: latestReplacements, request: mockRequest, @@ -231,7 +230,6 @@ describe('helpers', () => { langChainTimeout, llm, logger: mockLogger, - modelExists: false, onNewReplacements, replacements: latestReplacements, request: mockRequest, diff --git a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers.ts b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers.ts index 2a1450a9f7b9b..f016d6ac29118 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers.ts @@ -157,7 +157,6 @@ const formatAssistantToolParams = ({ langChainTimeout, llm, logger, - modelExists: false, // not required for attack discovery onNewReplacements, replacements: latestReplacements, request, diff --git a/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts b/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts index de154a1ddd96d..29a7527964677 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts @@ -236,7 +236,6 @@ export const postEvaluateRoute = ( llm, isOssModel, logger, - modelExists: isEnabledKnowledgeBase, request: skeletonRequest, alertsIndexPattern, // onNewReplacements, diff --git a/x-pack/plugins/elastic_assistant/server/types.ts b/x-pack/plugins/elastic_assistant/server/types.ts index 3117295810877..45bd5a4149b58 100755 --- a/x-pack/plugins/elastic_assistant/server/types.ts +++ b/x-pack/plugins/elastic_assistant/server/types.ts @@ -244,7 +244,6 @@ export interface AssistantToolParams { llm?: ActionsClientLlm | AssistantToolLlm; isOssModel?: boolean; logger: Logger; - modelExists: boolean; onNewReplacements?: (newReplacements: Replacements) => void; replacements?: Replacements; request: KibanaRequest< diff --git a/x-pack/plugins/security_solution/server/assistant/tools/alert_counts/alert_counts_tool.test.ts b/x-pack/plugins/security_solution/server/assistant/tools/alert_counts/alert_counts_tool.test.ts index 752f8e472a755..814a00853927f 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/alert_counts/alert_counts_tool.test.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/alert_counts/alert_counts_tool.test.ts @@ -29,13 +29,11 @@ describe('AlertCountsTool', () => { } as unknown as KibanaRequest; const isEnabledKnowledgeBase = true; const chain = {} as unknown as RetrievalQAChain; - const modelExists = true; const logger = loggerMock.create(); const rest = { isEnabledKnowledgeBase, chain, logger, - modelExists, }; beforeEach(() => { diff --git a/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/attack_discovery_tool.test.ts b/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/attack_discovery_tool.test.ts index 5d8fb0b51739a..4d06751f57d7d 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/attack_discovery_tool.test.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/attack_discovery_tool.test.ts @@ -75,7 +75,6 @@ describe('AttackDiscoveryTool', () => { isEnabledKnowledgeBase: false, llm, logger, - modelExists: false, onNewReplacements: jest.fn(), size, }; diff --git a/x-pack/plugins/security_solution/server/assistant/tools/esql/nl_to_esql_tool.test.ts b/x-pack/plugins/security_solution/server/assistant/tools/esql/nl_to_esql_tool.test.ts index f078bccb24a36..10b1fa21daefe 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/esql/nl_to_esql_tool.test.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/esql/nl_to_esql_tool.test.ts @@ -40,65 +40,18 @@ describe('NaturalLanguageESQLTool', () => { request, inference, connectorId, + isEnabledKnowledgeBase: true, }; describe('isSupported', () => { - it('returns false if isEnabledKnowledgeBase is false', () => { - const params = { - isEnabledKnowledgeBase: false, - modelExists: true, - ...rest, - }; - - expect(NL_TO_ESQL_TOOL.isSupported(params)).toBe(false); - }); - - it('returns false if modelExists is false (the ELSER model is not installed)', () => { - const params = { - isEnabledKnowledgeBase: true, - modelExists: false, // <-- ELSER model is not installed - ...rest, - }; - - expect(NL_TO_ESQL_TOOL.isSupported(params)).toBe(false); - }); - - it('returns true if isEnabledKnowledgeBase and modelExists are true', () => { - const params = { - isEnabledKnowledgeBase: true, - modelExists: true, - ...rest, - }; - - expect(NL_TO_ESQL_TOOL.isSupported(params)).toBe(true); + it('returns true if connectorId and inference have values', () => { + expect(NL_TO_ESQL_TOOL.isSupported(rest)).toBe(true); }); }); describe('getTool', () => { - it('returns null if isEnabledKnowledgeBase is false', () => { - const tool = NL_TO_ESQL_TOOL.getTool({ - isEnabledKnowledgeBase: false, - modelExists: true, - ...rest, - }); - - expect(tool).toBeNull(); - }); - - it('returns null if modelExists is false (the ELSER model is not installed)', () => { - const tool = NL_TO_ESQL_TOOL.getTool({ - isEnabledKnowledgeBase: true, - modelExists: false, // <-- ELSER model is not installed - ...rest, - }); - - expect(tool).toBeNull(); - }); - it('returns null if inference plugin is not provided', () => { const tool = NL_TO_ESQL_TOOL.getTool({ - isEnabledKnowledgeBase: true, - modelExists: true, ...rest, inference: undefined, }); @@ -108,8 +61,6 @@ describe('NaturalLanguageESQLTool', () => { it('returns null if connectorId is not provided', () => { const tool = NL_TO_ESQL_TOOL.getTool({ - isEnabledKnowledgeBase: true, - modelExists: true, ...rest, connectorId: undefined, }); @@ -117,10 +68,8 @@ describe('NaturalLanguageESQLTool', () => { expect(tool).toBeNull(); }); - it('should return a Tool instance if isEnabledKnowledgeBase and modelExists are true', () => { + it('should return a Tool instance when given required properties', () => { const tool = NL_TO_ESQL_TOOL.getTool({ - isEnabledKnowledgeBase: true, - modelExists: true, ...rest, }); @@ -129,8 +78,6 @@ describe('NaturalLanguageESQLTool', () => { it('should return a tool with the expected tags', () => { const tool = NL_TO_ESQL_TOOL.getTool({ - isEnabledKnowledgeBase: true, - modelExists: true, ...rest, }) as DynamicTool; @@ -139,8 +86,6 @@ describe('NaturalLanguageESQLTool', () => { it('should return tool with the expected description for OSS model', () => { const tool = NL_TO_ESQL_TOOL.getTool({ - isEnabledKnowledgeBase: true, - modelExists: true, isOssModel: true, ...rest, }) as DynamicTool; @@ -150,8 +95,6 @@ describe('NaturalLanguageESQLTool', () => { it('should return tool with the expected description for non-OSS model', () => { const tool = NL_TO_ESQL_TOOL.getTool({ - isEnabledKnowledgeBase: true, - modelExists: true, isOssModel: false, ...rest, }) as DynamicTool; diff --git a/x-pack/plugins/security_solution/server/assistant/tools/esql/nl_to_esql_tool.ts b/x-pack/plugins/security_solution/server/assistant/tools/esql/nl_to_esql_tool.ts index 96b865efeaed4..1205fb03b0458 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/esql/nl_to_esql_tool.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/esql/nl_to_esql_tool.ts @@ -13,6 +13,7 @@ import { naturalLanguageToEsql } from '@kbn/inference-plugin/server'; import { APP_UI_ID } from '../../../../common'; import { getPromptSuffixForOssModel } from './common'; +// select only some properties of AssistantToolParams export type ESQLToolParams = AssistantToolParams; const TOOL_NAME = 'NaturalLanguageESQLTool'; @@ -32,8 +33,8 @@ export const NL_TO_ESQL_TOOL: AssistantTool = { ...toolDetails, sourceRegister: APP_UI_ID, isSupported: (params: ESQLToolParams): params is ESQLToolParams => { - const { inference, connectorId, isEnabledKnowledgeBase, modelExists } = params; - return isEnabledKnowledgeBase && modelExists && inference != null && connectorId != null; + const { inference, connectorId } = params; + return inference != null && connectorId != null; }, getTool(params: ESQLToolParams) { if (!this.isSupported(params)) return null; diff --git a/x-pack/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_retrieval_tool.ts b/x-pack/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_retrieval_tool.ts index 7739de18857aa..cea2bdadf5970 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_retrieval_tool.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_retrieval_tool.ts @@ -25,8 +25,8 @@ export const KNOWLEDGE_BASE_RETRIEVAL_TOOL: AssistantTool = { ...toolDetails, sourceRegister: APP_UI_ID, isSupported: (params: AssistantToolParams): params is KnowledgeBaseRetrievalToolParams => { - const { kbDataClient, isEnabledKnowledgeBase, modelExists } = params; - return isEnabledKnowledgeBase && modelExists && kbDataClient != null; + const { kbDataClient, isEnabledKnowledgeBase } = params; + return isEnabledKnowledgeBase && kbDataClient != null; }, getTool(params: AssistantToolParams) { if (!this.isSupported(params)) return null; diff --git a/x-pack/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_write_tool.ts b/x-pack/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_write_tool.ts index 9b46c625e115b..4069eeeef5b97 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_write_tool.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_write_tool.ts @@ -28,8 +28,8 @@ export const KNOWLEDGE_BASE_WRITE_TOOL: AssistantTool = { ...toolDetails, sourceRegister: APP_UI_ID, isSupported: (params: AssistantToolParams): params is KnowledgeBaseWriteToolParams => { - const { isEnabledKnowledgeBase, kbDataClient, modelExists } = params; - return isEnabledKnowledgeBase && modelExists && kbDataClient != null; + const { isEnabledKnowledgeBase, kbDataClient } = params; + return isEnabledKnowledgeBase && kbDataClient != null; }, getTool(params: AssistantToolParams) { if (!this.isSupported(params)) return null; diff --git a/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool.test.ts b/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool.test.ts index 2b134dfd86335..09bae1639f1b1 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool.test.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool.test.ts @@ -32,14 +32,12 @@ describe('OpenAndAcknowledgedAlertsTool', () => { } as unknown as KibanaRequest; const isEnabledKnowledgeBase = true; const chain = {} as unknown as RetrievalQAChain; - const modelExists = true; const logger = loggerMock.create(); const rest = { isEnabledKnowledgeBase, esClient, chain, logger, - modelExists, }; const anonymizationFields = [ diff --git a/x-pack/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts b/x-pack/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts index 70e955dda8470..48e1619c2f00f 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts @@ -22,8 +22,8 @@ export const SECURITY_LABS_KNOWLEDGE_BASE_TOOL: AssistantTool = { ...toolDetails, sourceRegister: APP_UI_ID, isSupported: (params: AssistantToolParams): params is AssistantToolParams => { - const { kbDataClient, isEnabledKnowledgeBase, modelExists } = params; - return isEnabledKnowledgeBase && modelExists && kbDataClient != null; + const { kbDataClient, isEnabledKnowledgeBase } = params; + return isEnabledKnowledgeBase && kbDataClient != null; }, getTool(params: AssistantToolParams) { if (!this.isSupported(params)) return null; From 56b478fef283a0d77e19e2ccf65455c30f51ff2e Mon Sep 17 00:00:00 2001 From: Brad White Date: Thu, 10 Oct 2024 17:48:45 -0600 Subject: [PATCH 80/87] [CI] Skip ci for devcontainer changes (#195814) ## Summary For now it is not necessary to run any of CI for `.devcontainer` changes. --- .buildkite/pull_requests.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.buildkite/pull_requests.json b/.buildkite/pull_requests.json index 1f45c01042888..614d45969cdd7 100644 --- a/.buildkite/pull_requests.json +++ b/.buildkite/pull_requests.json @@ -30,7 +30,8 @@ "^\\.backportrc\\.json$", "^nav-kibana-dev\\.docnav\\.json$", "^src/dev/prs/kibana_qa_pr_list\\.json$", - "^\\.buildkite/pull_requests\\.json$" + "^\\.buildkite/pull_requests\\.json$", + "^\\.devcontainer/" ], "always_require_ci_on_changed": [ "^docs/developer/plugin-list.asciidoc$", From edd8f08e1b1e213bbe67c9f5ce9de5326ca94877 Mon Sep 17 00:00:00 2001 From: Philippe Oberti Date: Fri, 11 Oct 2024 03:51:44 +0200 Subject: [PATCH 81/87] [SecuritySolution][Threat Intelligence] - re-enable Cypress test skipped because of removal of bsearch (#195826) --- .../e2e/investigations/threat_intelligence/indicators.cy.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/threat_intelligence/indicators.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/threat_intelligence/indicators.cy.ts index f485ead495949..b0e5764469459 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/threat_intelligence/indicators.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/threat_intelligence/indicators.cy.ts @@ -67,8 +67,7 @@ const URL = '/app/security/threat_intelligence/indicators'; const URL_WITH_CONTRADICTORY_FILTERS = '/app/security/threat_intelligence/indicators?indicators=(filterQuery:(language:kuery,query:%27%27),filters:!((%27$state%27:(store:appState),meta:(alias:!n,disabled:!f,index:%27%27,key:threat.indicator.type,negate:!f,params:(query:file),type:phrase),query:(match_phrase:(threat.indicator.type:file))),(%27$state%27:(store:appState),meta:(alias:!n,disabled:!f,index:%27%27,key:threat.indicator.type,negate:!f,params:(query:url),type:phrase),query:(match_phrase:(threat.indicator.type:url)))),timeRange:(from:now/d,to:now/d))'; -// Failing: See https://github.com/elastic/kibana/issues/195804 -describe.skip('Single indicator', { tags: ['@ess'] }, () => { +describe('Single indicator', { tags: ['@ess'] }, () => { before(() => cy.task('esArchiverLoad', { archiveName: 'ti_indicators_data_single' })); after(() => cy.task('esArchiverUnload', { archiveName: 'ti_indicators_data_single' })); @@ -299,7 +298,7 @@ describe('Multiple indicators', { tags: ['@ess'] }, () => { cy.log('should reload the data when refresh button is pressed'); - cy.intercept(/bsearch/).as('search'); + cy.intercept('POST', '/internal/search/threatIntelligenceSearchStrategy').as('search'); cy.get(REFRESH_BUTTON).should('exist').click(); cy.wait('@search'); }); From b30c19f87b7e6ce9e320a99d2e63d2714f8150b9 Mon Sep 17 00:00:00 2001 From: "Joey F. Poon" Date: Fri, 11 Oct 2024 12:56:51 +0900 Subject: [PATCH 82/87] [Security Solution] enable cert check for usage-api calls (#194133) ## Summary Enables cert validation for usage-api requests if configs are provided. Also updated to use the usage-api url provided by configs. Maintains existing functionality if no configs are provided which is to be removed in a separate PR once configs are fully propagated. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../server/common/services/index.ts | 8 - .../services/usage_reporting_service.test.ts | 179 ++++++++++++++++++ .../services/usage_reporting_service.ts | 74 ++++++-- .../server/config.ts | 26 +-- .../server/constants.ts | 1 + .../server/plugin.ts | 6 + .../task_manager/usage_reporting_task.test.ts | 47 +++-- .../task_manager/usage_reporting_task.ts | 13 +- .../server/types.ts | 2 + .../tsconfig.json | 2 + .../test_suites/security/config.ts | 2 +- 11 files changed, 300 insertions(+), 60 deletions(-) delete mode 100644 x-pack/plugins/security_solution_serverless/server/common/services/index.ts create mode 100644 x-pack/plugins/security_solution_serverless/server/common/services/usage_reporting_service.test.ts diff --git a/x-pack/plugins/security_solution_serverless/server/common/services/index.ts b/x-pack/plugins/security_solution_serverless/server/common/services/index.ts deleted file mode 100644 index a76f6359f7e5b..0000000000000 --- a/x-pack/plugins/security_solution_serverless/server/common/services/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 { usageReportingService } from './usage_reporting_service'; diff --git a/x-pack/plugins/security_solution_serverless/server/common/services/usage_reporting_service.test.ts b/x-pack/plugins/security_solution_serverless/server/common/services/usage_reporting_service.test.ts new file mode 100644 index 0000000000000..e43df68cc200b --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/server/common/services/usage_reporting_service.test.ts @@ -0,0 +1,179 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 fetch from 'node-fetch'; +import https from 'https'; +import { merge } from 'lodash'; + +import { KBN_CERT_PATH, KBN_KEY_PATH, CA_CERT_PATH } from '@kbn/dev-utils'; + +import type { UsageApiConfigSchema } from '../../config'; +import type { UsageRecord } from '../../types'; + +import { UsageReportingService } from './usage_reporting_service'; +import { USAGE_REPORTING_ENDPOINT, USAGE_SERVICE_USAGE_URL } from '../../constants'; + +jest.mock('node-fetch'); +const { Response } = jest.requireActual('node-fetch'); + +describe('UsageReportingService', () => { + let usageApiConfig: UsageApiConfigSchema; + let service: UsageReportingService; + + function generateUsageApiConfig(overrides?: Partial): UsageApiConfigSchema { + const DEFAULT_USAGE_API_CONFIG = { enabled: false }; + usageApiConfig = merge(DEFAULT_USAGE_API_CONFIG, overrides); + + return usageApiConfig; + } + + function setupService( + usageApi: UsageApiConfigSchema = generateUsageApiConfig() + ): UsageReportingService { + service = new UsageReportingService(usageApi); + return service; + } + + function generateUsageRecord(overrides?: Partial): UsageRecord { + const date = new Date().toISOString(); + const DEFAULT_USAGE_RECORD = { + id: `usage-record-id-${date}`, + usage_timestamp: date, + creation_timestamp: date, + usage: {}, + source: {}, + } as UsageRecord; + return merge(DEFAULT_USAGE_RECORD, overrides); + } + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('usageApi configs not provided', () => { + beforeEach(() => { + setupService(); + }); + + it('should still work if usageApi.url is not provided', async () => { + const usageRecord = generateUsageRecord(); + const records: UsageRecord[] = [usageRecord]; + const mockResponse = new Response(null, { status: 200 }); + (fetch as jest.MockedFunction).mockResolvedValueOnce(mockResponse); + + const response = await service.reportUsage(records); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith(USAGE_SERVICE_USAGE_URL, { + method: 'post', + body: JSON.stringify(records), + headers: { 'Content-Type': 'application/json' }, + agent: expect.any(https.Agent), + }); + expect(response).toBe(mockResponse); + }); + + it('should use an agent with rejectUnauthorized false if config.enabled is false', async () => { + const usageRecord = generateUsageRecord(); + const records: UsageRecord[] = [usageRecord]; + const mockResponse = new Response(null, { status: 200 }); + (fetch as jest.MockedFunction).mockResolvedValueOnce(mockResponse); + + const response = await service.reportUsage(records); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith(USAGE_SERVICE_USAGE_URL, { + method: 'post', + body: JSON.stringify(records), + headers: { 'Content-Type': 'application/json' }, + agent: expect.objectContaining({ + options: expect.objectContaining({ rejectUnauthorized: false }), + }), + }); + expect(response).toBe(mockResponse); + }); + + it('should not set agent if the URL is not https', async () => { + const url = 'http://usage-api.example'; + setupService(generateUsageApiConfig({ url })); + const usageRecord = generateUsageRecord(); + const records: UsageRecord[] = [usageRecord]; + const mockResponse = new Response(null, { status: 200 }); + (fetch as jest.MockedFunction).mockResolvedValue(mockResponse); + + const response = await service.reportUsage(records); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith(`${url}${USAGE_REPORTING_ENDPOINT}`, { + method: 'post', + body: JSON.stringify(records), + headers: { 'Content-Type': 'application/json' }, + }); + expect(response).toBe(mockResponse); + }); + }); + + describe('usageApi configs provided', () => { + const DEFAULT_CONFIG = { + enabled: true, + url: 'https://usage-api.example', + tls: { + certificate: KBN_CERT_PATH, + key: KBN_KEY_PATH, + ca: CA_CERT_PATH, + }, + }; + + beforeEach(() => { + setupService(generateUsageApiConfig(DEFAULT_CONFIG)); + }); + + it('should use usageApi.url if provided', async () => { + const usageRecord = generateUsageRecord(); + const records: UsageRecord[] = [usageRecord]; + const mockResponse = new Response(null, { status: 200 }); + (fetch as jest.MockedFunction).mockResolvedValueOnce(mockResponse); + + const response = await service.reportUsage(records); + const url = `${DEFAULT_CONFIG.url}${USAGE_REPORTING_ENDPOINT}`; + + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith(url, { + method: 'post', + body: JSON.stringify(records), + headers: { 'Content-Type': 'application/json' }, + agent: expect.any(https.Agent), + }); + expect(response).toBe(mockResponse); + }); + + it('should use an agent with TLS configuration if config.enabled is true', async () => { + const usageRecord = generateUsageRecord(); + const records: UsageRecord[] = [usageRecord]; + const mockResponse = new Response(null, { status: 200 }); + (fetch as jest.MockedFunction).mockResolvedValueOnce(mockResponse); + + const response = await service.reportUsage(records); + const url = `${DEFAULT_CONFIG.url}${USAGE_REPORTING_ENDPOINT}`; + + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith(url, { + method: 'post', + body: JSON.stringify(records), + headers: { 'Content-Type': 'application/json' }, + agent: expect.objectContaining({ + options: expect.objectContaining({ + cert: expect.any(String), + key: expect.any(String), + ca: expect.arrayContaining([expect.any(String)]), + }), + }), + }); + expect(response).toBe(mockResponse); + }); + }); +}); diff --git a/x-pack/plugins/security_solution_serverless/server/common/services/usage_reporting_service.ts b/x-pack/plugins/security_solution_serverless/server/common/services/usage_reporting_service.ts index 0e47b982e692e..ee402872ef33a 100644 --- a/x-pack/plugins/security_solution_serverless/server/common/services/usage_reporting_service.ts +++ b/x-pack/plugins/security_solution_serverless/server/common/services/usage_reporting_service.ts @@ -5,29 +5,77 @@ * 2.0. */ -import type { Response } from 'node-fetch'; +import type { RequestInit, Response } from 'node-fetch'; + import fetch from 'node-fetch'; import https from 'https'; -import { USAGE_SERVICE_USAGE_URL } from '../../constants'; +import { SslConfig, sslSchema } from '@kbn/server-http-tools'; + import type { UsageRecord } from '../../types'; +import type { UsageApiConfigSchema, TlsConfigSchema } from '../../config'; + +import { USAGE_REPORTING_ENDPOINT, USAGE_SERVICE_USAGE_URL } from '../../constants'; -// TODO remove once we have the CA available -const agent = new https.Agent({ rejectUnauthorized: false }); export class UsageReportingService { - public async reportUsage( - records: UsageRecord[], - url = USAGE_SERVICE_USAGE_URL - ): Promise { - const isHttps = url.includes('https'); + private agent: https.Agent | undefined; - return fetch(url, { + constructor(private readonly config: UsageApiConfigSchema) {} + + public async reportUsage(records: UsageRecord[]): Promise { + const reqArgs: RequestInit = { method: 'post', body: JSON.stringify(records), headers: { 'Content-Type': 'application/json' }, - agent: isHttps ? agent : undefined, // Conditionally add agent if URL is HTTPS for supporting integration tests. + }; + if (this.usageApiUrl.includes('https')) { + reqArgs.agent = this.httpAgent; + } + return fetch(this.usageApiUrl, reqArgs); + } + + private get tlsConfigs(): NonNullable { + if (!this.config.tls) { + throw new Error('UsageReportingService: usageApi.tls configs not provided'); + } + + return this.config.tls; + } + + private get usageApiUrl(): string { + if (!this.config.url) { + return USAGE_SERVICE_USAGE_URL; + } + + return `${this.config.url}${USAGE_REPORTING_ENDPOINT}`; + } + + private get httpAgent(): https.Agent { + if (this.agent) { + return this.agent; + } + + if (!this.config.enabled) { + this.agent = new https.Agent({ rejectUnauthorized: false }); + return this.agent; + } + + const tlsConfig = new SslConfig( + sslSchema.validate({ + enabled: true, + certificate: this.tlsConfigs.certificate, + key: this.tlsConfigs.key, + certificateAuthorities: this.tlsConfigs.ca, + }) + ); + + this.agent = new https.Agent({ + rejectUnauthorized: tlsConfig.rejectUnauthorized, + cert: tlsConfig.certificate, + key: tlsConfig.key, + ca: tlsConfig.certificateAuthorities, }); + + return this.agent; } } - -export const usageReportingService = new UsageReportingService(); diff --git a/x-pack/plugins/security_solution_serverless/server/config.ts b/x-pack/plugins/security_solution_serverless/server/config.ts index 96e743a59b425..d4bafd9b9ddb9 100644 --- a/x-pack/plugins/security_solution_serverless/server/config.ts +++ b/x-pack/plugins/security_solution_serverless/server/config.ts @@ -16,19 +16,19 @@ import type { ExperimentalFeatures } from '../common/experimental_features'; import { productTypes } from '../common/config'; import { parseExperimentalConfigValue } from '../common/experimental_features'; -const usageApiConfig = schema.maybe( - schema.object({ - enabled: schema.maybe(schema.boolean()), - url: schema.string(), - tls: schema.maybe( - schema.object({ - certificate: schema.string(), - key: schema.string(), - ca: schema.string(), - }) - ), - }) -); +const tlsConfig = schema.object({ + certificate: schema.string(), + key: schema.string(), + ca: schema.string(), +}); +export type TlsConfigSchema = TypeOf; + +const usageApiConfig = schema.object({ + enabled: schema.boolean({ defaultValue: false }), + url: schema.maybe(schema.string()), + tls: schema.maybe(tlsConfig), +}); +export type UsageApiConfigSchema = TypeOf; export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: false }), diff --git a/x-pack/plugins/security_solution_serverless/server/constants.ts b/x-pack/plugins/security_solution_serverless/server/constants.ts index f4fcad6b760c6..411a7209682de 100644 --- a/x-pack/plugins/security_solution_serverless/server/constants.ts +++ b/x-pack/plugins/security_solution_serverless/server/constants.ts @@ -9,4 +9,5 @@ const namespace = 'elastic-system'; const USAGE_SERVICE_BASE_API_URL = `https://usage-api.${namespace}/api`; const USAGE_SERVICE_BASE_API_URL_V1 = `${USAGE_SERVICE_BASE_API_URL}/v1`; export const USAGE_SERVICE_USAGE_URL = `${USAGE_SERVICE_BASE_API_URL_V1}/usage`; +export const USAGE_REPORTING_ENDPOINT = '/api/v1/usage'; export const METERING_SERVICE_BATCH_SIZE = 1000; diff --git a/x-pack/plugins/security_solution_serverless/server/plugin.ts b/x-pack/plugins/security_solution_serverless/server/plugin.ts index 7161c5b684505..c249e48ca13a0 100644 --- a/x-pack/plugins/security_solution_serverless/server/plugin.ts +++ b/x-pack/plugins/security_solution_serverless/server/plugin.ts @@ -34,6 +34,7 @@ import { } from './endpoint/services'; import { NLPCleanupTask } from './task_manager/nlp_cleanup_task/nlp_cleanup_task'; import { telemetryEvents } from './telemetry/event_based_telemetry'; +import { UsageReportingService } from './common/services/usage_reporting_service'; export class SecuritySolutionServerlessPlugin implements @@ -49,11 +50,14 @@ export class SecuritySolutionServerlessPlugin private endpointUsageReportingTask: SecurityUsageReportingTask | undefined; private nlpCleanupTask: NLPCleanupTask | undefined; private readonly logger: Logger; + private readonly usageReportingService: UsageReportingService; constructor(private readonly initializerContext: PluginInitializerContext) { this.config = this.initializerContext.config.get(); this.logger = this.initializerContext.logger.get(); + this.usageReportingService = new UsageReportingService(this.config.usageApi); + const productTypesStr = JSON.stringify(this.config.productTypes, null, 2); this.logger.info(`Security Solution running with product types:\n${productTypesStr}`); } @@ -83,6 +87,7 @@ export class SecuritySolutionServerlessPlugin taskTitle: cloudSecurityMetringTaskProperties.taskTitle, version: cloudSecurityMetringTaskProperties.version, meteringCallback: cloudSecurityMetringTaskProperties.meteringCallback, + usageReportingService: this.usageReportingService, }); this.endpointUsageReportingTask = new SecurityUsageReportingTask({ @@ -95,6 +100,7 @@ export class SecuritySolutionServerlessPlugin meteringCallback: endpointMeteringService.getUsageRecords, taskManager: pluginsSetup.taskManager, cloudSetup: pluginsSetup.cloud, + usageReportingService: this.usageReportingService, }); this.nlpCleanupTask = new NLPCleanupTask({ diff --git a/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.test.ts b/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.test.ts index 66307e8f8a693..01c38ed6eed31 100644 --- a/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.test.ts +++ b/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.test.ts @@ -7,28 +7,26 @@ import { assign } from 'lodash'; +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import type { CoreSetup, ElasticsearchClient } from '@kbn/core/server'; import type { TaskManagerSetupContract, ConcreteTaskInstance, } from '@kbn/task-manager-plugin/server'; import type { CloudSetup } from '@kbn/cloud-plugin/server'; + import { TaskStatus } from '@kbn/task-manager-plugin/server'; import { getDeleteTaskRunResult } from '@kbn/task-manager-plugin/server/task'; import { coreMock } from '@kbn/core/server/mocks'; import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; -import { ProductLine, ProductTier } from '../../common/product'; - -import { usageReportingService } from '../common/services'; import type { ServerlessSecurityConfig } from '../config'; import type { SecurityUsageReportingTaskSetupContract, UsageRecord } from '../types'; +import { ProductLine, ProductTier } from '../../common/product'; import { SecurityUsageReportingTask } from './usage_reporting_task'; import { endpointMeteringService } from '../endpoint/services'; -import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; -import { USAGE_SERVICE_USAGE_URL } from '../constants'; describe('SecurityUsageReportingTask', () => { const TITLE = 'test-task-title'; @@ -45,7 +43,7 @@ describe('SecurityUsageReportingTask', () => { let mockEsClient: jest.Mocked; let mockCore: CoreSetup; let mockTaskManagerSetup: jest.Mocked; - let reportUsageSpy: jest.SpyInstance; + let reportUsageMock: jest.Mock; let meteringCallbackMock: jest.Mock; let taskArgs: SecurityUsageReportingTaskSetupContract; let usageRecord: UsageRecord; @@ -118,11 +116,24 @@ describe('SecurityUsageReportingTask', () => { taskTitle: TITLE, version: VERSION, meteringCallback: meteringCallbackMock, + usageReportingService: { + reportUsage: reportUsageMock, + }, }, overrides ); } + const USAGE_API_CONFIG = { + enabled: true, + url: 'https://usage-api-url', + tls: { + certificate: '', + key: '', + ca: '', + }, + }; + async function runTask(taskInstance = buildMockTaskInstance(), callNum: number = 0) { const mockTaskManagerStart = tmStartMock(); await mockTask.start({ taskManager: mockTaskManagerStart, interval: '5m' }); @@ -138,7 +149,7 @@ describe('SecurityUsageReportingTask', () => { .asInternalUser as jest.Mocked; mockTaskManagerSetup = tmSetupMock(); usageRecord = buildUsageRecord(); - reportUsageSpy = jest.spyOn(usageReportingService, 'reportUsage'); + reportUsageMock = jest.fn(); } describe('meteringCallback integration', () => { @@ -150,7 +161,7 @@ describe('SecurityUsageReportingTask', () => { productTypes: [ { product_line: ProductLine.endpoint, product_tier: ProductTier.complete }, ], - usageApi: { url: USAGE_SERVICE_USAGE_URL }, + usageApi: USAGE_API_CONFIG, } as ServerlessSecurityConfig, }); mockTask = new SecurityUsageReportingTask(taskArgs); @@ -199,9 +210,9 @@ describe('SecurityUsageReportingTask', () => { await runTasksUntilNoRunAt(); - expect(reportUsageSpy).toHaveBeenCalledTimes(3); + expect(reportUsageMock).toHaveBeenCalledTimes(3); batches.forEach((batch, i) => { - expect(reportUsageSpy).toHaveBeenNthCalledWith( + expect(reportUsageMock).toHaveBeenNthCalledWith( i + 1, expect.arrayContaining( batch.map(({ _source }) => @@ -209,8 +220,7 @@ describe('SecurityUsageReportingTask', () => { id: `endpoint-${_source.agent.id}-2021-09-01T00:00:00.000Z`, }) ) - ), - USAGE_SERVICE_USAGE_URL + ) ); }); }); @@ -227,7 +237,7 @@ describe('SecurityUsageReportingTask', () => { }); taskArgs = buildTaskArgs({ config: { - usageApi: { url: USAGE_SERVICE_USAGE_URL }, + usageApi: USAGE_API_CONFIG, } as ServerlessSecurityConfig, }); mockTask = new SecurityUsageReportingTask(taskArgs); @@ -273,7 +283,7 @@ describe('SecurityUsageReportingTask', () => { it('should report metering records', async () => { await runTask(); - expect(reportUsageSpy).toHaveBeenCalledWith( + expect(reportUsageMock).toHaveBeenCalledWith( expect.arrayContaining([ expect.objectContaining({ creation_timestamp: usageRecord.creation_timestamp, @@ -286,8 +296,7 @@ describe('SecurityUsageReportingTask', () => { usage: { period_seconds: 3600, quantity: 1, type: USAGE_TYPE }, usage_timestamp: usageRecord.usage_timestamp, }), - ]), - USAGE_SERVICE_USAGE_URL + ]) ); }); @@ -296,12 +305,12 @@ describe('SecurityUsageReportingTask', () => { expect(result).toEqual(getDeleteTaskRunResult()); - expect(reportUsageSpy).not.toHaveBeenCalled(); + expect(reportUsageMock).not.toHaveBeenCalled(); expect(meteringCallbackMock).not.toHaveBeenCalled(); }); describe('lastSuccessfulReport', () => { it('should set lastSuccessfulReport correctly if report success', async () => { - reportUsageSpy.mockResolvedValueOnce({ status: 201 }); + reportUsageMock.mockResolvedValueOnce({ status: 201 }); const taskInstance = buildMockTaskInstance(); const task = await runTask(taskInstance); const newLastSuccessfulReport = task?.state.lastSuccessfulReport; @@ -320,7 +329,7 @@ describe('SecurityUsageReportingTask', () => { describe('and response is NOT 201', () => { beforeEach(() => { - reportUsageSpy.mockResolvedValueOnce({ status: 500 }); + reportUsageMock.mockResolvedValueOnce({ status: 500 }); }); it('should set lastSuccessfulReport correctly', async () => { diff --git a/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.ts b/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.ts index 83ef25a849f2d..6eb682a84d474 100644 --- a/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.ts +++ b/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.ts @@ -8,10 +8,10 @@ import type { Response } from 'node-fetch'; import type { CoreSetup, Logger } from '@kbn/core/server'; import type { ConcreteTaskInstance } from '@kbn/task-manager-plugin/server'; -import { getDeleteTaskRunResult } from '@kbn/task-manager-plugin/server/task'; import type { CloudSetup } from '@kbn/cloud-plugin/server'; -import { usageReportingService } from '../common/services'; +import { getDeleteTaskRunResult } from '@kbn/task-manager-plugin/server/task'; + import type { MeteringCallback, SecurityUsageReportingTaskStartContract, @@ -19,6 +19,7 @@ import type { UsageRecord, } from '../types'; import type { ServerlessSecurityConfig } from '../config'; +import type { UsageReportingService } from '../common/services/usage_reporting_service'; import { stateSchemaByVersion, emptyState } from './task_state'; @@ -34,6 +35,7 @@ export class SecurityUsageReportingTask { private readonly version: string; private readonly logger: Logger; private readonly config: ServerlessSecurityConfig; + private readonly usageReportingService: UsageReportingService; constructor(setupContract: SecurityUsageReportingTaskSetupContract) { const { @@ -46,6 +48,7 @@ export class SecurityUsageReportingTask { taskTitle, version, meteringCallback, + usageReportingService, } = setupContract; this.cloudSetup = cloudSetup; @@ -53,6 +56,7 @@ export class SecurityUsageReportingTask { this.version = version; this.logger = logFactory.get(this.taskId); this.config = config; + this.usageReportingService = usageReportingService; try { taskManager.registerTaskDefinitions({ @@ -163,10 +167,7 @@ export class SecurityUsageReportingTask { try { this.logger.debug(`Sending ${usageRecords.length} usage records to the API`); - usageReportResponse = await usageReportingService.reportUsage( - usageRecords, - this.config.usageApi?.url - ); + usageReportResponse = await this.usageReportingService.reportUsage(usageRecords); if (!usageReportResponse.ok) { const errorResponse = await usageReportResponse.json(); diff --git a/x-pack/plugins/security_solution_serverless/server/types.ts b/x-pack/plugins/security_solution_serverless/server/types.ts index 4f3a7bf3c3db0..a838c410793c3 100644 --- a/x-pack/plugins/security_solution_serverless/server/types.ts +++ b/x-pack/plugins/security_solution_serverless/server/types.ts @@ -25,6 +25,7 @@ import type { IntegrationAssistantPluginSetup } from '@kbn/integration-assistant import type { ProductTier } from '../common/product'; import type { ServerlessSecurityConfig } from './config'; +import type { UsageReportingService } from './common/services/usage_reporting_service'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SecuritySolutionServerlessPluginSetup {} @@ -86,6 +87,7 @@ export interface SecurityUsageReportingTaskSetupContract { taskTitle: string; version: string; meteringCallback: MeteringCallback; + usageReportingService: UsageReportingService; } export interface SecurityUsageReportingTaskStartContract { diff --git a/x-pack/plugins/security_solution_serverless/tsconfig.json b/x-pack/plugins/security_solution_serverless/tsconfig.json index 55a4882655dc7..cb0518fc4dcd5 100644 --- a/x-pack/plugins/security_solution_serverless/tsconfig.json +++ b/x-pack/plugins/security_solution_serverless/tsconfig.json @@ -19,6 +19,7 @@ "@kbn/security-plugin", "@kbn/security-solution-ess", "@kbn/security-solution-plugin", + "@kbn/server-http-tools", "@kbn/serverless", "@kbn/security-solution-navigation", "@kbn/security-solution-upselling", @@ -46,5 +47,6 @@ "@kbn/logging", "@kbn/integration-assistant-plugin", "@kbn/cloud-security-posture-common", + "@kbn/dev-utils" ] } diff --git a/x-pack/test_serverless/api_integration/test_suites/security/config.ts b/x-pack/test_serverless/api_integration/test_suites/security/config.ts index d40cde3c25837..0b24438b81591 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/config.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/config.ts @@ -24,6 +24,6 @@ export default createTestConfig({ // useful for testing (also enabled in MKI QA) '--coreApp.allowDynamicConfigOverrides=true', `--xpack.securitySolutionServerless.cloudSecurityUsageReportingTaskInterval=5s`, - `--xpack.securitySolutionServerless.usageApi.url=http://localhost:8081/api/v1/usage`, + `--xpack.securitySolutionServerless.usageApi.url=http://localhost:8081`, ], }); From 3493be490b10b1510101ce7723ef8ee44e618853 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 11 Oct 2024 06:31:52 +0100 Subject: [PATCH 83/87] skip flaky suite (#194731) --- .../reporting_functional/reporting_and_security/management.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/test/reporting_functional/reporting_and_security/management.ts b/x-pack/test/reporting_functional/reporting_and_security/management.ts index 570c1bbdda4c7..b1a6c107b9bb7 100644 --- a/x-pack/test/reporting_functional/reporting_and_security/management.ts +++ b/x-pack/test/reporting_functional/reporting_and_security/management.ts @@ -57,6 +57,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { }); // FLAKY: https://github.com/elastic/kibana/issues/195144 + // FLAKY: https://github.com/elastic/kibana/issues/194731 describe.skip('Download report', () => { // use archived reports to allow reporting_user to view report jobs they've created before('log in as reporting user', async () => { From 5b697499978170937d8c0280b0cf184ee84b57ab Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 11 Oct 2024 09:19:21 +0200 Subject: [PATCH 84/87] [Fleet] Add cypress test against space awareness (#195372) --- .buildkite/ftr_platform_stateful_configs.yml | 2 + .../scripts/steps/functional/fleet_cypress.sh | 2 +- .../fleet/cypress.config.space_awareness.ts | 49 ++++++++++++ x-pack/plugins/fleet/cypress.config.ts | 1 + .../e2e/space_awareness/policies.cy.ts | 75 +++++++++++++++++++ x-pack/plugins/fleet/cypress/screens/fleet.ts | 5 ++ x-pack/plugins/fleet/cypress/tasks/cleanup.ts | 24 +++--- x-pack/plugins/fleet/cypress/tasks/common.ts | 17 +++++ x-pack/plugins/fleet/cypress/tasks/spaces.ts | 38 ++++++++++ x-pack/plugins/fleet/cypress/tsconfig.json | 1 + x-pack/plugins/fleet/package.json | 4 + .../components/settings/index.tsx | 1 + .../agent_policy/details_page/index.tsx | 1 + .../sections/agent_policy/list_page/index.tsx | 1 + x-pack/plugins/fleet/tsconfig.json | 2 + .../scripts/run_cypress/parallel.ts | 11 ++- .../cli_config.space_awareness.ts | 19 +++++ .../fleet_cypress/config.space_awareness.ts | 62 +++++++++++++++ 18 files changed, 302 insertions(+), 13 deletions(-) create mode 100644 x-pack/plugins/fleet/cypress.config.space_awareness.ts create mode 100644 x-pack/plugins/fleet/cypress/e2e/space_awareness/policies.cy.ts create mode 100644 x-pack/plugins/fleet/cypress/tasks/spaces.ts create mode 100644 x-pack/test/fleet_cypress/cli_config.space_awareness.ts create mode 100644 x-pack/test/fleet_cypress/config.space_awareness.ts diff --git a/.buildkite/ftr_platform_stateful_configs.yml b/.buildkite/ftr_platform_stateful_configs.yml index d8a538f82779e..092cdb12a19f6 100644 --- a/.buildkite/ftr_platform_stateful_configs.yml +++ b/.buildkite/ftr_platform_stateful_configs.yml @@ -34,7 +34,9 @@ disabled: # Cypress configs, for now these are still run manually - x-pack/test/fleet_cypress/cli_config.ts + - x-pack/test/fleet_cypress/cli_config.space_awareness.ts - x-pack/test/fleet_cypress/config.ts + - x-pack/test/fleet_cypress/config.space_awareness.ts - x-pack/test/fleet_cypress/visual_config.ts defaultQueue: 'n2-4-spot' diff --git a/.buildkite/scripts/steps/functional/fleet_cypress.sh b/.buildkite/scripts/steps/functional/fleet_cypress.sh index 43eb329f860b7..e050b73a91c3e 100755 --- a/.buildkite/scripts/steps/functional/fleet_cypress.sh +++ b/.buildkite/scripts/steps/functional/fleet_cypress.sh @@ -12,4 +12,4 @@ echo "--- Fleet Cypress tests (Chrome)" cd x-pack/plugins/fleet set +e -yarn cypress:run:reporter; status=$?; yarn junit:merge || :; exit $status +yarn cypress:run:reporter; status=$?; yarn cypress_space_awareness:run:reporter; space_status=$?; yarn junit:merge || :; [ "$status" -ne 0 ] && exit $status || [ "$space_status" -ne 0 ] && exit $space_status || exit 0 diff --git a/x-pack/plugins/fleet/cypress.config.space_awareness.ts b/x-pack/plugins/fleet/cypress.config.space_awareness.ts new file mode 100644 index 0000000000000..6efda828e6fbc --- /dev/null +++ b/x-pack/plugins/fleet/cypress.config.space_awareness.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { defineCypressConfig } from '@kbn/cypress-config'; + +// eslint-disable-next-line import/no-default-export +export default defineCypressConfig({ + defaultCommandTimeout: 60000, + requestTimeout: 60000, + responseTimeout: 60000, + execTimeout: 120000, + pageLoadTimeout: 120000, + + retries: { + runMode: 2, + }, + + env: { + grepFilterSpecs: false, + }, + + screenshotsFolder: '../../../target/kibana-fleet/cypress/screenshots', + trashAssetsBeforeRuns: false, + video: false, + videosFolder: '../../../target/kibana-fleet/cypress/videos', + viewportHeight: 900, + viewportWidth: 1440, + screenshotOnRunFailure: true, + + e2e: { + baseUrl: 'http://localhost:5601', + + experimentalRunAllSpecs: true, + experimentalMemoryManagement: true, + numTestsKeptInMemory: 3, + + specPattern: './cypress/e2e/space_awareness/**/*.cy.ts', + supportFile: './cypress/support/e2e.ts', + + setupNodeEvents(on, config) { + // eslint-disable-next-line @typescript-eslint/no-var-requires, @kbn/imports/no_boundary_crossing + return require('./cypress/plugins')(on, config); + }, + }, +}); diff --git a/x-pack/plugins/fleet/cypress.config.ts b/x-pack/plugins/fleet/cypress.config.ts index e4a5ad96938d6..2082142e23d7f 100644 --- a/x-pack/plugins/fleet/cypress.config.ts +++ b/x-pack/plugins/fleet/cypress.config.ts @@ -39,6 +39,7 @@ export default defineCypressConfig({ specPattern: './cypress/e2e/**/*.cy.ts', supportFile: './cypress/support/e2e.ts', + excludeSpecPattern: './cypress/e2e/space_awareness/**/*.cy.ts', setupNodeEvents(on, config) { // eslint-disable-next-line @typescript-eslint/no-var-requires, @kbn/imports/no_boundary_crossing diff --git a/x-pack/plugins/fleet/cypress/e2e/space_awareness/policies.cy.ts b/x-pack/plugins/fleet/cypress/e2e/space_awareness/policies.cy.ts new file mode 100644 index 0000000000000..8975de388248c --- /dev/null +++ b/x-pack/plugins/fleet/cypress/e2e/space_awareness/policies.cy.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor 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 { + ADD_AGENT_POLICY_BTN, + AGENT_POLICIES_TABLE, + AGENT_POLICY_CREATE_AGENT_POLICY_NAME_FIELD, + AGENT_POLICY_DETAILS_PAGE, + AGENT_POLICY_FLYOUT_CREATE_BUTTON, + AGENT_POLICY_SYSTEM_MONITORING_CHECKBOX, +} from '../../screens/fleet'; +import { login } from '../../tasks/login'; +import { createSpaces, enableSpaceAwareness } from '../../tasks/spaces'; +import { cleanupAgentPolicies } from '../../tasks/cleanup'; + +describe('Space aware policies creation', { testIsolation: false }, () => { + before(() => { + enableSpaceAwareness(); + createSpaces(); + cleanupAgentPolicies(); + cleanupAgentPolicies('test'); + login(); + }); + + beforeEach(() => { + cy.intercept('GET', /\/api\/fleet\/agent_policies/).as('getAgentPolicies'); + cy.intercept('GET', /\/internal\/fleet\/agent_policies_spaces/).as('getAgentPoliciesSpaces'); + }); + + const POLICY_NAME = `Policy 1 space test`; + const NO_AGENT_POLICIES = 'No agent policies'; + it('should allow to create an agent policy in the test space', () => { + cy.visit('/s/test/app/fleet/policies'); + + cy.getBySel(ADD_AGENT_POLICY_BTN).click(); + cy.getBySel(AGENT_POLICY_CREATE_AGENT_POLICY_NAME_FIELD).type(POLICY_NAME); + cy.getBySel(AGENT_POLICY_SYSTEM_MONITORING_CHECKBOX).uncheck(); + + cy.getBySel(AGENT_POLICY_FLYOUT_CREATE_BUTTON).click(); + cy.getBySel(AGENT_POLICIES_TABLE).contains(POLICY_NAME); + }); + + it('the created policy should not be visible in the default space', () => { + cy.visit('/app/fleet/policies'); + cy.wait('@getAgentPolicies'); + cy.getBySel(AGENT_POLICIES_TABLE).contains(NO_AGENT_POLICIES); + }); + + it('should allow to update that policy to belong to both test and default space', () => { + cy.visit('/s/test/app/fleet/policies'); + cy.getBySel(AGENT_POLICIES_TABLE).contains(POLICY_NAME).click(); + + cy.getBySel(AGENT_POLICY_DETAILS_PAGE.SETTINGS_TAB).click(); + cy.wait('@getAgentPoliciesSpaces'); + cy.getBySel(AGENT_POLICY_DETAILS_PAGE.SPACE_SELECTOR_COMBOBOX).click().type('default{enter}'); + + cy.getBySel(AGENT_POLICY_DETAILS_PAGE.SAVE_BUTTON).click(); + }); + + it('the policy should be visible in the test space', () => { + cy.visit('/s/test/app/fleet/policies'); + cy.wait('@getAgentPolicies'); + cy.getBySel(AGENT_POLICIES_TABLE).contains(POLICY_NAME); + }); + + it('the policy should be visible in the default space', () => { + cy.visit('/app/fleet/policies'); + cy.wait('@getAgentPolicies'); + cy.getBySel(AGENT_POLICIES_TABLE).contains(POLICY_NAME); + }); +}); diff --git a/x-pack/plugins/fleet/cypress/screens/fleet.ts b/x-pack/plugins/fleet/cypress/screens/fleet.ts index 4e1a0ac0f7e19..0bd449652a800 100644 --- a/x-pack/plugins/fleet/cypress/screens/fleet.ts +++ b/x-pack/plugins/fleet/cypress/screens/fleet.ts @@ -46,6 +46,8 @@ export const AGENT_POLICY_CREATE_AGENT_POLICY_NAME_FIELD = 'createAgentPolicyNam export const AGENT_POLICIES_FLYOUT_ADVANCED_DEFAULT_NAMESPACE_HEADER = 'defaultNamespaceHeader'; export const AGENT_POLICY_FLYOUT_CREATE_BUTTON = 'createAgentPolicyFlyoutBtn'; +export const AGENT_POLICIES_TABLE = 'agentPoliciesTable'; + export const ENROLLMENT_TOKENS = { CREATE_TOKEN_BUTTON: 'createEnrollmentTokenButton', CREATE_TOKEN_MODAL_NAME_FIELD: 'createEnrollmentTokenNameField', @@ -241,4 +243,7 @@ export const API_KEYS = { export const AGENT_POLICY_DETAILS_PAGE = { ADD_AGENT_LINK: 'addAgentLink', + SETTINGS_TAB: 'agentPolicySettingsTab', + SPACE_SELECTOR_COMBOBOX: 'spaceSelectorComboBox', + SAVE_BUTTON: 'agentPolicyDetailsSaveButton', }; diff --git a/x-pack/plugins/fleet/cypress/tasks/cleanup.ts b/x-pack/plugins/fleet/cypress/tasks/cleanup.ts index 5e179bc9207f1..bad8743b66bd1 100644 --- a/x-pack/plugins/fleet/cypress/tasks/cleanup.ts +++ b/x-pack/plugins/fleet/cypress/tasks/cleanup.ts @@ -7,18 +7,20 @@ import { request } from './common'; -export function cleanupAgentPolicies() { - request({ url: '/api/fleet/agent_policies' }).then((response: any) => { - response.body.items - .filter((policy: any) => policy.agents === 0) - .forEach((policy: any) => { - request({ - method: 'POST', - url: '/api/fleet/agent_policies/delete', - body: { agentPolicyId: policy.id }, +export function cleanupAgentPolicies(spaceId?: string) { + request({ url: `${spaceId ? `/s/${spaceId}` : ''}/api/fleet/agent_policies` }).then( + (response: any) => { + response.body.items + .filter((policy: any) => policy.agents === 0) + .forEach((policy: any) => { + request({ + method: 'POST', + url: `${spaceId ? `/s/${spaceId}` : ''}/api/fleet/agent_policies/delete`, + body: { agentPolicyId: policy.id }, + }); }); - }); - }); + } + ); } export function unenrollAgent() { diff --git a/x-pack/plugins/fleet/cypress/tasks/common.ts b/x-pack/plugins/fleet/cypress/tasks/common.ts index 250dea07f89d1..de6e117bac4cc 100644 --- a/x-pack/plugins/fleet/cypress/tasks/common.ts +++ b/x-pack/plugins/fleet/cypress/tasks/common.ts @@ -28,6 +28,12 @@ export const COMMON_API_HEADERS = Object.freeze({ 'Elastic-Api-Version': API_VERSIONS.public.v1, }); +export const COMMON_INTERNAL_API_HEADERS = Object.freeze({ + 'kbn-xsrf': 'cypress', + 'x-elastic-internal-origin': 'fleet', + 'Elastic-Api-Version': API_VERSIONS.internal.v1, +}); + // Replaces request - adds baseline authentication + global headers export const request = ({ headers, @@ -40,6 +46,17 @@ export const request = ({ }); }; +export const internalRequest = ({ + headers, + ...options +}: Partial): Cypress.Chainable> => { + return cy.request({ + auth: API_AUTH, + headers: { ...COMMON_INTERNAL_API_HEADERS, ...headers }, + ...options, + }); +}; + /** * For all the new features tours we show in the app, this method disables them * by setting their configs in the local storage. It prevents the tours from appearing diff --git a/x-pack/plugins/fleet/cypress/tasks/spaces.ts b/x-pack/plugins/fleet/cypress/tasks/spaces.ts new file mode 100644 index 0000000000000..31c1d12a968cb --- /dev/null +++ b/x-pack/plugins/fleet/cypress/tasks/spaces.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { request, internalRequest } from './common'; + +export function enableSpaceAwareness() { + return internalRequest({ + url: '/internal/fleet/enable_space_awareness', + failOnStatusCode: false, + method: 'POST', + }); +} + +export function createSpaces() { + return request({ + url: '/api/spaces/space', + failOnStatusCode: false, + method: 'POST', + body: { + id: 'test', + name: 'Test', + description: 'Test space', + color: '#aabbcc', + initials: 'TE', + disabledFeatures: [], + imageUrl: + 'data:image/png;base64,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', + }, + }).then((response: any) => { + if (response.status !== 200 && response.status !== 409) { + throw new Error(`Failed to create space test`); + } + }); +} diff --git a/x-pack/plugins/fleet/cypress/tsconfig.json b/x-pack/plugins/fleet/cypress/tsconfig.json index 6d1433482b1c2..5427996c27f17 100644 --- a/x-pack/plugins/fleet/cypress/tsconfig.json +++ b/x-pack/plugins/fleet/cypress/tsconfig.json @@ -3,6 +3,7 @@ "include": [ "**/*", "../cypress.config.ts", + "../cypress.config.space_awareness.ts", "../../../../typings/**/*" ], "exclude": [ diff --git a/x-pack/plugins/fleet/package.json b/x-pack/plugins/fleet/package.json index 3e20162ab1d91..dc0bc6a6bcacb 100644 --- a/x-pack/plugins/fleet/package.json +++ b/x-pack/plugins/fleet/package.json @@ -5,6 +5,10 @@ "private": true, "license": "Elastic License 2.0", "scripts": { + "cypress_space_awareness": "NODE_OPTIONS=--openssl-legacy-provider node ../security_solution/scripts/start_cypress_parallel --config-file ../fleet/cypress.config.space_awareness.ts --ftr-config-file ../../../x-pack/test/fleet_cypress/cli_config.space_awareness", + "cypress_space_awareness:open": "yarn cypress_space_awareness open", + "cypress_space_awareness:run": "yarn cypress_space_awareness run", + "cypress_space_awareness:run:reporter": "yarn cypress_space_awareness run --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=../fleet/cypress/reporter_config.json", "cypress": "NODE_OPTIONS=--openssl-legacy-provider node ../security_solution/scripts/start_cypress_parallel --config-file ../fleet/cypress.config.ts --ftr-config-file ../../../x-pack/test/fleet_cypress/cli_config", "cypress:open": "yarn cypress open", "cypress:run": "yarn cypress run", diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx index 14dcf6df21b9f..6e4f1e06b45a0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx @@ -244,6 +244,7 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>( Object.keys(validation).length > 0 || hasAdvancedSettingsErrors } + data-test-subj="agentPolicyDetailsSaveButton" iconType="save" color="primary" fill diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx index 0643ac82634d9..7f4c1a3b91ead 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx @@ -90,6 +90,7 @@ export const AgentPolicyDetailsPage: React.FunctionComponent = () => { name: i18n.translate('xpack.fleet.policyDetails.subTabs.settingsTabText', { defaultMessage: 'Settings', }), + 'data-test-subj': 'agentPolicySettingsTab', href: getHref('policy_details', { policyId, tabId: 'settings' }), isSelected: tabId === 'settings', }, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/index.tsx index dcbd29c1ef74e..2682a5239071d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/index.tsx @@ -356,6 +356,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => { loading={isLoading} + data-test-subj="agentPoliciesTable" noItemsMessage={ isLoading ? ( Date: Fri, 11 Oct 2024 09:37:30 +0200 Subject: [PATCH 85/87] [ES|QL] Removes the feedback from the advanced setting (#195770) ## Summary Removes the feedback link from the advanced setting per request. image --- src/plugins/esql/server/ui_settings.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/plugins/esql/server/ui_settings.ts b/src/plugins/esql/server/ui_settings.ts index 39be07971769b..1ddae41c9b241 100644 --- a/src/plugins/esql/server/ui_settings.ts +++ b/src/plugins/esql/server/ui_settings.ts @@ -21,15 +21,7 @@ export const getUiSettings: () => Record = () => ({ value: true, description: i18n.translate('esql.advancedSettings.enableESQLDescription', { defaultMessage: - 'This setting enables ES|QL in Kibana. By switching it off you will hide the ES|QL user interface from various applications. However, users will be able to access existing ES|QL saved searches, visualizations, etc. If you have feedback on this experience please reach out to us on {link}', - values: { - link: - `
    ` + - i18n.translate('esql.advancedSettings.enableESQL.discussLinkText', { - defaultMessage: 'https://ela.st/esql-feedback', - }) + - '', - }, + 'This setting enables ES|QL in Kibana. By switching it off you will hide the ES|QL user interface from various applications. However, users will be able to access existing ES|QL saved searches, visualizations, etc.', }), requiresPageReload: true, schema: schema.boolean(), From a2d6e102d37217c58bf771a17c77976575e87a84 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Fri, 11 Oct 2024 10:49:12 +0300 Subject: [PATCH 86/87] chore(security, eslint): apply platform security-specific rules to all exclusively owned paths. (#195711) ## Summary Apply platform security-specific rules to all exclusively owned paths. --- .eslintrc.js | 14 +++++++++++++ .../kbn-mock-idp-plugin/public/login_page.tsx | 19 ++++++++++-------- .../kbn-mock-idp-plugin/public/plugin.tsx | 10 ++++++---- .../public/reload_page_toast.tsx | 6 +++--- .../public/role_switcher.tsx | 10 ++++++---- packages/kbn-mock-idp-plugin/server/plugin.ts | 11 +++++----- packages/kbn-mock-idp-utils/src/utils.ts | 20 +++++++++---------- .../hooks/use_update_user_profile.test.tsx | 2 +- .../src/hooks/use_update_user_profile.tsx | 7 ++++--- .../src/services.tsx | 5 +++-- .../kbn-user-profile-components/src/types.ts | 2 +- .../src/user_avatar.tsx | 2 +- .../src/user_profile.ts | 1 + .../src/user_profiles_popover.test.tsx | 2 +- .../src/user_profiles_popover.tsx | 7 ++++--- .../src/user_profiles_selectable.test.tsx | 5 +++-- .../src/user_profiles_selectable.tsx | 6 +++--- .../src/user_tooltip.tsx | 8 ++++---- .../tests/manual_configuration.ts | 1 + .../manual_configuration_without_security.ts | 1 + .../tests/manual_configuration_without_tls.ts | 1 + .../src/components/api_key_badge.tsx | 6 ++++-- .../components/api_key_created_callout.tsx | 7 +++++-- .../src/components/api_key_flyout.tsx | 16 +++++++++------ .../src/components/api_key_status.tsx | 7 +++++-- .../src/components/api_keys_api_client.ts | 13 ++++++------ .../src/components/time_tool_tip.tsx | 2 +- .../src/privileges/privileges.ts | 2 +- .../authorization/authorization_service.ts | 2 +- .../plugin_types_public/src/plugin.ts | 1 + .../user_profile/user_profile_api_client.ts | 3 ++- .../src/audit/audit_service.ts | 1 - .../src/authentication/api_keys/api_keys.ts | 3 ++- .../authentication/authentication_service.ts | 2 +- .../check_privileges_dynamically.ts | 3 ++- .../check_saved_objects_privileges.ts | 1 + .../src/authorization/deprecations.ts | 1 - .../plugin_types_server/src/plugin.ts | 3 ++- .../src/__fixtures__/kibana_privileges.ts | 3 ++- .../src/kibana_privileges.test.ts | 5 +++-- .../src/kibana_privileges.ts | 4 ++-- .../feature_table.test.tsx | 2 +- .../kibana_privilege_table/feature_table.tsx | 2 +- .../feature_table_expanded_row.test.tsx | 2 +- .../privilege_form_calculator.test.ts | 2 +- .../server/routes/index.mock.ts | 2 +- .../server/routes/index.ts | 2 +- .../account_management_app.tsx | 3 ++- .../authentication_state_page.tsx | 3 ++- .../management/users/users_management_app.tsx | 3 ++- .../config.ts | 4 +++- .../ftr_provider_context.d.ts | 4 ++-- .../server/hidden_saved_object_routes.ts | 5 +++-- .../api_consumer_plugin/server/index.ts | 13 ++++++------ ...ncrypted_saved_objects_aad_include_list.ts | 3 ++- .../tests/encrypted_saved_objects_api.ts | 9 ++++----- .../encrypted_saved_objects_decryption.ts | 3 ++- .../tests/index.ts | 2 +- .../anonymous.config.ts | 3 ++- .../anonymous_es_anonymous.config.ts | 2 +- .../api_keys.config.ts | 3 ++- .../security_api_integration/audit.config.ts | 3 ++- .../security_api_integration/chips.config.ts | 3 ++- .../ftr_provider_context.d.ts | 5 +++-- .../http_bearer.config.ts | 4 +++- .../http_no_auth_providers.config.ts | 3 ++- .../kerberos.config.ts | 4 +++- .../kerberos_anonymous_access.config.ts | 2 +- .../login_selector.config.ts | 3 ++- .../security_api_integration/oidc.config.ts | 4 +++- .../oidc.http2.config.ts | 2 +- .../oidc_implicit_flow.config.ts | 2 +- .../packages/helpers/saml/saml_tools.ts | 5 +++-- .../security_api_integration/pki.config.ts | 4 +++- .../plugins/audit_log/server/plugin.ts | 2 +- .../plugins/oidc_provider/server/index.ts | 3 ++- .../plugins/saml_provider/server/index.ts | 5 +++-- .../saml_provider/server/init_routes.ts | 7 ++++--- .../user_profiles_consumer/server/index.ts | 9 +++++---- .../server/init_routes.ts | 5 +++-- .../security_api_integration/saml.config.ts | 4 +++- .../saml.http2.config.ts | 2 +- .../saml_cloud.config.ts | 3 ++- .../test/security_api_integration/services.ts | 2 +- .../session_concurrent_limit.config.ts | 4 +++- .../session_idle.config.ts | 4 +++- .../session_invalidate.config.ts | 4 +++- .../session_lifespan.config.ts | 4 +++- .../tests/anonymous/capabilities.ts | 2 +- .../tests/anonymous/index.ts | 2 +- .../tests/anonymous/login.ts | 9 ++++++--- .../tests/api_keys/grant_api_key.ts | 3 ++- .../tests/api_keys/has_active_key.ts | 1 + .../tests/api_keys/index.ts | 2 +- .../tests/audit/audit_log.ts | 4 +++- .../tests/audit/file_wrapper.ts | 1 + .../tests/audit/index.ts | 2 +- .../tests/chips/chips_cookie.ts | 6 ++++-- .../tests/chips/index.ts | 2 +- .../tests/http_bearer/access_token.ts | 3 ++- .../tests/http_bearer/index.ts | 2 +- .../tests/http_bearer/jwt.ts | 5 +++-- .../http_no_auth_providers/authentication.ts | 3 ++- .../tests/http_no_auth_providers/index.ts | 2 +- .../tests/kerberos/index.ts | 2 +- .../tests/kerberos/kerberos_login.ts | 13 +++++++----- .../login_selector/basic_functionality.ts | 11 ++++++---- .../tests/login_selector/index.ts | 2 +- .../oidc/authorization_code_flow/index.ts | 2 +- .../oidc/authorization_code_flow/oidc_auth.ts | 15 ++++++++------ .../tests/oidc/implicit_flow/index.ts | 2 +- .../tests/oidc/implicit_flow/oidc_auth.ts | 9 ++++++--- .../tests/pki/index.ts | 2 +- .../tests/pki/pki_auth.ts | 11 ++++++---- .../tests/saml/index.ts | 2 +- .../tests/saml/saml_login.ts | 13 +++++++----- .../tests/saml_cloud/index.ts | 2 +- .../tests/saml_cloud/saml_login.ts | 7 +++++-- .../tests/session_concurrent_limit/cleanup.ts | 17 +++++++++------- .../session_concurrent_limit/global_limit.ts | 11 ++++++---- .../tests/session_concurrent_limit/index.ts | 2 +- .../tests/session_idle/cleanup.ts | 11 ++++++---- .../tests/session_idle/expired.ts | 8 +++++--- .../tests/session_idle/extension.ts | 9 ++++++--- .../tests/session_idle/index.ts | 2 +- .../tests/session_invalidate/index.ts | 2 +- .../tests/session_invalidate/invalidate.ts | 11 ++++++---- .../tests/session_lifespan/cleanup.ts | 11 ++++++---- .../tests/session_lifespan/index.ts | 2 +- .../tests/token/audit.ts | 5 +++-- .../tests/token/header.ts | 2 +- .../tests/token/index.ts | 2 +- .../tests/token/login.ts | 3 ++- .../tests/token/logout.ts | 3 ++- .../tests/token/session.ts | 7 +++++-- .../tests/user_profiles/bulk_get.ts | 7 +++++-- .../tests/user_profiles/get_current.ts | 5 +++-- .../tests/user_profiles/index.ts | 2 +- .../tests/user_profiles/suggest.ts | 7 +++++-- .../security_api_integration/token.config.ts | 4 +++- .../user_profiles.config.ts | 4 +++- .../expired_session.config.ts | 6 ++++-- .../ftr_provider_context.d.ts | 6 +++--- .../insecure_cluster_warning.config.ts | 6 ++++-- .../login_selector.config.ts | 6 ++++-- .../test/security_functional/oidc.config.ts | 6 ++++-- .../security_functional/oidc.http2.config.ts | 2 +- .../plugins/test_endpoints/public/plugin.tsx | 10 +++++----- .../plugins/test_endpoints/server/index.ts | 7 ++++--- .../test_endpoints/server/init_routes.ts | 16 ++++++++------- .../test/security_functional/saml.config.ts | 6 ++++-- .../security_functional/saml.http2.config.ts | 3 ++- .../expired_session/basic_functionality.ts | 8 +++++--- .../tests/expired_session/index.ts | 2 +- .../tests/insecure_cluster_warning/index.ts | 2 +- .../insecure_cluster_warning.ts | 3 ++- .../login_selector/auth_provider_hint.ts | 6 ++++-- .../login_selector/basic_functionality.ts | 6 ++++-- .../tests/login_selector/index.ts | 2 +- .../login_selector/reset_session_page.ts | 3 ++- .../security_functional/tests/oidc/index.ts | 2 +- .../tests/oidc/url_capture.ts | 6 ++++-- .../security_functional/tests/saml/index.ts | 2 +- .../tests/saml/url_capture.ts | 6 ++++-- .../tests/user_profiles/client_side_apis.ts | 6 ++++-- .../tests/user_profiles/index.ts | 2 +- .../user_profiles.config.ts | 6 ++++-- .../spaces_api_integration/common/config.ts | 3 ++- .../common/ftr_provider_context.d.ts | 4 ++-- .../common/lib/create_users_and_roles.ts | 3 ++- .../common/lib/space_test_utils.ts | 1 + .../spaces_test_plugin/server/plugin.ts | 2 +- .../spaces_api_integration/common/services.ts | 2 +- .../common/suites/copy_to_space.ts | 18 +++++++++-------- .../common/suites/create.ts | 6 ++++-- .../common/suites/delete.ts | 10 ++++++---- .../suites/disable_legacy_url_aliases.ts | 8 +++++--- .../common/suites/get.ts | 6 ++++-- .../common/suites/get_all.ts | 6 ++++-- .../common/suites/get_shareable_references.ts | 18 +++++++++-------- .../suites/resolve_copy_to_space_conflicts.ts | 14 +++++++------ .../common/suites/update.ts | 6 ++++-- .../common/suites/update_objects_spaces.ts | 18 ++++++++--------- .../apis/copy_to_space/copy_to_space.ts | 2 +- .../apis/copy_to_space/index.ts | 2 +- .../security_and_spaces/apis/create.ts | 5 +++-- .../security_and_spaces/apis/delete.ts | 5 +++-- .../apis/disable_legacy_url_aliases.ts | 10 +++++----- .../security_and_spaces/apis/get.ts | 5 +++-- .../security_and_spaces/apis/get_all.ts | 5 +++-- .../apis/get_shareable_references.ts | 14 +++++++------ .../security_and_spaces/apis/index.ts | 2 +- .../apis/resolve_copy_to_space_conflicts.ts | 2 +- .../security_and_spaces/apis/update.ts | 5 +++-- .../apis/update_objects_spaces.ts | 14 ++++++------- .../spaces_only/apis/copy_to_space.ts | 2 +- .../spaces_only/apis/create.ts | 5 +++-- .../spaces_only/apis/delete.ts | 5 +++-- .../apis/disable_legacy_url_aliases.ts | 6 +++--- .../spaces_only/apis/get.ts | 5 +++-- .../spaces_only/apis/get_all.ts | 5 +++-- .../apis/get_shareable_references.ts | 8 ++++---- .../spaces_only/apis/index.ts | 2 +- .../apis/resolve_copy_to_space_conflicts.ts | 2 +- .../spaces_only/apis/update.ts | 5 +++-- .../spaces_only/apis/update_objects_spaces.ts | 6 +++--- .../spaces_only/telemetry/index.ts | 2 +- .../spaces_only/telemetry/telemetry.ts | 3 ++- 208 files changed, 644 insertions(+), 422 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index c604844089ef4..f2e54b2d116ae 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1815,9 +1815,23 @@ module.exports = { files: [ 'src/plugins/interactive_setup/**/*.{js,mjs,ts,tsx}', 'test/interactive_setup_api_integration/**/*.{js,mjs,ts,tsx}', + 'test/interactive_setup_functional/**/*.{js,mjs,ts,tsx}', + + 'packages/kbn-mock-idp-plugin/**/*.{js,mjs,ts,tsx}', + 'packages/kbn-mock-idp-utils/**/*.{js,mjs,ts,tsx}', + 'packages/kbn-security-hardening/**/*.{js,mjs,ts,tsx}', + 'packages/kbn-user-profile-components/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/encrypted_saved_objects/**/*.{js,mjs,ts,tsx}', + 'x-pack/test/encrypted_saved_objects_api_integration/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/security/**/*.{js,mjs,ts,tsx}', + 'x-pack/packages/security/**/*.{js,mjs,ts,tsx}', + 'x-pack/test/security_api_integration/**/*.{js,mjs,ts,tsx}', + 'x-pack/test/security_functional/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/spaces/**/*.{js,mjs,ts,tsx}', + 'x-pack/test/spaces_api_integration/**/*.{js,mjs,ts,tsx}', ], rules: { '@typescript-eslint/consistent-type-imports': 1, diff --git a/packages/kbn-mock-idp-plugin/public/login_page.tsx b/packages/kbn-mock-idp-plugin/public/login_page.tsx index eeb51e3aa37c1..92561ca51f584 100644 --- a/packages/kbn-mock-idp-plugin/public/login_page.tsx +++ b/packages/kbn-mock-idp-plugin/public/login_page.tsx @@ -7,21 +7,24 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { EuiButton, - EuiPageTemplate, - EuiEmptyPrompt, + EuiButtonEmpty, EuiComboBox, - EuiInlineEditTitle, + EuiEmptyPrompt, EuiFormRow, + EuiInlineEditTitle, + EuiPageTemplate, EuiSpacer, - EuiComboBoxOptionOption, - EuiButtonEmpty, } from '@elastic/eui'; -import React, { ChangeEvent, useEffect, useState, useRef } from 'react'; -import { FormikProvider, useFormik, Field, Form } from 'formik'; +import { Field, Form, FormikProvider, useFormik } from 'formik'; +import type { ChangeEvent } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; + +import type { CoreStart } from '@kbn/core-lifecycle-browser'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { CoreStart } from '@kbn/core-lifecycle-browser'; + import { useAuthenticator } from './role_switcher'; export const LoginPage = () => { diff --git a/packages/kbn-mock-idp-plugin/public/plugin.tsx b/packages/kbn-mock-idp-plugin/public/plugin.tsx index 0d168a5d6ec32..c1f733027f656 100644 --- a/packages/kbn-mock-idp-plugin/public/plugin.tsx +++ b/packages/kbn-mock-idp-plugin/public/plugin.tsx @@ -7,14 +7,16 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { PluginInitializer } from '@kbn/core-plugins-browser'; import React from 'react'; import ReactDOM from 'react-dom'; -import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; + +import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public'; +import type { PluginInitializer } from '@kbn/core-plugins-browser'; import { I18nProvider } from '@kbn/i18n-react'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { MOCK_IDP_LOGIN_PATH } from '@kbn/mock-idp-utils/src/constants'; -import type { CloudStart, CloudSetup } from '@kbn/cloud-plugin/public'; +import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; + import { RoleSwitcher } from './role_switcher'; export interface PluginSetupDependencies { diff --git a/packages/kbn-mock-idp-plugin/public/reload_page_toast.tsx b/packages/kbn-mock-idp-plugin/public/reload_page_toast.tsx index 8f21570555626..c3f93f3269da1 100644 --- a/packages/kbn-mock-idp-plugin/public/reload_page_toast.tsx +++ b/packages/kbn-mock-idp-plugin/public/reload_page_toast.tsx @@ -7,13 +7,13 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; -import { toMountPoint } from '@kbn/react-kibana-mount'; -import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import type { ToastInput } from '@kbn/core-notifications-browser'; import type { I18nStart } from '@kbn/core-i18n-browser'; +import type { ToastInput } from '@kbn/core-notifications-browser'; import type { ThemeServiceStart } from '@kbn/core-theme-browser'; +import { toMountPoint } from '@kbn/react-kibana-mount'; import type { AuthenticatedUser } from '@kbn/security-plugin-types-common'; export const DATA_TEST_SUBJ_PAGE_RELOAD_BUTTON = 'pageReloadButton'; diff --git a/packages/kbn-mock-idp-plugin/public/role_switcher.tsx b/packages/kbn-mock-idp-plugin/public/role_switcher.tsx index d0ffa3a67a697..347293abbc6c7 100644 --- a/packages/kbn-mock-idp-plugin/public/role_switcher.tsx +++ b/packages/kbn-mock-idp-plugin/public/role_switcher.tsx @@ -7,13 +7,15 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import { EuiButton, EuiContextMenu, EuiPopover } from '@elastic/eui'; import React, { useEffect, useState } from 'react'; -import { EuiButton, EuiPopover, EuiContextMenu } from '@elastic/eui'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { CoreStart } from '@kbn/core-lifecycle-browser'; import useAsyncFn from 'react-use/lib/useAsyncFn'; -import type { AuthenticatedUser } from '@kbn/security-plugin-types-common'; + +import type { CoreStart } from '@kbn/core-lifecycle-browser'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; import { MOCK_IDP_REALM_NAME, MOCK_IDP_REALM_TYPE } from '@kbn/mock-idp-utils/src/constants'; +import type { AuthenticatedUser } from '@kbn/security-plugin-types-common'; + import { createReloadPageToast } from './reload_page_toast'; import type { CreateSAMLResponseParams } from '../server'; diff --git a/packages/kbn-mock-idp-plugin/server/plugin.ts b/packages/kbn-mock-idp-plugin/server/plugin.ts index 27d5b96a4cbfc..fc9043099b197 100644 --- a/packages/kbn-mock-idp-plugin/server/plugin.ts +++ b/packages/kbn-mock-idp-plugin/server/plugin.ts @@ -7,17 +7,18 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { PluginInitializer, Plugin } from '@kbn/core-plugins-server'; +import { resolve } from 'path'; + +import type { CloudSetup } from '@kbn/cloud-plugin/server'; import { schema } from '@kbn/config-schema'; import type { TypeOf } from '@kbn/config-schema'; -import { MOCK_IDP_LOGIN_PATH, MOCK_IDP_LOGOUT_PATH, createSAMLResponse } from '@kbn/mock-idp-utils'; +import type { Plugin, PluginInitializer } from '@kbn/core-plugins-server'; import { + readRolesFromResource, SERVERLESS_ROLES_ROOT_PATH, STATEFUL_ROLES_ROOT_PATH, - readRolesFromResource, } from '@kbn/es'; -import { resolve } from 'path'; -import { CloudSetup } from '@kbn/cloud-plugin/server'; +import { createSAMLResponse, MOCK_IDP_LOGIN_PATH, MOCK_IDP_LOGOUT_PATH } from '@kbn/mock-idp-utils'; export interface PluginSetupDependencies { cloud: CloudSetup; diff --git a/packages/kbn-mock-idp-utils/src/utils.ts b/packages/kbn-mock-idp-utils/src/utils.ts index 85d37fdce5ab5..af07d69cfe936 100644 --- a/packages/kbn-mock-idp-utils/src/utils.ts +++ b/packages/kbn-mock-idp-utils/src/utils.ts @@ -7,23 +7,23 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { Client } from '@elastic/elasticsearch'; - -import { SignedXml } from 'xml-crypto'; -import { KBN_KEY_PATH, KBN_CERT_PATH } from '@kbn/dev-utils'; -import { readFile } from 'fs/promises'; +import type { Client } from '@elastic/elasticsearch'; import { X509Certificate } from 'crypto'; +import { readFile } from 'fs/promises'; +import { SignedXml } from 'xml-crypto'; + +import { KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; import { - MOCK_IDP_REALM_NAME, - MOCK_IDP_ENTITY_ID, - MOCK_IDP_ROLE_MAPPING_NAME, - MOCK_IDP_ATTRIBUTE_PRINCIPAL, - MOCK_IDP_ATTRIBUTE_ROLES, MOCK_IDP_ATTRIBUTE_EMAIL, MOCK_IDP_ATTRIBUTE_NAME, + MOCK_IDP_ATTRIBUTE_PRINCIPAL, + MOCK_IDP_ATTRIBUTE_ROLES, + MOCK_IDP_ENTITY_ID, MOCK_IDP_LOGIN_PATH, MOCK_IDP_LOGOUT_PATH, + MOCK_IDP_REALM_NAME, + MOCK_IDP_ROLE_MAPPING_NAME, } from './constants'; /** diff --git a/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.test.tsx b/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.test.tsx index 0837292d36ac6..87d41bd222637 100644 --- a/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.test.tsx +++ b/packages/kbn-user-profile-components/src/hooks/use_update_user_profile.test.tsx @@ -7,8 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React from 'react'; import { act, renderHook, type WrapperComponent } from '@testing-library/react-hooks'; +import React from 'react'; import { BehaviorSubject, first, lastValueFrom, of } from 'rxjs'; import { coreMock } from '@kbn/core/public/mocks'; 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 edf11d43b2c84..57aeec7a51d5a 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 @@ -8,13 +8,14 @@ */ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { useCallback, useRef, useState, useEffect } from 'react'; +import { merge } from 'lodash'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import useObservable from 'react-use/lib/useObservable'; + import { i18n } from '@kbn/i18n'; -import { merge } from 'lodash'; -import type { UserProfileData } from '../types'; import { useUserProfiles } from '../services'; +import type { UserProfileData } from '../types'; interface Props { notificationSuccess?: { diff --git a/packages/kbn-user-profile-components/src/services.tsx b/packages/kbn-user-profile-components/src/services.tsx index 726814f370412..7cf7a2d66c82f 100644 --- a/packages/kbn-user-profile-components/src/services.tsx +++ b/packages/kbn-user-profile-components/src/services.tsx @@ -7,12 +7,13 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { FC, PropsWithChildren, useContext } from 'react'; +import type { FC, PropsWithChildren } from 'react'; +import React, { useContext } from 'react'; import type { I18nStart } from '@kbn/core-i18n-browser'; import type { NotificationsStart, ToastOptions } from '@kbn/core-notifications-browser'; import type { ThemeServiceStart } from '@kbn/core-theme-browser'; -import { toMountPoint } from '@kbn/react-kibana-mount'; +import type { toMountPoint } from '@kbn/react-kibana-mount'; import type { UserProfileAPIClient } from './types'; diff --git a/packages/kbn-user-profile-components/src/types.ts b/packages/kbn-user-profile-components/src/types.ts index abdc9fba618ad..ff74061e0ef39 100644 --- a/packages/kbn-user-profile-components/src/types.ts +++ b/packages/kbn-user-profile-components/src/types.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { Observable } from 'rxjs'; +import type { Observable } from 'rxjs'; /** * Avatar stored in user profile. diff --git a/packages/kbn-user-profile-components/src/user_avatar.tsx b/packages/kbn-user-profile-components/src/user_avatar.tsx index 2b55e7909ef8c..5c31d7112afde 100644 --- a/packages/kbn-user-profile-components/src/user_avatar.tsx +++ b/packages/kbn-user-profile-components/src/user_avatar.tsx @@ -11,8 +11,8 @@ import type { EuiAvatarProps } from '@elastic/eui'; import { EuiAvatar, useEuiTheme } from '@elastic/eui'; import type { FunctionComponent } from 'react'; import React from 'react'; -import { UserProfileAvatarData } from './types'; +import type { UserProfileAvatarData } from './types'; import type { UserProfile, UserProfileUserInfo } from './user_profile'; import { getUserAvatarColor, diff --git a/packages/kbn-user-profile-components/src/user_profile.ts b/packages/kbn-user-profile-components/src/user_profile.ts index 082f6d11d7ec0..7c029f606fa7f 100644 --- a/packages/kbn-user-profile-components/src/user_profile.ts +++ b/packages/kbn-user-profile-components/src/user_profile.ts @@ -8,6 +8,7 @@ */ import { VISUALIZATION_COLORS } from '@elastic/eui'; + import type { UserProfileAvatarData, UserProfileData } from './types'; /** diff --git a/packages/kbn-user-profile-components/src/user_profiles_popover.test.tsx b/packages/kbn-user-profile-components/src/user_profiles_popover.test.tsx index 743dbf3232114..69d33763ba82b 100644 --- a/packages/kbn-user-profile-components/src/user_profiles_popover.test.tsx +++ b/packages/kbn-user-profile-components/src/user_profiles_popover.test.tsx @@ -9,8 +9,8 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { UserProfile } from './user_profile'; +import type { UserProfile } from './user_profile'; import { UserProfilesPopover } from './user_profiles_popover'; const userProfiles: UserProfile[] = [ diff --git a/packages/kbn-user-profile-components/src/user_profiles_popover.tsx b/packages/kbn-user-profile-components/src/user_profiles_popover.tsx index 197f6952110dd..decd3607133ab 100644 --- a/packages/kbn-user-profile-components/src/user_profiles_popover.tsx +++ b/packages/kbn-user-profile-components/src/user_profiles_popover.tsx @@ -7,12 +7,13 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { EuiPopoverProps, EuiContextMenuPanelProps } from '@elastic/eui'; +import type { EuiContextMenuPanelProps, EuiPopoverProps } from '@elastic/eui'; +import { EuiContextMenuPanel, EuiPopover, useGeneratedHtmlId } from '@elastic/eui'; import React from 'react'; -import { EuiPopover, EuiContextMenuPanel, useGeneratedHtmlId } from '@elastic/eui'; -import { UserProfilesSelectable, UserProfilesSelectableProps } from './user_profiles_selectable'; import type { UserProfileWithAvatar } from './user_avatar'; +import type { UserProfilesSelectableProps } from './user_profiles_selectable'; +import { UserProfilesSelectable } from './user_profiles_selectable'; /** * Props of {@link UserProfilesPopover} component diff --git a/packages/kbn-user-profile-components/src/user_profiles_selectable.test.tsx b/packages/kbn-user-profile-components/src/user_profiles_selectable.test.tsx index 8ba8fe6e61d07..3bd720e97d96d 100644 --- a/packages/kbn-user-profile-components/src/user_profiles_selectable.test.tsx +++ b/packages/kbn-user-profile-components/src/user_profiles_selectable.test.tsx @@ -7,10 +7,11 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { mountWithIntl as mount } from '@kbn/test-jest-helpers'; import React from 'react'; -import { UserProfile } from './user_profile'; +import { mountWithIntl as mount } from '@kbn/test-jest-helpers'; + +import type { UserProfile } from './user_profile'; import { UserProfilesSelectable } from './user_profiles_selectable'; const userProfiles: UserProfile[] = [ diff --git a/packages/kbn-user-profile-components/src/user_profiles_selectable.tsx b/packages/kbn-user-profile-components/src/user_profiles_selectable.tsx index 8dadde4427739..b6684f6e1fdb9 100644 --- a/packages/kbn-user-profile-components/src/user_profiles_selectable.tsx +++ b/packages/kbn-user-profile-components/src/user_profiles_selectable.tsx @@ -10,15 +10,15 @@ import type { EuiSelectableOption, EuiSelectableProps } from '@elastic/eui'; import { EuiButtonEmpty, + EuiCallOut, EuiFlexGroup, EuiFlexItem, + EuiHighlight, EuiHorizontalRule, EuiPanel, EuiSelectable, EuiSpacer, EuiText, - EuiCallOut, - EuiHighlight, } from '@elastic/eui'; import type { ReactNode } from 'react'; import React, { useEffect, useState } from 'react'; @@ -26,9 +26,9 @@ import React, { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { getUserDisplayLabel, getUserDisplayName } from './user_profile'; import type { UserProfileWithAvatar } from './user_avatar'; import { UserAvatar } from './user_avatar'; +import { getUserDisplayLabel, getUserDisplayName } from './user_profile'; const NULL_OPTION_KEY = 'null'; diff --git a/packages/kbn-user-profile-components/src/user_tooltip.tsx b/packages/kbn-user-profile-components/src/user_tooltip.tsx index 040e497adb497..80739d40802f5 100644 --- a/packages/kbn-user-profile-components/src/user_tooltip.tsx +++ b/packages/kbn-user-profile-components/src/user_tooltip.tsx @@ -7,15 +7,15 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { EuiText, EuiToolTipProps } from '@elastic/eui'; -import { EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import type { EuiToolTipProps } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; import type { FunctionComponent } from 'react'; import React from 'react'; -import type { UserProfileUserInfo } from './user_profile'; +import type { UserProfileAvatarData } from './types'; import { UserAvatar } from './user_avatar'; +import type { UserProfileUserInfo } from './user_profile'; import { getUserDisplayName } from './user_profile'; -import { UserProfileAvatarData } from './types'; /** * Props of {@link UserToolTip} component diff --git a/test/interactive_setup_functional/tests/manual_configuration.ts b/test/interactive_setup_functional/tests/manual_configuration.ts index 8d3956f1cd3c6..59b2391d27f54 100644 --- a/test/interactive_setup_functional/tests/manual_configuration.ts +++ b/test/interactive_setup_functional/tests/manual_configuration.ts @@ -8,6 +8,7 @@ */ import { getUrl, kibanaServerTestUser } from '@kbn/test'; + import type { FtrProviderContext } from '../../functional/ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { diff --git a/test/interactive_setup_functional/tests/manual_configuration_without_security.ts b/test/interactive_setup_functional/tests/manual_configuration_without_security.ts index af95c916622b7..e1fbb205dc53a 100644 --- a/test/interactive_setup_functional/tests/manual_configuration_without_security.ts +++ b/test/interactive_setup_functional/tests/manual_configuration_without_security.ts @@ -8,6 +8,7 @@ */ import { getUrl } from '@kbn/test'; + import type { FtrProviderContext } from '../../functional/ftr_provider_context'; export default function ({ getService, getPageObject }: FtrProviderContext) { diff --git a/test/interactive_setup_functional/tests/manual_configuration_without_tls.ts b/test/interactive_setup_functional/tests/manual_configuration_without_tls.ts index bd5bd3c5f33ab..666cc1cf05290 100644 --- a/test/interactive_setup_functional/tests/manual_configuration_without_tls.ts +++ b/test/interactive_setup_functional/tests/manual_configuration_without_tls.ts @@ -8,6 +8,7 @@ */ import { getUrl, kibanaServerTestUser } from '@kbn/test'; + import type { FtrProviderContext } from '../../functional/ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/packages/security/api_key_management/src/components/api_key_badge.tsx b/x-pack/packages/security/api_key_management/src/components/api_key_badge.tsx index d8fb2822a1e96..c0c1c2a1e823d 100644 --- a/x-pack/packages/security/api_key_management/src/components/api_key_badge.tsx +++ b/x-pack/packages/security/api_key_management/src/components/api_key_badge.tsx @@ -5,8 +5,10 @@ * 2.0. */ -import { EuiToolTip, EuiBadge } from '@elastic/eui'; -import React, { FunctionComponent } from 'react'; +import { EuiBadge, EuiToolTip } from '@elastic/eui'; +import type { FunctionComponent } from 'react'; +import React from 'react'; + import { FormattedMessage } from '@kbn/i18n-react'; export interface ApiKeyBadgeProps { diff --git a/x-pack/packages/security/api_key_management/src/components/api_key_created_callout.tsx b/x-pack/packages/security/api_key_management/src/components/api_key_created_callout.tsx index 1f2f0201d5a48..daea08b51bf96 100644 --- a/x-pack/packages/security/api_key_management/src/components/api_key_created_callout.tsx +++ b/x-pack/packages/security/api_key_management/src/components/api_key_created_callout.tsx @@ -6,10 +6,13 @@ */ import { EuiCallOut } from '@elastic/eui'; +import type { FunctionComponent } from 'react'; +import React from 'react'; + import { i18n } from '@kbn/i18n'; -import React, { FunctionComponent } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { CreateAPIKeyResult } from './api_keys_api_client'; + +import type { CreateAPIKeyResult } from './api_keys_api_client'; import { SelectableTokenField } from './token_field'; export interface ApiKeyCreatedCalloutProps { diff --git a/x-pack/packages/security/api_key_management/src/components/api_key_flyout.tsx b/x-pack/packages/security/api_key_management/src/components/api_key_flyout.tsx index 4a8fa74095957..1edaca0761c47 100644 --- a/x-pack/packages/security/api_key_management/src/components/api_key_flyout.tsx +++ b/x-pack/packages/security/api_key_management/src/components/api_key_flyout.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { ExclusiveUnion, htmlIdGenerator } from '@elastic/eui'; +import type { ExclusiveUnion } from '@elastic/eui'; import { EuiButton, EuiButtonEmpty, @@ -27,12 +27,13 @@ import { EuiSwitch, EuiText, EuiTitle, + htmlIdGenerator, useEuiTheme, } from '@elastic/eui'; import { Form, FormikProvider, useFormik } from 'formik'; import moment from 'moment-timezone'; -import { FunctionComponent, useRef } from 'react'; -import React, { useEffect, useState } from 'react'; +import type { FunctionComponent } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import useAsyncFn from 'react-use/lib/useAsyncFn'; import { CodeEditorField } from '@kbn/code-editor'; @@ -41,10 +42,13 @@ import { i18n } from '@kbn/i18n'; import { FormattedDate, FormattedMessage } from '@kbn/i18n-react'; import { useDarkMode, useKibana } from '@kbn/kibana-react-plugin/public'; import type { KibanaServerError } from '@kbn/kibana-utils-plugin/public'; - -import { Role } from '@kbn/security-plugin-types-common'; import { FormField, FormRow } from '@kbn/security-form-components'; -import type { ApiKeyRoleDescriptors, CategorizedApiKey } from '@kbn/security-plugin-types-common'; +import type { + ApiKeyRoleDescriptors, + CategorizedApiKey, + Role, +} from '@kbn/security-plugin-types-common'; + import { ApiKeyBadge, ApiKeyStatus, TimeToolTip } from '.'; import { APIKeysAPIClient } from './api_keys_api_client'; import type { diff --git a/x-pack/packages/security/api_key_management/src/components/api_key_status.tsx b/x-pack/packages/security/api_key_management/src/components/api_key_status.tsx index 677544866e3cf..c45ad145d9e51 100644 --- a/x-pack/packages/security/api_key_management/src/components/api_key_status.tsx +++ b/x-pack/packages/security/api_key_management/src/components/api_key_status.tsx @@ -7,9 +7,12 @@ import { EuiHealth } from '@elastic/eui'; import moment from 'moment'; -import React, { FunctionComponent } from 'react'; +import type { FunctionComponent } from 'react'; +import React from 'react'; + import { FormattedMessage } from '@kbn/i18n-react'; -import { CategorizedApiKey } from '@kbn/security-plugin-types-common'; +import type { CategorizedApiKey } from '@kbn/security-plugin-types-common'; + import { TimeToolTip } from './time_tool_tip'; export type ApiKeyStatusProps = Pick; diff --git a/x-pack/packages/security/api_key_management/src/components/api_keys_api_client.ts b/x-pack/packages/security/api_key_management/src/components/api_keys_api_client.ts index 30cc9f214ebf5..11f02ffe98782 100644 --- a/x-pack/packages/security/api_key_management/src/components/api_keys_api_client.ts +++ b/x-pack/packages/security/api_key_management/src/components/api_keys_api_client.ts @@ -5,9 +5,15 @@ * 2.0. */ +import type { Criteria } from '@elastic/eui'; import type { QueryContainer } from '@elastic/eui/src/components/search_bar/query/ast_to_es_query_dsl'; import type { HttpStart } from '@kbn/core/public'; +import type { + ApiKeyToInvalidate, + CategorizedApiKey, + QueryApiKeyResult, +} from '@kbn/security-plugin-types-common'; import type { CreateAPIKeyParams, CreateAPIKeyResult, @@ -15,13 +21,6 @@ import type { UpdateAPIKeyResult, } from '@kbn/security-plugin-types-server'; -import type { - ApiKeyToInvalidate, - CategorizedApiKey, - QueryApiKeyResult, -} from '@kbn/security-plugin-types-common'; -import type { Criteria } from '@elastic/eui'; - export type { CreateAPIKeyParams, CreateAPIKeyResult, UpdateAPIKeyParams, UpdateAPIKeyResult }; export interface QueryFilters { diff --git a/x-pack/packages/security/api_key_management/src/components/time_tool_tip.tsx b/x-pack/packages/security/api_key_management/src/components/time_tool_tip.tsx index dacfd7e0c1344..0864c6b7bf1d2 100644 --- a/x-pack/packages/security/api_key_management/src/components/time_tool_tip.tsx +++ b/x-pack/packages/security/api_key_management/src/components/time_tool_tip.tsx @@ -7,8 +7,8 @@ import { EuiToolTip } from '@elastic/eui'; import moment from 'moment'; +import type { FunctionComponent } from 'react'; import React from 'react'; -import { FunctionComponent } from 'react'; export interface TimeToolTipProps { timestamp: number; diff --git a/x-pack/packages/security/authorization_core/src/privileges/privileges.ts b/x-pack/packages/security/authorization_core/src/privileges/privileges.ts index 9fb8dd9f083e2..6b8acc4e4013a 100644 --- a/x-pack/packages/security/authorization_core/src/privileges/privileges.ts +++ b/x-pack/packages/security/authorization_core/src/privileges/privileges.ts @@ -12,8 +12,8 @@ import type { FeatureKibanaPrivilegesReference, } from '@kbn/features-plugin/common'; import type { FeaturesPluginSetup, KibanaFeature } from '@kbn/features-plugin/server'; - import type { SecurityLicense } from '@kbn/security-plugin-types-common'; + import { featurePrivilegeBuilderFactory } from './feature_privilege_builder'; import type { RawKibanaPrivileges } from './raw_kibana_privileges'; import type { Actions } from '../actions'; diff --git a/x-pack/packages/security/plugin_types_public/src/authorization/authorization_service.ts b/x-pack/packages/security/plugin_types_public/src/authorization/authorization_service.ts index f04acf8020b24..71e5c7360634f 100644 --- a/x-pack/packages/security/plugin_types_public/src/authorization/authorization_service.ts +++ b/x-pack/packages/security/plugin_types_public/src/authorization/authorization_service.ts @@ -5,8 +5,8 @@ * 2.0. */ -import type { RolesAPIClient } from '../roles'; import type { PrivilegesAPIClientPublicContract } from '../privileges'; +import type { RolesAPIClient } from '../roles'; export interface AuthorizationServiceSetup { /** diff --git a/x-pack/packages/security/plugin_types_public/src/plugin.ts b/x-pack/packages/security/plugin_types_public/src/plugin.ts index 06f3574388a36..4a3351494a631 100644 --- a/x-pack/packages/security/plugin_types_public/src/plugin.ts +++ b/x-pack/packages/security/plugin_types_public/src/plugin.ts @@ -6,6 +6,7 @@ */ import type { SecurityLicense } from '@kbn/security-plugin-types-common'; + import type { AuthenticationServiceSetup, AuthenticationServiceStart } from './authentication'; import type { AuthorizationServiceSetup, AuthorizationServiceStart } from './authorization'; import type { SecurityNavControlServiceStart } from './nav_control'; diff --git a/x-pack/packages/security/plugin_types_public/src/user_profile/user_profile_api_client.ts b/x-pack/packages/security/plugin_types_public/src/user_profile/user_profile_api_client.ts index eedf6e87a6483..862bcdccf942e 100644 --- a/x-pack/packages/security/plugin_types_public/src/user_profile/user_profile_api_client.ts +++ b/x-pack/packages/security/plugin_types_public/src/user_profile/user_profile_api_client.ts @@ -5,9 +5,10 @@ * 2.0. */ +import type { Observable } from 'rxjs'; + import type { CoreUserProfileDelegateContract } from '@kbn/core-user-profile-browser'; import type { UserProfileData } from '@kbn/core-user-profile-common'; -import type { Observable } from 'rxjs'; export type { GetUserProfileResponse, diff --git a/x-pack/packages/security/plugin_types_server/src/audit/audit_service.ts b/x-pack/packages/security/plugin_types_server/src/audit/audit_service.ts index e7b7f27b73b07..89524efb06339 100644 --- a/x-pack/packages/security/plugin_types_server/src/audit/audit_service.ts +++ b/x-pack/packages/security/plugin_types_server/src/audit/audit_service.ts @@ -6,7 +6,6 @@ */ import type { KibanaRequest } from '@kbn/core/server'; - import type { AuditLogger } from '@kbn/core-security-server'; export interface AuditServiceSetup { diff --git a/x-pack/packages/security/plugin_types_server/src/authentication/api_keys/api_keys.ts b/x-pack/packages/security/plugin_types_server/src/authentication/api_keys/api_keys.ts index c331802c7f693..f8d9f085a3246 100644 --- a/x-pack/packages/security/plugin_types_server/src/authentication/api_keys/api_keys.ts +++ b/x-pack/packages/security/plugin_types_server/src/authentication/api_keys/api_keys.ts @@ -6,7 +6,8 @@ */ import { schema } from '@kbn/config-schema'; -import { getKibanaRoleSchema, elasticsearchRoleSchema } from '../../authorization'; + +import { elasticsearchRoleSchema, getKibanaRoleSchema } from '../../authorization'; export const restApiKeySchema = schema.object({ type: schema.maybe(schema.literal('rest')), diff --git a/x-pack/packages/security/plugin_types_server/src/authentication/authentication_service.ts b/x-pack/packages/security/plugin_types_server/src/authentication/authentication_service.ts index 5d066bb6565ca..f5ee4ef7f25c1 100644 --- a/x-pack/packages/security/plugin_types_server/src/authentication/authentication_service.ts +++ b/x-pack/packages/security/plugin_types_server/src/authentication/authentication_service.ts @@ -6,8 +6,8 @@ */ import type { KibanaRequest } from '@kbn/core/server'; -import type { AuthenticatedUser } from '@kbn/security-plugin-types-common'; import type { APIKeysService } from '@kbn/core-security-server'; +import type { AuthenticatedUser } from '@kbn/security-plugin-types-common'; /** * Authentication services available on the security plugin's start contract. diff --git a/x-pack/packages/security/plugin_types_server/src/authorization/check_privileges_dynamically.ts b/x-pack/packages/security/plugin_types_server/src/authorization/check_privileges_dynamically.ts index f9663dddc64d0..df250f99bd004 100644 --- a/x-pack/packages/security/plugin_types_server/src/authorization/check_privileges_dynamically.ts +++ b/x-pack/packages/security/plugin_types_server/src/authorization/check_privileges_dynamically.ts @@ -6,9 +6,10 @@ */ import type { KibanaRequest } from '@kbn/core/server'; + import type { - CheckPrivilegesPayload, CheckPrivilegesOptions, + CheckPrivilegesPayload, CheckPrivilegesResponse, } from './check_privileges'; diff --git a/x-pack/packages/security/plugin_types_server/src/authorization/check_saved_objects_privileges.ts b/x-pack/packages/security/plugin_types_server/src/authorization/check_saved_objects_privileges.ts index 4b42723c83286..88d808b105503 100644 --- a/x-pack/packages/security/plugin_types_server/src/authorization/check_saved_objects_privileges.ts +++ b/x-pack/packages/security/plugin_types_server/src/authorization/check_saved_objects_privileges.ts @@ -6,6 +6,7 @@ */ import type { KibanaRequest } from '@kbn/core/server'; + import type { CheckPrivilegesResponse } from './check_privileges'; export type CheckSavedObjectsPrivilegesWithRequest = ( diff --git a/x-pack/packages/security/plugin_types_server/src/authorization/deprecations.ts b/x-pack/packages/security/plugin_types_server/src/authorization/deprecations.ts index 68cc61067e3c0..6567fdebf2b90 100644 --- a/x-pack/packages/security/plugin_types_server/src/authorization/deprecations.ts +++ b/x-pack/packages/security/plugin_types_server/src/authorization/deprecations.ts @@ -6,7 +6,6 @@ */ import type { DeprecationsDetails, GetDeprecationsContext } from '@kbn/core/server'; - import type { Role } from '@kbn/security-plugin-types-common'; export interface PrivilegeDeprecationsRolesByFeatureIdResponse { diff --git a/x-pack/packages/security/plugin_types_server/src/plugin.ts b/x-pack/packages/security/plugin_types_server/src/plugin.ts index c8222163785bf..7d37935ab760a 100644 --- a/x-pack/packages/security/plugin_types_server/src/plugin.ts +++ b/x-pack/packages/security/plugin_types_server/src/plugin.ts @@ -6,9 +6,10 @@ */ import type { SecurityLicense } from '@kbn/security-plugin-types-common'; + import type { AuditServiceSetup } from './audit'; -import type { PrivilegeDeprecationsService, AuthorizationServiceSetup } from './authorization'; import type { AuthenticationServiceStart } from './authentication'; +import type { AuthorizationServiceSetup, PrivilegeDeprecationsService } from './authorization'; import type { UserProfileServiceStart } from './user_profile'; /** diff --git a/x-pack/packages/security/role_management_model/src/__fixtures__/kibana_privileges.ts b/x-pack/packages/security/role_management_model/src/__fixtures__/kibana_privileges.ts index 2dc5078038033..13b34b94bf06b 100644 --- a/x-pack/packages/security/role_management_model/src/__fixtures__/kibana_privileges.ts +++ b/x-pack/packages/security/role_management_model/src/__fixtures__/kibana_privileges.ts @@ -12,8 +12,9 @@ import { subFeaturePrivilegeIterator, } from '@kbn/features-plugin/server/feature_privilege_iterator'; import type { LicenseType } from '@kbn/licensing-plugin/server'; -import type { SecurityLicenseFeatures } from '@kbn/security-plugin-types-common'; import { Actions, privilegesFactory } from '@kbn/security-authorization-core'; +import type { SecurityLicenseFeatures } from '@kbn/security-plugin-types-common'; + import { KibanaPrivileges } from '../kibana_privileges'; const featuresPluginService = (): jest.Mocked => { diff --git a/x-pack/packages/security/role_management_model/src/kibana_privileges.test.ts b/x-pack/packages/security/role_management_model/src/kibana_privileges.test.ts index 6102c853db51b..9fcd04cc2074d 100644 --- a/x-pack/packages/security/role_management_model/src/kibana_privileges.test.ts +++ b/x-pack/packages/security/role_management_model/src/kibana_privileges.test.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { KibanaPrivilege } from './kibana_privilege'; -import { KibanaPrivileges, isGlobalPrivilegeDefinition } from './kibana_privileges'; import type { RoleKibanaPrivilege } from '@kbn/security-plugin-types-common'; + import { createRawKibanaPrivileges, kibanaFeatures } from './__fixtures__'; +import { KibanaPrivilege } from './kibana_privilege'; +import { isGlobalPrivilegeDefinition, KibanaPrivileges } from './kibana_privileges'; describe('kibana_privilege', () => { describe('isGlobalPrivilegeDefinition', () => { diff --git a/x-pack/packages/security/role_management_model/src/kibana_privileges.ts b/x-pack/packages/security/role_management_model/src/kibana_privileges.ts index e78ee9b105bbf..a54ee72cf308a 100644 --- a/x-pack/packages/security/role_management_model/src/kibana_privileges.ts +++ b/x-pack/packages/security/role_management_model/src/kibana_privileges.ts @@ -6,9 +6,9 @@ */ import type { KibanaFeature } from '@kbn/features-plugin/common'; - -import type { RoleKibanaPrivilege } from '@kbn/security-plugin-types-common'; import type { RawKibanaPrivileges } from '@kbn/security-authorization-core'; +import type { RoleKibanaPrivilege } from '@kbn/security-plugin-types-common'; + import { KibanaPrivilege } from './kibana_privilege'; import { PrivilegeCollection } from './privilege_collection'; import { SecuredFeature } from './secured_feature'; diff --git a/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.test.tsx b/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.test.tsx index 2380088dd713f..2c858e7bb6ff6 100644 --- a/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.test.tsx +++ b/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.test.tsx @@ -9,13 +9,13 @@ import { EuiAccordion, EuiIconTip } from '@elastic/eui'; import React from 'react'; import type { KibanaFeature, SubFeatureConfig } from '@kbn/features-plugin/public'; +import type { Role } from '@kbn/security-plugin-types-common'; import { createFeature, createKibanaPrivileges, kibanaFeatures, } from '@kbn/security-role-management-model/src/__fixtures__'; import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers'; -import type { Role } from '@kbn/security-plugin-types-common'; import { getDisplayedFeaturePrivileges } from './__fixtures__'; import { FeatureTable } from './feature_table'; diff --git a/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.tsx b/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.tsx index 6fef00ccecec9..2f77b55ce5bac 100644 --- a/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.tsx +++ b/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.tsx @@ -33,9 +33,9 @@ import type { Role } from '@kbn/security-plugin-types-common'; import type { KibanaPrivileges, SecuredFeature } from '@kbn/security-role-management-model'; import { ChangeAllPrivilegesControl } from './change_all_privileges'; +import { FeatureTableCell } from './components/feature_table_cell'; import { FeatureTableExpandedRow } from './feature_table_expanded_row'; import { NO_PRIVILEGE_VALUE } from '../constants'; -import { FeatureTableCell } from './components/feature_table_cell'; import type { PrivilegeFormCalculator } from '../privilege_form_calculator'; interface Props { diff --git a/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table_expanded_row.test.tsx b/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table_expanded_row.test.tsx index 5e4f4ce021d44..e4f1755604dc5 100644 --- a/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table_expanded_row.test.tsx +++ b/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table_expanded_row.test.tsx @@ -8,11 +8,11 @@ import { act } from '@testing-library/react'; import React from 'react'; +import type { Role } from '@kbn/security-plugin-types-common'; import { createKibanaPrivileges, kibanaFeatures, } from '@kbn/security-role-management-model/src/__fixtures__'; -import type { Role } from '@kbn/security-plugin-types-common'; import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers'; import { FeatureTableExpandedRow } from './feature_table_expanded_row'; diff --git a/x-pack/packages/security/ui_components/src/privilege_form_calculator/privilege_form_calculator.test.ts b/x-pack/packages/security/ui_components/src/privilege_form_calculator/privilege_form_calculator.test.ts index e61134b816ffa..7977ee693721b 100644 --- a/x-pack/packages/security/ui_components/src/privilege_form_calculator/privilege_form_calculator.test.ts +++ b/x-pack/packages/security/ui_components/src/privilege_form_calculator/privilege_form_calculator.test.ts @@ -5,11 +5,11 @@ * 2.0. */ +import type { Role } from '@kbn/security-plugin-types-common'; import { createKibanaPrivileges, kibanaFeatures, } from '@kbn/security-role-management-model/src/__fixtures__'; -import type { Role } from '@kbn/security-plugin-types-common'; import { PrivilegeFormCalculator } from './privilege_form_calculator'; diff --git a/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts b/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts index e6263521f690d..4d453f64b6954 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { BuildFlavor } from '@kbn/config'; +import type { BuildFlavor } from '@kbn/config'; import { httpServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; import type { ConfigType } from '../config'; diff --git a/x-pack/plugins/encrypted_saved_objects/server/routes/index.ts b/x-pack/plugins/encrypted_saved_objects/server/routes/index.ts index 14d1933e8b765..aa3499ee46055 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/routes/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/routes/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { BuildFlavor } from '@kbn/config'; +import type { BuildFlavor } from '@kbn/config'; import type { IRouter, Logger } from '@kbn/core/server'; import type { PublicMethodsOf } from '@kbn/utility-types'; diff --git a/x-pack/plugins/security/public/account_management/account_management_app.tsx b/x-pack/plugins/security/public/account_management/account_management_app.tsx index 6a812cf3ccb83..c224d5cbc40a1 100644 --- a/x-pack/plugins/security/public/account_management/account_management_app.tsx +++ b/x-pack/plugins/security/public/account_management/account_management_app.tsx @@ -6,7 +6,8 @@ */ import type { History } from 'history'; -import React, { FC, PropsWithChildren } from 'react'; +import type { FC, PropsWithChildren } from 'react'; +import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import type { diff --git a/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.tsx b/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.tsx index eea9763420884..e491192b79d52 100644 --- a/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.tsx +++ b/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.tsx @@ -8,7 +8,8 @@ import './authentication_state_page.scss'; import { EuiIcon, EuiImage, EuiSpacer, EuiTitle } from '@elastic/eui'; -import React, { FC, PropsWithChildren } from 'react'; +import type { FC, PropsWithChildren } from 'react'; +import React from 'react'; interface Props { className?: string; diff --git a/x-pack/plugins/security/public/management/users/users_management_app.tsx b/x-pack/plugins/security/public/management/users/users_management_app.tsx index aac45ae6c7934..eb005b68d812c 100644 --- a/x-pack/plugins/security/public/management/users/users_management_app.tsx +++ b/x-pack/plugins/security/public/management/users/users_management_app.tsx @@ -6,7 +6,8 @@ */ import type { History } from 'history'; -import React, { FC, PropsWithChildren } from 'react'; +import type { FC, PropsWithChildren } from 'react'; +import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Redirect } from 'react-router-dom'; diff --git a/x-pack/test/encrypted_saved_objects_api_integration/config.ts b/x-pack/test/encrypted_saved_objects_api_integration/config.ts index 0b2585a94e954..433cbb9e36151 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/config.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/config.ts @@ -6,7 +6,9 @@ */ import path from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/encrypted_saved_objects_api_integration/ftr_provider_context.d.ts b/x-pack/test/encrypted_saved_objects_api_integration/ftr_provider_context.d.ts index aa56557c09df8..b05bbc8f6318d 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/ftr_provider_context.d.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/ftr_provider_context.d.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test'; +import type { GenericFtrProviderContext } from '@kbn/test'; -import { services } from './services'; +import type { services } from './services'; export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin/server/hidden_saved_object_routes.ts b/x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin/server/hidden_saved_object_routes.ts index 363d463139e4c..d0edb19e6a639 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin/server/hidden_saved_object_routes.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin/server/hidden_saved_object_routes.ts @@ -6,8 +6,9 @@ */ import { schema } from '@kbn/config-schema'; -import { IRouter, CoreSetup, SavedObject } from '@kbn/core/server'; -import { PluginsSetup, PluginsStart } from '.'; +import type { CoreSetup, IRouter, SavedObject } from '@kbn/core/server'; + +import type { PluginsSetup, PluginsStart } from '.'; export function registerHiddenSORoutes( router: IRouter, diff --git a/x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin/server/index.ts b/x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin/server/index.ts index f73d88fc79746..c7946b2e68131 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin/server/index.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin/server/index.ts @@ -5,20 +5,21 @@ * 2.0. */ -import { deepFreeze } from '@kbn/std'; -import { +import { schema } from '@kbn/config-schema'; +import type { CoreSetup, PluginInitializer, + SavedObject, SavedObjectsNamespaceType, SavedObjectUnsanitizedDoc, - SavedObject, } from '@kbn/core/server'; -import { schema } from '@kbn/config-schema'; -import { +import type { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart, } from '@kbn/encrypted-saved-objects-plugin/server'; -import { SpacesPluginSetup } from '@kbn/spaces-plugin/server'; +import type { SpacesPluginSetup } from '@kbn/spaces-plugin/server'; +import { deepFreeze } from '@kbn/std'; + import { registerHiddenSORoutes } from './hidden_saved_object_routes'; const SAVED_OBJECT_WITH_SECRET_TYPE = 'saved-object-with-secret'; diff --git a/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_aad_include_list.ts b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_aad_include_list.ts index fd6c0305f88ce..ae80cae697db9 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_aad_include_list.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_aad_include_list.ts @@ -7,7 +7,8 @@ import { MAIN_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../ftr_provider_context'; + +import type { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts index 62a9545afa77d..4687a01858260 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts @@ -5,13 +5,12 @@ * 2.0. */ -import expect from '@kbn/expect'; import type { SavedObject } from '@kbn/core/server'; import { MAIN_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; -import { - descriptorToArray, - SavedObjectDescriptor, -} from '@kbn/encrypted-saved-objects-plugin/server/crypto'; +import type { SavedObjectDescriptor } from '@kbn/encrypted-saved-objects-plugin/server/crypto'; +import { descriptorToArray } from '@kbn/encrypted-saved-objects-plugin/server/crypto'; +import expect from '@kbn/expect'; + import type { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_decryption.ts b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_decryption.ts index 8b0471cd34a9a..99428c2f0797f 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_decryption.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_decryption.ts @@ -7,7 +7,8 @@ import { MAIN_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../ftr_provider_context'; + +import type { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/encrypted_saved_objects_api_integration/tests/index.ts b/x-pack/test/encrypted_saved_objects_api_integration/tests/index.ts index c3188d0f5d0c3..1b14ec04811cd 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/tests/index.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/tests/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import type { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('encryptedSavedObjects', function encryptedSavedObjectsSuite() { diff --git a/x-pack/test/security_api_integration/anonymous.config.ts b/x-pack/test/security_api_integration/anonymous.config.ts index a53584b547efd..396514f540d6d 100644 --- a/x-pack/test/security_api_integration/anonymous.config.ts +++ b/x-pack/test/security_api_integration/anonymous.config.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; import { resolve } from 'path'; +import type { FtrConfigProviderContext } from '@kbn/test'; + export default async function ({ readConfigFile }: FtrConfigProviderContext) { const kibanaAPITestsConfig = await readConfigFile( require.resolve('@kbn/test-suites-src/api_integration/config') diff --git a/x-pack/test/security_api_integration/anonymous_es_anonymous.config.ts b/x-pack/test/security_api_integration/anonymous_es_anonymous.config.ts index 60691769729fa..78d49634bc472 100644 --- a/x-pack/test/security_api_integration/anonymous_es_anonymous.config.ts +++ b/x-pack/test/security_api_integration/anonymous_es_anonymous.config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; +import type { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const anonymousAPITestsConfig = await readConfigFile(require.resolve('./anonymous.config.ts')); diff --git a/x-pack/test/security_api_integration/api_keys.config.ts b/x-pack/test/security_api_integration/api_keys.config.ts index a1ec0e428845a..23295e91a8741 100644 --- a/x-pack/test/security_api_integration/api_keys.config.ts +++ b/x-pack/test/security_api_integration/api_keys.config.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/audit.config.ts b/x-pack/test/security_api_integration/audit.config.ts index 052f241470bee..858aeedbebcdb 100644 --- a/x-pack/test/security_api_integration/audit.config.ts +++ b/x-pack/test/security_api_integration/audit.config.ts @@ -6,7 +6,8 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + +import type { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); diff --git a/x-pack/test/security_api_integration/chips.config.ts b/x-pack/test/security_api_integration/chips.config.ts index b91a4ebfcc3e9..2aedcc767b615 100644 --- a/x-pack/test/security_api_integration/chips.config.ts +++ b/x-pack/test/security_api_integration/chips.config.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; import { resolve } from 'path'; +import type { FtrConfigProviderContext } from '@kbn/test'; + export default async function ({ readConfigFile }: FtrConfigProviderContext) { const kibanaAPITestsConfig = await readConfigFile( require.resolve('@kbn/test-suites-src/api_integration/config') diff --git a/x-pack/test/security_api_integration/ftr_provider_context.d.ts b/x-pack/test/security_api_integration/ftr_provider_context.d.ts index 647664d640466..b05bbc8f6318d 100644 --- a/x-pack/test/security_api_integration/ftr_provider_context.d.ts +++ b/x-pack/test/security_api_integration/ftr_provider_context.d.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test'; -import { services } from './services'; +import type { GenericFtrProviderContext } from '@kbn/test'; + +import type { services } from './services'; export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/security_api_integration/http_bearer.config.ts b/x-pack/test/security_api_integration/http_bearer.config.ts index 87f0b39cbb648..6a01822811d20 100644 --- a/x-pack/test/security_api_integration/http_bearer.config.ts +++ b/x-pack/test/security_api_integration/http_bearer.config.ts @@ -5,8 +5,10 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; import { resolve } from 'path'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/http_no_auth_providers.config.ts b/x-pack/test/security_api_integration/http_no_auth_providers.config.ts index b46bbe3134dd4..5df4eefe1212a 100644 --- a/x-pack/test/security_api_integration/http_no_auth_providers.config.ts +++ b/x-pack/test/security_api_integration/http_no_auth_providers.config.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/kerberos.config.ts b/x-pack/test/security_api_integration/kerberos.config.ts index ba948efde4cd9..28bed3bfe25ee 100644 --- a/x-pack/test/security_api_integration/kerberos.config.ts +++ b/x-pack/test/security_api_integration/kerberos.config.ts @@ -6,7 +6,9 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/kerberos_anonymous_access.config.ts b/x-pack/test/security_api_integration/kerberos_anonymous_access.config.ts index 355f0e90bcd91..d315ad0cff42e 100644 --- a/x-pack/test/security_api_integration/kerberos_anonymous_access.config.ts +++ b/x-pack/test/security_api_integration/kerberos_anonymous_access.config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; +import type { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const kerberosAPITestsConfig = await readConfigFile(require.resolve('./kerberos.config.ts')); diff --git a/x-pack/test/security_api_integration/login_selector.config.ts b/x-pack/test/security_api_integration/login_selector.config.ts index 47371973028f0..9517cb2a45e8e 100644 --- a/x-pack/test/security_api_integration/login_selector.config.ts +++ b/x-pack/test/security_api_integration/login_selector.config.ts @@ -7,8 +7,9 @@ import { readFileSync } from 'fs'; import { resolve } from 'path'; + import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; -import { FtrConfigProviderContext } from '@kbn/test'; +import type { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const kibanaAPITestsConfig = await readConfigFile( diff --git a/x-pack/test/security_api_integration/oidc.config.ts b/x-pack/test/security_api_integration/oidc.config.ts index 43cc5292ea5c2..28baa97f1dcc4 100644 --- a/x-pack/test/security_api_integration/oidc.config.ts +++ b/x-pack/test/security_api_integration/oidc.config.ts @@ -6,7 +6,9 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/oidc.http2.config.ts b/x-pack/test/security_api_integration/oidc.http2.config.ts index 957813ceb046e..034e008132890 100644 --- a/x-pack/test/security_api_integration/oidc.http2.config.ts +++ b/x-pack/test/security_api_integration/oidc.http2.config.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; import { CA_CERT_PATH } from '@kbn/dev-utils'; +import type { FtrConfigProviderContext } from '@kbn/test'; import { configureHTTP2 } from '@kbn/test-suites-src/common/configure_http2'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/oidc_implicit_flow.config.ts b/x-pack/test/security_api_integration/oidc_implicit_flow.config.ts index 3b9edcbec6826..94841661f6ed1 100644 --- a/x-pack/test/security_api_integration/oidc_implicit_flow.config.ts +++ b/x-pack/test/security_api_integration/oidc_implicit_flow.config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; +import type { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const oidcAPITestsConfig = await readConfigFile(require.resolve('./oidc.config.ts')); diff --git a/x-pack/test/security_api_integration/packages/helpers/saml/saml_tools.ts b/x-pack/test/security_api_integration/packages/helpers/saml/saml_tools.ts index 255625082407b..b1dd8f851caf0 100644 --- a/x-pack/test/security_api_integration/packages/helpers/saml/saml_tools.ts +++ b/x-pack/test/security_api_integration/packages/helpers/saml/saml_tools.ts @@ -9,10 +9,11 @@ import crypto from 'crypto'; import fs from 'fs'; import { stringify } from 'query-string'; import url from 'url'; -import zlib from 'zlib'; import { promisify } from 'util'; -import { parseString } from 'xml2js'; import { SignedXml } from 'xml-crypto'; +import { parseString } from 'xml2js'; +import zlib from 'zlib'; + import { KBN_KEY_PATH } from '@kbn/dev-utils'; /** diff --git a/x-pack/test/security_api_integration/pki.config.ts b/x-pack/test/security_api_integration/pki.config.ts index 49c23c8d80b79..e10be72339984 100644 --- a/x-pack/test/security_api_integration/pki.config.ts +++ b/x-pack/test/security_api_integration/pki.config.ts @@ -6,8 +6,10 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/plugins/audit_log/server/plugin.ts b/x-pack/test/security_api_integration/plugins/audit_log/server/plugin.ts index d7495dfd80f8d..647faccd0c0d0 100644 --- a/x-pack/test/security_api_integration/plugins/audit_log/server/plugin.ts +++ b/x-pack/test/security_api_integration/plugins/audit_log/server/plugin.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Plugin, CoreSetup } from '@kbn/core/server'; +import type { CoreSetup, Plugin } from '@kbn/core/server'; export class AuditTrailTestPlugin implements Plugin { public setup(core: CoreSetup) { diff --git a/x-pack/test/security_api_integration/plugins/oidc_provider/server/index.ts b/x-pack/test/security_api_integration/plugins/oidc_provider/server/index.ts index 82e460a6a4c27..b049f74d0a69f 100644 --- a/x-pack/test/security_api_integration/plugins/oidc_provider/server/index.ts +++ b/x-pack/test/security_api_integration/plugins/oidc_provider/server/index.ts @@ -5,7 +5,8 @@ * 2.0. */ -import type { PluginInitializer, Plugin } from '@kbn/core/server'; +import type { Plugin, PluginInitializer } from '@kbn/core/server'; + import { initRoutes } from './init_routes'; export const plugin: PluginInitializer = async (): Promise => ({ diff --git a/x-pack/test/security_api_integration/plugins/saml_provider/server/index.ts b/x-pack/test/security_api_integration/plugins/saml_provider/server/index.ts index ad297baf7246f..865240e32e9f0 100644 --- a/x-pack/test/security_api_integration/plugins/saml_provider/server/index.ts +++ b/x-pack/test/security_api_integration/plugins/saml_provider/server/index.ts @@ -5,8 +5,9 @@ * 2.0. */ -import type { PluginInitializer, Plugin } from '@kbn/core/server'; -import { CloudSetup } from '@kbn/cloud-plugin/server'; +import type { CloudSetup } from '@kbn/cloud-plugin/server'; +import type { Plugin, PluginInitializer } from '@kbn/core/server'; + import { initRoutes } from './init_routes'; export interface PluginSetupDependencies { diff --git a/x-pack/test/security_api_integration/plugins/saml_provider/server/init_routes.ts b/x-pack/test/security_api_integration/plugins/saml_provider/server/init_routes.ts index ea23e04201a61..f9e84caca0531 100644 --- a/x-pack/test/security_api_integration/plugins/saml_provider/server/init_routes.ts +++ b/x-pack/test/security_api_integration/plugins/saml_provider/server/init_routes.ts @@ -5,12 +5,13 @@ * 2.0. */ -import { CoreSetup, PluginInitializerContext } from '@kbn/core/server'; +import type { CoreSetup, PluginInitializerContext } from '@kbn/core/server'; import { - getSAMLResponse, getSAMLRequestId, + getSAMLResponse, } from '@kbn/security-api-integration-helpers/saml/saml_tools'; -import { PluginSetupDependencies } from '.'; + +import type { PluginSetupDependencies } from '.'; export function initRoutes( pluginContext: PluginInitializerContext, diff --git a/x-pack/test/security_api_integration/plugins/user_profiles_consumer/server/index.ts b/x-pack/test/security_api_integration/plugins/user_profiles_consumer/server/index.ts index 3922cc233cca9..3c598e3278da1 100644 --- a/x-pack/test/security_api_integration/plugins/user_profiles_consumer/server/index.ts +++ b/x-pack/test/security_api_integration/plugins/user_profiles_consumer/server/index.ts @@ -5,10 +5,11 @@ * 2.0. */ -import type { PluginInitializer, Plugin, CoreSetup } from '@kbn/core/server'; -import { FeaturesPluginSetup, FeaturesPluginStart } from '@kbn/features-plugin/server'; -import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server'; -import { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server'; +import type { CoreSetup, Plugin, PluginInitializer } from '@kbn/core/server'; +import type { FeaturesPluginSetup, FeaturesPluginStart } from '@kbn/features-plugin/server'; +import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server'; +import type { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server'; + import { initRoutes } from './init_routes'; export interface PluginSetupDependencies { diff --git a/x-pack/test/security_api_integration/plugins/user_profiles_consumer/server/init_routes.ts b/x-pack/test/security_api_integration/plugins/user_profiles_consumer/server/init_routes.ts index 091e50ff17350..d7144dd69a1df 100644 --- a/x-pack/test/security_api_integration/plugins/user_profiles_consumer/server/init_routes.ts +++ b/x-pack/test/security_api_integration/plugins/user_profiles_consumer/server/init_routes.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { CoreSetup } from '@kbn/core/server'; import { schema } from '@kbn/config-schema'; -import { PluginStartDependencies } from '.'; +import type { CoreSetup } from '@kbn/core/server'; + +import type { PluginStartDependencies } from '.'; export function initRoutes(core: CoreSetup) { const router = core.http.createRouter(); diff --git a/x-pack/test/security_api_integration/saml.config.ts b/x-pack/test/security_api_integration/saml.config.ts index 3cd8b55f4117b..1168fe9ed196b 100644 --- a/x-pack/test/security_api_integration/saml.config.ts +++ b/x-pack/test/security_api_integration/saml.config.ts @@ -6,7 +6,9 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/saml.http2.config.ts b/x-pack/test/security_api_integration/saml.http2.config.ts index 0d063188efe9c..ddcd8ac7da446 100644 --- a/x-pack/test/security_api_integration/saml.http2.config.ts +++ b/x-pack/test/security_api_integration/saml.http2.config.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; import { CA_CERT_PATH } from '@kbn/dev-utils'; +import type { FtrConfigProviderContext } from '@kbn/test'; import { configureHTTP2 } from '@kbn/test-suites-src/common/configure_http2'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/saml_cloud.config.ts b/x-pack/test/security_api_integration/saml_cloud.config.ts index ce392b8a092c1..86450379a4364 100644 --- a/x-pack/test/security_api_integration/saml_cloud.config.ts +++ b/x-pack/test/security_api_integration/saml_cloud.config.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/services.ts b/x-pack/test/security_api_integration/services.ts index c2298b21b55e6..6b12d42f6e3d0 100644 --- a/x-pack/test/security_api_integration/services.ts +++ b/x-pack/test/security_api_integration/services.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { services as commonServices } from '../common/services'; import { services as apiIntegrationServices } from '../api_integration/services'; +import { services as commonServices } from '../common/services'; export const services = { ...commonServices, diff --git a/x-pack/test/security_api_integration/session_concurrent_limit.config.ts b/x-pack/test/security_api_integration/session_concurrent_limit.config.ts index 63da7220f3959..ff3e9f590a749 100644 --- a/x-pack/test/security_api_integration/session_concurrent_limit.config.ts +++ b/x-pack/test/security_api_integration/session_concurrent_limit.config.ts @@ -6,7 +6,9 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; // the default export of config files must be a config provider diff --git a/x-pack/test/security_api_integration/session_idle.config.ts b/x-pack/test/security_api_integration/session_idle.config.ts index a4943fb5388f4..9509d919c8ddd 100644 --- a/x-pack/test/security_api_integration/session_idle.config.ts +++ b/x-pack/test/security_api_integration/session_idle.config.ts @@ -6,7 +6,9 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; // the default export of config files must be a config provider diff --git a/x-pack/test/security_api_integration/session_invalidate.config.ts b/x-pack/test/security_api_integration/session_invalidate.config.ts index 2e0dd37f054e0..33be8e8f8f0d2 100644 --- a/x-pack/test/security_api_integration/session_invalidate.config.ts +++ b/x-pack/test/security_api_integration/session_invalidate.config.ts @@ -6,7 +6,9 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; // the default export of config files must be a config provider diff --git a/x-pack/test/security_api_integration/session_lifespan.config.ts b/x-pack/test/security_api_integration/session_lifespan.config.ts index 41e723a736809..9b37f45327c5c 100644 --- a/x-pack/test/security_api_integration/session_lifespan.config.ts +++ b/x-pack/test/security_api_integration/session_lifespan.config.ts @@ -6,7 +6,9 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; // the default export of config files must be a config provider diff --git a/x-pack/test/security_api_integration/tests/anonymous/capabilities.ts b/x-pack/test/security_api_integration/tests/anonymous/capabilities.ts index baa9e9ff419a4..f4f43b070f0d5 100644 --- a/x-pack/test/security_api_integration/tests/anonymous/capabilities.ts +++ b/x-pack/test/security_api_integration/tests/anonymous/capabilities.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/security_api_integration/tests/anonymous/index.ts b/x-pack/test/security_api_integration/tests/anonymous/index.ts index 0f976589483a8..59bf0f1d75d34 100644 --- a/x-pack/test/security_api_integration/tests/anonymous/index.ts +++ b/x-pack/test/security_api_integration/tests/anonymous/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Anonymous access', function () { diff --git a/x-pack/test/security_api_integration/tests/anonymous/login.ts b/x-pack/test/security_api_integration/tests/anonymous/login.ts index b97067ba04e7d..3495bd4d71dac 100644 --- a/x-pack/test/security_api_integration/tests/anonymous/login.ts +++ b/x-pack/test/security_api_integration/tests/anonymous/login.ts @@ -5,11 +5,14 @@ * 2.0. */ +import { resolve } from 'path'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + import expect from '@kbn/expect'; -import { parse as parseCookie, Cookie } from 'tough-cookie'; import { adminTestUser } from '@kbn/test'; -import { resolve } from 'path'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; import { FileWrapper } from '../audit/file_wrapper'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/security_api_integration/tests/api_keys/grant_api_key.ts b/x-pack/test/security_api_integration/tests/api_keys/grant_api_key.ts index bc559f47ed21d..c6e69d9beff1c 100644 --- a/x-pack/test/security_api_integration/tests/api_keys/grant_api_key.ts +++ b/x-pack/test/security_api_integration/tests/api_keys/grant_api_key.ts @@ -7,7 +7,8 @@ import expect from '@kbn/expect'; import { adminTestUser } from '@kbn/test'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/api_keys/has_active_key.ts b/x-pack/test/security_api_integration/tests/api_keys/has_active_key.ts index 6b14df9ad489e..6a6c98271a888 100644 --- a/x-pack/test/security_api_integration/tests/api_keys/has_active_key.ts +++ b/x-pack/test/security_api_integration/tests/api_keys/has_active_key.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; + import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/security_api_integration/tests/api_keys/index.ts b/x-pack/test/security_api_integration/tests/api_keys/index.ts index a36a76c0c9566..11debcafe7471 100644 --- a/x-pack/test/security_api_integration/tests/api_keys/index.ts +++ b/x-pack/test/security_api_integration/tests/api_keys/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Api Keys', function () { diff --git a/x-pack/test/security_api_integration/tests/audit/audit_log.ts b/x-pack/test/security_api_integration/tests/audit/audit_log.ts index e8c5d03bfc562..083ae82d4e406 100644 --- a/x-pack/test/security_api_integration/tests/audit/audit_log.ts +++ b/x-pack/test/security_api_integration/tests/audit/audit_log.ts @@ -6,9 +6,11 @@ */ import Path from 'path'; + import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; + import { FileWrapper } from './file_wrapper'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/security_api_integration/tests/audit/file_wrapper.ts b/x-pack/test/security_api_integration/tests/audit/file_wrapper.ts index b542ef0f8e354..2c26b07d8bd1a 100644 --- a/x-pack/test/security_api_integration/tests/audit/file_wrapper.ts +++ b/x-pack/test/security_api_integration/tests/audit/file_wrapper.ts @@ -6,6 +6,7 @@ */ import Fs from 'fs'; + import type { RetryService } from '@kbn/ftr-common-functional-services'; export class FileWrapper { diff --git a/x-pack/test/security_api_integration/tests/audit/index.ts b/x-pack/test/security_api_integration/tests/audit/index.ts index 96b2ceb5ae3a7..aeb67988e56ff 100644 --- a/x-pack/test/security_api_integration/tests/audit/index.ts +++ b/x-pack/test/security_api_integration/tests/audit/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Audit Log', function () { diff --git a/x-pack/test/security_api_integration/tests/chips/chips_cookie.ts b/x-pack/test/security_api_integration/tests/chips/chips_cookie.ts index 9a6a811578664..753bae24ae9c7 100644 --- a/x-pack/test/security_api_integration/tests/chips/chips_cookie.ts +++ b/x-pack/test/security_api_integration/tests/chips/chips_cookie.ts @@ -13,9 +13,11 @@ */ import { parse as parseCookie } from 'tough-cookie'; -import { adminTestUser } from '@kbn/test'; + import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { adminTestUser } from '@kbn/test'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/chips/index.ts b/x-pack/test/security_api_integration/tests/chips/index.ts index 2379a5feae5d8..f391d6d2bc496 100644 --- a/x-pack/test/security_api_integration/tests/chips/index.ts +++ b/x-pack/test/security_api_integration/tests/chips/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - CHIPS support', function () { diff --git a/x-pack/test/security_api_integration/tests/http_bearer/access_token.ts b/x-pack/test/security_api_integration/tests/http_bearer/access_token.ts index 3e94adfc3ece8..26a70bb286a1f 100644 --- a/x-pack/test/security_api_integration/tests/http_bearer/access_token.ts +++ b/x-pack/test/security_api_integration/tests/http_bearer/access_token.ts @@ -7,7 +7,8 @@ import expect from '@kbn/expect'; import { adminTestUser } from '@kbn/test'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/http_bearer/index.ts b/x-pack/test/security_api_integration/tests/http_bearer/index.ts index 66619192bca48..950dcabc6b88a 100644 --- a/x-pack/test/security_api_integration/tests/http_bearer/index.ts +++ b/x-pack/test/security_api_integration/tests/http_bearer/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - HTTP Bearer', function () { diff --git a/x-pack/test/security_api_integration/tests/http_bearer/jwt.ts b/x-pack/test/security_api_integration/tests/http_bearer/jwt.ts index 4806bc410cd84..2ccfdeec77cb5 100644 --- a/x-pack/test/security_api_integration/tests/http_bearer/jwt.ts +++ b/x-pack/test/security_api_integration/tests/http_bearer/jwt.ts @@ -6,9 +6,10 @@ */ import expect from '@kbn/expect'; -import { AuthenticatedUser } from '@kbn/security-plugin-types-common'; +import type { AuthenticatedUser } from '@kbn/security-plugin-types-common'; import { adminTestUser } from '@kbn/test'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/http_no_auth_providers/authentication.ts b/x-pack/test/security_api_integration/tests/http_no_auth_providers/authentication.ts index 42951f8c3fed3..c9b0c601316ba 100644 --- a/x-pack/test/security_api_integration/tests/http_no_auth_providers/authentication.ts +++ b/x-pack/test/security_api_integration/tests/http_no_auth_providers/authentication.ts @@ -7,7 +7,8 @@ import expect from '@kbn/expect'; import { adminTestUser } from '@kbn/test'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/http_no_auth_providers/index.ts b/x-pack/test/security_api_integration/tests/http_no_auth_providers/index.ts index 23096b2449c9f..1ed90a8c24729 100644 --- a/x-pack/test/security_api_integration/tests/http_no_auth_providers/index.ts +++ b/x-pack/test/security_api_integration/tests/http_no_auth_providers/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - HTTP no authentication providers are enabled', function () { diff --git a/x-pack/test/security_api_integration/tests/kerberos/index.ts b/x-pack/test/security_api_integration/tests/kerberos/index.ts index 828ce7220458f..dc101b5de7c8b 100644 --- a/x-pack/test/security_api_integration/tests/kerberos/index.ts +++ b/x-pack/test/security_api_integration/tests/kerberos/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Kerberos', function () { diff --git a/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts b/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts index 82ec3b062405c..edfc037c936fe 100644 --- a/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts +++ b/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts @@ -5,17 +5,20 @@ * 2.0. */ -import expect from '@kbn/expect'; import { expect as jestExpect } from 'expect'; -import { parse as parseCookie, Cookie } from 'tough-cookie'; -import { setTimeout as setTimeoutAsync } from 'timers/promises'; -import { adminTestUser } from '@kbn/test'; import { resolve } from 'path'; +import { setTimeout as setTimeoutAsync } from 'timers/promises'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + +import expect from '@kbn/expect'; import { getMutualAuthenticationResponseToken, getSPNEGOToken, } from '@kbn/security-api-integration-helpers/kerberos/kerberos_tools'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { adminTestUser } from '@kbn/test'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; import { FileWrapper } from '../audit/file_wrapper'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts b/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts index 1f2dc1ab43775..6536c9a08063d 100644 --- a/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts +++ b/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts @@ -5,22 +5,25 @@ * 2.0. */ -import { parse as parseCookie, Cookie } from 'tough-cookie'; import { readFileSync } from 'fs'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; import url from 'url'; + import { CA_CERT_PATH } from '@kbn/dev-utils'; import expect from '@kbn/expect'; -import type { AuthenticationProvider } from '@kbn/security-plugin/common'; -import { getStateAndNonce } from '@kbn/security-api-integration-helpers/oidc/oidc_tools'; import { getMutualAuthenticationResponseToken, getSPNEGOToken, } from '@kbn/security-api-integration-helpers/kerberos/kerberos_tools'; +import { getStateAndNonce } from '@kbn/security-api-integration-helpers/oidc/oidc_tools'; import { getSAMLRequestId, getSAMLResponse, } from '@kbn/security-api-integration-helpers/saml/saml_tools'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { AuthenticationProvider } from '@kbn/security-plugin/common'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const randomness = getService('randomness'); diff --git a/x-pack/test/security_api_integration/tests/login_selector/index.ts b/x-pack/test/security_api_integration/tests/login_selector/index.ts index e3698340d3967..65fdef8e465e3 100644 --- a/x-pack/test/security_api_integration/tests/login_selector/index.ts +++ b/x-pack/test/security_api_integration/tests/login_selector/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Login Selector', function () { diff --git a/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/index.ts b/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/index.ts index 2c8edc1569bd2..e4f9c6805c3de 100644 --- a/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/index.ts +++ b/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import type { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - OIDC (Authorization Code Flow)', function () { diff --git a/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts b/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts index 4c20b091f6e11..c14379ce753af 100644 --- a/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts +++ b/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts @@ -5,14 +5,17 @@ * 2.0. */ -import expect from '@kbn/expect'; -import { parse as parseCookie, Cookie } from 'tough-cookie'; -import url from 'url'; -import { setTimeout as setTimeoutAsync } from 'timers/promises'; -import { adminTestUser } from '@kbn/test'; import { resolve } from 'path'; +import { setTimeout as setTimeoutAsync } from 'timers/promises'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; +import url from 'url'; + +import expect from '@kbn/expect'; import { getStateAndNonce } from '@kbn/security-api-integration-helpers/oidc/oidc_tools'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { adminTestUser } from '@kbn/test'; + +import type { FtrProviderContext } from '../../../ftr_provider_context'; import { FileWrapper } from '../../audit/file_wrapper'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/security_api_integration/tests/oidc/implicit_flow/index.ts b/x-pack/test/security_api_integration/tests/oidc/implicit_flow/index.ts index 7479ba8e7bd81..6c3215f0f34c1 100644 --- a/x-pack/test/security_api_integration/tests/oidc/implicit_flow/index.ts +++ b/x-pack/test/security_api_integration/tests/oidc/implicit_flow/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import type { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - OIDC (Implicit Flow)', function () { diff --git a/x-pack/test/security_api_integration/tests/oidc/implicit_flow/oidc_auth.ts b/x-pack/test/security_api_integration/tests/oidc/implicit_flow/oidc_auth.ts index af5a78e7d4450..3a72b2ebe5fb7 100644 --- a/x-pack/test/security_api_integration/tests/oidc/implicit_flow/oidc_auth.ts +++ b/x-pack/test/security_api_integration/tests/oidc/implicit_flow/oidc_auth.ts @@ -5,15 +5,18 @@ * 2.0. */ -import expect from '@kbn/expect'; import { JSDOM } from 'jsdom'; -import { parse as parseCookie, Cookie } from 'tough-cookie'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; import { format as formatURL } from 'url'; + +import expect from '@kbn/expect'; import { createTokens, getStateAndNonce, } from '@kbn/security-api-integration-helpers/oidc/oidc_tools'; -import { FtrProviderContext } from '../../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/pki/index.ts b/x-pack/test/security_api_integration/tests/pki/index.ts index 9926f16619898..9d428f969d12e 100644 --- a/x-pack/test/security_api_integration/tests/pki/index.ts +++ b/x-pack/test/security_api_integration/tests/pki/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - PKI', function () { diff --git a/x-pack/test/security_api_integration/tests/pki/pki_auth.ts b/x-pack/test/security_api_integration/tests/pki/pki_auth.ts index 37fae88942f34..bd77e029c1529 100644 --- a/x-pack/test/security_api_integration/tests/pki/pki_auth.ts +++ b/x-pack/test/security_api_integration/tests/pki/pki_auth.ts @@ -5,15 +5,18 @@ * 2.0. */ -import expect from '@kbn/expect'; import { expect as jestExpect } from 'expect'; -import { parse as parseCookie, Cookie } from 'tough-cookie'; -import { setTimeout as setTimeoutAsync } from 'timers/promises'; import { readFileSync } from 'fs'; import { resolve } from 'path'; +import { setTimeout as setTimeoutAsync } from 'timers/promises'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + import { CA_CERT_PATH } from '@kbn/dev-utils'; +import expect from '@kbn/expect'; import { adminTestUser } from '@kbn/test'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; import { FileWrapper } from '../audit/file_wrapper'; const CA_CERT = readFileSync(CA_CERT_PATH); diff --git a/x-pack/test/security_api_integration/tests/saml/index.ts b/x-pack/test/security_api_integration/tests/saml/index.ts index 3597f1a6104ec..bacf31dd3a81a 100644 --- a/x-pack/test/security_api_integration/tests/saml/index.ts +++ b/x-pack/test/security_api_integration/tests/saml/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - SAML', function () { diff --git a/x-pack/test/security_api_integration/tests/saml/saml_login.ts b/x-pack/test/security_api_integration/tests/saml/saml_login.ts index e80c19c40526d..71994bc5512ca 100644 --- a/x-pack/test/security_api_integration/tests/saml/saml_login.ts +++ b/x-pack/test/security_api_integration/tests/saml/saml_login.ts @@ -5,19 +5,22 @@ * 2.0. */ -import { stringify } from 'query-string'; -import url from 'url'; import { resolve } from 'path'; +import { stringify } from 'query-string'; import { setTimeout as setTimeoutAsync } from 'timers/promises'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; +import url from 'url'; + import expect from '@kbn/expect'; -import { parse as parseCookie, Cookie } from 'tough-cookie'; -import { adminTestUser } from '@kbn/test'; import { getLogoutRequest, getSAMLRequestId, getSAMLResponse, } from '@kbn/security-api-integration-helpers/saml/saml_tools'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { adminTestUser } from '@kbn/test'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; import { FileWrapper } from '../audit/file_wrapper'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/security_api_integration/tests/saml_cloud/index.ts b/x-pack/test/security_api_integration/tests/saml_cloud/index.ts index a2b39ad247151..61b4a166f666c 100644 --- a/x-pack/test/security_api_integration/tests/saml_cloud/index.ts +++ b/x-pack/test/security_api_integration/tests/saml_cloud/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Cloud SAML', function () { diff --git a/x-pack/test/security_api_integration/tests/saml_cloud/saml_login.ts b/x-pack/test/security_api_integration/tests/saml_cloud/saml_login.ts index 7c585fcf4ffae..1282654506300 100644 --- a/x-pack/test/security_api_integration/tests/saml_cloud/saml_login.ts +++ b/x-pack/test/security_api_integration/tests/saml_cloud/saml_login.ts @@ -5,10 +5,13 @@ * 2.0. */ +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + import expect from '@kbn/expect'; -import { parse as parseCookie, Cookie } from 'tough-cookie'; import { getSAMLResponse } from '@kbn/security-api-integration-helpers/saml/saml_tools'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const randomness = getService('randomness'); diff --git a/x-pack/test/security_api_integration/tests/session_concurrent_limit/cleanup.ts b/x-pack/test/security_api_integration/tests/session_concurrent_limit/cleanup.ts index 6aa782ad260df..cac4f3b678149 100644 --- a/x-pack/test/security_api_integration/tests/session_concurrent_limit/cleanup.ts +++ b/x-pack/test/security_api_integration/tests/session_concurrent_limit/cleanup.ts @@ -5,23 +5,26 @@ * 2.0. */ -import { parse as parseCookie, Cookie } from 'tough-cookie'; -import { setTimeout as setTimeoutAsync } from 'timers/promises'; -import expect from '@kbn/expect'; -import { adminTestUser } from '@kbn/test'; -import type { AuthenticationProvider } from '@kbn/security-plugin/common'; -import { +import type { AggregateName, AggregationsMultiTermsAggregate, AggregationsMultiTermsBucket, AggregationsTopHitsAggregate, SearchTotalHits, } from '@elastic/elasticsearch/lib/api/types'; +import { setTimeout as setTimeoutAsync } from 'timers/promises'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + +import expect from '@kbn/expect'; import { getSAMLRequestId, getSAMLResponse, } from '@kbn/security-api-integration-helpers/saml/saml_tools'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { AuthenticationProvider } from '@kbn/security-plugin/common'; +import { adminTestUser } from '@kbn/test'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/session_concurrent_limit/global_limit.ts b/x-pack/test/security_api_integration/tests/session_concurrent_limit/global_limit.ts index df120e033377c..b1bb30e1a6cb6 100644 --- a/x-pack/test/security_api_integration/tests/session_concurrent_limit/global_limit.ts +++ b/x-pack/test/security_api_integration/tests/session_concurrent_limit/global_limit.ts @@ -5,15 +5,18 @@ * 2.0. */ -import { parse as parseCookie, Cookie } from 'tough-cookie'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + import expect from '@kbn/expect'; -import { adminTestUser } from '@kbn/test'; -import type { AuthenticationProvider } from '@kbn/security-plugin/common'; import { getSAMLRequestId, getSAMLResponse, } from '@kbn/security-api-integration-helpers/saml/saml_tools'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { AuthenticationProvider } from '@kbn/security-plugin/common'; +import { adminTestUser } from '@kbn/test'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/session_concurrent_limit/index.ts b/x-pack/test/security_api_integration/tests/session_concurrent_limit/index.ts index 61463eaccb09b..ce4b0f61a708f 100644 --- a/x-pack/test/security_api_integration/tests/session_concurrent_limit/index.ts +++ b/x-pack/test/security_api_integration/tests/session_concurrent_limit/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Session Concurrent Limit', function () { diff --git a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts index 4a74e3938467e..9c0f92b2c0182 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts @@ -5,16 +5,19 @@ * 2.0. */ -import { parse as parseCookie, Cookie } from 'tough-cookie'; import { setTimeout as setTimeoutAsync } from 'timers/promises'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + import expect from '@kbn/expect'; -import { adminTestUser } from '@kbn/test'; -import type { AuthenticationProvider } from '@kbn/security-plugin/common'; import { getSAMLRequestId, getSAMLResponse, } from '@kbn/security-api-integration-helpers/saml/saml_tools'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { AuthenticationProvider } from '@kbn/security-plugin/common'; +import { adminTestUser } from '@kbn/test'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/session_idle/expired.ts b/x-pack/test/security_api_integration/tests/session_idle/expired.ts index 14c7370068634..f5eb553a462ff 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/expired.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/expired.ts @@ -5,12 +5,14 @@ * 2.0. */ -import { parse as parseCookie } from 'tough-cookie'; import { setTimeout as setTimeoutAsync } from 'timers/promises'; +import { parse as parseCookie } from 'tough-cookie'; + import expect from '@kbn/expect'; -import { adminTestUser } from '@kbn/test'; import { SESSION_ERROR_REASON_HEADER } from '@kbn/security-plugin/common/constants'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { adminTestUser } from '@kbn/test'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/session_idle/extension.ts b/x-pack/test/security_api_integration/tests/session_idle/extension.ts index 83a3d1a534919..71e1c370387de 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/extension.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/extension.ts @@ -5,10 +5,13 @@ * 2.0. */ -import { parse as parseCookie, Cookie } from 'tough-cookie'; -import expect from '@kbn/expect'; import { setTimeout as setTimeoutAsync } from 'timers/promises'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + +import expect from '@kbn/expect'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/session_idle/index.ts b/x-pack/test/security_api_integration/tests/session_idle/index.ts index 14015874ddb61..d23861046ba59 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/index.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Session Idle', function () { diff --git a/x-pack/test/security_api_integration/tests/session_invalidate/index.ts b/x-pack/test/security_api_integration/tests/session_invalidate/index.ts index dcfb3d7fc5259..e244b08d03719 100644 --- a/x-pack/test/security_api_integration/tests/session_invalidate/index.ts +++ b/x-pack/test/security_api_integration/tests/session_invalidate/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Session Invalidate', function () { diff --git a/x-pack/test/security_api_integration/tests/session_invalidate/invalidate.ts b/x-pack/test/security_api_integration/tests/session_invalidate/invalidate.ts index 7c4d344a481d0..184b46a7d13c2 100644 --- a/x-pack/test/security_api_integration/tests/session_invalidate/invalidate.ts +++ b/x-pack/test/security_api_integration/tests/session_invalidate/invalidate.ts @@ -5,15 +5,18 @@ * 2.0. */ -import { parse as parseCookie, Cookie } from 'tough-cookie'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + import expect from '@kbn/expect'; -import { adminTestUser } from '@kbn/test'; -import type { AuthenticationProvider } from '@kbn/security-plugin/common'; import { getSAMLRequestId, getSAMLResponse, } from '@kbn/security-api-integration-helpers/saml/saml_tools'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { AuthenticationProvider } from '@kbn/security-plugin/common'; +import { adminTestUser } from '@kbn/test'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts b/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts index 5f7efaefd6242..77de7d5915a45 100644 --- a/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts +++ b/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts @@ -5,16 +5,19 @@ * 2.0. */ -import { parse as parseCookie, Cookie } from 'tough-cookie'; import { setTimeout as setTimeoutAsync } from 'timers/promises'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + import expect from '@kbn/expect'; -import { adminTestUser } from '@kbn/test'; -import type { AuthenticationProvider } from '@kbn/security-plugin/common'; import { getSAMLRequestId, getSAMLResponse, } from '@kbn/security-api-integration-helpers/saml/saml_tools'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { AuthenticationProvider } from '@kbn/security-plugin/common'; +import { adminTestUser } from '@kbn/test'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/session_lifespan/index.ts b/x-pack/test/security_api_integration/tests/session_lifespan/index.ts index e297805b4ab3c..e8f96c7f83f98 100644 --- a/x-pack/test/security_api_integration/tests/session_lifespan/index.ts +++ b/x-pack/test/security_api_integration/tests/session_lifespan/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Session Lifespan', function () { diff --git a/x-pack/test/security_api_integration/tests/token/audit.ts b/x-pack/test/security_api_integration/tests/token/audit.ts index 0c97fb9c3cdc4..7bd32b733b175 100644 --- a/x-pack/test/security_api_integration/tests/token/audit.ts +++ b/x-pack/test/security_api_integration/tests/token/audit.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { parse as parseCookie } from 'tough-cookie'; import { resolve } from 'path'; +import { parse as parseCookie } from 'tough-cookie'; + import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; import { FileWrapper } from '../audit/file_wrapper'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/security_api_integration/tests/token/header.ts b/x-pack/test/security_api_integration/tests/token/header.ts index 74707aee68931..304d2f4b0e4dd 100644 --- a/x-pack/test/security_api_integration/tests/token/header.ts +++ b/x-pack/test/security_api_integration/tests/token/header.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/token/index.ts b/x-pack/test/security_api_integration/tests/token/index.ts index e38f5148d644d..d4ed1589370b4 100644 --- a/x-pack/test/security_api_integration/tests/token/index.ts +++ b/x-pack/test/security_api_integration/tests/token/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Token', function () { diff --git a/x-pack/test/security_api_integration/tests/token/login.ts b/x-pack/test/security_api_integration/tests/token/login.ts index 25e7bb3251687..a05e78c1c1309 100644 --- a/x-pack/test/security_api_integration/tests/token/login.ts +++ b/x-pack/test/security_api_integration/tests/token/login.ts @@ -6,7 +6,8 @@ */ import { parse as parseCookie } from 'tough-cookie'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/token/logout.ts b/x-pack/test/security_api_integration/tests/token/logout.ts index 1a2385e434ca4..5c85f22964089 100644 --- a/x-pack/test/security_api_integration/tests/token/logout.ts +++ b/x-pack/test/security_api_integration/tests/token/logout.ts @@ -6,7 +6,8 @@ */ import { parse as parseCookie } from 'tough-cookie'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/token/session.ts b/x-pack/test/security_api_integration/tests/token/session.ts index 3e8d44ce247cc..1e90f278ea594 100644 --- a/x-pack/test/security_api_integration/tests/token/session.ts +++ b/x-pack/test/security_api_integration/tests/token/session.ts @@ -5,9 +5,12 @@ * 2.0. */ -import { parse as parseCookie, Cookie } from 'tough-cookie'; +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/x-pack/test/security_api_integration/tests/user_profiles/bulk_get.ts b/x-pack/test/security_api_integration/tests/user_profiles/bulk_get.ts index 8955a06261848..7c5accb39937c 100644 --- a/x-pack/test/security_api_integration/tests/user_profiles/bulk_get.ts +++ b/x-pack/test/security_api_integration/tests/user_profiles/bulk_get.ts @@ -5,9 +5,12 @@ * 2.0. */ +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + import expect from '@kbn/expect'; -import { parse as parseCookie, Cookie } from 'tough-cookie'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/security_api_integration/tests/user_profiles/get_current.ts b/x-pack/test/security_api_integration/tests/user_profiles/get_current.ts index 44896f9792247..ec41bc3d80648 100644 --- a/x-pack/test/security_api_integration/tests/user_profiles/get_current.ts +++ b/x-pack/test/security_api_integration/tests/user_profiles/get_current.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { parse as parseCookie } from 'tough-cookie'; import { expect } from 'expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { parse as parseCookie } from 'tough-cookie'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); diff --git a/x-pack/test/security_api_integration/tests/user_profiles/index.ts b/x-pack/test/security_api_integration/tests/user_profiles/index.ts index ab91926127f41..19197cfebbbcb 100644 --- a/x-pack/test/security_api_integration/tests/user_profiles/index.ts +++ b/x-pack/test/security_api_integration/tests/user_profiles/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - User Profiles', function () { diff --git a/x-pack/test/security_api_integration/tests/user_profiles/suggest.ts b/x-pack/test/security_api_integration/tests/user_profiles/suggest.ts index cf58f1b35d3b5..26b60abfc04d3 100644 --- a/x-pack/test/security_api_integration/tests/user_profiles/suggest.ts +++ b/x-pack/test/security_api_integration/tests/user_profiles/suggest.ts @@ -5,9 +5,12 @@ * 2.0. */ +import type { Cookie } from 'tough-cookie'; +import { parse as parseCookie } from 'tough-cookie'; + import expect from '@kbn/expect'; -import { parse as parseCookie, Cookie } from 'tough-cookie'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/security_api_integration/token.config.ts b/x-pack/test/security_api_integration/token.config.ts index 9021ab8dc2a58..5eefa10dc7621 100644 --- a/x-pack/test/security_api_integration/token.config.ts +++ b/x-pack/test/security_api_integration/token.config.ts @@ -5,8 +5,10 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; import { resolve } from 'path'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/user_profiles.config.ts b/x-pack/test/security_api_integration/user_profiles.config.ts index 43d25920b2fd9..31b548e2dc3e6 100644 --- a/x-pack/test/security_api_integration/user_profiles.config.ts +++ b/x-pack/test/security_api_integration/user_profiles.config.ts @@ -6,7 +6,9 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_functional/expired_session.config.ts b/x-pack/test/security_functional/expired_session.config.ts index 82dd1cb36caf2..b02a96dd41360 100644 --- a/x-pack/test/security_functional/expired_session.config.ts +++ b/x-pack/test/security_functional/expired_session.config.ts @@ -6,9 +6,11 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; -import { services } from '../functional/services'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { pageObjects } from '../functional/page_objects'; +import { services } from '../functional/services'; // the default export of config files must be a config provider // that returns an object with the projects config values diff --git a/x-pack/test/security_functional/ftr_provider_context.d.ts b/x-pack/test/security_functional/ftr_provider_context.d.ts index 66d4e37b795ca..87ecf00ddae2d 100644 --- a/x-pack/test/security_functional/ftr_provider_context.d.ts +++ b/x-pack/test/security_functional/ftr_provider_context.d.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test'; +import type { GenericFtrProviderContext } from '@kbn/test'; -import { pageObjects } from '../functional/page_objects'; -import { services } from '../functional/services'; +import type { pageObjects } from '../functional/page_objects'; +import type { services } from '../functional/services'; export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/security_functional/insecure_cluster_warning.config.ts b/x-pack/test/security_functional/insecure_cluster_warning.config.ts index fa89c80653c5d..4a43c9f928f0f 100644 --- a/x-pack/test/security_functional/insecure_cluster_warning.config.ts +++ b/x-pack/test/security_functional/insecure_cluster_warning.config.ts @@ -6,9 +6,11 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; -import { services } from '../functional/services'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { pageObjects } from '../functional/page_objects'; +import { services } from '../functional/services'; // the default export of config files must be a config provider // that returns an object with the projects config values diff --git a/x-pack/test/security_functional/login_selector.config.ts b/x-pack/test/security_functional/login_selector.config.ts index 2a35fc77a0316..d6abf075281f0 100644 --- a/x-pack/test/security_functional/login_selector.config.ts +++ b/x-pack/test/security_functional/login_selector.config.ts @@ -6,9 +6,11 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; -import { services } from '../functional/services'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { pageObjects } from '../functional/page_objects'; +import { services } from '../functional/services'; // the default export of config files must be a config provider // that returns an object with the projects config values diff --git a/x-pack/test/security_functional/oidc.config.ts b/x-pack/test/security_functional/oidc.config.ts index a50975c0ee7bb..4773d2e81497f 100644 --- a/x-pack/test/security_functional/oidc.config.ts +++ b/x-pack/test/security_functional/oidc.config.ts @@ -6,9 +6,11 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; -import { services } from '../functional/services'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { pageObjects } from '../functional/page_objects'; +import { services } from '../functional/services'; // the default export of config files must be a config provider // that returns an object with the projects config values diff --git a/x-pack/test/security_functional/oidc.http2.config.ts b/x-pack/test/security_functional/oidc.http2.config.ts index 79335988411f2..fbba56727f284 100644 --- a/x-pack/test/security_functional/oidc.http2.config.ts +++ b/x-pack/test/security_functional/oidc.http2.config.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; import { CA_CERT_PATH } from '@kbn/dev-utils'; +import type { FtrConfigProviderContext } from '@kbn/test'; import { configureHTTP2 } from '@kbn/test-suites-src/common/configure_http2'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_functional/plugins/test_endpoints/public/plugin.tsx b/x-pack/test/security_functional/plugins/test_endpoints/public/plugin.tsx index 716187792615d..a8f0be086fe06 100644 --- a/x-pack/test/security_functional/plugins/test_endpoints/public/plugin.tsx +++ b/x-pack/test/security_functional/plugins/test_endpoints/public/plugin.tsx @@ -5,12 +5,12 @@ * 2.0. */ -import type { CoreSetup, Plugin } from '@kbn/core/public'; -import ReactDOM from 'react-dom'; import React from 'react'; -import { debounce, filter, first } from 'rxjs'; -import { timer } from 'rxjs'; -import { SecurityPluginStart } from '@kbn/security-plugin/public'; +import ReactDOM from 'react-dom'; +import { debounce, filter, first, timer } from 'rxjs'; + +import type { CoreSetup, Plugin } from '@kbn/core/public'; +import type { SecurityPluginStart } from '@kbn/security-plugin/public'; export interface PluginStartDependencies { security: SecurityPluginStart; diff --git a/x-pack/test/security_functional/plugins/test_endpoints/server/index.ts b/x-pack/test/security_functional/plugins/test_endpoints/server/index.ts index 93097b9fa712e..178531cd3e6c2 100644 --- a/x-pack/test/security_functional/plugins/test_endpoints/server/index.ts +++ b/x-pack/test/security_functional/plugins/test_endpoints/server/index.ts @@ -5,12 +5,13 @@ * 2.0. */ -import { PluginInitializer, Plugin, CoreSetup } from '@kbn/core/server'; -import { +import type { CoreSetup, Plugin, PluginInitializer } from '@kbn/core/server'; +import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server'; +import type { TaskManagerSetupContract, TaskManagerStartContract, } from '@kbn/task-manager-plugin/server'; -import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server'; + import { initRoutes } from './init_routes'; export interface PluginSetupDependencies { diff --git a/x-pack/test/security_functional/plugins/test_endpoints/server/init_routes.ts b/x-pack/test/security_functional/plugins/test_endpoints/server/init_routes.ts index 70a077305ba39..23fb20c02e42a 100644 --- a/x-pack/test/security_functional/plugins/test_endpoints/server/init_routes.ts +++ b/x-pack/test/security_functional/plugins/test_endpoints/server/init_routes.ts @@ -5,17 +5,19 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; import { errors } from '@elastic/elasticsearch'; -import { CoreSetup, CoreStart, PluginInitializerContext } from '@kbn/core/server'; + +import { schema } from '@kbn/config-schema'; +import type { CoreSetup, CoreStart, PluginInitializerContext } from '@kbn/core/server'; +import { ROUTE_TAG_AUTH_FLOW } from '@kbn/security-plugin/server'; +import { restApiKeySchema } from '@kbn/security-plugin-types-server'; import type { - TaskManagerStartContract, - ConcreteTaskInstance, BulkUpdateTaskResult, + ConcreteTaskInstance, + TaskManagerStartContract, } from '@kbn/task-manager-plugin/server'; -import { restApiKeySchema } from '@kbn/security-plugin-types-server'; -import { ROUTE_TAG_AUTH_FLOW } from '@kbn/security-plugin/server'; -import { PluginStartDependencies } from '.'; + +import type { PluginStartDependencies } from '.'; export const SESSION_INDEX_CLEANUP_TASK_NAME = 'session_cleanup'; diff --git a/x-pack/test/security_functional/saml.config.ts b/x-pack/test/security_functional/saml.config.ts index 52ac62336c681..91afd72c97747 100644 --- a/x-pack/test/security_functional/saml.config.ts +++ b/x-pack/test/security_functional/saml.config.ts @@ -6,9 +6,11 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; -import { services } from '../functional/services'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { pageObjects } from '../functional/page_objects'; +import { services } from '../functional/services'; // the default export of config files must be a config provider // that returns an object with the projects config values diff --git a/x-pack/test/security_functional/saml.http2.config.ts b/x-pack/test/security_functional/saml.http2.config.ts index dc9bac7cbdeb9..34131c82e0090 100644 --- a/x-pack/test/security_functional/saml.http2.config.ts +++ b/x-pack/test/security_functional/saml.http2.config.ts @@ -12,8 +12,9 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; + import { CA_CERT_PATH } from '@kbn/dev-utils'; +import type { FtrConfigProviderContext } from '@kbn/test'; import { configureHTTP2 } from '@kbn/test-suites-src/common/configure_http2'; // the default export of config files must be a config provider diff --git a/x-pack/test/security_functional/tests/expired_session/basic_functionality.ts b/x-pack/test/security_functional/tests/expired_session/basic_functionality.ts index bdefbe94c9b22..7997ad5507280 100644 --- a/x-pack/test/security_functional/tests/expired_session/basic_functionality.ts +++ b/x-pack/test/security_functional/tests/expired_session/basic_functionality.ts @@ -5,11 +5,13 @@ * 2.0. */ -import expect from '@kbn/expect'; -import { SESSION_ERROR_REASON_HEADER } from '@kbn/security-plugin/common/constants'; import { setTimeout as setTimeoutAsync } from 'timers/promises'; import { parse } from 'url'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import expect from '@kbn/expect'; +import { SESSION_ERROR_REASON_HEADER } from '@kbn/security-plugin/common/constants'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); diff --git a/x-pack/test/security_functional/tests/expired_session/index.ts b/x-pack/test/security_functional/tests/expired_session/index.ts index d2d741b2b8393..15f6c6ad508e6 100644 --- a/x-pack/test/security_functional/tests/expired_session/index.ts +++ b/x-pack/test/security_functional/tests/expired_session/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app - expired session', function () { diff --git a/x-pack/test/security_functional/tests/insecure_cluster_warning/index.ts b/x-pack/test/security_functional/tests/insecure_cluster_warning/index.ts index 5878deb83c6a4..e737cf91f1c20 100644 --- a/x-pack/test/security_functional/tests/insecure_cluster_warning/index.ts +++ b/x-pack/test/security_functional/tests/insecure_cluster_warning/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app - insecure cluster warning', function () { diff --git a/x-pack/test/security_functional/tests/insecure_cluster_warning/insecure_cluster_warning.ts b/x-pack/test/security_functional/tests/insecure_cluster_warning/insecure_cluster_warning.ts index 32640b653a491..9306b290a0ec1 100644 --- a/x-pack/test/security_functional/tests/insecure_cluster_warning/insecure_cluster_warning.ts +++ b/x-pack/test/security_functional/tests/insecure_cluster_warning/insecure_cluster_warning.ts @@ -6,7 +6,8 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common']); diff --git a/x-pack/test/security_functional/tests/login_selector/auth_provider_hint.ts b/x-pack/test/security_functional/tests/login_selector/auth_provider_hint.ts index d99b17d69f461..bcb41c609e512 100644 --- a/x-pack/test/security_functional/tests/login_selector/auth_provider_hint.ts +++ b/x-pack/test/security_functional/tests/login_selector/auth_provider_hint.ts @@ -5,9 +5,11 @@ * 2.0. */ -import expect from '@kbn/expect'; import { parse } from 'url'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import expect from '@kbn/expect'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); diff --git a/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts b/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts index fc808b4c33cbd..49bc6038d88c8 100644 --- a/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts +++ b/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts @@ -5,9 +5,11 @@ * 2.0. */ -import expect from '@kbn/expect'; import { parse } from 'url'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import expect from '@kbn/expect'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); diff --git a/x-pack/test/security_functional/tests/login_selector/index.ts b/x-pack/test/security_functional/tests/login_selector/index.ts index ae81b307987cc..584d5410feb2d 100644 --- a/x-pack/test/security_functional/tests/login_selector/index.ts +++ b/x-pack/test/security_functional/tests/login_selector/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app - login selector', function () { diff --git a/x-pack/test/security_functional/tests/login_selector/reset_session_page.ts b/x-pack/test/security_functional/tests/login_selector/reset_session_page.ts index 4d7eb666a4bc3..0dd935f86be82 100644 --- a/x-pack/test/security_functional/tests/login_selector/reset_session_page.ts +++ b/x-pack/test/security_functional/tests/login_selector/reset_session_page.ts @@ -6,7 +6,8 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects, updateBaselines }: FtrProviderContext) { const screenshots = getService('screenshots'); diff --git a/x-pack/test/security_functional/tests/oidc/index.ts b/x-pack/test/security_functional/tests/oidc/index.ts index 37490a0193089..2a49047d81a85 100644 --- a/x-pack/test/security_functional/tests/oidc/index.ts +++ b/x-pack/test/security_functional/tests/oidc/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app - OIDC interactions', function () { diff --git a/x-pack/test/security_functional/tests/oidc/url_capture.ts b/x-pack/test/security_functional/tests/oidc/url_capture.ts index 6553ef193fc3b..624e373c5eeca 100644 --- a/x-pack/test/security_functional/tests/oidc/url_capture.ts +++ b/x-pack/test/security_functional/tests/oidc/url_capture.ts @@ -5,9 +5,11 @@ * 2.0. */ -import expect from '@kbn/expect'; import { parse } from 'url'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import expect from '@kbn/expect'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const find = getService('find'); diff --git a/x-pack/test/security_functional/tests/saml/index.ts b/x-pack/test/security_functional/tests/saml/index.ts index ebf97ebf8edfb..1cf0c4cedc7bb 100644 --- a/x-pack/test/security_functional/tests/saml/index.ts +++ b/x-pack/test/security_functional/tests/saml/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app - SAML interactions', function () { diff --git a/x-pack/test/security_functional/tests/saml/url_capture.ts b/x-pack/test/security_functional/tests/saml/url_capture.ts index 0193d3d870701..72bb342716cd0 100644 --- a/x-pack/test/security_functional/tests/saml/url_capture.ts +++ b/x-pack/test/security_functional/tests/saml/url_capture.ts @@ -5,9 +5,11 @@ * 2.0. */ -import expect from '@kbn/expect'; import { parse } from 'url'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import expect from '@kbn/expect'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const find = getService('find'); diff --git a/x-pack/test/security_functional/tests/user_profiles/client_side_apis.ts b/x-pack/test/security_functional/tests/user_profiles/client_side_apis.ts index 0e1fb7879d81a..c6675eb526632 100644 --- a/x-pack/test/security_functional/tests/user_profiles/client_side_apis.ts +++ b/x-pack/test/security_functional/tests/user_profiles/client_side_apis.ts @@ -5,10 +5,12 @@ * 2.0. */ -import expect from '@kbn/expect'; import { parse as parseCookie } from 'tough-cookie'; + +import expect from '@kbn/expect'; import { adminTestUser } from '@kbn/test'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['security', 'common']); diff --git a/x-pack/test/security_functional/tests/user_profiles/index.ts b/x-pack/test/security_functional/tests/user_profiles/index.ts index 85c74150dd3bd..4bb317365845f 100644 --- a/x-pack/test/security_functional/tests/user_profiles/index.ts +++ b/x-pack/test/security_functional/tests/user_profiles/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app - user profiles', function () { diff --git a/x-pack/test/security_functional/user_profiles.config.ts b/x-pack/test/security_functional/user_profiles.config.ts index e5b65db64c383..c52d29cd392d6 100644 --- a/x-pack/test/security_functional/user_profiles.config.ts +++ b/x-pack/test/security_functional/user_profiles.config.ts @@ -6,9 +6,11 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test'; -import { services } from '../functional/services'; + +import type { FtrConfigProviderContext } from '@kbn/test'; + import { pageObjects } from '../functional/page_objects'; +import { services } from '../functional/services'; // the default export of config files must be a config provider // that returns an object with the projects config values diff --git a/x-pack/test/spaces_api_integration/common/config.ts b/x-pack/test/spaces_api_integration/common/config.ts index f2074fac8bea1..466b34b65ce84 100644 --- a/x-pack/test/spaces_api_integration/common/config.ts +++ b/x-pack/test/spaces_api_integration/common/config.ts @@ -6,8 +6,9 @@ */ import path from 'path'; + import { REPO_ROOT } from '@kbn/repo-info'; -import { FtrConfigProviderContext } from '@kbn/test'; +import type { FtrConfigProviderContext } from '@kbn/test'; interface CreateTestConfigOptions { license: string; diff --git a/x-pack/test/spaces_api_integration/common/ftr_provider_context.d.ts b/x-pack/test/spaces_api_integration/common/ftr_provider_context.d.ts index aa56557c09df8..b05bbc8f6318d 100644 --- a/x-pack/test/spaces_api_integration/common/ftr_provider_context.d.ts +++ b/x-pack/test/spaces_api_integration/common/ftr_provider_context.d.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test'; +import type { GenericFtrProviderContext } from '@kbn/test'; -import { services } from './services'; +import type { services } from './services'; export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/spaces_api_integration/common/lib/create_users_and_roles.ts b/x-pack/test/spaces_api_integration/common/lib/create_users_and_roles.ts index b66c4a02a5bd6..2f93cc09fd032 100644 --- a/x-pack/test/spaces_api_integration/common/lib/create_users_and_roles.ts +++ b/x-pack/test/spaces_api_integration/common/lib/create_users_and_roles.ts @@ -5,8 +5,9 @@ * 2.0. */ -import { Agent as SuperTestAgent } from 'supertest'; import type { Client } from '@elastic/elasticsearch'; +import type { Agent as SuperTestAgent } from 'supertest'; + import { AUTHENTICATION } from './authentication'; export const createUsersAndRoles = async (es: Client, supertest: SuperTestAgent) => { diff --git a/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts b/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts index 8de4482474d8e..3c30208933550 100644 --- a/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts +++ b/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts @@ -6,6 +6,7 @@ */ import type { Client } from '@elastic/elasticsearch'; + import { ALL_SAVED_OBJECT_INDICES } from '@kbn/core-saved-objects-server'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; diff --git a/x-pack/test/spaces_api_integration/common/plugins/spaces_test_plugin/server/plugin.ts b/x-pack/test/spaces_api_integration/common/plugins/spaces_test_plugin/server/plugin.ts index 07cc3bcd28c1b..56a226def6ba4 100644 --- a/x-pack/test/spaces_api_integration/common/plugins/spaces_test_plugin/server/plugin.ts +++ b/x-pack/test/spaces_api_integration/common/plugins/spaces_test_plugin/server/plugin.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CoreSetup } from '@kbn/core/server'; +import type { CoreSetup } from '@kbn/core/server'; export class Plugin { constructor() {} diff --git a/x-pack/test/spaces_api_integration/common/services.ts b/x-pack/test/spaces_api_integration/common/services.ts index 8b24d9b23e675..44ca7ec39c582 100644 --- a/x-pack/test/spaces_api_integration/common/services.ts +++ b/x-pack/test/spaces_api_integration/common/services.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { services as commonServices } from '../../common/services'; import { services as apiIntegrationServices } from '../../api_integration/services'; +import { services as commonServices } from '../../common/services'; export const services = { ...commonServices, diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts index 8de127aeaefe0..4cb2506977123 100644 --- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts @@ -5,18 +5,20 @@ * 2.0. */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import expect from '@kbn/expect'; -import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; -import { CopyResponse } from '@kbn/spaces-plugin/server/lib/copy_to_spaces'; -import { - SavedObjectsImportFailure, +import type { SuperTest } from 'supertest'; + +import type { SavedObjectsImportAmbiguousConflictError, + SavedObjectsImportFailure, } from '@kbn/core/server'; -import { SuperTest } from 'supertest'; -import { getAggregatedSpaceData, getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; +import expect from '@kbn/expect'; +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; +import type { CopyResponse } from '@kbn/spaces-plugin/server/lib/copy_to_spaces'; + import { getTestDataLoader, SPACE_1, SPACE_2 } from '../../../common/lib/test_data_loader'; import type { FtrProviderContext } from '../ftr_provider_context'; +import { getAggregatedSpaceData, getUrlPrefix } from '../lib/space_test_utils'; +import type { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; type TestResponse = Record; diff --git a/x-pack/test/spaces_api_integration/common/suites/create.ts b/x-pack/test/spaces_api_integration/common/suites/create.ts index ce3113ec9639c..3c65ba8aba156 100644 --- a/x-pack/test/spaces_api_integration/common/suites/create.ts +++ b/x-pack/test/spaces_api_integration/common/suites/create.ts @@ -5,10 +5,12 @@ * 2.0. */ +import type { SuperTest } from 'supertest'; + import expect from '@kbn/expect'; -import { SuperTest } from 'supertest'; + import { getTestScenariosForSpace } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; +import type { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; interface CreateTest { statusCode: number; diff --git a/x-pack/test/spaces_api_integration/common/suites/delete.ts b/x-pack/test/spaces_api_integration/common/suites/delete.ts index fd04b79fd1ef5..1a75bc219ae1f 100644 --- a/x-pack/test/spaces_api_integration/common/suites/delete.ts +++ b/x-pack/test/spaces_api_integration/common/suites/delete.ts @@ -5,13 +5,15 @@ * 2.0. */ -import expect from '@kbn/expect'; -import { SuperTest } from 'supertest'; import type { Client } from '@elastic/elasticsearch'; +import type { SuperTest } from 'supertest'; + import { ALL_SAVED_OBJECT_INDICES } from '@kbn/core-saved-objects-server'; -import { getAggregatedSpaceData, getTestScenariosForSpace } from '../lib/space_test_utils'; +import expect from '@kbn/expect'; + import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; +import { getAggregatedSpaceData, getTestScenariosForSpace } from '../lib/space_test_utils'; +import type { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; interface DeleteTest { statusCode: number; diff --git a/x-pack/test/spaces_api_integration/common/suites/disable_legacy_url_aliases.ts b/x-pack/test/spaces_api_integration/common/suites/disable_legacy_url_aliases.ts index 5889a10479f31..a00729e7eca8c 100644 --- a/x-pack/test/spaces_api_integration/common/suites/disable_legacy_url_aliases.ts +++ b/x-pack/test/spaces_api_integration/common/suites/disable_legacy_url_aliases.ts @@ -5,18 +5,20 @@ * 2.0. */ -import expect from '@kbn/expect'; -import type { Agent as SuperTestAgent } from 'supertest'; import type { Client } from '@elastic/elasticsearch'; +import type { Agent as SuperTestAgent } from 'supertest'; + import type { LegacyUrlAlias } from '@kbn/core-saved-objects-base-server-internal'; import { MAIN_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; -import { SPACES } from '../lib/spaces'; +import expect from '@kbn/expect'; + import { getUrlPrefix } from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; import type { ExpectResponseBody, TestDefinition, TestSuite, } from '../../../saved_object_api_integration/common/lib/types'; +import { SPACES } from '../lib/spaces'; export interface DisableLegacyUrlAliasesTestDefinition extends TestDefinition { request: { diff --git a/x-pack/test/spaces_api_integration/common/suites/get.ts b/x-pack/test/spaces_api_integration/common/suites/get.ts index a733bd21c5fc2..cb3879591ed25 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get.ts @@ -5,10 +5,12 @@ * 2.0. */ +import type { SuperAgent } from 'superagent'; + import expect from '@kbn/expect'; -import { SuperAgent } from 'superagent'; + import { getTestScenariosForSpace } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; +import type { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; interface GetTest { statusCode: number; diff --git a/x-pack/test/spaces_api_integration/common/suites/get_all.ts b/x-pack/test/spaces_api_integration/common/suites/get_all.ts index 88625c3d9b51e..236c98d9364b9 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get_all.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get_all.ts @@ -5,10 +5,12 @@ * 2.0. */ +import type { SuperTest } from 'supertest'; + import expect from '@kbn/expect'; -import { SuperTest } from 'supertest'; + import { getTestScenariosForSpace } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; +import type { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; interface GetAllTest { statusCode: number; diff --git a/x-pack/test/spaces_api_integration/common/suites/get_shareable_references.ts b/x-pack/test/spaces_api_integration/common/suites/get_shareable_references.ts index c1630bc288169..553aedbab958b 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get_shareable_references.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get_shareable_references.ts @@ -5,24 +5,26 @@ * 2.0. */ -import expect from '@kbn/expect'; -import { deepFreeze } from '@kbn/std'; -import { Agent as SuperTestAgent } from 'supertest'; -import { - SavedObjectsCollectMultiNamespaceReferencesResponse, +import type { Agent as SuperTestAgent } from 'supertest'; + +import type { SavedObjectReferenceWithContext, + SavedObjectsCollectMultiNamespaceReferencesResponse, } from '@kbn/core/server'; -import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; -import { SPACES } from '../lib/spaces'; +import expect from '@kbn/expect'; +import { deepFreeze } from '@kbn/std'; + import { expectResponses, getUrlPrefix, } from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; -import { +import type { ExpectResponseBody, TestDefinition, TestSuite, } from '../../../saved_object_api_integration/common/lib/types'; +import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; export interface GetShareableReferencesTestDefinition extends TestDefinition { request: { diff --git a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts index 4c3e9c834a0f5..e07d56a95ba24 100644 --- a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts @@ -5,15 +5,17 @@ * 2.0. */ +import type { SuperTest } from 'supertest'; + +import type { SavedObject } from '@kbn/core/server'; import expect from '@kbn/expect'; -import { SavedObject } from '@kbn/core/server'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; -import { CopyResponse } from '@kbn/spaces-plugin/server/lib/copy_to_spaces'; -import { SuperTest } from 'supertest'; -import { getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; -import type { FtrProviderContext } from '../ftr_provider_context'; +import type { CopyResponse } from '@kbn/spaces-plugin/server/lib/copy_to_spaces'; + import { getTestDataLoader, SPACE_1, SPACE_2 } from '../../../common/lib/test_data_loader'; +import type { FtrProviderContext } from '../ftr_provider_context'; +import { getUrlPrefix } from '../lib/space_test_utils'; +import type { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; type TestResponse = Record; diff --git a/x-pack/test/spaces_api_integration/common/suites/update.ts b/x-pack/test/spaces_api_integration/common/suites/update.ts index 41e5d97f20514..62226bf4dbb8d 100644 --- a/x-pack/test/spaces_api_integration/common/suites/update.ts +++ b/x-pack/test/spaces_api_integration/common/suites/update.ts @@ -5,10 +5,12 @@ * 2.0. */ +import type { SuperTest } from 'supertest'; + import expect from '@kbn/expect'; -import { SuperTest } from 'supertest'; + import { getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; +import type { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; interface UpdateTest { statusCode: number; diff --git a/x-pack/test/spaces_api_integration/common/suites/update_objects_spaces.ts b/x-pack/test/spaces_api_integration/common/suites/update_objects_spaces.ts index 777581d9aa5a0..05ed64e1e6047 100644 --- a/x-pack/test/spaces_api_integration/common/suites/update_objects_spaces.ts +++ b/x-pack/test/spaces_api_integration/common/suites/update_objects_spaces.ts @@ -5,26 +5,26 @@ * 2.0. */ -import expect from '@kbn/expect'; import type { Client } from '@elastic/elasticsearch'; import type { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types'; -import { without, uniq } from 'lodash'; -import { Agent as SuperTestAgent } from 'supertest'; -import { - SavedObjectsErrorHelpers, - SavedObjectsUpdateObjectsSpacesResponse, -} from '@kbn/core/server'; +import { uniq, without } from 'lodash'; +import type { Agent as SuperTestAgent } from 'supertest'; + +import type { SavedObjectsUpdateObjectsSpacesResponse } from '@kbn/core/server'; +import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import { ALL_SAVED_OBJECT_INDICES } from '@kbn/core-saved-objects-server'; -import { SPACES } from '../lib/spaces'; +import expect from '@kbn/expect'; + import { expectResponses, getUrlPrefix, } from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; -import { +import type { ExpectResponseBody, TestDefinition, TestSuite, } from '../../../saved_object_api_integration/common/lib/types'; +import { SPACES } from '../lib/spaces'; export interface UpdateObjectsSpacesTestDefinition extends TestDefinition { request: { diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space/copy_to_space.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space/copy_to_space.ts index 90b2cc67439d7..b7e10cb500e80 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space/copy_to_space.ts @@ -5,10 +5,10 @@ * 2.0. */ +import type { FtrProviderContext } from '../../../common/ftr_provider_context'; import { AUTHENTICATION } from '../../../common/lib/authentication'; import { SPACES } from '../../../common/lib/spaces'; import { copyToSpaceTestSuiteFactory } from '../../../common/suites/copy_to_space'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function copyToSpaceSpacesAndSecuritySuite(context: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space/index.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space/index.ts index c1edb1e5e5ac1..79a6e69cb6f32 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space/index.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space/index.ts @@ -5,8 +5,8 @@ * 2.0. */ +import type { FtrProviderContext } from '../../../common/ftr_provider_context'; import { createUsersAndRoles } from '../../../common/lib/create_users_and_roles'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile, getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/create.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/create.ts index 3195f35c734cb..6e5bc1649cb3f 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/create.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/create.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { SuperTest } from 'supertest'; +import type { SuperTest } from 'supertest'; + +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; import { createTestSuiteFactory } from '../../common/suites/create'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createSpacesOnlySuite({ getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/delete.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/delete.ts index 60bcb91125858..01fd306a7f558 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/delete.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/delete.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { SuperTest } from 'supertest'; +import type { SuperTest } from 'supertest'; + +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; import { deleteTestSuiteFactory } from '../../common/suites/delete'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function deleteSpaceTestSuite({ getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/disable_legacy_url_aliases.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/disable_legacy_url_aliases.ts index cd00a3d8b7ee6..2498ce166bcce 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/disable_legacy_url_aliases.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/disable_legacy_url_aliases.ts @@ -5,16 +5,16 @@ * 2.0. */ -import { SPACES } from '../../common/lib/spaces'; import { getTestScenarios } from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; -import { TestUser } from '../../../saved_object_api_integration/common/lib/types'; +import type { TestUser } from '../../../saved_object_api_integration/common/lib/types'; +import type { FtrProviderContext } from '../../common/ftr_provider_context'; +import { SPACES } from '../../common/lib/spaces'; +import type { DisableLegacyUrlAliasesTestDefinition } from '../../common/suites/disable_legacy_url_aliases'; import { disableLegacyUrlAliasesTestSuiteFactory, - TEST_CASE_TARGET_TYPE, TEST_CASE_SOURCE_ID, - DisableLegacyUrlAliasesTestDefinition, + TEST_CASE_TARGET_TYPE, } from '../../common/suites/disable_legacy_url_aliases'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; const { DEFAULT: { spaceId: DEFAULT_SPACE_ID }, diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get.ts index 7354138b7987e..cc24ffa0d891a 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { SuperTest } from 'supertest'; +import type { SuperTest } from 'supertest'; + +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; import { getTestSuiteFactory } from '../../common/suites/get'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function getSpaceTestSuite({ getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts index d2c3b8be03be2..1c2db5f6bcd7c 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { SuperTest } from 'supertest'; +import type { SuperTest } from 'supertest'; + +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; import { getAllTestSuiteFactory } from '../../common/suites/get_all'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function getAllSpacesTestSuite({ getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_shareable_references.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_shareable_references.ts index d3466dd511e82..093e01dd8fbbf 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_shareable_references.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_shareable_references.ts @@ -5,17 +5,19 @@ * 2.0. */ -import { SPACES } from '../../common/lib/spaces'; import { getTestScenarios } from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; -import { TestUser } from '../../../saved_object_api_integration/common/lib/types'; -import { - getShareableReferencesTestSuiteFactory, +import type { TestUser } from '../../../saved_object_api_integration/common/lib/types'; +import type { FtrProviderContext } from '../../common/ftr_provider_context'; +import { SPACES } from '../../common/lib/spaces'; +import type { GetShareableReferencesTestCase, GetShareableReferencesTestDefinition, - TEST_CASE_OBJECTS, +} from '../../common/suites/get_shareable_references'; +import { EXPECTED_RESULTS, + getShareableReferencesTestSuiteFactory, + TEST_CASE_OBJECTS, } from '../../common/suites/get_shareable_references'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; const { DEFAULT: { spaceId: DEFAULT_SPACE_ID }, diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts index ae5f7b48a2809..756d47308f7fc 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts @@ -5,8 +5,8 @@ * 2.0. */ +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { createUsersAndRoles } from '../../common/lib/create_users_and_roles'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile, getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts index 2f1788ae348f9..da5054436dc69 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/resolve_copy_to_space_conflicts.ts @@ -5,10 +5,10 @@ * 2.0. */ +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; import { resolveCopyToSpaceConflictsSuite } from '../../common/suites/resolve_copy_to_space_conflicts'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function resolveCopyToSpaceConflictsTestSuite(context: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/update.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/update.ts index 40ddc6549edcc..57fd1c5b1a202 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/update.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/update.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { SuperTest } from 'supertest'; +import type { SuperTest } from 'supertest'; + +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { AUTHENTICATION } from '../../common/lib/authentication'; import { SPACES } from '../../common/lib/spaces'; import { updateTestSuiteFactory } from '../../common/suites/update'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function updateSpaceTestSuite({ getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/update_objects_spaces.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/update_objects_spaces.ts index c6a97337e6ad9..ab7e4db5908f6 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/update_objects_spaces.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/update_objects_spaces.ts @@ -5,19 +5,19 @@ * 2.0. */ -import { SPACES } from '../../common/lib/spaces'; import { - testCaseFailures, getTestScenarios, + testCaseFailures, } from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; -import { TestUser } from '../../../saved_object_api_integration/common/lib/types'; +import type { TestUser } from '../../../saved_object_api_integration/common/lib/types'; +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; -import { - updateObjectsSpacesTestSuiteFactory, - UpdateObjectsSpacesTestDefinition, +import { SPACES } from '../../common/lib/spaces'; +import type { UpdateObjectsSpacesTestCase, + UpdateObjectsSpacesTestDefinition, } from '../../common/suites/update_objects_spaces'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { updateObjectsSpacesTestSuiteFactory } from '../../common/suites/update_objects_spaces'; const { DEFAULT: { spaceId: DEFAULT_SPACE_ID }, diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts index 4139e94610f08..3867d528ef374 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/copy_to_space.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { copyToSpaceTestSuiteFactory } from '../../common/suites/copy_to_space'; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/create.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/create.ts index eea6204267bdd..c98754d60eec3 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/create.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/create.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { SuperTest } from 'supertest'; +import type { SuperTest } from 'supertest'; + +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { SPACES } from '../../common/lib/spaces'; import { createTestSuiteFactory } from '../../common/suites/create'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createSpacesOnlySuite({ getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/delete.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/delete.ts index f066018744818..da50b9c37f190 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/delete.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/delete.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { SuperTest } from 'supertest'; +import type { SuperTest } from 'supertest'; + +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { SPACES } from '../../common/lib/spaces'; import { deleteTestSuiteFactory } from '../../common/suites/delete'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function deleteSpaceTestSuite({ getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/disable_legacy_url_aliases.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/disable_legacy_url_aliases.ts index 32e774e2de636..1818beef05118 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/disable_legacy_url_aliases.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/disable_legacy_url_aliases.ts @@ -5,14 +5,14 @@ * 2.0. */ +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { SPACES } from '../../common/lib/spaces'; +import type { DisableLegacyUrlAliasesTestCase } from '../../common/suites/disable_legacy_url_aliases'; import { disableLegacyUrlAliasesTestSuiteFactory, - DisableLegacyUrlAliasesTestCase, - TEST_CASE_TARGET_TYPE, TEST_CASE_SOURCE_ID, + TEST_CASE_TARGET_TYPE, } from '../../common/suites/disable_legacy_url_aliases'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; const { DEFAULT: { spaceId: DEFAULT_SPACE_ID }, diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/get.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/get.ts index e7f9acc06b655..5cca880f2a7f4 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/get.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/get.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { SuperTest } from 'supertest'; +import type { SuperTest } from 'supertest'; + +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { SPACES } from '../../common/lib/spaces'; import { getTestSuiteFactory } from '../../common/suites/get'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function getSpaceTestSuite({ getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/get_all.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/get_all.ts index 6331c843649fa..4b52f25eecbea 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/get_all.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/get_all.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { SuperTest } from 'supertest'; +import type { SuperTest } from 'supertest'; + +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { SPACES } from '../../common/lib/spaces'; import { getAllTestSuiteFactory } from '../../common/suites/get_all'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function getAllSpacesTestSuite({ getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/get_shareable_references.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/get_shareable_references.ts index 5eec1dda83e5a..15fef24ad0d69 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/get_shareable_references.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/get_shareable_references.ts @@ -5,15 +5,15 @@ * 2.0. */ -import { SPACES } from '../../common/lib/spaces'; import { getTestScenarios } from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import type { FtrProviderContext } from '../../common/ftr_provider_context'; +import { SPACES } from '../../common/lib/spaces'; +import type { GetShareableReferencesTestCase } from '../../common/suites/get_shareable_references'; import { + EXPECTED_RESULTS, getShareableReferencesTestSuiteFactory, - GetShareableReferencesTestCase, TEST_CASE_OBJECTS, - EXPECTED_RESULTS, } from '../../common/suites/get_shareable_references'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; const { DEFAULT: { spaceId: DEFAULT_SPACE_ID }, diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts index 50c97b9ca0a75..433ce2c6c444c 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function spacesOnlyTestSuite({ loadTestFile }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/resolve_copy_to_space_conflicts.ts index 2248c67cd8219..9fc16ed64abb8 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/resolve_copy_to_space_conflicts.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { resolveCopyToSpaceConflictsSuite } from '../../common/suites/resolve_copy_to_space_conflicts'; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/update.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/update.ts index 40e4df87e74e9..4a4055896e546 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/update.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/update.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { SuperTest } from 'supertest'; +import type { SuperTest } from 'supertest'; + +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { SPACES } from '../../common/lib/spaces'; import { updateTestSuiteFactory } from '../../common/suites/update'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function updateSpaceTestSuite({ getService }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/update_objects_spaces.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/update_objects_spaces.ts index a8c2bdce2a3a5..c45aa9edc6741 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/update_objects_spaces.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/update_objects_spaces.ts @@ -5,15 +5,15 @@ * 2.0. */ -import { SPACES } from '../../common/lib/spaces'; import { - testCaseFailures, getTestScenarios, + testCaseFailures, } from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import type { FtrProviderContext } from '../../common/ftr_provider_context'; import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; +import { SPACES } from '../../common/lib/spaces'; import type { UpdateObjectsSpacesTestCase } from '../../common/suites/update_objects_spaces'; import { updateObjectsSpacesTestSuiteFactory } from '../../common/suites/update_objects_spaces'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; const { DEFAULT: { spaceId: DEFAULT_SPACE_ID }, diff --git a/x-pack/test/spaces_api_integration/spaces_only/telemetry/index.ts b/x-pack/test/spaces_api_integration/spaces_only/telemetry/index.ts index d2cd3fb8a4d6e..3b7f3e3232b99 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/telemetry/index.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/telemetry/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function spacesOnlyTestSuite({ loadTestFile }: FtrProviderContext) { diff --git a/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts b/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts index 98b3c88354593..a2b73f597414a 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts @@ -6,7 +6,8 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; + +import type { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ getService }: FtrProviderContext) { From 3079ef2062d39ae9fb5e6dc8fc5eab864f453995 Mon Sep 17 00:00:00 2001 From: Jeramy Soucy Date: Fri, 11 Oct 2024 09:58:32 +0200 Subject: [PATCH 87/87] Fixes `/internal/security/me` serverless test (#195846) ## Summary Replaces the name check with role check. The full name of the user is different between local and MKI environments. --- .../test_suites/common/platform_security/authentication.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 041c005855d0f..644f0c5b852a7 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 @@ -160,7 +160,7 @@ export default function ({ getService }: FtrProviderContext) { type: 'saml', }, enabled: true, - full_name: 'test viewer', + roles: [expect.stringContaining('viewer')], }) ); expect(status).toBe(200);

    $JcxZ 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 literal 0 HcmV?d00001 diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/public/index.ts index ab2dea089dcf1..76e643c6ae0d5 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/public/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/public/index.ts @@ -19,6 +19,7 @@ import type { RenderFunction, DiscoveredDataset, } from './types'; +import elasticAiAssistantImg from './assets/elastic_ai_assistant.png'; export type { ObservabilityAIAssistantPublicSetup, @@ -101,6 +102,8 @@ export { aiAssistantPreferredAIAssistantType, } from '../common/ui_settings/settings_keys'; +export const elasticAiAssistantImage = elasticAiAssistantImg; + export const plugin: PluginInitializer< ObservabilityAIAssistantPublicSetup, ObservabilityAIAssistantPublicStart, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/application.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/application.tsx index c554fc81d5de7..ce043ef395ee4 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/application.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/application.tsx @@ -9,10 +9,10 @@ import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config'; import type { History } from 'history'; import React from 'react'; import type { Observable } from 'rxjs'; -import { observabilityAIAssistantRouter } from './routes/config'; -import type { ObservabilityAIAssistantAppService } from './service/create_app_service'; +import type { AIAssistantAppService } from '@kbn/ai-assistant'; import type { ObservabilityAIAssistantAppPluginStartDependencies } from './types'; import { SharedProviders } from './utils/shared_providers'; +import { observabilityAIAssistantRouter } from './routes/config'; // This is the Conversation application. @@ -26,7 +26,7 @@ export function Application({ coreStart: CoreStart; history: History; pluginsStart: ObservabilityAIAssistantAppPluginStartDependencies; - service: ObservabilityAIAssistantAppService; + service: AIAssistantAppService; theme$: Observable; }) { return ( @@ -36,7 +36,7 @@ export function Application({ service={service} theme$={theme$} > - + diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx index 66a66ecc07dc0..2c2af65accb59 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx @@ -12,17 +12,15 @@ import { v4 } from 'uuid'; import useObservable from 'react-use/lib/useObservable'; import { i18n } from '@kbn/i18n'; import { CoreStart } from '@kbn/core-lifecycle-browser'; -import { useObservabilityAIAssistantAppService } from '../../hooks/use_observability_ai_assistant_app_service'; -import { ChatFlyout } from '../chat/chat_flyout'; +import { AIAssistantAppService, useAIAssistantAppService, ChatFlyout } from '@kbn/ai-assistant'; import { useKibana } from '../../hooks/use_kibana'; import { useTheme } from '../../hooks/use_theme'; import { useNavControlScreenContext } from '../../hooks/use_nav_control_screen_context'; import { SharedProviders } from '../../utils/shared_providers'; -import { ObservabilityAIAssistantAppService } from '../../service/create_app_service'; import { ObservabilityAIAssistantAppPluginStartDependencies } from '../../types'; interface NavControlWithProviderDeps { - appService: ObservabilityAIAssistantAppService; + appService: AIAssistantAppService; coreStart: CoreStart; pluginsStart: ObservabilityAIAssistantAppPluginStartDependencies; } @@ -45,10 +43,12 @@ export const NavControlWithProvider = ({ }; export function NavControl() { - const service = useObservabilityAIAssistantAppService(); + const service = useAIAssistantAppService(); const { services: { + application, + http, notifications, plugins: { start: { @@ -162,6 +162,13 @@ export function NavControl() { onClose={() => { setIsOpen(false); }} + navigateToConversation={(conversationId: string) => { + application.navigateToUrl( + http.basePath.prepend( + `/app/observabilityAIAssistant/conversations/${conversationId || ''}` + ) + ); + }} /> ) : undefined} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/lazy_nav_control.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/lazy_nav_control.tsx index bed86909af417..adef91ceea53e 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/lazy_nav_control.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/lazy_nav_control.tsx @@ -8,8 +8,8 @@ import { dynamic } from '@kbn/shared-ux-utility'; import React from 'react'; import { CoreStart } from '@kbn/core-lifecycle-browser'; +import { AIAssistantAppService } from '@kbn/ai-assistant'; import { useIsNavControlVisible } from '../../hooks/is_nav_control_visible'; -import { ObservabilityAIAssistantAppService } from '../../service/create_app_service'; import { ObservabilityAIAssistantAppPluginStartDependencies } from '../../types'; const LazyNavControlWithProvider = dynamic(() => @@ -17,7 +17,7 @@ const LazyNavControlWithProvider = dynamic(() => ); interface NavControlInitiatorProps { - appService: ObservabilityAIAssistantAppService; + appService: AIAssistantAppService; coreStart: CoreStart; pluginsStart: ObservabilityAIAssistantAppPluginStartDependencies; } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/context/observability_ai_assistant_app_service_provider.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/context/observability_ai_assistant_app_service_provider.tsx deleted file mode 100644 index 9de7f023b4d10..0000000000000 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/context/observability_ai_assistant_app_service_provider.tsx +++ /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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createContext } from 'react'; -import type { ObservabilityAIAssistantAppService } from '../service/create_app_service'; - -export const ObservabilityAIAssistantAppServiceContext = createContext< - ObservabilityAIAssistantAppService | undefined ->(undefined); - -export const ObservabilityAIAssistantAppServiceProvider = - ObservabilityAIAssistantAppServiceContext.Provider; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_kibana.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_kibana.ts index f836c3dac6159..deaabffeeb50d 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_kibana.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_kibana.ts @@ -7,10 +7,13 @@ import React from 'react'; import { Subject } from 'rxjs'; -import { useChat } from './use_chat'; const ObservabilityAIAssistantMultipaneFlyoutContext = React.createContext(undefined); +function useChat() { + return { next: () => {}, messages: [], setMessages: () => {}, state: undefined, stop: () => {} }; +} + export function useKibana() { return { services: { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_nav_control_screen_context.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_nav_control_screen_context.ts index 10195bf38651e..d068f592c4310 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_nav_control_screen_context.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_nav_control_screen_context.ts @@ -8,11 +8,11 @@ import { useEffect, useState } from 'react'; import datemath from '@elastic/datemath'; import moment from 'moment'; +import { useAIAssistantAppService } from '@kbn/ai-assistant'; import { useKibana } from './use_kibana'; -import { useObservabilityAIAssistantAppService } from './use_observability_ai_assistant_app_service'; export function useNavControlScreenContext() { - const service = useObservabilityAIAssistantAppService(); + const service = useAIAssistantAppService(); const { services: { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_observability_ai_assistant_app_service.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_observability_ai_assistant_app_service.ts deleted file mode 100644 index 9c86f29565f48..0000000000000 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_observability_ai_assistant_app_service.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { useContext } from 'react'; -import { ObservabilityAIAssistantAppServiceContext } from '../context/observability_ai_assistant_app_service_provider'; - -export function useObservabilityAIAssistantAppService() { - const services = useContext(ObservabilityAIAssistantAppServiceContext); - - if (!services) { - throw new Error( - 'ObservabilityAIAssistantContext not set. Did you wrap your component in ``?' - ); - } - - return services; -} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/i18n.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/i18n.ts deleted file mode 100644 index dcc28d7ff531a..0000000000000 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/i18n.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const ASSISTANT_SETUP_TITLE = i18n.translate( - 'xpack.observabilityAiAssistant.assistantSetup.title', - { - defaultMessage: 'Welcome to Elastic AI Assistant', - } -); - -export const EMPTY_CONVERSATION_TITLE = i18n.translate( - 'xpack.observabilityAiAssistant.emptyConversationTitle', - { defaultMessage: 'New conversation' } -); - -export const UPGRADE_LICENSE_TITLE = i18n.translate( - 'xpack.observabilityAiAssistant.incorrectLicense.title', - { - defaultMessage: 'Upgrade your license', - } -); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/plugin.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/plugin.tsx index 9817cc65362d6..1904eebffb2a8 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/plugin.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/plugin.tsx @@ -17,13 +17,13 @@ import { import type { Logger } from '@kbn/logging'; import { i18n } from '@kbn/i18n'; import { AI_ASSISTANT_APP_ID } from '@kbn/deeplinks-observability'; +import { createAppService, AIAssistantAppService } from '@kbn/ai-assistant'; import type { ObservabilityAIAssistantAppPluginSetupDependencies, ObservabilityAIAssistantAppPluginStartDependencies, ObservabilityAIAssistantAppPublicSetup, ObservabilityAIAssistantAppPublicStart, } from './types'; -import { createAppService, ObservabilityAIAssistantAppService } from './service/create_app_service'; import { getObsAIAssistantConnectorType } from './rule_connector'; import { NavControlInitiator } from './components/nav_control/lazy_nav_control'; @@ -40,7 +40,7 @@ export class ObservabilityAIAssistantAppPlugin > { logger: Logger; - appService: ObservabilityAIAssistantAppService | undefined; + appService: AIAssistantAppService | undefined; constructor(context: PluginInitializerContext) { this.logger = context.logger.get(); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/routes/config.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/routes/config.tsx index ed0ac18302cc5..545c69a990ace 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/routes/config.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/routes/config.tsx @@ -9,8 +9,8 @@ import { createRouter, Outlet } from '@kbn/typed-react-router-config'; import * as t from 'io-ts'; import React from 'react'; import { Redirect } from 'react-router-dom'; +import { ConversationViewWithProps } from './conversations/conversation_view_with_props'; import { ObservabilityAIAssistantPageTemplate } from '../components/page_template'; -import { ConversationView } from './conversations/conversation_view'; /** * The array of route definitions to be used when the application @@ -28,7 +28,7 @@ const observabilityAIAssistantRoutes = { ), children: { '/conversations/new': { - element: , + element: , }, '/conversations/{conversationId}': { params: t.intersection([ @@ -43,7 +43,7 @@ const observabilityAIAssistantRoutes = { }), }), ]), - element: , + element: , }, '/conversations': { element: , diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/routes/conversations/conversation_view_with_props.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/routes/conversations/conversation_view_with_props.tsx new file mode 100644 index 0000000000000..c57b8e2c66c71 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/routes/conversations/conversation_view_with_props.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 { ConversationView } from '@kbn/ai-assistant'; +import { useObservabilityAIAssistantParams } from '../../hooks/use_observability_ai_assistant_params'; +import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router'; + +export function ConversationViewWithProps() { + const { path } = useObservabilityAIAssistantParams('/conversations/*'); + const conversationId = 'conversationId' in path ? path.conversationId : undefined; + const observabilityAIAssistantRouter = useObservabilityAIAssistantRouter(); + function navigateToConversation(nextConversationId?: string) { + if (nextConversationId) { + observabilityAIAssistantRouter.push('/conversations/{conversationId}', { + path: { + conversationId: nextConversationId, + }, + query: {}, + }); + } else { + observabilityAIAssistantRouter.push('/conversations/new', { path: {}, query: {} }); + } + } + return ( + + observabilityAIAssistantRouter.link(`/conversations/{conversationId}`, { + path: { + conversationId: id, + }, + }) + } + /> + ); +} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/shared_providers.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/shared_providers.tsx index eaa441b34a008..49776f4622250 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/shared_providers.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/utils/shared_providers.tsx @@ -11,8 +11,7 @@ import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; import React, { useMemo } from 'react'; import type { Observable } from 'rxjs'; -import { ObservabilityAIAssistantAppServiceProvider } from '../context/observability_ai_assistant_app_service_provider'; -import type { ObservabilityAIAssistantAppService } from '../service/create_app_service'; +import { AIAssistantAppService } from '@kbn/ai-assistant'; import type { ObservabilityAIAssistantAppPluginStartDependencies } from '../types'; export function SharedProviders({ @@ -25,7 +24,7 @@ export function SharedProviders({ children: React.ReactElement; coreStart: CoreStart; pluginsStart: ObservabilityAIAssistantAppPluginStartDependencies; - service: ObservabilityAIAssistantAppService; + service: AIAssistantAppService; theme$: Observable; }) { const theme = useMemo(() => { @@ -45,11 +44,7 @@ export function SharedProviders({ }} > - - - {children} - - + {children} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json b/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json index 84fe8f0b93911..f5b6d1db53885 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json @@ -20,16 +20,8 @@ "@kbn/typed-react-router-config", "@kbn/i18n", "@kbn/management-settings-ids", - "@kbn/security-plugin", - "@kbn/ui-theme", - "@kbn/actions-plugin", - "@kbn/user-profile-components", - "@kbn/core-http-browser", "@kbn/triggers-actions-ui-plugin", "@kbn/shared-ux-utility", - "@kbn/i18n-react", - "@kbn/code-editor", - "@kbn/monaco", "@kbn/data-views-plugin", "@kbn/lens-embeddable-utils", "@kbn/lens-plugin", @@ -38,7 +30,6 @@ "@kbn/esql-utils", "@kbn/visualization-utils", "@kbn/ai-assistant-management-plugin", - "@kbn/utility-types-jest", "@kbn/kibana-react-plugin", "@kbn/licensing-plugin", "@kbn/logging", @@ -56,6 +47,11 @@ "@kbn/apm-synthtrace-client", "@kbn/alerting-plugin", "@kbn/apm-synthtrace", + "@kbn/esql-datagrid", + "@kbn/alerting-comparators", + "@kbn/core-lifecycle-browser", + "@kbn/inference-plugin", + "@kbn/ai-assistant", "@kbn/apm-utils", "@kbn/config-schema", "@kbn/es-query", @@ -63,17 +59,17 @@ "@kbn/esql-validation-autocomplete", "@kbn/esql-ast", "@kbn/field-types", + "@kbn/security-plugin", + "@kbn/observability-plugin", + "@kbn/actions-plugin", "@kbn/stack-connectors-plugin", "@kbn/features-plugin", "@kbn/serverless", "@kbn/task-manager-plugin", "@kbn/cloud-plugin", - "@kbn/observability-plugin", - "@kbn/esql-datagrid", - "@kbn/alerting-comparators", - "@kbn/core-lifecycle-browser", - "@kbn/inference-plugin", - "@kbn/logs-data-access-plugin" + "@kbn/logs-data-access-plugin", ], - "exclude": ["target/**/*"] + "exclude": [ + "target/**/*" + ] } diff --git a/x-pack/plugins/search_assistant/kibana.jsonc b/x-pack/plugins/search_assistant/kibana.jsonc index 85579b76a1e80..8391ee14e0d88 100644 --- a/x-pack/plugins/search_assistant/kibana.jsonc +++ b/x-pack/plugins/search_assistant/kibana.jsonc @@ -12,13 +12,19 @@ "searchAssistant" ], "requiredPlugins": [ + "actions", + "licensing", "observabilityAIAssistant", - "observabilityAIAssistantApp" + "observabilityAIAssistantApp", + "triggersActionsUi", + "share" ], "optionalPlugins": [ "cloud", "usageCollection", ], - "requiredBundles": [] + "requiredBundles": [ + "kibanaReact" + ] } } diff --git a/x-pack/plugins/search_assistant/public/application.tsx b/x-pack/plugins/search_assistant/public/application.tsx index 071c51f4b6e13..1bbf7063ec373 100644 --- a/x-pack/plugins/search_assistant/public/application.tsx +++ b/x-pack/plugins/search_assistant/public/application.tsx @@ -7,31 +7,28 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import type { CoreStart } from '@kbn/core/public'; +import type { AppMountParameters, CoreStart } from '@kbn/core/public'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { I18nProvider } from '@kbn/i18n-react'; -import { Router } from '@kbn/shared-ux-router'; import type { SearchAssistantPluginStartDependencies } from './types'; -import { SearchAssistantRouter } from './router'; +import { SearchAssistantRouter } from './components/routes/router'; export const renderApp = ( core: CoreStart, services: SearchAssistantPluginStartDependencies, - element: HTMLElement + appMountParameters: AppMountParameters ) => { ReactDOM.render( - - - + , - element + appMountParameters.element ); - return () => ReactDOM.unmountComponentAtNode(element); + return () => ReactDOM.unmountComponentAtNode(appMountParameters.element); }; diff --git a/x-pack/plugins/search_assistant/public/components/page_template.tsx b/x-pack/plugins/search_assistant/public/components/page_template.tsx new file mode 100644 index 0000000000000..e9fb3a45e9e2b --- /dev/null +++ b/x-pack/plugins/search_assistant/public/components/page_template.tsx @@ -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. + */ +import React from 'react'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; + +export function SearchAIAssistantPageTemplate({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/x-pack/plugins/search_assistant/public/components/routes/conversations/conversation_view_with_props.tsx b/x-pack/plugins/search_assistant/public/components/routes/conversations/conversation_view_with_props.tsx new file mode 100644 index 0000000000000..545ff1ceb7370 --- /dev/null +++ b/x-pack/plugins/search_assistant/public/components/routes/conversations/conversation_view_with_props.tsx @@ -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. + */ + +import React from 'react'; +import { ConversationView } from '@kbn/ai-assistant'; +import { useParams } from 'react-router-dom'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; + +export function ConversationViewWithProps() { + const { conversationId } = useParams<{ conversationId?: string }>(); + const { + services: { application, http }, + } = useKibana(); + function navigateToConversation(nextConversationId?: string) { + application?.navigateToUrl( + http?.basePath.prepend(`/app/searchAssistant/conversations/${nextConversationId || ''}`) || '' + ); + } + return ( + + http?.basePath.prepend(`/app/searchAssistant/conversations/${id || ''}`) || '' + } + /> + ); +} diff --git a/x-pack/plugins/search_assistant/public/components/routes/router.tsx b/x-pack/plugins/search_assistant/public/components/routes/router.tsx new file mode 100644 index 0000000000000..154bc2ab46a3e --- /dev/null +++ b/x-pack/plugins/search_assistant/public/components/routes/router.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 { History } from 'history'; +import { Route, Router, Routes } from '@kbn/shared-ux-router'; +import { Redirect } from 'react-router-dom'; +import { SearchAIAssistantPageTemplate } from '../page_template'; +import { ConversationViewWithProps } from './conversations/conversation_view_with_props'; + +export const SearchAssistantRouter: React.FC<{ history: History }> = ({ history }) => { + return ( + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/search_assistant/public/components/search_assistant.tsx b/x-pack/plugins/search_assistant/public/components/search_assistant.tsx deleted file mode 100644 index 9c227a4e7b73f..0000000000000 --- a/x-pack/plugins/search_assistant/public/components/search_assistant.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 { EuiPageTemplate } from '@elastic/eui'; -import React from 'react'; -import { App } from './app'; - -export const SearchAssistantPage: React.FC = () => { - return ( - - - - ); -}; diff --git a/x-pack/plugins/search_assistant/public/index.ts b/x-pack/plugins/search_assistant/public/index.ts index c2b16e857b53e..cb84f8519fd96 100644 --- a/x-pack/plugins/search_assistant/public/index.ts +++ b/x-pack/plugins/search_assistant/public/index.ts @@ -5,9 +5,19 @@ * 2.0. */ -import { SearchAssistantPlugin } from './plugin'; +import { PluginInitializer, PluginInitializerContext } from '@kbn/core/public'; +import { PublicConfigType, SearchAssistantPlugin } from './plugin'; +import { + SearchAssistantPluginSetup, + SearchAssistantPluginStart, + SearchAssistantPluginStartDependencies, +} from './types'; + +export const plugin: PluginInitializer< + SearchAssistantPluginSetup, + SearchAssistantPluginStart, + {}, + SearchAssistantPluginStartDependencies +> = (context: PluginInitializerContext) => new SearchAssistantPlugin(context); -export function plugin() { - return new SearchAssistantPlugin(); -} export type { SearchAssistantPluginSetup, SearchAssistantPluginStart } from './types'; diff --git a/x-pack/plugins/search_assistant/public/plugin.ts b/x-pack/plugins/search_assistant/public/plugin.ts index 8ba22a48df9ff..1c09502c154ad 100644 --- a/x-pack/plugins/search_assistant/public/plugin.ts +++ b/x-pack/plugins/search_assistant/public/plugin.ts @@ -5,19 +5,71 @@ * 2.0. */ -import type { CoreSetup, Plugin } from '@kbn/core/public'; +import { + DEFAULT_APP_CATEGORIES, + type CoreSetup, + type Plugin, + CoreStart, + AppMountParameters, + PluginInitializerContext, +} from '@kbn/core/public'; +import { i18n } from '@kbn/i18n'; import type { SearchAssistantPluginSetup, SearchAssistantPluginStart, SearchAssistantPluginStartDependencies, } from './types'; +export interface PublicConfigType { + ui: { + enabled: boolean; + }; +} + export class SearchAssistantPlugin - implements Plugin + implements + Plugin< + SearchAssistantPluginSetup, + SearchAssistantPluginStart, + {}, + SearchAssistantPluginStartDependencies + > { + private readonly config: PublicConfigType; + + constructor(private readonly context: PluginInitializerContext) { + this.config = this.context.config.get(); + } + public setup( core: CoreSetup ): SearchAssistantPluginSetup { + if (!this.config.ui.enabled) { + return {}; + } + + core.application.register({ + id: 'searchAssistant', + title: i18n.translate('xpack.searchAssistant.appTitle', { + defaultMessage: 'Search Assistant', + }), + euiIconType: 'logoEnterpriseSearch', + appRoute: '/app/searchAssistant', + category: DEFAULT_APP_CATEGORIES.search, + visibleIn: [], + deepLinks: [], + mount: async (appMountParameters: AppMountParameters) => { + // Load application bundle and Get start services + const [{ renderApp }, [coreStart, pluginsStart]] = await Promise.all([ + import('./application'), + core.getStartServices() as Promise< + [CoreStart, SearchAssistantPluginStartDependencies, unknown] + >, + ]); + + return renderApp(coreStart, pluginsStart, appMountParameters); + }, + }); return {}; } diff --git a/x-pack/plugins/search_assistant/public/router.tsx b/x-pack/plugins/search_assistant/public/router.tsx deleted file mode 100644 index a25f865b4f74a..0000000000000 --- a/x-pack/plugins/search_assistant/public/router.tsx +++ /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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Route, Routes } from '@kbn/shared-ux-router'; -import React from 'react'; -import { SearchAssistantPage } from './components/search_assistant'; - -export const SearchAssistantRouter: React.FC = () => { - return ( - - - - - - ); -}; diff --git a/x-pack/plugins/search_assistant/public/types.ts b/x-pack/plugins/search_assistant/public/types.ts index f05592414a9dc..b1a5d6164b1f1 100644 --- a/x-pack/plugins/search_assistant/public/types.ts +++ b/x-pack/plugins/search_assistant/public/types.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { AppMountParameters } from '@kbn/core/public'; import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; import { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public'; @@ -16,7 +15,6 @@ export interface SearchAssistantPluginSetup {} export interface SearchAssistantPluginStart {} export interface SearchAssistantPluginStartDependencies { - history: AppMountParameters['history']; observabilityAIAssistant: ObservabilityAIAssistantPublicStart; usageCollection?: UsageCollectionStart; } diff --git a/x-pack/plugins/search_assistant/server/config.ts b/x-pack/plugins/search_assistant/server/config.ts index a09b7ac51b7b7..5ca081ec8a667 100644 --- a/x-pack/plugins/search_assistant/server/config.ts +++ b/x-pack/plugins/search_assistant/server/config.ts @@ -9,11 +9,19 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { PluginConfigDescriptor } from '@kbn/core/server'; const configSchema = schema.object({ - enabled: schema.boolean({ defaultValue: false }), + enabled: schema.boolean({ defaultValue: true }), + ui: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + }), }); export type SearchAssistantConfig = TypeOf; export const config: PluginConfigDescriptor = { + exposeToBrowser: { + ui: { + enabled: true, + }, + }, schema: configSchema, }; diff --git a/x-pack/plugins/search_assistant/tsconfig.json b/x-pack/plugins/search_assistant/tsconfig.json index 090356cf1f440..d865d2bdbff83 100644 --- a/x-pack/plugins/search_assistant/tsconfig.json +++ b/x-pack/plugins/search_assistant/tsconfig.json @@ -16,11 +16,13 @@ "@kbn/react-kibana-context-render", "@kbn/kibana-react-plugin", "@kbn/i18n-react", - "@kbn/shared-ux-router", "@kbn/shared-ux-page-kibana-template", "@kbn/usage-collection-plugin", "@kbn/observability-ai-assistant-plugin", - "@kbn/config-schema" + "@kbn/config-schema", + "@kbn/ai-assistant", + "@kbn/i18n", + "@kbn/shared-ux-router" ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index c6f8753f75b9e..41d1b6ab8b3d1 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -9376,6 +9376,94 @@ "xpack.actions.serverSideErrors.unavailableLicenseInformationErrorMessage": "Les actions sont indisponibles - les informations de licence ne sont pas disponibles actuellement.", "xpack.actions.subActionsFramework.urlValidationError": "Erreur lors de la validation de l'URL : {message}", "xpack.actions.urlAllowedHostsConfigurationError": "Le {field} cible \"{value}\" n'est pas ajouté à la configuration Kibana xpack.actions.allowedHosts", + "xpack.aiAssistant.askAssistantButton.buttonLabel": "Demander à l'assistant", + "xpack.aiAssistant.askAssistantButton.popoverContent": "Obtenez des informations relatives à vos données grâce à l'assistant d'Elastic", + "xpack.aiAssistant.assistantSetup.title": "Bienvenue sur l'assistant d'intelligence artificielle d'Elastic", + "xpack.aiAssistant.chatActionsMenu.euiButtonIcon.menuLabel": "Menu", + "xpack.aiAssistant.chatActionsMenu.euiToolTip.moreActionsLabel": "Plus d'actions", + "xpack.aiAssistant.chatCollapsedItems.hideEvents": "Masquer {count} événements", + "xpack.aiAssistant.chatCollapsedItems.showEvents": "Montrer {count} événements", + "xpack.aiAssistant.chatCollapsedItems.toggleButtonLabel": "Afficher/masquer les éléments", + "xpack.aiAssistant.chatFlyout.euiButtonIcon.expandConversationListLabel": "Développer la liste des conversations", + "xpack.aiAssistant.chatFlyout.euiButtonIcon.newChatLabel": "Nouveau chat", + "xpack.aiAssistant.chatFlyout.euiToolTip.collapseConversationListLabel": "Réduire la liste des conversations", + "xpack.aiAssistant.chatFlyout.euiToolTip.expandConversationListLabel": "Développer la liste des conversations", + "xpack.aiAssistant.chatFlyout.euiToolTip.newChatLabel": "Nouveau chat", + "xpack.aiAssistant.chatHeader.actions.connector": "Connecteur", + "xpack.aiAssistant.chatHeader.actions.copyConversation": "Copier la conversation", + "xpack.aiAssistant.chatHeader.actions.knowledgeBase": "Gérer la base de connaissances", + "xpack.aiAssistant.chatHeader.actions.settings": "Réglages de l'assistant d'IA", + "xpack.aiAssistant.chatHeader.actions.title": "Actions", + "xpack.aiAssistant.chatHeader.editConversationInput": "Modifier la conversation", + "xpack.aiAssistant.chatHeader.euiButtonIcon.navigateToConversationsLabel": "Accéder aux conversations", + "xpack.aiAssistant.chatHeader.euiButtonIcon.toggleFlyoutModeLabel": "Afficher / Masquer le mode menu volant", + "xpack.aiAssistant.chatHeader.euiToolTip.flyoutModeLabel.dock": "Ancrer le chat", + "xpack.aiAssistant.chatHeader.euiToolTip.flyoutModeLabel.undock": "Désancrer le chat", + "xpack.aiAssistant.chatHeader.euiToolTip.navigateToConversationsLabel": "Accéder aux conversations", + "xpack.aiAssistant.chatPromptEditor.codeEditor.payloadEditorLabel": "payloadEditor", + "xpack.aiAssistant.chatPromptEditor.euiButtonIcon.submitLabel": "Envoyer", + "xpack.aiAssistant.chatTimeline.actions.copyMessage": "Copier le message", + "xpack.aiAssistant.chatTimeline.actions.copyMessageSuccessful": "Message copié", + "xpack.aiAssistant.chatTimeline.actions.editPrompt": "Modifier l'invite", + "xpack.aiAssistant.chatTimeline.actions.inspectPrompt": "Inspecter l'invite", + "xpack.aiAssistant.chatTimeline.messages.elasticAssistant.label": "Assistant d'Elastic", + "xpack.aiAssistant.chatTimeline.messages.system.label": "Système", + "xpack.aiAssistant.chatTimeline.messages.user.label": "Vous", + "xpack.aiAssistant.checkingKbAvailability": "Vérification de la disponibilité de la base de connaissances", + "xpack.aiAssistant.conversationStartTitle": "a démarré une conversation", + "xpack.aiAssistant.couldNotFindConversationContent": "Impossible de trouver une conversation avec l'ID {conversationId}. Assurez-vous que la conversation existe et que vous y avez accès.", + "xpack.aiAssistant.couldNotFindConversationTitle": "Conversation introuvable", + "xpack.aiAssistant.disclaimer.disclaimerLabel": "Ce chat est soutenu par une intégration avec votre fournisseur LLM. Il arrive que les grands modèles de langage (LLM) présentent comme correctes des informations incorrectes. Elastic prend en charge la configuration ainsi que la connexion au fournisseur LLM et à votre base de connaissances, mais n'est pas responsable des réponses fournies par le LLM.", + "xpack.aiAssistant.emptyConversationTitle": "Nouvelle conversation", + "xpack.aiAssistant.errorSettingUpKnowledgeBase": "Impossible de configurer la base de connaissances", + "xpack.aiAssistant.errorUpdatingConversation": "Impossible de mettre à jour la conversation", + "xpack.aiAssistant.executedFunctionFailureEvent": "impossible d'exécuter la fonction {functionName}", + "xpack.aiAssistant.failedToGetStatus": "Échec de l'obtention du statut du modèle.", + "xpack.aiAssistant.failedToSetupKnowledgeBase": "Échec de la configuration de la base de connaissances.", + "xpack.aiAssistant.flyout.confirmDeleteButtonText": "Supprimer la conversation", + "xpack.aiAssistant.flyout.confirmDeleteConversationContent": "Cette action ne peut pas être annulée.", + "xpack.aiAssistant.flyout.confirmDeleteConversationTitle": "Supprimer cette conversation ?", + "xpack.aiAssistant.flyout.failedToDeleteConversation": "Impossible de supprimer la conversation", + "xpack.aiAssistant.functionListPopover.euiButtonIcon.selectAFunctionLabel": "Sélectionner la fonction", + "xpack.aiAssistant.functionListPopover.euiToolTip.clearFunction": "Effacer la fonction", + "xpack.aiAssistant.functionListPopover.euiToolTip.selectAFunctionLabel": "Sélectionner une fonction", + "xpack.aiAssistant.hideExpandConversationButton.hide": "Masquer les chats", + "xpack.aiAssistant.hideExpandConversationButton.show": "Afficher les chats", + "xpack.aiAssistant.incorrectLicense.body": "Une licence d'entreprise est requise pour utiliser l'assistant d'intelligence artificielle d'Elastic.", + "xpack.aiAssistant.incorrectLicense.manageLicense": "Gérer la licence", + "xpack.aiAssistant.incorrectLicense.subscriptionPlansButton": "Plans d'abonnement", + "xpack.aiAssistant.incorrectLicense.title": "Mettez votre licence à niveau", + "xpack.aiAssistant.initialSetupPanel.setupConnector.buttonLabel": "Configurer un connecteur GenAI", + "xpack.aiAssistant.initialSetupPanel.setupConnector.description2": "Commencez à travailler avec l'assistant AI Elastic en configurant un connecteur pour votre fournisseur d'IA. Le modèle doit prendre en charge les appels de fonction. Lorsque vous utilisez OpenAI ou Azure, nous vous recommandons d'utiliser GPT4.", + "xpack.aiAssistant.installingKb": "Configuration de la base de connaissances", + "xpack.aiAssistant.newChatButton": "Nouveau chat", + "xpack.aiAssistant.poweredByModel": "Alimenté par {model}", + "xpack.aiAssistant.prompt.functionList.filter": "Filtre", + "xpack.aiAssistant.prompt.functionList.functionList": "Liste de fonctions", + "xpack.aiAssistant.prompt.placeholder": "Envoyer un message à l'assistant", + "xpack.aiAssistant.promptEditorNaturalLanguage.euiSelectable.selectAnOptionLabel": "Sélectionner une option", + "xpack.aiAssistant.settingsPage.goToConnectorsButtonLabel": "Gérer les connecteurs", + "xpack.aiAssistant.setupKb": "Améliorez votre expérience en configurant la base de connaissances.", + "xpack.aiAssistant.simulatedFunctionCallingCalloutLabel": "L'appel de fonctions simulées est activé. Vous risquez de voir les performances se dégrader.", + "xpack.aiAssistant.suggestedFunctionEvent": "a demandé la fonction {functionName}", + "xpack.aiAssistant.technicalPreviewBadgeDescription": "GTP4 est nécessaire pour bénéficier d'une meilleure expérience avec les appels de fonctions (par exemple lors de la réalisation d'analyse de la cause d'un problème, de la visualisation de données et autres). GPT3.5 peut fonctionner pour certains des workflows les plus simples comme les explications d'erreurs ou pour bénéficier d'une expérience comparable à ChatGPT au sein de Kibana à partir du moment où les appels de fonctions ne sont pas fréquents.", + "xpack.aiAssistant.userExecutedFunctionEvent": "a exécuté la fonction {functionName}", + "xpack.aiAssistant.userSuggestedFunctionEvent": "a demandé la fonction {functionName}", + "xpack.aiAssistant.welcomeMessage.div.checkTrainedModelsToLabel": " {retryInstallingLink} ou vérifiez {trainedModelsLink} pour vous assurer que {modelName} est déployé et en cours d'exécution.", + "xpack.aiAssistant.welcomeMessage.div.settingUpKnowledgeBaseLabel": "Configuration de la base de connaissances", + "xpack.aiAssistant.welcomeMessage.inspectErrorsButtonEmptyLabel": "Inspecter les problèmes", + "xpack.aiAssistant.welcomeMessage.issuesDescriptionListTitleLabel": "Problèmes", + "xpack.aiAssistant.welcomeMessage.knowledgeBaseSuccessfullyInstalledLabel": "La base de connaissances a été installée avec succès", + "xpack.aiAssistant.welcomeMessage.modelIsNotDeployedLabel": "Le modèle {modelName} n'est pas déployé", + "xpack.aiAssistant.welcomeMessage.modelIsNotFullyAllocatedLabel": "L'état d'allocation de {modelName} est {allocationState}", + "xpack.aiAssistant.welcomeMessage.modelIsNotStartedLabel": "L'état de déploiement de {modelName} est {deploymentState}", + "xpack.aiAssistant.welcomeMessage.retryButtonLabel": "Installer la base de connaissances", + "xpack.aiAssistant.welcomeMessage.trainedModelsLinkLabel": "Modèles entraînés", + "xpack.aiAssistant.welcomeMessage.weAreSettingUpTextLabel": "Nous configurons votre base de connaissances. Cette opération peut prendre quelques minutes. Vous pouvez continuer à utiliser l'Assistant lors de ce processus.", + "xpack.aiAssistant.welcomeMessageConnectors.connectorsErrorTextLabel": "Impossible de charger les connecteurs", + "xpack.aiAssistant.welcomeMessageConnectors.connectorsForbiddenTextLabel": "Vous n'avez pas les autorisations requises pour charger les connecteurs", + "xpack.aiAssistant.welcomeMessageKnowledgeBase.yourKnowledgeBaseIsNotSetUpCorrectlyLabel": "Votre base de connaissances n'a pas été configurée.", + "xpack.aiAssistant.welcomeMessageKnowledgeBaseSetupErrorPanel.retryInstallingLinkLabel": "Réessayer l'installation", "xpack.aiops.actions.openChangePointInMlAppName": "Ouvrir dans AIOps Labs", "xpack.aiops.analysis.columnSelectorAriaLabel": "Filtrer les colonnes", "xpack.aiops.analysis.columnSelectorNotEnoughColumnsSelected": "Au moins une colonne doit être sélectionnée.", @@ -32542,92 +32630,27 @@ "xpack.observabilityAiAssistant.app.starterPrompts.whatAreSlos.prompt": "Que signifie \"SLO\" ?", "xpack.observabilityAiAssistant.app.starterPrompts.whatAreSlos.title": "SLO", "xpack.observabilityAiAssistant.appTitle": "Assistant d'intelligence artificielle d'Observability", - "xpack.observabilityAiAssistant.askAssistantButton.buttonLabel": "Demander à l'assistant", - "xpack.observabilityAiAssistant.askAssistantButton.popoverContent": "Obtenez des informations relatives à vos données grâce à l'assistant d'Elastic", - "xpack.observabilityAiAssistant.askAssistantButton.popoverTitle": "Assistant d'IA pour Observability", - "xpack.observabilityAiAssistant.assistantSetup.title": "Bienvenue sur l'assistant d'intelligence artificielle d'Elastic", "xpack.observabilityAiAssistant.changesList.dotImpactHigh": "Élevé", "xpack.observabilityAiAssistant.changesList.dotImpactLow": "Bas", "xpack.observabilityAiAssistant.changesList.dotImpactMedium": "Moyenne", "xpack.observabilityAiAssistant.changesList.labelColumnTitle": "Étiquette", "xpack.observabilityAiAssistant.changesList.noChangesDetected": "Aucun changement détecté", "xpack.observabilityAiAssistant.changesList.trendColumnTitle": "Tendance", - "xpack.observabilityAiAssistant.chatActionsMenu.euiButtonIcon.menuLabel": "Menu", - "xpack.observabilityAiAssistant.chatActionsMenu.euiToolTip.moreActionsLabel": "Plus d'actions", - "xpack.observabilityAiAssistant.chatCollapsedItems.hideEvents": "Masquer {count} événements", - "xpack.observabilityAiAssistant.chatCollapsedItems.showEvents": "Montrer {count} événements", - "xpack.observabilityAiAssistant.chatCollapsedItems.toggleButtonLabel": "Afficher/masquer les éléments", "xpack.observabilityAiAssistant.chatCompletionError.conversationNotFoundError": "Conversation introuvable", "xpack.observabilityAiAssistant.chatCompletionError.internalServerError": "Une erreur s'est produite au niveau du serveur interne", "xpack.observabilityAiAssistant.chatCompletionError.tokenLimitReachedError": "Limite de token atteinte. La limite de token est {tokenLimit}, mais la conversation actuelle a {tokenCount} tokens.", - "xpack.observabilityAiAssistant.chatFlyout.euiButtonIcon.expandConversationListLabel": "Développer la liste des conversations", - "xpack.observabilityAiAssistant.chatFlyout.euiButtonIcon.newChatLabel": "Nouveau chat", - "xpack.observabilityAiAssistant.chatFlyout.euiFlyoutResizable.aiAssistantForObservabilityLabel": "Menu volant du chat de l'assistant d'IA pour Observability", - "xpack.observabilityAiAssistant.chatFlyout.euiToolTip.collapseConversationListLabel": "Réduire la liste des conversations", - "xpack.observabilityAiAssistant.chatFlyout.euiToolTip.expandConversationListLabel": "Développer la liste des conversations", - "xpack.observabilityAiAssistant.chatFlyout.euiToolTip.newChatLabel": "Nouveau chat", - "xpack.observabilityAiAssistant.chatHeader.actions.connector": "Connecteur", - "xpack.observabilityAiAssistant.chatHeader.actions.copyConversation": "Copier la conversation", - "xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase": "Gérer la base de connaissances", - "xpack.observabilityAiAssistant.chatHeader.actions.settings": "Réglages de l'assistant d'IA", - "xpack.observabilityAiAssistant.chatHeader.actions.title": "Actions", - "xpack.observabilityAiAssistant.chatHeader.editConversationInput": "Modifier la conversation", - "xpack.observabilityAiAssistant.chatHeader.euiButtonIcon.navigateToConversationsLabel": "Accéder aux conversations", - "xpack.observabilityAiAssistant.chatHeader.euiButtonIcon.toggleFlyoutModeLabel": "Afficher / Masquer le mode menu volant", - "xpack.observabilityAiAssistant.chatHeader.euiToolTip.flyoutModeLabel.dock": "Ancrer le chat", - "xpack.observabilityAiAssistant.chatHeader.euiToolTip.flyoutModeLabel.undock": "Désancrer le chat", - "xpack.observabilityAiAssistant.chatHeader.euiToolTip.navigateToConversationsLabel": "Accéder aux conversations", - "xpack.observabilityAiAssistant.chatPromptEditor.codeEditor.payloadEditorLabel": "payloadEditor", - "xpack.observabilityAiAssistant.chatPromptEditor.euiButtonIcon.submitLabel": "Envoyer", "xpack.observabilityAiAssistant.chatService.div.helloLabel": "Bonjour", - "xpack.observabilityAiAssistant.chatTimeline.actions.copyMessage": "Copier le message", - "xpack.observabilityAiAssistant.chatTimeline.actions.copyMessageSuccessful": "Message copié", - "xpack.observabilityAiAssistant.chatTimeline.actions.editPrompt": "Modifier l'invite", - "xpack.observabilityAiAssistant.chatTimeline.actions.inspectPrompt": "Inspecter l'invite", - "xpack.observabilityAiAssistant.chatTimeline.messages.elasticAssistant.label": "Assistant d'Elastic", - "xpack.observabilityAiAssistant.chatTimeline.messages.system.label": "Système", - "xpack.observabilityAiAssistant.chatTimeline.messages.user.label": "Vous", - "xpack.observabilityAiAssistant.checkingKbAvailability": "Vérification de la disponibilité de la base de connaissances", "xpack.observabilityAiAssistant.connectorSelector.connectorSelectLabel": "Connecteur :", "xpack.observabilityAiAssistant.connectorSelector.empty": "Aucun connecteur", "xpack.observabilityAiAssistant.connectorSelector.error": "Impossible de charger les connecteurs", - "xpack.observabilityAiAssistant.conversationList.deleteConversationIconLabel": "Supprimer", - "xpack.observabilityAiAssistant.conversationList.errorMessage": "Échec de chargement", - "xpack.observabilityAiAssistant.conversationList.noConversations": "Aucune conversation", - "xpack.observabilityAiAssistant.conversationList.title": "Précédemment", "xpack.observabilityAiAssistant.conversationsDeepLinkTitle": "Conversations", - "xpack.observabilityAiAssistant.conversationStartTitle": "a démarré une conversation", - "xpack.observabilityAiAssistant.couldNotFindConversationContent": "Impossible de trouver une conversation avec l'ID {conversationId}. Assurez-vous que la conversation existe et que vous y avez accès.", - "xpack.observabilityAiAssistant.couldNotFindConversationTitle": "Conversation introuvable", - "xpack.observabilityAiAssistant.disclaimer.disclaimerLabel": "Ce chat est soutenu par une intégration avec votre fournisseur LLM. Il arrive que les grands modèles de langage (LLM) présentent comme correctes des informations incorrectes. Elastic prend en charge la configuration ainsi que la connexion au fournisseur LLM et à votre base de connaissances, mais n'est pas responsable des réponses fournies par le LLM.", - "xpack.observabilityAiAssistant.emptyConversationTitle": "Nouvelle conversation", - "xpack.observabilityAiAssistant.errorSettingUpKnowledgeBase": "Impossible de configurer la base de connaissances", - "xpack.observabilityAiAssistant.errorUpdatingConversation": "Impossible de mettre à jour la conversation", - "xpack.observabilityAiAssistant.executedFunctionFailureEvent": "impossible d'exécuter la fonction {functionName}", "xpack.observabilityAiAssistant.experimentalTitle": "Version d'évaluation technique", "xpack.observabilityAiAssistant.experimentalTooltip": "Cette fonctionnalité est en version d'évaluation technique et pourra être modifiée ou retirée complètement dans une future version. Elastic s'efforcera de corriger tout problème, mais les fonctionnalités des versions d'évaluation technique ne sont pas soumises aux SLA de support des fonctionnalités officielles en disponibilité générale.", "xpack.observabilityAiAssistant.failedLoadingResponseText": "Échec de chargement de la réponse", - "xpack.observabilityAiAssistant.failedToGetStatus": "Échec de l'obtention du statut du modèle.", "xpack.observabilityAiAssistant.failedToLoadResponse": "Échec du chargement d'une réponse de l'assistant d'intelligence artificielle", - "xpack.observabilityAiAssistant.failedToSetupKnowledgeBase": "Échec de la configuration de la base de connaissances.", "xpack.observabilityAiAssistant.featureRegistry.featureName": "Assistant d'intelligence artificielle d'Observability", "xpack.observabilityAiAssistant.feedbackButtons.em.thanksForYourFeedbackLabel": "Merci pour vos retours", - "xpack.observabilityAiAssistant.flyout.confirmDeleteButtonText": "Supprimer la conversation", - "xpack.observabilityAiAssistant.flyout.confirmDeleteConversationContent": "Cette action ne peut pas être annulée.", - "xpack.observabilityAiAssistant.flyout.confirmDeleteConversationTitle": "Supprimer cette conversation ?", - "xpack.observabilityAiAssistant.flyout.failedToDeleteConversation": "Impossible de supprimer la conversation", "xpack.observabilityAiAssistant.functionCallLimitExceeded": "\n\nRemarque : l'Assistant a essayé d'appeler une fonction, même si la limite a été dépassée", - "xpack.observabilityAiAssistant.functionListPopover.euiButtonIcon.selectAFunctionLabel": "Sélectionner la fonction", - "xpack.observabilityAiAssistant.functionListPopover.euiToolTip.clearFunction": "Effacer la fonction", - "xpack.observabilityAiAssistant.functionListPopover.euiToolTip.selectAFunctionLabel": "Sélectionner une fonction", - "xpack.observabilityAiAssistant.hideExpandConversationButton.hide": "Masquer les chats", - "xpack.observabilityAiAssistant.hideExpandConversationButton.show": "Afficher les chats", - "xpack.observabilityAiAssistant.incorrectLicense.body": "Une licence d'entreprise est requise pour utiliser l'assistant d'intelligence artificielle d'Elastic.", - "xpack.observabilityAiAssistant.incorrectLicense.manageLicense": "Gérer la licence", - "xpack.observabilityAiAssistant.incorrectLicense.subscriptionPlansButton": "Plans d'abonnement", - "xpack.observabilityAiAssistant.incorrectLicense.title": "Mettez votre licence à niveau", - "xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.buttonLabel": "Configurer un connecteur GenAI", - "xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.description2": "Commencez à travailler avec l'assistant AI Elastic en configurant un connecteur pour votre fournisseur d'IA. Le modèle doit prendre en charge les appels de fonction. Lorsque vous utilisez OpenAI ou Azure, nous vous recommandons d'utiliser GPT4.", "xpack.observabilityAiAssistant.insight.actions": "Actions", "xpack.observabilityAiAssistant.insight.actions.connector": "Connecteur", "xpack.observabilityAiAssistant.insight.actions.editPrompt": "Modifier l'invite", @@ -32645,7 +32668,6 @@ "xpack.observabilityAiAssistant.insight.response.startChat": "Lancer le chat", "xpack.observabilityAiAssistant.insight.sendPromptEdit": "Envoyer l'invite", "xpack.observabilityAiAssistant.insightModifiedPrompt": "Cette information a été modifiée.", - "xpack.observabilityAiAssistant.installingKb": "Configuration de la base de connaissances", "xpack.observabilityAiAssistant.lensESQLFunction.displayChart": "Afficher le graphique", "xpack.observabilityAiAssistant.lensESQLFunction.displayTable": "Afficher le tableau", "xpack.observabilityAiAssistant.lensESQLFunction.edit": "Modifier la visualisation", @@ -32660,42 +32682,14 @@ "xpack.observabilityAiAssistant.missingCredentialsCallout.title": "Informations d'identification manquantes", "xpack.observabilityAiAssistant.navControl.initFailureErrorTitle": "Échec de l'initialisation de l'assistant d'IA d'Observability", "xpack.observabilityAiAssistant.navControl.openTheAIAssistantPopoverLabel": "Ouvrir l'assistant d'IA", - "xpack.observabilityAiAssistant.newChatButton": "Nouveau chat", - "xpack.observabilityAiAssistant.poweredByModel": "Alimenté par {model}", - "xpack.observabilityAiAssistant.prompt.functionList.filter": "Filtre", - "xpack.observabilityAiAssistant.prompt.functionList.functionList": "Liste de fonctions", - "xpack.observabilityAiAssistant.prompt.placeholder": "Envoyer un message à l'assistant", - "xpack.observabilityAiAssistant.promptEditorNaturalLanguage.euiSelectable.selectAnOptionLabel": "Sélectionner une option", "xpack.observabilityAiAssistant.regenerateResponseButtonLabel": "Régénérer", "xpack.observabilityAiAssistant.requiredConnectorField": "Connecteur obligatoire.", "xpack.observabilityAiAssistant.requiredMessageTextField": "Le message est requis.", "xpack.observabilityAiAssistant.resetDefaultPrompt": "Réinitialiser à la valeur par défaut", "xpack.observabilityAiAssistant.runThisQuery": "Afficher les résultats", - "xpack.observabilityAiAssistant.settingsPage.goToConnectorsButtonLabel": "Gérer les connecteurs", - "xpack.observabilityAiAssistant.setupKb": "Améliorez votre expérience en configurant la base de connaissances.", - "xpack.observabilityAiAssistant.simulatedFunctionCallingCalloutLabel": "L'appel de fonctions simulées est activé. Vous risquez de voir les performances se dégrader.", "xpack.observabilityAiAssistant.stopGeneratingButtonLabel": "Arrêter la génération", - "xpack.observabilityAiAssistant.suggestedFunctionEvent": "a demandé la fonction {functionName}", - "xpack.observabilityAiAssistant.technicalPreviewBadgeDescription": "GTP4 est nécessaire pour bénéficier d'une meilleure expérience avec les appels de fonctions (par exemple lors de la réalisation d'analyse de la cause d'un problème, de la visualisation de données et autres). GPT3.5 peut fonctionner pour certains des workflows les plus simples comme les explications d'erreurs ou pour bénéficier d'une expérience comparable à ChatGPT au sein de Kibana à partir du moment où les appels de fonctions ne sont pas fréquents.", "xpack.observabilityAiAssistant.tokenLimitError": "La conversation dépasse la limite de token. La limite de token maximale est **{tokenLimit}**, mais la conversation a **{tokenCount}** tokens. Veuillez démarrer une nouvelle conversation pour continuer.", - "xpack.observabilityAiAssistant.userExecutedFunctionEvent": "a exécuté la fonction {functionName}", - "xpack.observabilityAiAssistant.userSuggestedFunctionEvent": "a demandé la fonction {functionName}", "xpack.observabilityAiAssistant.visualizeThisQuery": "Visualiser cette requête", - "xpack.observabilityAiAssistant.welcomeMessage.div.checkTrainedModelsToLabel": " {retryInstallingLink} ou vérifiez {trainedModelsLink} pour vous assurer que {modelName} est déployé et en cours d'exécution.", - "xpack.observabilityAiAssistant.welcomeMessage.div.settingUpKnowledgeBaseLabel": "Configuration de la base de connaissances", - "xpack.observabilityAiAssistant.welcomeMessage.inspectErrorsButtonEmptyLabel": "Inspecter les problèmes", - "xpack.observabilityAiAssistant.welcomeMessage.issuesDescriptionListTitleLabel": "Problèmes", - "xpack.observabilityAiAssistant.welcomeMessage.knowledgeBaseSuccessfullyInstalledLabel": "La base de connaissances a été installée avec succès", - "xpack.observabilityAiAssistant.welcomeMessage.modelIsNotDeployedLabel": "Le modèle {modelName} n'est pas déployé", - "xpack.observabilityAiAssistant.welcomeMessage.modelIsNotFullyAllocatedLabel": "L'état d'allocation de {modelName} est {allocationState}", - "xpack.observabilityAiAssistant.welcomeMessage.modelIsNotStartedLabel": "L'état de déploiement de {modelName} est {deploymentState}", - "xpack.observabilityAiAssistant.welcomeMessage.retryButtonLabel": "Installer la base de connaissances", - "xpack.observabilityAiAssistant.welcomeMessage.trainedModelsLinkLabel": "Modèles entraînés", - "xpack.observabilityAiAssistant.welcomeMessage.weAreSettingUpTextLabel": "Nous configurons votre base de connaissances. Cette opération peut prendre quelques minutes. Vous pouvez continuer à utiliser l'Assistant lors de ce processus.", - "xpack.observabilityAiAssistant.welcomeMessageConnectors.connectorsErrorTextLabel": "Impossible de charger les connecteurs", - "xpack.observabilityAiAssistant.welcomeMessageConnectors.connectorsForbiddenTextLabel": "Vous n'avez pas les autorisations requises pour charger les connecteurs", - "xpack.observabilityAiAssistant.welcomeMessageKnowledgeBase.yourKnowledgeBaseIsNotSetUpCorrectlyLabel": "Votre base de connaissances n'a pas été configurée.", - "xpack.observabilityAiAssistant.welcomeMessageKnowledgeBaseSetupErrorPanel.retryInstallingLinkLabel": "Réessayer l'installation", "xpack.observabilityAiAssistantManagement.apmSettings.save.error": "Une erreur s'est produite lors de l'enregistrement des paramètres", "xpack.observabilityAiAssistantManagement.app.description": "Gérer l'Assistant d'IA pour Observability.", "xpack.observabilityAiAssistantManagement.app.title": "Assistant d'IA pour Observability", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 19a01d7325113..9361689702bb4 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9130,6 +9130,94 @@ "xpack.actions.serverSideErrors.unavailableLicenseInformationErrorMessage": "グラフを利用できません。現在ライセンス情報が利用できません。", "xpack.actions.subActionsFramework.urlValidationError": "URLの検証エラー:{message}", "xpack.actions.urlAllowedHostsConfigurationError": "ターゲット{field}「{value}」はKibana構成xpack.actions.allowedHostsに追加されていません", + "xpack.aiAssistant.askAssistantButton.buttonLabel": "アシスタントに聞く", + "xpack.aiAssistant.askAssistantButton.popoverContent": "Elastic Assistantでデータに関するインサイトを得ましょう", + "xpack.aiAssistant.assistantSetup.title": "Elastic AI Assistantへようこそ", + "xpack.aiAssistant.chatActionsMenu.euiButtonIcon.menuLabel": "メニュー", + "xpack.aiAssistant.chatActionsMenu.euiToolTip.moreActionsLabel": "さらにアクションを表示", + "xpack.aiAssistant.chatCollapsedItems.hideEvents": "{count}件のイベントを非表示", + "xpack.aiAssistant.chatCollapsedItems.showEvents": "{count}件のイベントを表示", + "xpack.aiAssistant.chatCollapsedItems.toggleButtonLabel": "アイテムを表示/非表示", + "xpack.aiAssistant.chatFlyout.euiButtonIcon.expandConversationListLabel": "会話リストを展開", + "xpack.aiAssistant.chatFlyout.euiButtonIcon.newChatLabel": "新しいチャット", + "xpack.aiAssistant.chatFlyout.euiToolTip.collapseConversationListLabel": "会話リストを折りたたむ", + "xpack.aiAssistant.chatFlyout.euiToolTip.expandConversationListLabel": "会話リストを展開", + "xpack.aiAssistant.chatFlyout.euiToolTip.newChatLabel": "新しいチャット", + "xpack.aiAssistant.chatHeader.actions.connector": "コネクター", + "xpack.aiAssistant.chatHeader.actions.copyConversation": "会話をコピー", + "xpack.aiAssistant.chatHeader.actions.knowledgeBase": "ナレッジベースを管理", + "xpack.aiAssistant.chatHeader.actions.settings": "AI Assistant設定", + "xpack.aiAssistant.chatHeader.actions.title": "アクション", + "xpack.aiAssistant.chatHeader.editConversationInput": "会話を編集", + "xpack.aiAssistant.chatHeader.euiButtonIcon.navigateToConversationsLabel": "会話に移動", + "xpack.aiAssistant.chatHeader.euiButtonIcon.toggleFlyoutModeLabel": "フライアウトモードを切り替え", + "xpack.aiAssistant.chatHeader.euiToolTip.flyoutModeLabel.dock": "チャットを固定", + "xpack.aiAssistant.chatHeader.euiToolTip.flyoutModeLabel.undock": "チャットを固定解除", + "xpack.aiAssistant.chatHeader.euiToolTip.navigateToConversationsLabel": "会話に移動", + "xpack.aiAssistant.chatPromptEditor.codeEditor.payloadEditorLabel": "payloadEditor", + "xpack.aiAssistant.chatPromptEditor.euiButtonIcon.submitLabel": "送信", + "xpack.aiAssistant.chatTimeline.actions.copyMessage": "メッセージをコピー", + "xpack.aiAssistant.chatTimeline.actions.copyMessageSuccessful": "コピーされたメッセージ", + "xpack.aiAssistant.chatTimeline.actions.editPrompt": "プロンプトを編集", + "xpack.aiAssistant.chatTimeline.actions.inspectPrompt": "プロンプトを検査", + "xpack.aiAssistant.chatTimeline.messages.elasticAssistant.label": "Elastic Assistant", + "xpack.aiAssistant.chatTimeline.messages.system.label": "システム", + "xpack.aiAssistant.chatTimeline.messages.user.label": "あなた", + "xpack.aiAssistant.checkingKbAvailability": "ナレッジベースの利用可能性を確認中", + "xpack.aiAssistant.conversationStartTitle": "会話を開始しました", + "xpack.aiAssistant.couldNotFindConversationContent": "id {conversationId}の会話が見つかりませんでした。会話が存在し、それにアクセスできることを確認してください。", + "xpack.aiAssistant.couldNotFindConversationTitle": "会話が見つかりません", + "xpack.aiAssistant.disclaimer.disclaimerLabel": "このチャットは、LLMプロバイダーとの統合によって提供されています。LLMは、正しくない情報を正しい情報であるかのように表示する場合があることが知られています。Elasticは、構成やLLMプロバイダーへの接続、お客様のナレッジベースへの接続はサポートしますが、LLMの応答については責任を負いません。", + "xpack.aiAssistant.emptyConversationTitle": "新しい会話", + "xpack.aiAssistant.errorSettingUpKnowledgeBase": "ナレッジベースをセットアップできませんでした", + "xpack.aiAssistant.errorUpdatingConversation": "会話を更新できませんでした", + "xpack.aiAssistant.executedFunctionFailureEvent": "関数{functionName}の実行に失敗しました", + "xpack.aiAssistant.failedToGetStatus": "モデルステータスを取得できませんでした。", + "xpack.aiAssistant.failedToSetupKnowledgeBase": "ナレッジベースをセットアップできませんでした。", + "xpack.aiAssistant.flyout.confirmDeleteButtonText": "会話を削除", + "xpack.aiAssistant.flyout.confirmDeleteConversationContent": "この操作は元に戻すことができません。", + "xpack.aiAssistant.flyout.confirmDeleteConversationTitle": "この会話を削除しますか?", + "xpack.aiAssistant.flyout.failedToDeleteConversation": "会話を削除できませんでした", + "xpack.aiAssistant.functionListPopover.euiButtonIcon.selectAFunctionLabel": "関数を選択", + "xpack.aiAssistant.functionListPopover.euiToolTip.clearFunction": "関数を消去", + "xpack.aiAssistant.functionListPopover.euiToolTip.selectAFunctionLabel": "関数を選択", + "xpack.aiAssistant.hideExpandConversationButton.hide": "チャットを非表示", + "xpack.aiAssistant.hideExpandConversationButton.show": "チャットを表示", + "xpack.aiAssistant.incorrectLicense.body": "Elastic AI Assistantを使用するにはEnterpriseライセンスが必要です。", + "xpack.aiAssistant.incorrectLicense.manageLicense": "ライセンスの管理", + "xpack.aiAssistant.incorrectLicense.subscriptionPlansButton": "サブスクリプションオプション", + "xpack.aiAssistant.incorrectLicense.title": "ライセンスをアップグレード", + "xpack.aiAssistant.initialSetupPanel.setupConnector.buttonLabel": "GenAIコネクターをセットアップ", + "xpack.aiAssistant.initialSetupPanel.setupConnector.description2": "Elastic AI Assistantの使用を開始するには、AIプロバイダーのコネクターを設定します。モデルは関数呼び出しをサポートしている必要があります。OpenAIまたはAzureを使用するときには、GPT4を使用することをお勧めします。", + "xpack.aiAssistant.installingKb": "ナレッジベースをセットアップ中", + "xpack.aiAssistant.newChatButton": "新しいチャット", + "xpack.aiAssistant.poweredByModel": "{model}で駆動", + "xpack.aiAssistant.prompt.functionList.filter": "フィルター", + "xpack.aiAssistant.prompt.functionList.functionList": "関数リスト", + "xpack.aiAssistant.prompt.placeholder": "アシスタントにメッセージを送信", + "xpack.aiAssistant.promptEditorNaturalLanguage.euiSelectable.selectAnOptionLabel": "オプションを選択", + "xpack.aiAssistant.settingsPage.goToConnectorsButtonLabel": "コネクターを管理", + "xpack.aiAssistant.setupKb": "ナレッジベースを設定することで、エクスペリエンスが改善されます。", + "xpack.aiAssistant.simulatedFunctionCallingCalloutLabel": "シミュレートされた関数呼び出しが有効です。パフォーマンスが劣化する場合があります。", + "xpack.aiAssistant.suggestedFunctionEvent": "関数{functionName}を要求しました", + "xpack.aiAssistant.technicalPreviewBadgeDescription": "関数呼び出し(根本原因分析やデータの視覚化など)を使用する際に、より一貫性のあるエクスペリエンスを実現するために、GPT4が必要です。GPT3.5は、エラーの説明などのシンプルなワークフローの一部や、頻繁な関数呼び出しの使用が必要とされないKibana内のエクスペリエンスなどのChatGPTで機能します。", + "xpack.aiAssistant.userExecutedFunctionEvent": "関数{functionName}を実行しました", + "xpack.aiAssistant.userSuggestedFunctionEvent": "関数{functionName}を要求しました", + "xpack.aiAssistant.welcomeMessage.div.checkTrainedModelsToLabel": " {retryInstallingLink}か、{trainedModelsLink}を確認して、{modelName}がデプロイされ、実行中であることを確かめてください。", + "xpack.aiAssistant.welcomeMessage.div.settingUpKnowledgeBaseLabel": "ナレッジベースをセットアップ中", + "xpack.aiAssistant.welcomeMessage.inspectErrorsButtonEmptyLabel": "問題を検査", + "xpack.aiAssistant.welcomeMessage.issuesDescriptionListTitleLabel": "問題", + "xpack.aiAssistant.welcomeMessage.knowledgeBaseSuccessfullyInstalledLabel": "ナレッジベースは正常にインストールされました", + "xpack.aiAssistant.welcomeMessage.modelIsNotDeployedLabel": "モデル\"{modelName}\"はデプロイされていません", + "xpack.aiAssistant.welcomeMessage.modelIsNotFullyAllocatedLabel": "\"{modelName}\"の割り当て状態は{allocationState}です", + "xpack.aiAssistant.welcomeMessage.modelIsNotStartedLabel": "\"{modelName}\"のデプロイ状態は{deploymentState}です", + "xpack.aiAssistant.welcomeMessage.retryButtonLabel": "ナレッジベースをインストール", + "xpack.aiAssistant.welcomeMessage.trainedModelsLinkLabel": "学習済みモデル", + "xpack.aiAssistant.welcomeMessage.weAreSettingUpTextLabel": "ナレッジベースをセットアップしています。これには数分かかる場合があります。この処理の実行中には、アシスタントを使用し続けることができます。", + "xpack.aiAssistant.welcomeMessageConnectors.connectorsErrorTextLabel": "コネクターを読み込めませんでした", + "xpack.aiAssistant.welcomeMessageConnectors.connectorsForbiddenTextLabel": "コネクターを取得するために必要な権限が不足しています", + "xpack.aiAssistant.welcomeMessageKnowledgeBase.yourKnowledgeBaseIsNotSetUpCorrectlyLabel": "ナレッジベースはセットアップされていません。", + "xpack.aiAssistant.welcomeMessageKnowledgeBaseSetupErrorPanel.retryInstallingLinkLabel": "インストールを再試行", "xpack.aiops.actions.openChangePointInMlAppName": "AIOps Labsで開く", "xpack.aiops.analysis.columnSelectorAriaLabel": "列のフィルタリング", "xpack.aiops.analysis.columnSelectorNotEnoughColumnsSelected": "1つ以上の列を選択する必要があります。", @@ -32289,92 +32377,27 @@ "xpack.observabilityAiAssistant.app.starterPrompts.whatAreSlos.prompt": "SLOとは何ですか?", "xpack.observabilityAiAssistant.app.starterPrompts.whatAreSlos.title": "SLO", "xpack.observabilityAiAssistant.appTitle": "オブザーバビリティAI Assistant", - "xpack.observabilityAiAssistant.askAssistantButton.buttonLabel": "アシスタントに聞く", - "xpack.observabilityAiAssistant.askAssistantButton.popoverContent": "Elastic Assistantでデータに関するインサイトを得ましょう", - "xpack.observabilityAiAssistant.askAssistantButton.popoverTitle": "AI Assistant for Observability", - "xpack.observabilityAiAssistant.assistantSetup.title": "Elastic AI Assistantへようこそ", "xpack.observabilityAiAssistant.changesList.dotImpactHigh": "高", "xpack.observabilityAiAssistant.changesList.dotImpactLow": "低", "xpack.observabilityAiAssistant.changesList.dotImpactMedium": "中", "xpack.observabilityAiAssistant.changesList.labelColumnTitle": "ラベル", "xpack.observabilityAiAssistant.changesList.noChangesDetected": "変更が検出されません", "xpack.observabilityAiAssistant.changesList.trendColumnTitle": "傾向", - "xpack.observabilityAiAssistant.chatActionsMenu.euiButtonIcon.menuLabel": "メニュー", - "xpack.observabilityAiAssistant.chatActionsMenu.euiToolTip.moreActionsLabel": "さらにアクションを表示", - "xpack.observabilityAiAssistant.chatCollapsedItems.hideEvents": "{count}件のイベントを非表示", - "xpack.observabilityAiAssistant.chatCollapsedItems.showEvents": "{count}件のイベントを表示", - "xpack.observabilityAiAssistant.chatCollapsedItems.toggleButtonLabel": "アイテムを表示/非表示", "xpack.observabilityAiAssistant.chatCompletionError.conversationNotFoundError": "会話が見つかりません", "xpack.observabilityAiAssistant.chatCompletionError.internalServerError": "内部サーバーエラーが発生しました", "xpack.observabilityAiAssistant.chatCompletionError.tokenLimitReachedError": "トークンの上限に達しました。トークンの上限は{tokenLimit}ですが、現在の会話には{tokenCount}個のトークンがあります。", - "xpack.observabilityAiAssistant.chatFlyout.euiButtonIcon.expandConversationListLabel": "会話リストを展開", - "xpack.observabilityAiAssistant.chatFlyout.euiButtonIcon.newChatLabel": "新しいチャット", - "xpack.observabilityAiAssistant.chatFlyout.euiFlyoutResizable.aiAssistantForObservabilityLabel": "AI assistant for Observabilityチャットフライアウト", - "xpack.observabilityAiAssistant.chatFlyout.euiToolTip.collapseConversationListLabel": "会話リストを折りたたむ", - "xpack.observabilityAiAssistant.chatFlyout.euiToolTip.expandConversationListLabel": "会話リストを展開", - "xpack.observabilityAiAssistant.chatFlyout.euiToolTip.newChatLabel": "新しいチャット", - "xpack.observabilityAiAssistant.chatHeader.actions.connector": "コネクター", - "xpack.observabilityAiAssistant.chatHeader.actions.copyConversation": "会話をコピー", - "xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase": "ナレッジベースを管理", - "xpack.observabilityAiAssistant.chatHeader.actions.settings": "AI Assistant設定", - "xpack.observabilityAiAssistant.chatHeader.actions.title": "アクション", - "xpack.observabilityAiAssistant.chatHeader.editConversationInput": "会話を編集", - "xpack.observabilityAiAssistant.chatHeader.euiButtonIcon.navigateToConversationsLabel": "会話に移動", - "xpack.observabilityAiAssistant.chatHeader.euiButtonIcon.toggleFlyoutModeLabel": "フライアウトモードを切り替え", - "xpack.observabilityAiAssistant.chatHeader.euiToolTip.flyoutModeLabel.dock": "チャットを固定", - "xpack.observabilityAiAssistant.chatHeader.euiToolTip.flyoutModeLabel.undock": "チャットを固定解除", - "xpack.observabilityAiAssistant.chatHeader.euiToolTip.navigateToConversationsLabel": "会話に移動", - "xpack.observabilityAiAssistant.chatPromptEditor.codeEditor.payloadEditorLabel": "payloadEditor", - "xpack.observabilityAiAssistant.chatPromptEditor.euiButtonIcon.submitLabel": "送信", "xpack.observabilityAiAssistant.chatService.div.helloLabel": "こんにちは", - "xpack.observabilityAiAssistant.chatTimeline.actions.copyMessage": "メッセージをコピー", - "xpack.observabilityAiAssistant.chatTimeline.actions.copyMessageSuccessful": "コピーされたメッセージ", - "xpack.observabilityAiAssistant.chatTimeline.actions.editPrompt": "プロンプトを編集", - "xpack.observabilityAiAssistant.chatTimeline.actions.inspectPrompt": "プロンプトを検査", - "xpack.observabilityAiAssistant.chatTimeline.messages.elasticAssistant.label": "Elastic Assistant", - "xpack.observabilityAiAssistant.chatTimeline.messages.system.label": "システム", - "xpack.observabilityAiAssistant.chatTimeline.messages.user.label": "あなた", - "xpack.observabilityAiAssistant.checkingKbAvailability": "ナレッジベースの利用可能性を確認中", "xpack.observabilityAiAssistant.connectorSelector.connectorSelectLabel": "コネクター:", "xpack.observabilityAiAssistant.connectorSelector.empty": "コネクターなし", "xpack.observabilityAiAssistant.connectorSelector.error": "コネクターを読み込めませんでした", - "xpack.observabilityAiAssistant.conversationList.deleteConversationIconLabel": "削除", - "xpack.observabilityAiAssistant.conversationList.errorMessage": "の読み込みに失敗しました", - "xpack.observabilityAiAssistant.conversationList.noConversations": "会話なし", - "xpack.observabilityAiAssistant.conversationList.title": "以前", "xpack.observabilityAiAssistant.conversationsDeepLinkTitle": "会話", - "xpack.observabilityAiAssistant.conversationStartTitle": "会話を開始しました", - "xpack.observabilityAiAssistant.couldNotFindConversationContent": "id {conversationId}の会話が見つかりませんでした。会話が存在し、それにアクセスできることを確認してください。", - "xpack.observabilityAiAssistant.couldNotFindConversationTitle": "会話が見つかりません", - "xpack.observabilityAiAssistant.disclaimer.disclaimerLabel": "このチャットは、LLMプロバイダーとの統合によって提供されています。LLMは、正しくない情報を正しい情報であるかのように表示する場合があることが知られています。Elasticは、構成やLLMプロバイダーへの接続、お客様のナレッジベースへの接続はサポートしますが、LLMの応答については責任を負いません。", - "xpack.observabilityAiAssistant.emptyConversationTitle": "新しい会話", - "xpack.observabilityAiAssistant.errorSettingUpKnowledgeBase": "ナレッジベースをセットアップできませんでした", - "xpack.observabilityAiAssistant.errorUpdatingConversation": "会話を更新できませんでした", - "xpack.observabilityAiAssistant.executedFunctionFailureEvent": "関数{functionName}の実行に失敗しました", "xpack.observabilityAiAssistant.experimentalTitle": "テクニカルプレビュー", "xpack.observabilityAiAssistant.experimentalTooltip": "この機能はテクニカルプレビュー中であり、将来のリリースでは変更されたり完全に削除されたりする場合があります。Elasticはすべての問題の修正に努めますが、テクニカルプレビュー中の機能には正式なGA機能のサポートSLAが適用されません。", "xpack.observabilityAiAssistant.failedLoadingResponseText": "応答の読み込みに失敗しました", - "xpack.observabilityAiAssistant.failedToGetStatus": "モデルステータスを取得できませんでした。", "xpack.observabilityAiAssistant.failedToLoadResponse": "AIアシスタントからの応答を読み込めませんでした", - "xpack.observabilityAiAssistant.failedToSetupKnowledgeBase": "ナレッジベースをセットアップできませんでした。", "xpack.observabilityAiAssistant.featureRegistry.featureName": "オブザーバビリティAI Assistant", "xpack.observabilityAiAssistant.feedbackButtons.em.thanksForYourFeedbackLabel": "フィードバックをご提供いただき、ありがとうございました。", - "xpack.observabilityAiAssistant.flyout.confirmDeleteButtonText": "会話を削除", - "xpack.observabilityAiAssistant.flyout.confirmDeleteConversationContent": "この操作は元に戻すことができません。", - "xpack.observabilityAiAssistant.flyout.confirmDeleteConversationTitle": "この会話を削除しますか?", - "xpack.observabilityAiAssistant.flyout.failedToDeleteConversation": "会話を削除できませんでした", "xpack.observabilityAiAssistant.functionCallLimitExceeded": "\n\n注:Assistantは、上限を超過しても、関数を呼び出そうとします。", - "xpack.observabilityAiAssistant.functionListPopover.euiButtonIcon.selectAFunctionLabel": "関数を選択", - "xpack.observabilityAiAssistant.functionListPopover.euiToolTip.clearFunction": "関数を消去", - "xpack.observabilityAiAssistant.functionListPopover.euiToolTip.selectAFunctionLabel": "関数を選択", - "xpack.observabilityAiAssistant.hideExpandConversationButton.hide": "チャットを非表示", - "xpack.observabilityAiAssistant.hideExpandConversationButton.show": "チャットを表示", - "xpack.observabilityAiAssistant.incorrectLicense.body": "Elastic AI Assistantを使用するにはEnterpriseライセンスが必要です。", - "xpack.observabilityAiAssistant.incorrectLicense.manageLicense": "ライセンスの管理", - "xpack.observabilityAiAssistant.incorrectLicense.subscriptionPlansButton": "サブスクリプションオプション", - "xpack.observabilityAiAssistant.incorrectLicense.title": "ライセンスをアップグレード", - "xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.buttonLabel": "GenAIコネクターをセットアップ", - "xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.description2": "Elastic AI Assistantの使用を開始するには、AIプロバイダーのコネクターを設定します。モデルは関数呼び出しをサポートしている必要があります。OpenAIまたはAzureを使用するときには、GPT4を使用することをお勧めします。", "xpack.observabilityAiAssistant.insight.actions": "アクション", "xpack.observabilityAiAssistant.insight.actions.connector": "コネクター", "xpack.observabilityAiAssistant.insight.actions.editPrompt": "プロンプトを編集", @@ -32392,7 +32415,6 @@ "xpack.observabilityAiAssistant.insight.response.startChat": "チャットを開始", "xpack.observabilityAiAssistant.insight.sendPromptEdit": "プロンプトを送信", "xpack.observabilityAiAssistant.insightModifiedPrompt": "このインサイトは修正されました。", - "xpack.observabilityAiAssistant.installingKb": "ナレッジベースをセットアップ中", "xpack.observabilityAiAssistant.lensESQLFunction.displayChart": "グラフを表示", "xpack.observabilityAiAssistant.lensESQLFunction.displayTable": "表を表示", "xpack.observabilityAiAssistant.lensESQLFunction.edit": "ビジュアライゼーションを編集", @@ -32407,42 +32429,14 @@ "xpack.observabilityAiAssistant.missingCredentialsCallout.title": "資格情報がありません", "xpack.observabilityAiAssistant.navControl.initFailureErrorTitle": "オブザーバビリティAI Assistantを初期化できませんでした", "xpack.observabilityAiAssistant.navControl.openTheAIAssistantPopoverLabel": "AI Assistantを開く", - "xpack.observabilityAiAssistant.newChatButton": "新しいチャット", - "xpack.observabilityAiAssistant.poweredByModel": "{model}で駆動", - "xpack.observabilityAiAssistant.prompt.functionList.filter": "フィルター", - "xpack.observabilityAiAssistant.prompt.functionList.functionList": "関数リスト", - "xpack.observabilityAiAssistant.prompt.placeholder": "アシスタントにメッセージを送信", - "xpack.observabilityAiAssistant.promptEditorNaturalLanguage.euiSelectable.selectAnOptionLabel": "オプションを選択", "xpack.observabilityAiAssistant.regenerateResponseButtonLabel": "再生成", "xpack.observabilityAiAssistant.requiredConnectorField": "コネクターが必要です。", "xpack.observabilityAiAssistant.requiredMessageTextField": "メッセージが必要です。", "xpack.observabilityAiAssistant.resetDefaultPrompt": "デフォルトにリセット", "xpack.observabilityAiAssistant.runThisQuery": "結果を表示", - "xpack.observabilityAiAssistant.settingsPage.goToConnectorsButtonLabel": "コネクターを管理", - "xpack.observabilityAiAssistant.setupKb": "ナレッジベースを設定することで、エクスペリエンスが改善されます。", - "xpack.observabilityAiAssistant.simulatedFunctionCallingCalloutLabel": "シミュレートされた関数呼び出しが有効です。パフォーマンスが劣化する場合があります。", "xpack.observabilityAiAssistant.stopGeneratingButtonLabel": "生成を停止", - "xpack.observabilityAiAssistant.suggestedFunctionEvent": "関数{functionName}を要求しました", - "xpack.observabilityAiAssistant.technicalPreviewBadgeDescription": "関数呼び出し(根本原因分析やデータの視覚化など)を使用する際に、より一貫性のあるエクスペリエンスを実現するために、GPT4が必要です。GPT3.5は、エラーの説明などのシンプルなワークフローの一部や、頻繁な関数呼び出しの使用が必要とされないKibana内のエクスペリエンスなどのChatGPTで機能します。", "xpack.observabilityAiAssistant.tokenLimitError": "会話はトークンの上限を超えました。トークンの最大上限は**{tokenLimit}**ですが、現在の会話には**{tokenCount}**個のトークンがあります。続行するには、新しい会話を開始してください。", - "xpack.observabilityAiAssistant.userExecutedFunctionEvent": "関数{functionName}を実行しました", - "xpack.observabilityAiAssistant.userSuggestedFunctionEvent": "関数{functionName}を要求しました", "xpack.observabilityAiAssistant.visualizeThisQuery": "このクエリーを可視化", - "xpack.observabilityAiAssistant.welcomeMessage.div.checkTrainedModelsToLabel": " {retryInstallingLink}か、{trainedModelsLink}を確認して、{modelName}がデプロイされ、実行中であることを確かめてください。", - "xpack.observabilityAiAssistant.welcomeMessage.div.settingUpKnowledgeBaseLabel": "ナレッジベースをセットアップ中", - "xpack.observabilityAiAssistant.welcomeMessage.inspectErrorsButtonEmptyLabel": "問題を検査", - "xpack.observabilityAiAssistant.welcomeMessage.issuesDescriptionListTitleLabel": "問題", - "xpack.observabilityAiAssistant.welcomeMessage.knowledgeBaseSuccessfullyInstalledLabel": "ナレッジベースは正常にインストールされました", - "xpack.observabilityAiAssistant.welcomeMessage.modelIsNotDeployedLabel": "モデル\"{modelName}\"はデプロイされていません", - "xpack.observabilityAiAssistant.welcomeMessage.modelIsNotFullyAllocatedLabel": "\"{modelName}\"の割り当て状態は{allocationState}です", - "xpack.observabilityAiAssistant.welcomeMessage.modelIsNotStartedLabel": "\"{modelName}\"のデプロイ状態は{deploymentState}です", - "xpack.observabilityAiAssistant.welcomeMessage.retryButtonLabel": "ナレッジベースをインストール", - "xpack.observabilityAiAssistant.welcomeMessage.trainedModelsLinkLabel": "学習済みモデル", - "xpack.observabilityAiAssistant.welcomeMessage.weAreSettingUpTextLabel": "ナレッジベースをセットアップしています。これには数分かかる場合があります。この処理の実行中には、アシスタントを使用し続けることができます。", - "xpack.observabilityAiAssistant.welcomeMessageConnectors.connectorsErrorTextLabel": "コネクターを読み込めませんでした", - "xpack.observabilityAiAssistant.welcomeMessageConnectors.connectorsForbiddenTextLabel": "コネクターを取得するために必要な権限が不足しています", - "xpack.observabilityAiAssistant.welcomeMessageKnowledgeBase.yourKnowledgeBaseIsNotSetUpCorrectlyLabel": "ナレッジベースはセットアップされていません。", - "xpack.observabilityAiAssistant.welcomeMessageKnowledgeBaseSetupErrorPanel.retryInstallingLinkLabel": "インストールを再試行", "xpack.observabilityAiAssistantManagement.apmSettings.save.error": "設定の保存中にエラーが発生しました", "xpack.observabilityAiAssistantManagement.app.description": "AI Assistant for Observabilityを管理します。", "xpack.observabilityAiAssistantManagement.app.title": "AI Assistant for Observability", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 30ac0196e8993..0a17edfeb80ce 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9147,6 +9147,94 @@ "xpack.actions.serverSideErrors.unavailableLicenseInformationErrorMessage": "操作不可用 - 许可信息当前不可用。", "xpack.actions.subActionsFramework.urlValidationError": "验证 URL 时出错:{message}", "xpack.actions.urlAllowedHostsConfigurationError": "目标 {field} 的“{value}”未添加到 Kibana 配置 xpack.actions.allowedHosts", + "xpack.aiAssistant.askAssistantButton.buttonLabel": "询问助手", + "xpack.aiAssistant.askAssistantButton.popoverContent": "使用 Elastic 助手深入了解您的数据", + "xpack.aiAssistant.assistantSetup.title": "欢迎使用 Elastic AI 助手", + "xpack.aiAssistant.chatActionsMenu.euiButtonIcon.menuLabel": "菜单", + "xpack.aiAssistant.chatActionsMenu.euiToolTip.moreActionsLabel": "更多操作", + "xpack.aiAssistant.chatCollapsedItems.hideEvents": "隐藏 {count} 个事件", + "xpack.aiAssistant.chatCollapsedItems.showEvents": "显示 {count} 个事件", + "xpack.aiAssistant.chatCollapsedItems.toggleButtonLabel": "显示/隐藏项目", + "xpack.aiAssistant.chatFlyout.euiButtonIcon.expandConversationListLabel": "展开对话列表", + "xpack.aiAssistant.chatFlyout.euiButtonIcon.newChatLabel": "新聊天", + "xpack.aiAssistant.chatFlyout.euiToolTip.collapseConversationListLabel": "折叠对话列表", + "xpack.aiAssistant.chatFlyout.euiToolTip.expandConversationListLabel": "展开对话列表", + "xpack.aiAssistant.chatFlyout.euiToolTip.newChatLabel": "新聊天", + "xpack.aiAssistant.chatHeader.actions.connector": "连接器", + "xpack.aiAssistant.chatHeader.actions.copyConversation": "复制对话", + "xpack.aiAssistant.chatHeader.actions.knowledgeBase": "管理知识库", + "xpack.aiAssistant.chatHeader.actions.settings": "AI 助手设置", + "xpack.aiAssistant.chatHeader.actions.title": "操作", + "xpack.aiAssistant.chatHeader.editConversationInput": "编辑对话", + "xpack.aiAssistant.chatHeader.euiButtonIcon.navigateToConversationsLabel": "导航到对话", + "xpack.aiAssistant.chatHeader.euiButtonIcon.toggleFlyoutModeLabel": "切换浮出控件模式", + "xpack.aiAssistant.chatHeader.euiToolTip.flyoutModeLabel.dock": "停靠聊天", + "xpack.aiAssistant.chatHeader.euiToolTip.flyoutModeLabel.undock": "取消停靠聊天", + "xpack.aiAssistant.chatHeader.euiToolTip.navigateToConversationsLabel": "导航到对话", + "xpack.aiAssistant.chatPromptEditor.codeEditor.payloadEditorLabel": "payloadEditor", + "xpack.aiAssistant.chatPromptEditor.euiButtonIcon.submitLabel": "提交", + "xpack.aiAssistant.chatTimeline.actions.copyMessage": "复制消息", + "xpack.aiAssistant.chatTimeline.actions.copyMessageSuccessful": "已复制消息", + "xpack.aiAssistant.chatTimeline.actions.editPrompt": "编辑提示", + "xpack.aiAssistant.chatTimeline.actions.inspectPrompt": "检查提示", + "xpack.aiAssistant.chatTimeline.messages.elasticAssistant.label": "Elastic 助手", + "xpack.aiAssistant.chatTimeline.messages.system.label": "系统", + "xpack.aiAssistant.chatTimeline.messages.user.label": "您", + "xpack.aiAssistant.checkingKbAvailability": "正在检查知识库的可用性", + "xpack.aiAssistant.conversationStartTitle": "已开始对话", + "xpack.aiAssistant.couldNotFindConversationContent": "找不到 ID 为 {conversationId} 的对话。请确保该对话存在并且您具有访问权限。", + "xpack.aiAssistant.couldNotFindConversationTitle": "未找到对话", + "xpack.aiAssistant.disclaimer.disclaimerLabel": "通过集成 LLM 提供商来支持此聊天。众所周知,LLM 有时会提供错误信息,好像它是正确的。Elastic 支持配置并连接到 LLM 提供商和知识库,但不对 LLM 响应负责。", + "xpack.aiAssistant.emptyConversationTitle": "新对话", + "xpack.aiAssistant.errorSettingUpKnowledgeBase": "无法设置知识库", + "xpack.aiAssistant.errorUpdatingConversation": "无法更新对话", + "xpack.aiAssistant.executedFunctionFailureEvent": "无法执行函数 {functionName}", + "xpack.aiAssistant.failedToGetStatus": "无法获取模型状态。", + "xpack.aiAssistant.failedToSetupKnowledgeBase": "无法设置知识库。", + "xpack.aiAssistant.flyout.confirmDeleteButtonText": "删除对话", + "xpack.aiAssistant.flyout.confirmDeleteConversationContent": "此操作无法撤消。", + "xpack.aiAssistant.flyout.confirmDeleteConversationTitle": "删除此对话?", + "xpack.aiAssistant.flyout.failedToDeleteConversation": "无法删除对话", + "xpack.aiAssistant.functionListPopover.euiButtonIcon.selectAFunctionLabel": "选择函数", + "xpack.aiAssistant.functionListPopover.euiToolTip.clearFunction": "清除函数", + "xpack.aiAssistant.functionListPopover.euiToolTip.selectAFunctionLabel": "选择函数", + "xpack.aiAssistant.hideExpandConversationButton.hide": "隐藏聊天", + "xpack.aiAssistant.hideExpandConversationButton.show": "显示聊天", + "xpack.aiAssistant.incorrectLicense.body": "您需要企业级许可证才能使用 Elastic AI 助手。", + "xpack.aiAssistant.incorrectLicense.manageLicense": "管理许可证", + "xpack.aiAssistant.incorrectLicense.subscriptionPlansButton": "订阅计划", + "xpack.aiAssistant.incorrectLicense.title": "升级您的许可证", + "xpack.aiAssistant.initialSetupPanel.setupConnector.buttonLabel": "设置 GenAI 连接器", + "xpack.aiAssistant.initialSetupPanel.setupConnector.description2": "通过为您的 AI 提供商设置连接器,开始使用 Elastic AI 助手。此模型需要支持函数调用。使用 OpenAI 或 Azure 时,建议使用 GPT4。", + "xpack.aiAssistant.installingKb": "正在设置知识库", + "xpack.aiAssistant.newChatButton": "新聊天", + "xpack.aiAssistant.poweredByModel": "由 {model} 提供支持", + "xpack.aiAssistant.prompt.functionList.filter": "筛选", + "xpack.aiAssistant.prompt.functionList.functionList": "函数列表", + "xpack.aiAssistant.prompt.placeholder": "向助手发送消息", + "xpack.aiAssistant.promptEditorNaturalLanguage.euiSelectable.selectAnOptionLabel": "选择选项", + "xpack.aiAssistant.settingsPage.goToConnectorsButtonLabel": "管理连接器", + "xpack.aiAssistant.setupKb": "通过设置知识库来改进体验。", + "xpack.aiAssistant.simulatedFunctionCallingCalloutLabel": "模拟函数调用已启用。您可能会面临性能降级。", + "xpack.aiAssistant.suggestedFunctionEvent": "已请求函数 {functionName}", + "xpack.aiAssistant.technicalPreviewBadgeDescription": "需要 GPT4 以在使用函数调用时(例如,执行根本原因分析、数据可视化等时候)获得更加一致的体验。GPT3.5 可作用于某些更简单的工作流(如解释错误),或在 Kibana 中获得不需要频繁使用函数调用的与 ChatGPT 类似的体验。", + "xpack.aiAssistant.userExecutedFunctionEvent": "已执行函数 {functionName}", + "xpack.aiAssistant.userSuggestedFunctionEvent": "已请求函数 {functionName}", + "xpack.aiAssistant.welcomeMessage.div.checkTrainedModelsToLabel": " {retryInstallingLink} 或检查 {trainedModelsLink},确保 {modelName} 已部署并正在运行。", + "xpack.aiAssistant.welcomeMessage.div.settingUpKnowledgeBaseLabel": "正在设置知识库", + "xpack.aiAssistant.welcomeMessage.inspectErrorsButtonEmptyLabel": "检查问题", + "xpack.aiAssistant.welcomeMessage.issuesDescriptionListTitleLabel": "问题", + "xpack.aiAssistant.welcomeMessage.knowledgeBaseSuccessfullyInstalledLabel": "已成功安装知识库", + "xpack.aiAssistant.welcomeMessage.modelIsNotDeployedLabel": "未部署模型 {modelName}", + "xpack.aiAssistant.welcomeMessage.modelIsNotFullyAllocatedLabel": "{modelName} 的分配状态为 {allocationState}", + "xpack.aiAssistant.welcomeMessage.modelIsNotStartedLabel": "{modelName} 的部署状态为 {deploymentState}", + "xpack.aiAssistant.welcomeMessage.retryButtonLabel": "安装知识库", + "xpack.aiAssistant.welcomeMessage.trainedModelsLinkLabel": "已训练模型", + "xpack.aiAssistant.welcomeMessage.weAreSettingUpTextLabel": "我们正在设置您的知识库。这可能需要若干分钟。此进程处于运行状态时,您可以继续使用该助手。", + "xpack.aiAssistant.welcomeMessageConnectors.connectorsErrorTextLabel": "无法加载连接器", + "xpack.aiAssistant.welcomeMessageConnectors.connectorsForbiddenTextLabel": "缺少获取连接器所需的权限", + "xpack.aiAssistant.welcomeMessageKnowledgeBase.yourKnowledgeBaseIsNotSetUpCorrectlyLabel": "尚未设置您的知识库。", + "xpack.aiAssistant.welcomeMessageKnowledgeBaseSetupErrorPanel.retryInstallingLinkLabel": "重试安装", "xpack.aiops.actions.openChangePointInMlAppName": "在 Aiops 实验室中打开", "xpack.aiops.analysis.columnSelectorAriaLabel": "筛选列", "xpack.aiops.analysis.columnSelectorNotEnoughColumnsSelected": "必须至少选择一列。", @@ -32331,92 +32419,27 @@ "xpack.observabilityAiAssistant.app.starterPrompts.whatAreSlos.prompt": "什么是 SLO?", "xpack.observabilityAiAssistant.app.starterPrompts.whatAreSlos.title": "SLO", "xpack.observabilityAiAssistant.appTitle": "Observability AI 助手", - "xpack.observabilityAiAssistant.askAssistantButton.buttonLabel": "询问助手", - "xpack.observabilityAiAssistant.askAssistantButton.popoverContent": "使用 Elastic 助手深入了解您的数据", - "xpack.observabilityAiAssistant.askAssistantButton.popoverTitle": "适用于 Observability 的 AI 助手", - "xpack.observabilityAiAssistant.assistantSetup.title": "欢迎使用 Elastic AI 助手", "xpack.observabilityAiAssistant.changesList.dotImpactHigh": "高", "xpack.observabilityAiAssistant.changesList.dotImpactLow": "低", "xpack.observabilityAiAssistant.changesList.dotImpactMedium": "中", "xpack.observabilityAiAssistant.changesList.labelColumnTitle": "标签", "xpack.observabilityAiAssistant.changesList.noChangesDetected": "未检测到更改", "xpack.observabilityAiAssistant.changesList.trendColumnTitle": "趋势", - "xpack.observabilityAiAssistant.chatActionsMenu.euiButtonIcon.menuLabel": "菜单", - "xpack.observabilityAiAssistant.chatActionsMenu.euiToolTip.moreActionsLabel": "更多操作", - "xpack.observabilityAiAssistant.chatCollapsedItems.hideEvents": "隐藏 {count} 个事件", - "xpack.observabilityAiAssistant.chatCollapsedItems.showEvents": "显示 {count} 个事件", - "xpack.observabilityAiAssistant.chatCollapsedItems.toggleButtonLabel": "显示/隐藏项目", "xpack.observabilityAiAssistant.chatCompletionError.conversationNotFoundError": "未找到对话", "xpack.observabilityAiAssistant.chatCompletionError.internalServerError": "发生内部服务器错误", "xpack.observabilityAiAssistant.chatCompletionError.tokenLimitReachedError": "达到了词元限制。词元限制为 {tokenLimit},但当前对话具有 {tokenCount} 个词元。", - "xpack.observabilityAiAssistant.chatFlyout.euiButtonIcon.expandConversationListLabel": "展开对话列表", - "xpack.observabilityAiAssistant.chatFlyout.euiButtonIcon.newChatLabel": "新聊天", - "xpack.observabilityAiAssistant.chatFlyout.euiFlyoutResizable.aiAssistantForObservabilityLabel": "适用于 Observability 聊天浮出控件的 AI 助手", - "xpack.observabilityAiAssistant.chatFlyout.euiToolTip.collapseConversationListLabel": "折叠对话列表", - "xpack.observabilityAiAssistant.chatFlyout.euiToolTip.expandConversationListLabel": "展开对话列表", - "xpack.observabilityAiAssistant.chatFlyout.euiToolTip.newChatLabel": "新聊天", - "xpack.observabilityAiAssistant.chatHeader.actions.connector": "连接器", - "xpack.observabilityAiAssistant.chatHeader.actions.copyConversation": "复制对话", - "xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase": "管理知识库", - "xpack.observabilityAiAssistant.chatHeader.actions.settings": "AI 助手设置", - "xpack.observabilityAiAssistant.chatHeader.actions.title": "操作", - "xpack.observabilityAiAssistant.chatHeader.editConversationInput": "编辑对话", - "xpack.observabilityAiAssistant.chatHeader.euiButtonIcon.navigateToConversationsLabel": "导航到对话", - "xpack.observabilityAiAssistant.chatHeader.euiButtonIcon.toggleFlyoutModeLabel": "切换浮出控件模式", - "xpack.observabilityAiAssistant.chatHeader.euiToolTip.flyoutModeLabel.dock": "停靠聊天", - "xpack.observabilityAiAssistant.chatHeader.euiToolTip.flyoutModeLabel.undock": "取消停靠聊天", - "xpack.observabilityAiAssistant.chatHeader.euiToolTip.navigateToConversationsLabel": "导航到对话", - "xpack.observabilityAiAssistant.chatPromptEditor.codeEditor.payloadEditorLabel": "payloadEditor", - "xpack.observabilityAiAssistant.chatPromptEditor.euiButtonIcon.submitLabel": "提交", "xpack.observabilityAiAssistant.chatService.div.helloLabel": "您好", - "xpack.observabilityAiAssistant.chatTimeline.actions.copyMessage": "复制消息", - "xpack.observabilityAiAssistant.chatTimeline.actions.copyMessageSuccessful": "已复制消息", - "xpack.observabilityAiAssistant.chatTimeline.actions.editPrompt": "编辑提示", - "xpack.observabilityAiAssistant.chatTimeline.actions.inspectPrompt": "检查提示", - "xpack.observabilityAiAssistant.chatTimeline.messages.elasticAssistant.label": "Elastic 助手", - "xpack.observabilityAiAssistant.chatTimeline.messages.system.label": "系统", - "xpack.observabilityAiAssistant.chatTimeline.messages.user.label": "您", - "xpack.observabilityAiAssistant.checkingKbAvailability": "正在检查知识库的可用性", "xpack.observabilityAiAssistant.connectorSelector.connectorSelectLabel": "连接器:", "xpack.observabilityAiAssistant.connectorSelector.empty": "无连接器", "xpack.observabilityAiAssistant.connectorSelector.error": "无法加载连接器", - "xpack.observabilityAiAssistant.conversationList.deleteConversationIconLabel": "删除", - "xpack.observabilityAiAssistant.conversationList.errorMessage": "无法加载", - "xpack.observabilityAiAssistant.conversationList.noConversations": "无对话", - "xpack.observabilityAiAssistant.conversationList.title": "以前", "xpack.observabilityAiAssistant.conversationsDeepLinkTitle": "对话", - "xpack.observabilityAiAssistant.conversationStartTitle": "已开始对话", - "xpack.observabilityAiAssistant.couldNotFindConversationContent": "找不到 ID 为 {conversationId} 的对话。请确保该对话存在并且您具有访问权限。", - "xpack.observabilityAiAssistant.couldNotFindConversationTitle": "未找到对话", - "xpack.observabilityAiAssistant.disclaimer.disclaimerLabel": "通过集成 LLM 提供商来支持此聊天。众所周知,LLM 有时会提供错误信息,好像它是正确的。Elastic 支持配置并连接到 LLM 提供商和知识库,但不对 LLM 响应负责。", - "xpack.observabilityAiAssistant.emptyConversationTitle": "新对话", - "xpack.observabilityAiAssistant.errorSettingUpKnowledgeBase": "无法设置知识库", - "xpack.observabilityAiAssistant.errorUpdatingConversation": "无法更新对话", - "xpack.observabilityAiAssistant.executedFunctionFailureEvent": "无法执行函数 {functionName}", "xpack.observabilityAiAssistant.experimentalTitle": "技术预览", "xpack.observabilityAiAssistant.experimentalTooltip": "此功能处于技术预览状态,在未来版本中可能会更改或完全移除。Elastic 将努力修复任何问题,但处于技术预览状态的功能不受正式 GA 功能支持 SLA 的约束。", "xpack.observabilityAiAssistant.failedLoadingResponseText": "无法加载响应", - "xpack.observabilityAiAssistant.failedToGetStatus": "无法获取模型状态。", "xpack.observabilityAiAssistant.failedToLoadResponse": "无法加载来自 AI 助手的响应", - "xpack.observabilityAiAssistant.failedToSetupKnowledgeBase": "无法设置知识库。", "xpack.observabilityAiAssistant.featureRegistry.featureName": "Observability AI 助手", "xpack.observabilityAiAssistant.feedbackButtons.em.thanksForYourFeedbackLabel": "感谢您提供反馈", - "xpack.observabilityAiAssistant.flyout.confirmDeleteButtonText": "删除对话", - "xpack.observabilityAiAssistant.flyout.confirmDeleteConversationContent": "此操作无法撤消。", - "xpack.observabilityAiAssistant.flyout.confirmDeleteConversationTitle": "删除此对话?", - "xpack.observabilityAiAssistant.flyout.failedToDeleteConversation": "无法删除对话", "xpack.observabilityAiAssistant.functionCallLimitExceeded": "\n\n注意:即使超出了限制,助手仍尝试调用了函数", - "xpack.observabilityAiAssistant.functionListPopover.euiButtonIcon.selectAFunctionLabel": "选择函数", - "xpack.observabilityAiAssistant.functionListPopover.euiToolTip.clearFunction": "清除函数", - "xpack.observabilityAiAssistant.functionListPopover.euiToolTip.selectAFunctionLabel": "选择函数", - "xpack.observabilityAiAssistant.hideExpandConversationButton.hide": "隐藏聊天", - "xpack.observabilityAiAssistant.hideExpandConversationButton.show": "显示聊天", - "xpack.observabilityAiAssistant.incorrectLicense.body": "您需要企业级许可证才能使用 Elastic AI 助手。", - "xpack.observabilityAiAssistant.incorrectLicense.manageLicense": "管理许可证", - "xpack.observabilityAiAssistant.incorrectLicense.subscriptionPlansButton": "订阅计划", - "xpack.observabilityAiAssistant.incorrectLicense.title": "升级您的许可证", - "xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.buttonLabel": "设置 GenAI 连接器", - "xpack.observabilityAiAssistant.initialSetupPanel.setupConnector.description2": "通过为您的 AI 提供商设置连接器,开始使用 Elastic AI 助手。此模型需要支持函数调用。使用 OpenAI 或 Azure 时,建议使用 GPT4。", "xpack.observabilityAiAssistant.insight.actions": "操作", "xpack.observabilityAiAssistant.insight.actions.connector": "连接器", "xpack.observabilityAiAssistant.insight.actions.editPrompt": "编辑提示", @@ -32434,7 +32457,6 @@ "xpack.observabilityAiAssistant.insight.response.startChat": "开始聊天", "xpack.observabilityAiAssistant.insight.sendPromptEdit": "发送提示", "xpack.observabilityAiAssistant.insightModifiedPrompt": "此洞察已被修改。", - "xpack.observabilityAiAssistant.installingKb": "正在设置知识库", "xpack.observabilityAiAssistant.lensESQLFunction.displayChart": "显示图表", "xpack.observabilityAiAssistant.lensESQLFunction.displayTable": "显示表", "xpack.observabilityAiAssistant.lensESQLFunction.edit": "编辑可视化", @@ -32449,42 +32471,14 @@ "xpack.observabilityAiAssistant.missingCredentialsCallout.title": "凭据缺失", "xpack.observabilityAiAssistant.navControl.initFailureErrorTitle": "无法初始化 Observability AI 助手", "xpack.observabilityAiAssistant.navControl.openTheAIAssistantPopoverLabel": "打开 AI 助手", - "xpack.observabilityAiAssistant.newChatButton": "新聊天", - "xpack.observabilityAiAssistant.poweredByModel": "由 {model} 提供支持", - "xpack.observabilityAiAssistant.prompt.functionList.filter": "筛选", - "xpack.observabilityAiAssistant.prompt.functionList.functionList": "函数列表", - "xpack.observabilityAiAssistant.prompt.placeholder": "向助手发送消息", - "xpack.observabilityAiAssistant.promptEditorNaturalLanguage.euiSelectable.selectAnOptionLabel": "选择选项", "xpack.observabilityAiAssistant.regenerateResponseButtonLabel": "重新生成", "xpack.observabilityAiAssistant.requiredConnectorField": "“连接器”必填。", "xpack.observabilityAiAssistant.requiredMessageTextField": "“消息”必填。", "xpack.observabilityAiAssistant.resetDefaultPrompt": "重置为默认值", "xpack.observabilityAiAssistant.runThisQuery": "显示结果", - "xpack.observabilityAiAssistant.settingsPage.goToConnectorsButtonLabel": "管理连接器", - "xpack.observabilityAiAssistant.setupKb": "通过设置知识库来改进体验。", - "xpack.observabilityAiAssistant.simulatedFunctionCallingCalloutLabel": "模拟函数调用已启用。您可能会面临性能降级。", "xpack.observabilityAiAssistant.stopGeneratingButtonLabel": "停止生成", - "xpack.observabilityAiAssistant.suggestedFunctionEvent": "已请求函数 {functionName}", - "xpack.observabilityAiAssistant.technicalPreviewBadgeDescription": "需要 GPT4 以在使用函数调用时(例如,执行根本原因分析、数据可视化等时候)获得更加一致的体验。GPT3.5 可作用于某些更简单的工作流(如解释错误),或在 Kibana 中获得不需要频繁使用函数调用的与 ChatGPT 类似的体验。", "xpack.observabilityAiAssistant.tokenLimitError": "此对话已超出词元限制。最大词元限制为 **{tokenLimit}**,但当前对话具有 **{tokenCount}** 个词元。请启动新对话以继续。", - "xpack.observabilityAiAssistant.userExecutedFunctionEvent": "已执行函数 {functionName}", - "xpack.observabilityAiAssistant.userSuggestedFunctionEvent": "已请求函数 {functionName}", "xpack.observabilityAiAssistant.visualizeThisQuery": "可视化此查询", - "xpack.observabilityAiAssistant.welcomeMessage.div.checkTrainedModelsToLabel": " {retryInstallingLink} 或检查 {trainedModelsLink},确保 {modelName} 已部署并正在运行。", - "xpack.observabilityAiAssistant.welcomeMessage.div.settingUpKnowledgeBaseLabel": "正在设置知识库", - "xpack.observabilityAiAssistant.welcomeMessage.inspectErrorsButtonEmptyLabel": "检查问题", - "xpack.observabilityAiAssistant.welcomeMessage.issuesDescriptionListTitleLabel": "问题", - "xpack.observabilityAiAssistant.welcomeMessage.knowledgeBaseSuccessfullyInstalledLabel": "已成功安装知识库", - "xpack.observabilityAiAssistant.welcomeMessage.modelIsNotDeployedLabel": "未部署模型 {modelName}", - "xpack.observabilityAiAssistant.welcomeMessage.modelIsNotFullyAllocatedLabel": "{modelName} 的分配状态为 {allocationState}", - "xpack.observabilityAiAssistant.welcomeMessage.modelIsNotStartedLabel": "{modelName} 的部署状态为 {deploymentState}", - "xpack.observabilityAiAssistant.welcomeMessage.retryButtonLabel": "安装知识库", - "xpack.observabilityAiAssistant.welcomeMessage.trainedModelsLinkLabel": "已训练模型", - "xpack.observabilityAiAssistant.welcomeMessage.weAreSettingUpTextLabel": "我们正在设置您的知识库。这可能需要若干分钟。此进程处于运行状态时,您可以继续使用该助手。", - "xpack.observabilityAiAssistant.welcomeMessageConnectors.connectorsErrorTextLabel": "无法加载连接器", - "xpack.observabilityAiAssistant.welcomeMessageConnectors.connectorsForbiddenTextLabel": "缺少获取连接器所需的权限", - "xpack.observabilityAiAssistant.welcomeMessageKnowledgeBase.yourKnowledgeBaseIsNotSetUpCorrectlyLabel": "尚未设置您的知识库。", - "xpack.observabilityAiAssistant.welcomeMessageKnowledgeBaseSetupErrorPanel.retryInstallingLinkLabel": "重试安装", "xpack.observabilityAiAssistantManagement.apmSettings.save.error": "保存设置时出错", "xpack.observabilityAiAssistantManagement.app.description": "管理适用于 Observability 的 AI 助手。", "xpack.observabilityAiAssistantManagement.app.title": "适用于 Observability 的 AI 助手", diff --git a/yarn.lock b/yarn.lock index 911cbb9d9f175..abc5b5ee2874d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3315,6 +3315,10 @@ version "0.0.0" uid "" +"@kbn/ai-assistant@link:x-pack/packages/kbn-ai-assistant": + version "0.0.0" + uid "" + "@kbn/aiops-change-point-detection@link:x-pack/packages/ml/aiops_change_point_detection": version "0.0.0" uid "" From 4899e807426673b6a41cb6366f4cd57b551bd170 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Thu, 10 Oct 2024 15:53:03 +0200 Subject: [PATCH 50/87] Update PULL_REQUEST_TEMPLATE.md (#195766) ## Summary Add a bit more explicit call to follow the Release Notes guidelines --- .github/PULL_REQUEST_TEMPLATE.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d07f60cf09253..737eedabadfa0 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -36,4 +36,6 @@ When forming the risk matrix, consider some of the following examples and how th ### 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) +- [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#_add_your_labels) +- [ ] This will appear in the **Release Notes** and follow the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) + From 129c0a1e7f716985deffef68371d21a52c8f1c3c Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 10 Oct 2024 16:16:52 +0200 Subject: [PATCH 51/87] [dev docs] Add recently viewed docs (#195001) ## Summary Add Recently Viewed dev docs --------- Co-authored-by: Tim Sullivan Co-authored-by: Clint Andrew Hall --- dev_docs/nav-kibana-dev.docnav.json | 4 ++ .../chrome_recently_accessed.mdx | 66 ++++++++++++++++++ .../chrome_recently_accessed.png | Bin 0 -> 125734 bytes dev_docs/shared_ux/shared_ux_landing.mdx | 5 ++ 4 files changed, 75 insertions(+) create mode 100644 dev_docs/shared_ux/chrome_recently_accessed/chrome_recently_accessed.mdx create mode 100644 dev_docs/shared_ux/chrome_recently_accessed/chrome_recently_accessed.png diff --git a/dev_docs/nav-kibana-dev.docnav.json b/dev_docs/nav-kibana-dev.docnav.json index 8b8cd64a44664..a7d696fc10574 100644 --- a/dev_docs/nav-kibana-dev.docnav.json +++ b/dev_docs/nav-kibana-dev.docnav.json @@ -278,6 +278,10 @@ { "id": "kibDevReactKibanaContext", "label": "Kibana React Contexts" + }, + { + "id": "kibDevDocsChromeRecentlyAccessed", + "label": "Recently Viewed" } ] }, diff --git a/dev_docs/shared_ux/chrome_recently_accessed/chrome_recently_accessed.mdx b/dev_docs/shared_ux/chrome_recently_accessed/chrome_recently_accessed.mdx new file mode 100644 index 0000000000000..cca466bcf1ac3 --- /dev/null +++ b/dev_docs/shared_ux/chrome_recently_accessed/chrome_recently_accessed.mdx @@ -0,0 +1,66 @@ +--- +id: kibDevDocsChromeRecentlyAccessed +slug: /kibana-dev-docs/chrome/recently-accessed +title: Chrome Recently Viewed +description: How to use chrome's recently accessed service to add your links to the recently viewed list in the side navigation. +date: 2024-10-04 +tags: ['kibana', 'dev', 'contributor', 'chrome', 'navigation', 'shared-ux'] +--- + +## Introduction + +The service allows applications to register recently visited objects. These items are displayed in the "Recently Viewed" section of a side navigation menu, providing users with quick access to their previously visited resources. This service includes methods for adding, retrieving, and subscribing to the recently accessed history. + +![Recently viewed section in the sidenav](./chrome_recently_accessed.png) + +## Guidelines + +The service should be used thoughtfully to provide users with easy access to key resources they've interacted with. Unlike browser history, this feature is for important items that users may want to revisit. + +### DOs + +- Register important resources that users may want to revisit. Like a dashboard, a saved search, or another specific object. +- Update the link when the state of the current resource changes. For example, if a user changes the time range while on a dashboard, update the recently viewed link to reflect the latest viewed state where possible. See below for instructions on how to update the link when state changes. + +### DON'Ts + +- Don't register every page view. +- Don't register temporary or transient states as individual items. +- Prevent overloading. Keep the list focused on high-value resources. +- Don't add a recently viewed object without first speaking to relevant Product Managers. + +## Usage + +To register an item with the `ChromeRecentlyAccessed` service, provide a unique `id`, a `label`, and a `link`. The `id` is used to identify and deduplicate the item, the `label` is displayed in the "Recently Viewed" list and the `link` is used to navigate to the item when selected. + +```ts +const link = '/app/map/1234'; +const label = 'Map 1234'; +const id = 'map-1234'; + +coreStart.chrome.recentlyAccessed.add(link, label, id); +``` + +To update the link when state changes, add another item with the same `id`. This will replace the existing item in the "Recently Viewed" list. + +```ts +const link = '/app/map/1234'; +const label = 'Map 1234'; + +coreStart.chrome.recentlyAccessed.add(`/app/map/1234`, label, id); + +// User changes the time range and we want to update the link in the "Recently Viewed" list +coreStart.chrome.recentlyAccessed.add( + `/app/map/1234?timeRangeFrom=now-30m&timeRangeTo=now`, + label, + id +); +``` + +## Implementation details + +The services is based on package. This package provides a `RecentlyAccessedService` that uses browser local storage to manage records of recently accessed objects. Internally it implements the queue with a maximum length of 20 items. When the queue is full, the oldest item is removed. +Applications can create their own instance of `RecentlyAccessedService` to manage their own list of recently accessed items scoped to their application. + +- is a service available via `coreStart.chrome.recentlyAccessed` and should be used to add items to chrome's sidenav. +- is package that `ChromeRecentlyAccessed` is using internally and the package can be used to create your own instance and manage your own list of recently accessed items that is independent for chrome's sidenav. \ No newline at end of file diff --git a/dev_docs/shared_ux/chrome_recently_accessed/chrome_recently_accessed.png b/dev_docs/shared_ux/chrome_recently_accessed/chrome_recently_accessed.png new file mode 100644 index 0000000000000000000000000000000000000000..41d3913b048a21fa23573f26de02d45ffb21f6a0 GIT binary patch literal 125734 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