From f48ded9686505d63fe40427c26d6f2b22e462fa0 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Mon, 18 Nov 2024 09:49:10 -0600 Subject: [PATCH 01/14] Respect system dark mode in ESQL editor (#200233) When `uiSettings.overrides.theme:darkMode: true` is set, the ESQL editor uses dark mode. When `uiSettings.overrides.theme:darkMode: system` is set, the ESQL editor uses light mode, while the rest of the UI uses dark mode. Update the ESQL theme creation to create both light and dark variations of the theme at startup and use the theme in React component to determine which ESQL theme to use at runtime. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Stratoula Kalafateli --- .../kbn-esql-editor/src/esql_editor.test.tsx | 7 +- packages/kbn-esql-editor/src/esql_editor.tsx | 14 +- packages/kbn-monaco/index.ts | 2 +- packages/kbn-monaco/src/esql/index.ts | 4 +- packages/kbn-monaco/src/esql/lib/constants.ts | 3 +- .../src/esql/lib/esql_theme.test.ts | 8 +- .../kbn-monaco/src/esql/lib/esql_theme.ts | 316 +++++++++--------- packages/kbn-monaco/src/register_globals.ts | 5 +- 8 files changed, 187 insertions(+), 172 deletions(-) diff --git a/packages/kbn-esql-editor/src/esql_editor.test.tsx b/packages/kbn-esql-editor/src/esql_editor.test.tsx index ac00604e5508b..c572ff5355585 100644 --- a/packages/kbn-esql-editor/src/esql_editor.test.tsx +++ b/packages/kbn-esql-editor/src/esql_editor.test.tsx @@ -16,23 +16,20 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { ESQLEditor } from './esql_editor'; import type { ESQLEditorProps } from './types'; import { ReactWrapper } from 'enzyme'; -import { of } from 'rxjs'; +import { coreMock } from '@kbn/core/server/mocks'; describe('ESQLEditor', () => { const uiConfig: Record = {}; const uiSettings = { get: (key: string) => uiConfig[key], } as IUiSettingsClient; - const theme = { - theme$: of({ darkMode: false }), - }; const services = { uiSettings, settings: { client: uiSettings, }, - theme, + core: coreMock.createStart(), }; function renderESQLEditorComponent(testProps: ESQLEditorProps) { diff --git a/packages/kbn-esql-editor/src/esql_editor.tsx b/packages/kbn-esql-editor/src/esql_editor.tsx index e8ca582ac5229..636bb0b13ff17 100644 --- a/packages/kbn-esql-editor/src/esql_editor.tsx +++ b/packages/kbn-esql-editor/src/esql_editor.tsx @@ -25,7 +25,14 @@ import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { AggregateQuery } from '@kbn/es-query'; import type { ExpressionsStart } from '@kbn/expressions-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { ESQLLang, ESQL_LANG_ID, ESQL_THEME_ID, monaco, type ESQLCallbacks } from '@kbn/monaco'; +import { + ESQLLang, + ESQL_LANG_ID, + ESQL_DARK_THEME_ID, + ESQL_LIGHT_THEME_ID, + monaco, + type ESQLCallbacks, +} from '@kbn/monaco'; import memoize from 'lodash/memoize'; import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; @@ -91,7 +98,8 @@ export const ESQLEditor = memo(function ESQLEditor({ fieldsMetadata, uiSettings, } = kibana.services; - const timeZone = core?.uiSettings?.get('dateFormat:tz'); + const darkMode = core.theme?.getTheme().darkMode; + const timeZone = uiSettings?.get('dateFormat:tz'); const histogramBarTarget = uiSettings?.get('histogram:barTarget') ?? 50; const [code, setCode] = useState(query.esql ?? ''); // To make server side errors less "sticky", register the state of the code when submitting @@ -597,7 +605,7 @@ export const ESQLEditor = memo(function ESQLEditor({ vertical: 'auto', }, scrollBeyondLastLine: false, - theme: ESQL_THEME_ID, + theme: darkMode ? ESQL_DARK_THEME_ID : ESQL_LIGHT_THEME_ID, wordWrap: 'on', wrappingIndent: 'none', }; diff --git a/packages/kbn-monaco/index.ts b/packages/kbn-monaco/index.ts index ba8b0edb68e1a..283c3150302b7 100644 --- a/packages/kbn-monaco/index.ts +++ b/packages/kbn-monaco/index.ts @@ -20,7 +20,7 @@ export { } from './src/monaco_imports'; export { XJsonLang } from './src/xjson'; export { SQLLang } from './src/sql'; -export { ESQL_LANG_ID, ESQL_THEME_ID, ESQLLang } from './src/esql'; +export { ESQL_LANG_ID, ESQL_DARK_THEME_ID, ESQL_LIGHT_THEME_ID, ESQLLang } from './src/esql'; export type { ESQLCallbacks } from '@kbn/esql-validation-autocomplete'; export * from './src/painless'; diff --git a/packages/kbn-monaco/src/esql/index.ts b/packages/kbn-monaco/src/esql/index.ts index b14a2ab18ba75..64d49b155cc42 100644 --- a/packages/kbn-monaco/src/esql/index.ts +++ b/packages/kbn-monaco/src/esql/index.ts @@ -7,6 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { ESQL_LANG_ID, ESQL_THEME_ID } from './lib/constants'; +export { ESQL_LANG_ID, ESQL_DARK_THEME_ID, ESQL_LIGHT_THEME_ID } from './lib/constants'; export { ESQLLang } from './language'; -export { buildESQlTheme } from './lib/esql_theme'; +export { buildESQLTheme } from './lib/esql_theme'; diff --git a/packages/kbn-monaco/src/esql/lib/constants.ts b/packages/kbn-monaco/src/esql/lib/constants.ts index b0b0588b3ff4a..56f2f85ab074e 100644 --- a/packages/kbn-monaco/src/esql/lib/constants.ts +++ b/packages/kbn-monaco/src/esql/lib/constants.ts @@ -8,6 +8,7 @@ */ export const ESQL_LANG_ID = 'esql'; -export const ESQL_THEME_ID = 'esqlTheme'; +export const ESQL_LIGHT_THEME_ID = 'esqlThemeLight'; +export const ESQL_DARK_THEME_ID = 'esqlThemeDark'; export const ESQL_TOKEN_POSTFIX = '.esql'; diff --git a/packages/kbn-monaco/src/esql/lib/esql_theme.test.ts b/packages/kbn-monaco/src/esql/lib/esql_theme.test.ts index 237996a7fbcaa..c2a200e650804 100644 --- a/packages/kbn-monaco/src/esql/lib/esql_theme.test.ts +++ b/packages/kbn-monaco/src/esql/lib/esql_theme.test.ts @@ -9,12 +9,12 @@ import { ESQLErrorListener, getLexer as _getLexer } from '@kbn/esql-ast'; import { ESQL_TOKEN_POSTFIX } from './constants'; -import { buildESQlTheme } from './esql_theme'; +import { buildESQLTheme } from './esql_theme'; import { CharStreams } from 'antlr4'; describe('ESQL Theme', () => { it('should not have multiple rules for a single token', () => { - const theme = buildESQlTheme(); + const theme = buildESQLTheme({ darkMode: false }); const seen = new Set(); const duplicates: string[] = []; @@ -40,7 +40,7 @@ describe('ESQL Theme', () => { .map((name) => name!.toLowerCase()); it('every rule should apply to a valid lexical name', () => { - const theme = buildESQlTheme(); + const theme = buildESQLTheme({ darkMode: false }); // These names aren't from the lexer... they are added on our side // see packages/kbn-monaco/src/esql/lib/esql_token_helpers.ts @@ -62,7 +62,7 @@ describe('ESQL Theme', () => { }); it('every valid lexical name should have a corresponding rule', () => { - const theme = buildESQlTheme(); + const theme = buildESQLTheme({ darkMode: false }); const tokenIDs = theme.rules.map((rule) => rule.token.replace(ESQL_TOKEN_POSTFIX, '')); const validExceptions = [ diff --git a/packages/kbn-monaco/src/esql/lib/esql_theme.ts b/packages/kbn-monaco/src/esql/lib/esql_theme.ts index 330e55de86155..07a4d723b63e8 100644 --- a/packages/kbn-monaco/src/esql/lib/esql_theme.ts +++ b/packages/kbn-monaco/src/esql/lib/esql_theme.ts @@ -7,169 +7,177 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { euiThemeVars, darkMode } from '@kbn/ui-theme'; +import { euiDarkVars, euiLightVars } from '@kbn/ui-theme'; import { themeRuleGroupBuilderFactory } from '../../common/theme'; import { ESQL_TOKEN_POSTFIX } from './constants'; import { monaco } from '../../monaco_imports'; const buildRuleGroup = themeRuleGroupBuilderFactory(ESQL_TOKEN_POSTFIX); -export const buildESQlTheme = (): monaco.editor.IStandaloneThemeData => ({ - base: darkMode ? 'vs-dark' : 'vs', - inherit: true, - rules: [ - // base - ...buildRuleGroup( - [ - 'explain', - 'ws', - 'assign', - 'comma', - 'dot', - 'opening_bracket', - 'closing_bracket', - 'quoted_identifier', - 'unquoted_identifier', - 'pipe', - ], - euiThemeVars.euiTextColor - ), +export const buildESQLTheme = ({ + darkMode, +}: { + darkMode: boolean; +}): monaco.editor.IStandaloneThemeData => { + const euiThemeVars = darkMode ? euiDarkVars : euiLightVars; - // source commands - ...buildRuleGroup( - ['from', 'row', 'show'], - euiThemeVars.euiColorPrimaryText, - true // isBold - ), + return { + base: darkMode ? 'vs-dark' : 'vs', + inherit: true, + rules: [ + // base + ...buildRuleGroup( + [ + 'explain', + 'ws', + 'assign', + 'comma', + 'dot', + 'opening_bracket', + 'closing_bracket', + 'quoted_identifier', + 'unquoted_identifier', + 'pipe', + ], + euiThemeVars.euiTextColor + ), - // commands - ...buildRuleGroup( - [ - 'dev_metrics', - 'metadata', - 'mv_expand', - 'stats', - 'dev_inlinestats', - 'dissect', - 'grok', - 'keep', - 'rename', - 'drop', - 'eval', - 'sort', - 'by', - 'where', - 'not', - 'is', - 'like', - 'rlike', - 'in', - 'as', - 'limit', - 'dev_lookup', - 'null', - 'enrich', - 'on', - 'with', - 'asc', - 'desc', - 'nulls_order', - ], - euiThemeVars.euiColorAccentText, - true // isBold - ), + // source commands + ...buildRuleGroup( + ['from', 'row', 'show'], + euiThemeVars.euiColorPrimaryText, + true // isBold + ), - // functions - ...buildRuleGroup(['functions'], euiThemeVars.euiColorPrimaryText), + // commands + ...buildRuleGroup( + [ + 'dev_metrics', + 'metadata', + 'mv_expand', + 'stats', + 'dev_inlinestats', + 'dissect', + 'grok', + 'keep', + 'rename', + 'drop', + 'eval', + 'sort', + 'by', + 'where', + 'not', + 'is', + 'like', + 'rlike', + 'in', + 'as', + 'limit', + 'dev_lookup', + 'null', + 'enrich', + 'on', + 'with', + 'asc', + 'desc', + 'nulls_order', + ], + euiThemeVars.euiColorAccentText, + true // isBold + ), - // operators - ...buildRuleGroup( - [ - 'or', - 'and', - 'rp', // ')' - 'lp', // '(' - 'eq', // '==' - 'cieq', // '=~' - 'neq', // '!=' - 'lt', // '<' - 'lte', // '<=' - 'gt', // '>' - 'gte', // '>=' - 'plus', // '+' - 'minus', // '-' - 'asterisk', // '*' - 'slash', // '/' - 'percent', // '%' - 'cast_op', // '::' - ], - euiThemeVars.euiColorPrimaryText - ), + // functions + ...buildRuleGroup(['functions'], euiThemeVars.euiColorPrimaryText), - // comments - ...buildRuleGroup( - [ - 'line_comment', - 'multiline_comment', - 'expr_line_comment', - 'expr_multiline_comment', - 'explain_line_comment', - 'explain_multiline_comment', - 'project_line_comment', - 'project_multiline_comment', - 'rename_line_comment', - 'rename_multiline_comment', - 'from_line_comment', - 'from_multiline_comment', - 'enrich_line_comment', - 'enrich_multiline_comment', - 'mvexpand_line_comment', - 'mvexpand_multiline_comment', - 'enrich_field_line_comment', - 'enrich_field_multiline_comment', - 'lookup_line_comment', - 'lookup_multiline_comment', - 'lookup_field_line_comment', - 'lookup_field_multiline_comment', - 'show_line_comment', - 'show_multiline_comment', - 'setting', - 'setting_line_comment', - 'settting_multiline_comment', - 'metrics_line_comment', - 'metrics_multiline_comment', - 'closing_metrics_line_comment', - 'closing_metrics_multiline_comment', - ], - euiThemeVars.euiColorDisabledText - ), + // operators + ...buildRuleGroup( + [ + 'or', + 'and', + 'rp', // ')' + 'lp', // '(' + 'eq', // '==' + 'cieq', // '=~' + 'neq', // '!=' + 'lt', // '<' + 'lte', // '<=' + 'gt', // '>' + 'gte', // '>=' + 'plus', // '+' + 'minus', // '-' + 'asterisk', // '*' + 'slash', // '/' + 'percent', // '%' + 'cast_op', // '::' + ], + euiThemeVars.euiColorPrimaryText + ), - // values - ...buildRuleGroup( - [ - 'quoted_string', - 'integer_literal', - 'decimal_literal', - 'named_or_positional_param', - 'param', - 'timespan_literal', - ], - euiThemeVars.euiColorSuccessText - ), - ], - colors: { - 'editor.foreground': euiThemeVars.euiTextColor, - 'editor.background': euiThemeVars.euiColorEmptyShade, - 'editor.lineHighlightBackground': euiThemeVars.euiColorLightestShade, - 'editor.lineHighlightBorder': euiThemeVars.euiColorLightestShade, - 'editor.selectionHighlightBackground': euiThemeVars.euiColorLightestShade, - 'editor.selectionHighlightBorder': euiThemeVars.euiColorLightShade, - 'editorSuggestWidget.background': euiThemeVars.euiColorEmptyShade, - 'editorSuggestWidget.border': euiThemeVars.euiColorEmptyShade, - 'editorSuggestWidget.focusHighlightForeground': euiThemeVars.euiColorEmptyShade, - 'editorSuggestWidget.foreground': euiThemeVars.euiTextColor, - 'editorSuggestWidget.highlightForeground': euiThemeVars.euiColorPrimary, - 'editorSuggestWidget.selectedBackground': euiThemeVars.euiColorPrimary, - 'editorSuggestWidget.selectedForeground': euiThemeVars.euiColorEmptyShade, - }, -}); + // comments + ...buildRuleGroup( + [ + 'line_comment', + 'multiline_comment', + 'expr_line_comment', + 'expr_multiline_comment', + 'explain_line_comment', + 'explain_multiline_comment', + 'project_line_comment', + 'project_multiline_comment', + 'rename_line_comment', + 'rename_multiline_comment', + 'from_line_comment', + 'from_multiline_comment', + 'enrich_line_comment', + 'enrich_multiline_comment', + 'mvexpand_line_comment', + 'mvexpand_multiline_comment', + 'enrich_field_line_comment', + 'enrich_field_multiline_comment', + 'lookup_line_comment', + 'lookup_multiline_comment', + 'lookup_field_line_comment', + 'lookup_field_multiline_comment', + 'show_line_comment', + 'show_multiline_comment', + 'setting', + 'setting_line_comment', + 'settting_multiline_comment', + 'metrics_line_comment', + 'metrics_multiline_comment', + 'closing_metrics_line_comment', + 'closing_metrics_multiline_comment', + ], + euiThemeVars.euiColorDisabledText + ), + + // values + ...buildRuleGroup( + [ + 'quoted_string', + 'integer_literal', + 'decimal_literal', + 'named_or_positional_param', + 'param', + 'timespan_literal', + ], + euiThemeVars.euiColorSuccessText + ), + ], + colors: { + 'editor.foreground': euiThemeVars.euiTextColor, + 'editor.background': euiThemeVars.euiColorEmptyShade, + 'editor.lineHighlightBackground': euiThemeVars.euiColorLightestShade, + 'editor.lineHighlightBorder': euiThemeVars.euiColorLightestShade, + 'editor.selectionHighlightBackground': euiThemeVars.euiColorLightestShade, + 'editor.selectionHighlightBorder': euiThemeVars.euiColorLightShade, + 'editorSuggestWidget.background': euiThemeVars.euiColorEmptyShade, + 'editorSuggestWidget.border': euiThemeVars.euiColorEmptyShade, + 'editorSuggestWidget.focusHighlightForeground': euiThemeVars.euiColorEmptyShade, + 'editorSuggestWidget.foreground': euiThemeVars.euiTextColor, + 'editorSuggestWidget.highlightForeground': euiThemeVars.euiColorPrimary, + 'editorSuggestWidget.selectedBackground': euiThemeVars.euiColorPrimary, + 'editorSuggestWidget.selectedForeground': euiThemeVars.euiColorEmptyShade, + }, + }; +}; diff --git a/packages/kbn-monaco/src/register_globals.ts b/packages/kbn-monaco/src/register_globals.ts index b4d9c07f78c79..32b8fb0ef2ece 100644 --- a/packages/kbn-monaco/src/register_globals.ts +++ b/packages/kbn-monaco/src/register_globals.ts @@ -11,7 +11,7 @@ import { XJsonLang } from './xjson'; import { PainlessLang } from './painless'; import { SQLLang } from './sql'; import { monaco } from './monaco_imports'; -import { ESQL_THEME_ID, ESQLLang, buildESQlTheme } from './esql'; +import { ESQL_DARK_THEME_ID, ESQL_LIGHT_THEME_ID, ESQLLang, buildESQLTheme } from './esql'; import { YAML_LANG_ID } from './yaml'; import { registerLanguage, registerTheme } from './helpers'; import { ConsoleLang, ConsoleOutputLang, CONSOLE_THEME_ID, buildConsoleTheme } from './console'; @@ -50,7 +50,8 @@ registerLanguage(ConsoleOutputLang); /** * Register custom themes */ -registerTheme(ESQL_THEME_ID, buildESQlTheme()); +registerTheme(ESQL_LIGHT_THEME_ID, buildESQLTheme({ darkMode: false })); +registerTheme(ESQL_DARK_THEME_ID, buildESQLTheme({ darkMode: true })); registerTheme(CONSOLE_THEME_ID, buildConsoleTheme()); registerTheme(CODE_EDITOR_LIGHT_THEME_ID, buildLightTheme()); registerTheme(CODE_EDITOR_DARK_THEME_ID, buildDarkTheme()); From a6dfa6b6f616826d3f17aae8b4be88b0d0afd9ea Mon Sep 17 00:00:00 2001 From: Saarika Bhasi <55930906+saarikabhasi@users.noreply.github.com> Date: Mon, 18 Nov 2024 21:45:05 +0530 Subject: [PATCH 02/14] [Search][Onboarding] Enable or disable index actions based on user privileges (#197869) ## Summary Disable or show index actions in the search indices index details page based on user privileges. Stack should work as it is, this change is relevant to Serverless search index details page. based on user privilege below buttons are disabled: 1. delete index in index action context menu 2. delete documents in data tab 3. add field in mappings tab 4. edit index settings in setting tab ### Screen recording https://github.com/user-attachments/assets/047f0d9f-b6c3-4665-9cff-a7a4d978b03b ### 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 - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine --- .../components/result/result.tsx | 3 + .../components/result/result_types.ts | 1 + .../components/result/rich_result_header.tsx | 67 ++- .../src/types.ts | 2 + .../details_page/details_page_mappings.tsx | 4 +- .../details_page_mappings_content.tsx | 40 +- .../details_page/details_page_settings.tsx | 4 +- .../details_page_settings_content.tsx | 38 +- .../index_mapping_with_context.tsx | 7 +- .../index_settings_with_context.tsx | 6 +- .../plugins/search_indices/common/routes.ts | 2 +- x-pack/plugins/search_indices/common/types.ts | 3 +- .../components/create_index/create_index.tsx | 13 +- .../create_index/create_index_page.tsx | 4 +- .../index_documents/document_list.tsx | 9 +- .../index_documents/index_documents.tsx | 15 +- .../components/indices/details_page.tsx | 12 +- .../indices/details_page_mappings.tsx | 18 +- .../indices/details_page_menu_item.tsx | 22 +- .../indices/details_page_settings.tsx | 16 +- .../components/shared/create_index_form.tsx | 6 +- .../components/start/elasticsearch_start.tsx | 14 +- .../public/components/start/start_page.tsx | 5 +- .../public/hooks/api/use_user_permissions.ts | 8 +- .../search_indices/server/lib/status.test.ts | 51 ++- .../search_indices/server/lib/status.ts | 10 +- .../search_indices/server/routes/status.ts | 14 +- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../page_objects/index_management_page.ts | 22 + .../search/search_indices/status.ts | 45 +- .../svl_search_index_detail_page.ts | 64 ++- .../index_management/index_detail.ts | 9 + .../test_suites/search/search_index_detail.ts | 406 +++++++++++------- 35 files changed, 638 insertions(+), 305 deletions(-) diff --git a/packages/kbn-search-index-documents/components/result/result.tsx b/packages/kbn-search-index-documents/components/result/result.tsx index 207a4770b97f2..ff3447229d8ed 100644 --- a/packages/kbn-search-index-documents/components/result/result.tsx +++ b/packages/kbn-search-index-documents/components/result/result.tsx @@ -37,6 +37,7 @@ export interface ResultProps { compactCard?: boolean; onDocumentClick?: () => void; onDocumentDelete?: () => void; + hasDeleteDocumentsPrivilege?: boolean; } export const Result: React.FC = ({ @@ -47,6 +48,7 @@ export const Result: React.FC = ({ showScore = false, onDocumentClick, onDocumentDelete, + hasDeleteDocumentsPrivilege, }) => { const [isExpanded, setIsExpanded] = useState(false); const tooltipText = @@ -97,6 +99,7 @@ export const Result: React.FC = ({ metaData={{ ...metaData, onDocumentDelete, + hasDeleteDocumentsPrivilege, }} /> )} diff --git a/packages/kbn-search-index-documents/components/result/result_types.ts b/packages/kbn-search-index-documents/components/result/result_types.ts index 420951333a05d..fd132b8d30069 100644 --- a/packages/kbn-search-index-documents/components/result/result_types.ts +++ b/packages/kbn-search-index-documents/components/result/result_types.ts @@ -23,6 +23,7 @@ export interface MetaDataProps { title?: string; score?: SearchHit['_score']; showScore?: boolean; + hasDeleteDocumentsPrivilege?: boolean; } export interface FieldProps { diff --git a/packages/kbn-search-index-documents/components/result/rich_result_header.tsx b/packages/kbn-search-index-documents/components/result/rich_result_header.tsx index 98d2ea5a64de0..d4fa39fbe5edf 100644 --- a/packages/kbn-search-index-documents/components/result/rich_result_header.tsx +++ b/packages/kbn-search-index-documents/components/result/rich_result_header.tsx @@ -24,10 +24,12 @@ import { EuiTextColor, EuiTitle, useEuiTheme, + EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; +import { FormattedMessage } from '@kbn/i18n-react'; import { MetaDataProps } from './result_types'; interface Props { @@ -60,6 +62,7 @@ const MetadataPopover: React.FC = ({ onDocumentDelete, score, showScore = false, + hasDeleteDocumentsPrivilege, }) => { const [popoverIsOpen, setPopoverIsOpen] = useState(false); const closePopover = () => setPopoverIsOpen(false); @@ -85,9 +88,10 @@ const MetadataPopover: React.FC = ({ return ( - {i18n.translate('searchIndexDocuments.result.header.metadata.title', { - defaultMessage: 'Document metadata', - })} + = ({ @@ -118,22 +125,40 @@ const MetadataPopover: React.FC = ({ {onDocumentDelete && ( - ) => { - e.stopPropagation(); - onDocumentDelete(); - closePopover(); - }} - fullWidth + - {i18n.translate('searchIndexDocuments.result.header.metadata.deleteDocument', { - defaultMessage: 'Delete document', - })} - + ) => { + e.stopPropagation(); + onDocumentDelete(); + closePopover(); + }} + fullWidth + > + + + )} diff --git a/x-pack/packages/index-management/index_management_shared_types/src/types.ts b/x-pack/packages/index-management/index_management_shared_types/src/types.ts index ec5c7938d6b4b..02404ddec6213 100644 --- a/x-pack/packages/index-management/index_management_shared_types/src/types.ts +++ b/x-pack/packages/index-management/index_management_shared_types/src/types.ts @@ -79,9 +79,11 @@ export interface Index { export interface IndexMappingProps { index?: Index; showAboutMappings?: boolean; + hasUpdateMappingsPrivilege?: boolean; } export interface IndexSettingProps { indexName: string; + hasUpdateSettingsPrivilege?: boolean; } export interface SendRequestResponse { data: D | null; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_mappings.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_mappings.tsx index 77360fd85ad9a..10f5f8be36b85 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_mappings.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_mappings.tsx @@ -19,7 +19,8 @@ import { useLoadIndexMappings } from '../../../../services'; export const DetailsPageMappings: FunctionComponent<{ index?: Index; showAboutMappings?: boolean; -}> = ({ index, showAboutMappings = true }) => { + hasUpdateMappingsPrivilege?: boolean; +}> = ({ index, showAboutMappings = true, hasUpdateMappingsPrivilege }) => { const { isLoading, data, error, resendRequest } = useLoadIndexMappings(index?.name || ''); const [jsonError, setJsonError] = useState(false); @@ -95,6 +96,7 @@ export const DetailsPageMappings: FunctionComponent<{ jsonData={data} showAboutMappings={showAboutMappings} refetchMapping={resendRequest} + hasUpdateMappingsPrivilege={hasUpdateMappingsPrivilege} /> ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_mappings_content.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_mappings_content.tsx index 567d3f782f6f1..e2f9cb68ad90d 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_mappings_content.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_mappings_content.tsx @@ -22,6 +22,7 @@ import { EuiText, EuiTitle, useGeneratedHtmlId, + EuiToolTip, } from '@elastic/eui'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; @@ -68,7 +69,8 @@ export const DetailsPageMappingsContent: FunctionComponent<{ showAboutMappings: boolean; jsonData: any; refetchMapping: () => void; -}> = ({ index, data, jsonData, refetchMapping, showAboutMappings }) => { + hasUpdateMappingsPrivilege?: boolean; +}> = ({ index, data, jsonData, refetchMapping, showAboutMappings, hasUpdateMappingsPrivilege }) => { const { services: { extensionsService }, core: { @@ -475,18 +477,32 @@ export const DetailsPageMappingsContent: FunctionComponent<{ {!index.hidden && ( {!isAddingFields ? ( - - - + + + + ) : ( updateMappings()} diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_settings.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_settings.tsx index e04b4798c4041..0c6f844f2c068 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_settings.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_settings.tsx @@ -15,7 +15,8 @@ import { DetailsPageSettingsContent } from './details_page_settings_content'; export const DetailsPageSettings: FunctionComponent<{ indexName: string; -}> = ({ indexName }) => { + hasUpdateSettingsPrivilege?: boolean; +}> = ({ indexName, hasUpdateSettingsPrivilege }) => { const { isLoading, data, error, resendRequest } = useLoadIndexSettings(indexName); if (isLoading) { @@ -76,6 +77,7 @@ export const DetailsPageSettings: FunctionComponent<{ data={data} indexName={indexName} reloadIndexSettings={resendRequest} + hasUpdateSettingsPrivilege={hasUpdateSettingsPrivilege} /> ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_settings_content.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_settings_content.tsx index 51ca47ba5c673..95ce72cf59abf 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_settings_content.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_settings_content.tsx @@ -19,6 +19,7 @@ import { EuiSwitch, EuiSwitchEvent, EuiText, + EuiToolTip, } from '@elastic/eui'; import { css } from '@emotion/react'; import _ from 'lodash'; @@ -69,12 +70,14 @@ interface Props { data: IndexSettingsResponse; indexName: string; reloadIndexSettings: () => void; + hasUpdateSettingsPrivilege?: boolean; } export const DetailsPageSettingsContent: FunctionComponent = ({ data, indexName, reloadIndexSettings, + hasUpdateSettingsPrivilege, }) => { const [isEditMode, setIsEditMode] = useState(false); const { @@ -184,17 +187,32 @@ export const DetailsPageSettingsContent: FunctionComponent = ({ - + + data-test-subj="indexDetailsSettingsEditModeSwitchToolTip" + > + + } + checked={isEditMode} + onChange={onEditModeChange} + disabled={hasUpdateSettingsPrivilege === false} + /> + + diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_mapping_with_context.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_mapping_with_context.tsx index a341b0fb67813..5b795f57c161b 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_mapping_with_context.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_mapping_with_context.tsx @@ -20,6 +20,7 @@ export const IndexMappingWithContext: React.FC = ( dependencies, index, showAboutMappings, + hasUpdateMappingsPrivilege, }) => { // this normally happens when the index management app is rendered // but if components are embedded elsewhere that setup is skipped, so we have to do it here @@ -42,7 +43,11 @@ export const IndexMappingWithContext: React.FC = ( }; return ( - + ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_settings_with_context.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_settings_with_context.tsx index d56c2c46e8ec4..57aba9cda5941 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_settings_with_context.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_settings_with_context.tsx @@ -20,6 +20,7 @@ export const IndexSettingsWithContext: React.FC = dependencies, indexName, usageCollection, + hasUpdateSettingsPrivilege, }) => { // this normally happens when the index management app is rendered // but if components are embedded elsewhere that setup is skipped, so we have to do it here @@ -46,7 +47,10 @@ export const IndexSettingsWithContext: React.FC = }; return ( - + ); }; diff --git a/x-pack/plugins/search_indices/common/routes.ts b/x-pack/plugins/search_indices/common/routes.ts index 9ffe1d09d3db5..f527fa676e2a0 100644 --- a/x-pack/plugins/search_indices/common/routes.ts +++ b/x-pack/plugins/search_indices/common/routes.ts @@ -6,7 +6,7 @@ */ export const GET_STATUS_ROUTE = '/internal/search_indices/status'; -export const GET_USER_PRIVILEGES_ROUTE = '/internal/search_indices/start_privileges'; +export const GET_USER_PRIVILEGES_ROUTE = '/internal/search_indices/start_privileges/{indexName}'; export const POST_CREATE_INDEX_ROUTE = '/internal/search_indices/indices/create'; diff --git a/x-pack/plugins/search_indices/common/types.ts b/x-pack/plugins/search_indices/common/types.ts index ef3a46e0301b5..ae5f53a9d073c 100644 --- a/x-pack/plugins/search_indices/common/types.ts +++ b/x-pack/plugins/search_indices/common/types.ts @@ -12,7 +12,8 @@ export interface IndicesStatusResponse { export interface UserStartPrivilegesResponse { privileges: { canCreateApiKeys: boolean; - canCreateIndex: boolean; + canManageIndex: boolean; + canDeleteDocuments: boolean; }; } diff --git a/x-pack/plugins/search_indices/public/components/create_index/create_index.tsx b/x-pack/plugins/search_indices/public/components/create_index/create_index.tsx index d8ce8073c691e..f09ae3856c097 100644 --- a/x-pack/plugins/search_indices/public/components/create_index/create_index.tsx +++ b/x-pack/plugins/search_indices/public/components/create_index/create_index.tsx @@ -7,10 +7,11 @@ import React, { useCallback, useState } from 'react'; -import type { IndicesStatusResponse, UserStartPrivilegesResponse } from '../../../common'; +import type { IndicesStatusResponse } from '../../../common'; import { AnalyticsEvents } from '../../analytics/constants'; import { AvailableLanguages } from '../../code_examples'; +import { useUserPrivilegesQuery } from '../../hooks/api/use_user_permissions'; import { useKibana } from '../../hooks/use_kibana'; import { useUsageTracker } from '../../hooks/use_usage_tracker'; import { CreateIndexFormState } from '../../types'; @@ -31,9 +32,8 @@ function initCreateIndexState() { }; } -export interface CreateIndexProps { +interface CreateIndexProps { indicesData?: IndicesStatusResponse; - userPrivileges?: UserStartPrivilegesResponse; } enum CreateIndexViewMode { @@ -41,14 +41,15 @@ enum CreateIndexViewMode { Code = 'code', } -export const CreateIndex = ({ indicesData, userPrivileges }: CreateIndexProps) => { +export const CreateIndex = ({ indicesData }: CreateIndexProps) => { const { application } = useKibana().services; + const [formState, setFormState] = useState(initCreateIndexState); + const { data: userPrivileges } = useUserPrivilegesQuery(formState.defaultIndexName); const [createIndexView, setCreateIndexView] = useState( - userPrivileges?.privileges.canCreateIndex === false + userPrivileges?.privileges.canManageIndex === false ? CreateIndexViewMode.Code : CreateIndexViewMode.UI ); - const [formState, setFormState] = useState(initCreateIndexState); const usageTracker = useUsageTracker(); const onChangeView = useCallback( (id: string) => { diff --git a/x-pack/plugins/search_indices/public/components/create_index/create_index_page.tsx b/x-pack/plugins/search_indices/public/components/create_index/create_index_page.tsx index d8601e95760d7..56ee5f49c5339 100644 --- a/x-pack/plugins/search_indices/public/components/create_index/create_index_page.tsx +++ b/x-pack/plugins/search_indices/public/components/create_index/create_index_page.tsx @@ -13,7 +13,6 @@ import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { useKibana } from '../../hooks/use_kibana'; import { useIndicesStatusQuery } from '../../hooks/api/use_indices_status'; -import { useUserPrivilegesQuery } from '../../hooks/api/use_user_permissions'; import { LoadIndicesStatusError } from '../shared/load_indices_status_error'; import { CreateIndex } from './create_index'; @@ -32,7 +31,6 @@ export const CreateIndexPage = () => { isError: hasIndicesStatusFetchError, error: indicesFetchError, } = useIndicesStatusQuery(); - const { data: userPrivileges } = useUserPrivilegesQuery(); const embeddableConsole = useMemo( () => (consolePlugin?.EmbeddableConsole ? : null), @@ -51,7 +49,7 @@ export const CreateIndexPage = () => { {isInitialLoading && } {hasIndicesStatusFetchError && } {!isInitialLoading && !hasIndicesStatusFetchError && ( - + )} {embeddableConsole} diff --git a/x-pack/plugins/search_indices/public/components/index_documents/document_list.tsx b/x-pack/plugins/search_indices/public/components/index_documents/document_list.tsx index e86d1c5ad818a..cf9cce4928d01 100644 --- a/x-pack/plugins/search_indices/public/components/index_documents/document_list.tsx +++ b/x-pack/plugins/search_indices/public/components/index_documents/document_list.tsx @@ -20,9 +20,15 @@ export interface DocumentListProps { indexName: string; docs: SearchHit[]; mappingProperties: Record; + hasDeleteDocumentsPrivilege: boolean; } -export const DocumentList = ({ indexName, docs, mappingProperties }: DocumentListProps) => { +export const DocumentList = ({ + indexName, + docs, + mappingProperties, + hasDeleteDocumentsPrivilege, +}: DocumentListProps) => { const { mutate } = useDeleteDocument(indexName); return ( @@ -39,6 +45,7 @@ export const DocumentList = ({ indexName, docs, mappingProperties }: DocumentLis mutate({ id: doc._id! }); }} compactCard={false} + hasDeleteDocumentsPrivilege={hasDeleteDocumentsPrivilege} /> diff --git a/x-pack/plugins/search_indices/public/components/index_documents/index_documents.tsx b/x-pack/plugins/search_indices/public/components/index_documents/index_documents.tsx index 83595913cece3..5e14275a492f8 100644 --- a/x-pack/plugins/search_indices/public/components/index_documents/index_documents.tsx +++ b/x-pack/plugins/search_indices/public/components/index_documents/index_documents.tsx @@ -5,28 +5,34 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiProgress, EuiSpacer } from '@elastic/eui'; import { useIndexMapping } from '../../hooks/api/use_index_mappings'; import { AddDocumentsCodeExample } from './add_documents_code_example'; import { IndexDocuments as IndexDocumentsType } from '../../hooks/api/use_document_search'; import { DocumentList } from './document_list'; +import type { UserStartPrivilegesResponse } from '../../../common'; interface IndexDocumentsProps { indexName: string; indexDocuments?: IndexDocumentsType; isInitialLoading: boolean; + userPrivileges?: UserStartPrivilegesResponse; } export const IndexDocuments: React.FC = ({ indexName, indexDocuments, isInitialLoading, + userPrivileges, }) => { const { data: mappingData } = useIndexMapping(indexName); const docs = indexDocuments?.results?.data ?? []; const mappingProperties = mappingData?.mappings?.properties ?? {}; + const hasDeleteDocumentsPrivilege: boolean = useMemo(() => { + return userPrivileges?.privileges.canDeleteDocuments ?? false; + }, [userPrivileges]); return ( @@ -38,7 +44,12 @@ export const IndexDocuments: React.FC = ({ )} {docs.length > 0 && ( - + )} diff --git a/x-pack/plugins/search_indices/public/components/indices/details_page.tsx b/x-pack/plugins/search_indices/public/components/indices/details_page.tsx index c672bb51493f6..fb09943710dc6 100644 --- a/x-pack/plugins/search_indices/public/components/indices/details_page.tsx +++ b/x-pack/plugins/search_indices/public/components/indices/details_page.tsx @@ -37,6 +37,7 @@ import { SearchIndexDetailsPageMenuItemPopover } from './details_page_menu_item' import { useIndexDocumentSearch } from '../../hooks/api/use_document_search'; import { useUsageTracker } from '../../contexts/usage_tracker_context'; import { AnalyticsEvents } from '../../analytics/constants'; +import { useUserPrivilegesQuery } from '../../hooks/api/use_user_permissions'; import { usePageChrome } from '../../hooks/use_page_chrome'; import { IndexManagementBreadcrumbs } from '../shared/breadcrumbs'; @@ -60,6 +61,7 @@ export const SearchIndexDetailsPage = () => { } = useIndexMapping(indexName); const { data: indexDocuments, isInitialLoading: indexDocumentsIsInitialLoading } = useIndexDocumentSearch(indexName); + const { data: userPrivileges } = useUserPrivilegesQuery(indexName); const navigateToPlayground = useCallback(async () => { const playgroundLocator = share.url.locators.get('PLAYGROUND_LOCATOR_ID'); @@ -97,6 +99,7 @@ export const SearchIndexDetailsPage = () => { indexName={indexName} indexDocuments={indexDocuments} isInitialLoading={indexDocumentsIsInitialLoading} + userPrivileges={userPrivileges} /> ), 'data-test-subj': `${SearchIndexDetailsTabs.DATA}Tab`, @@ -106,7 +109,7 @@ export const SearchIndexDetailsPage = () => { name: i18n.translate('xpack.searchIndices.mappingsTabLabel', { defaultMessage: 'Mappings', }), - content: , + content: , 'data-test-subj': `${SearchIndexDetailsTabs.MAPPINGS}Tab`, }, { @@ -114,11 +117,13 @@ export const SearchIndexDetailsPage = () => { name: i18n.translate('xpack.searchIndices.settingsTabLabel', { defaultMessage: 'Settings', }), - content: , + content: ( + + ), 'data-test-subj': `${SearchIndexDetailsTabs.SETTINGS}Tab`, }, ]; - }, [index, indexName, indexDocuments, indexDocumentsIsInitialLoading]); + }, [index, indexName, indexDocuments, indexDocumentsIsInitialLoading, userPrivileges]); const [selectedTab, setSelectedTab] = useState(detailsPageTabs[0]); useEffect(() => { @@ -256,6 +261,7 @@ export const SearchIndexDetailsPage = () => { , diff --git a/x-pack/plugins/search_indices/public/components/indices/details_page_mappings.tsx b/x-pack/plugins/search_indices/public/components/indices/details_page_mappings.tsx index c90d5cad94c83..4ce415b5aba3c 100644 --- a/x-pack/plugins/search_indices/public/components/indices/details_page_mappings.tsx +++ b/x-pack/plugins/search_indices/public/components/indices/details_page_mappings.tsx @@ -10,10 +10,16 @@ import { Index } from '@kbn/index-management-shared-types'; import React from 'react'; import { useMemo } from 'react'; import { useKibana } from '../../hooks/use_kibana'; +import type { UserStartPrivilegesResponse } from '../../../common'; + export interface SearchIndexDetailsMappingsProps { index?: Index; + userPrivileges?: UserStartPrivilegesResponse; } -export const SearchIndexDetailsMappings = ({ index }: SearchIndexDetailsMappingsProps) => { +export const SearchIndexDetailsMappings = ({ + index, + userPrivileges, +}: SearchIndexDetailsMappingsProps) => { const { indexManagement, history } = useKibana().services; const IndexMappingComponent = useMemo( @@ -21,10 +27,18 @@ export const SearchIndexDetailsMappings = ({ index }: SearchIndexDetailsMappings [indexManagement, history] ); + const hasUpdateMappingsPrivilege = useMemo(() => { + return userPrivileges?.privileges.canManageIndex === true; + }, [userPrivileges]); + return ( <> - + ); }; diff --git a/x-pack/plugins/search_indices/public/components/indices/details_page_menu_item.tsx b/x-pack/plugins/search_indices/public/components/indices/details_page_menu_item.tsx index df45cdab7fba7..9e059660b01ab 100644 --- a/x-pack/plugins/search_indices/public/components/indices/details_page_menu_item.tsx +++ b/x-pack/plugins/search_indices/public/components/indices/details_page_menu_item.tsx @@ -14,21 +14,27 @@ import { EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { ReactElement, useState } from 'react'; +import React, { ReactElement, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { useKibana } from '../../hooks/use_kibana'; +import type { UserStartPrivilegesResponse } from '../../../common'; interface SearchIndexDetailsPageMenuItemPopoverProps { handleDeleteIndexModal: () => void; showApiReference: boolean; + userPrivileges?: UserStartPrivilegesResponse; } export const SearchIndexDetailsPageMenuItemPopover = ({ showApiReference = false, handleDeleteIndexModal, + userPrivileges, }: SearchIndexDetailsPageMenuItemPopoverProps) => { const [showMoreOptions, setShowMoreOptions] = useState(false); const { docLinks } = useKibana().services; + const canManageIndex = useMemo(() => { + return userPrivileges?.privileges.canManageIndex === true; + }, [userPrivileges]); const contextMenuItems = [ showApiReference && ( } + icon={} size="s" onClick={handleDeleteIndexModal} data-test-subj="moreOptionsDeleteIndex" - color="danger" + toolTipContent={ + !canManageIndex + ? i18n.translate('xpack.searchIndices.moreOptions.deleteIndex.permissionToolTip', { + defaultMessage: 'You do not have permission to delete an index', + }) + : undefined + } + toolTipProps={{ 'data-test-subj': 'moreOptionsDeleteIndexTooltip' }} + disabled={!canManageIndex} > - + { +export const SearchIndexDetailsSettings = ({ + indexName, + userPrivileges, +}: SearchIndexDetailsSettingsProps) => { const { indexManagement, history } = useKibana().services; + const hasUpdateSettingsPrivilege = useMemo(() => { + return userPrivileges?.privileges.canManageIndex === true; + }, [userPrivileges]); + const IndexSettingsComponent = useMemo( () => indexManagement.getIndexSettingsComponent({ history }), [indexManagement, history] @@ -24,7 +33,10 @@ export const SearchIndexDetailsSettings = ({ indexName }: SearchIndexDetailsSett return ( <> - + ); }; diff --git a/x-pack/plugins/search_indices/public/components/shared/create_index_form.tsx b/x-pack/plugins/search_indices/public/components/shared/create_index_form.tsx index ba2f83cb273da..56c8be57a04d3 100644 --- a/x-pack/plugins/search_indices/public/components/shared/create_index_form.tsx +++ b/x-pack/plugins/search_indices/public/components/shared/create_index_form.tsx @@ -73,7 +73,7 @@ export const CreateIndexForm = ({ name="indexName" value={indexName} isInvalid={indexNameHasError} - disabled={userPrivileges?.privileges?.canCreateIndex === false} + disabled={userPrivileges?.privileges?.canManageIndex === false} onChange={onIndexNameChange} placeholder={i18n.translate('xpack.searchIndices.shared.createIndex.name.placeholder', { defaultMessage: 'Enter a name for your index', @@ -85,7 +85,7 @@ export const CreateIndexForm = ({ {i18n.translate('xpack.searchIndices.shared.createIndex.permissionTooltip', { defaultMessage: 'You do not have permission to create an index.', @@ -101,7 +101,7 @@ export const CreateIndexForm = ({ iconType="sparkles" data-test-subj="createIndexBtn" disabled={ - userPrivileges?.privileges?.canCreateIndex === false || + userPrivileges?.privileges?.canManageIndex === false || indexNameHasError || isLoading } diff --git a/x-pack/plugins/search_indices/public/components/start/elasticsearch_start.tsx b/x-pack/plugins/search_indices/public/components/start/elasticsearch_start.tsx index 3f3063ddb150e..7b525250ff493 100644 --- a/x-pack/plugins/search_indices/public/components/start/elasticsearch_start.tsx +++ b/x-pack/plugins/search_indices/public/components/start/elasticsearch_start.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import type { IndicesStatusResponse, UserStartPrivilegesResponse } from '../../../common'; +import type { IndicesStatusResponse } from '../../../common'; import { AnalyticsEvents } from '../../analytics/constants'; import { AvailableLanguages } from '../../code_examples'; @@ -22,6 +22,7 @@ import { CreateIndexFormState, CreateIndexViewMode } from '../../types'; import { CreateIndexPanel } from '../shared/create_index_panel'; import { useKibana } from '../../hooks/use_kibana'; +import { useUserPrivilegesQuery } from '../../hooks/api/use_user_permissions'; function initCreateIndexState(): CreateIndexFormState { const defaultIndexName = generateRandomIndexName(); @@ -34,17 +35,18 @@ function initCreateIndexState(): CreateIndexFormState { export interface ElasticsearchStartProps { indicesData?: IndicesStatusResponse; - userPrivileges?: UserStartPrivilegesResponse; } -export const ElasticsearchStart = ({ userPrivileges }: ElasticsearchStartProps) => { +export const ElasticsearchStart: React.FC = () => { const { application } = useKibana().services; + const [formState, setFormState] = useState(initCreateIndexState); + const { data: userPrivileges } = useUserPrivilegesQuery(formState.defaultIndexName); + const [createIndexView, setCreateIndexViewMode] = useState( - userPrivileges?.privileges.canCreateIndex === false + userPrivileges?.privileges.canManageIndex === false ? CreateIndexViewMode.Code : CreateIndexViewMode.UI ); - const [formState, setFormState] = useState(initCreateIndexState); const usageTracker = useUsageTracker(); useEffect(() => { @@ -52,7 +54,7 @@ export const ElasticsearchStart = ({ userPrivileges }: ElasticsearchStartProps) }, [usageTracker]); useEffect(() => { if (userPrivileges === undefined) return; - if (userPrivileges.privileges.canCreateIndex === false) { + if (userPrivileges.privileges.canManageIndex === false) { setCreateIndexViewMode(CreateIndexViewMode.Code); } }, [userPrivileges]); diff --git a/x-pack/plugins/search_indices/public/components/start/start_page.tsx b/x-pack/plugins/search_indices/public/components/start/start_page.tsx index 4dabec2e5fa98..b21eb82c8dcbc 100644 --- a/x-pack/plugins/search_indices/public/components/start/start_page.tsx +++ b/x-pack/plugins/search_indices/public/components/start/start_page.tsx @@ -13,7 +13,6 @@ import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { useKibana } from '../../hooks/use_kibana'; import { useIndicesStatusQuery } from '../../hooks/api/use_indices_status'; -import { useUserPrivilegesQuery } from '../../hooks/api/use_user_permissions'; import { useIndicesRedirect } from './hooks/use_indices_redirect'; import { ElasticsearchStart } from './elasticsearch_start'; @@ -33,7 +32,7 @@ export const ElasticsearchStartPage = () => { isError: hasIndicesStatusFetchError, error: indicesFetchError, } = useIndicesStatusQuery(); - const { data: userPrivileges } = useUserPrivilegesQuery(); + usePageChrome(PageTitle, [...IndexManagementBreadcrumbs, { text: PageTitle }]); const embeddableConsole = useMemo( @@ -53,7 +52,7 @@ export const ElasticsearchStartPage = () => { {isInitialLoading && } {hasIndicesStatusFetchError && } {!isInitialLoading && !hasIndicesStatusFetchError && ( - + )} {embeddableConsole} diff --git a/x-pack/plugins/search_indices/public/hooks/api/use_user_permissions.ts b/x-pack/plugins/search_indices/public/hooks/api/use_user_permissions.ts index d3f4f34887157..ca5cbd10468e9 100644 --- a/x-pack/plugins/search_indices/public/hooks/api/use_user_permissions.ts +++ b/x-pack/plugins/search_indices/public/hooks/api/use_user_permissions.ts @@ -7,16 +7,18 @@ import { useQuery } from '@tanstack/react-query'; -import { GET_USER_PRIVILEGES_ROUTE } from '../../../common/routes'; import type { UserStartPrivilegesResponse } from '../../../common/types'; import { QueryKeys } from '../../constants'; import { useKibana } from '../use_kibana'; -export const useUserPrivilegesQuery = () => { +export const useUserPrivilegesQuery = (indexName: string) => { const { http } = useKibana().services; return useQuery({ queryKey: [QueryKeys.FetchUserStartPrivileges], - queryFn: () => http.get(GET_USER_PRIVILEGES_ROUTE), + queryFn: () => + http.get( + `/internal/search_indices/start_privileges/${indexName}` + ), }); }; diff --git a/x-pack/plugins/search_indices/server/lib/status.test.ts b/x-pack/plugins/search_indices/server/lib/status.test.ts index ff5a8fc1eadd5..bf2250fc8707e 100644 --- a/x-pack/plugins/search_indices/server/lib/status.test.ts +++ b/x-pack/plugins/search_indices/server/lib/status.test.ts @@ -116,6 +116,7 @@ describe('status api lib', function () { }); describe('fetchUserStartPrivileges', function () { + const testIndexName = 'search-zbd1'; it('should return privileges true', async () => { const result: SecurityHasPrivilegesResponse = { application: {}, @@ -124,17 +125,20 @@ describe('status api lib', function () { }, has_all_requested: true, index: { - 'test-index-name': { - create_index: true, + [testIndexName]: { + delete: true, + manage: true, }, }, username: 'unit-test', }; + mockClient.security.hasPrivileges.mockResolvedValue(result); - await expect(fetchUserStartPrivileges(client, logger)).resolves.toEqual({ + await expect(fetchUserStartPrivileges(client, logger, testIndexName)).resolves.toEqual({ privileges: { - canCreateIndex: true, + canManageIndex: true, + canDeleteDocuments: true, canCreateApiKeys: true, }, }); @@ -144,8 +148,8 @@ describe('status api lib', function () { cluster: ['manage_api_key'], index: [ { - names: ['test-index-name'], - privileges: ['create_index'], + names: [testIndexName], + privileges: ['manage', 'delete'], }, ], }); @@ -158,17 +162,19 @@ describe('status api lib', function () { }, has_all_requested: false, index: { - 'test-index-name': { - create_index: false, + [testIndexName]: { + manage: false, + delete: false, }, }, username: 'unit-test', }; mockClient.security.hasPrivileges.mockResolvedValue(result); - await expect(fetchUserStartPrivileges(client, logger)).resolves.toEqual({ + await expect(fetchUserStartPrivileges(client, logger, testIndexName)).resolves.toEqual({ privileges: { - canCreateIndex: false, + canManageIndex: false, + canDeleteDocuments: false, canCreateApiKeys: false, }, }); @@ -181,17 +187,19 @@ describe('status api lib', function () { }, has_all_requested: false, index: { - 'test-index-name': { - create_index: true, + [testIndexName]: { + manage: true, + delete: true, }, }, username: 'unit-test', }; mockClient.security.hasPrivileges.mockResolvedValue(result); - await expect(fetchUserStartPrivileges(client, logger)).resolves.toEqual({ + await expect(fetchUserStartPrivileges(client, logger, testIndexName)).resolves.toEqual({ privileges: { - canCreateIndex: true, + canManageIndex: true, + canDeleteDocuments: true, canCreateApiKeys: false, }, }); @@ -202,17 +210,19 @@ describe('status api lib', function () { cluster: {}, has_all_requested: true, index: { - 'test-index-name': { - create_index: true, + [testIndexName]: { + manage: true, + delete: false, }, }, username: 'unit-test', }; mockClient.security.hasPrivileges.mockResolvedValue(result); - await expect(fetchUserStartPrivileges(client, logger)).resolves.toEqual({ + await expect(fetchUserStartPrivileges(client, logger, testIndexName)).resolves.toEqual({ privileges: { - canCreateIndex: true, + canManageIndex: true, + canDeleteDocuments: false, canCreateApiKeys: false, }, }); @@ -220,9 +230,10 @@ describe('status api lib', function () { it('should default privileges on exceptions', async () => { mockClient.security.hasPrivileges.mockRejectedValue(new Error('Boom!!')); - await expect(fetchUserStartPrivileges(client, logger)).resolves.toEqual({ + await expect(fetchUserStartPrivileges(client, logger, testIndexName)).resolves.toEqual({ privileges: { - canCreateIndex: false, + canManageIndex: false, + canDeleteDocuments: false, canCreateApiKeys: false, }, }); diff --git a/x-pack/plugins/search_indices/server/lib/status.ts b/x-pack/plugins/search_indices/server/lib/status.ts index 752e897ab1707..44ee6cf59abd3 100644 --- a/x-pack/plugins/search_indices/server/lib/status.ts +++ b/x-pack/plugins/search_indices/server/lib/status.ts @@ -38,7 +38,7 @@ export async function fetchIndicesStatus( export async function fetchUserStartPrivileges( client: ElasticsearchClient, logger: Logger, - indexName: string = 'test-index-name' + indexName: string ): Promise { try { const securityCheck = await client.security.hasPrivileges({ @@ -46,14 +46,15 @@ export async function fetchUserStartPrivileges( index: [ { names: [indexName], - privileges: ['create_index'], + privileges: ['manage', 'delete'], }, ], }); return { privileges: { - canCreateIndex: securityCheck?.index?.[indexName]?.create_index ?? false, + canManageIndex: securityCheck?.index?.[indexName]?.manage ?? false, + canDeleteDocuments: securityCheck?.index?.[indexName]?.delete ?? false, canCreateApiKeys: securityCheck?.cluster?.manage_api_key ?? false, }, }; @@ -62,7 +63,8 @@ export async function fetchUserStartPrivileges( logger.error(e); return { privileges: { - canCreateIndex: false, + canManageIndex: false, + canDeleteDocuments: false, canCreateApiKeys: false, }, }; diff --git a/x-pack/plugins/search_indices/server/routes/status.ts b/x-pack/plugins/search_indices/server/routes/status.ts index b135499634487..3ed068780f7d8 100644 --- a/x-pack/plugins/search_indices/server/routes/status.ts +++ b/x-pack/plugins/search_indices/server/routes/status.ts @@ -8,6 +8,7 @@ import type { IRouter } from '@kbn/core/server'; import type { Logger } from '@kbn/logging'; +import { schema } from '@kbn/config-schema'; import { GET_STATUS_ROUTE, GET_USER_PRIVILEGES_ROUTE } from '../../common/routes'; import { fetchIndicesStatus, fetchUserStartPrivileges } from '../lib/status'; @@ -35,15 +36,22 @@ export function registerStatusRoutes(router: IRouter, logger: Logger) { router.get( { path: GET_USER_PRIVILEGES_ROUTE, - validate: {}, + validate: { + params: schema.object({ + indexName: schema.string(), + }), + }, options: { access: 'internal', }, }, - async (context, _request, response) => { + async (context, request, response) => { const core = await context.core; const client = core.elasticsearch.client.asCurrentUser; - const body = await fetchUserStartPrivileges(client, logger); + + const { indexName } = request.params; + + const body = await fetchUserStartPrivileges(client, logger, indexName); return response.ok({ body, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index c9d88a7c0f8ed..fef8997584f80 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -7471,7 +7471,6 @@ "searchIndexDocuments.result.expandTooltip.showMore": "Afficher {amount} champs en plus", "searchIndexDocuments.result.header.metadata.deleteDocument": "Supprimer le document", "searchIndexDocuments.result.header.metadata.icon.ariaLabel": "Métadonnées pour le document : {id}", - "searchIndexDocuments.result.header.metadata.score": "Score", "searchIndexDocuments.result.header.metadata.title": "Métadonnées du document", "searchIndexDocuments.result.title.id": "ID de document : {id}", "searchIndexDocuments.result.value.denseVector.copy": "Copier le vecteur", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index fffed2d59a462..78eec61c64215 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7463,7 +7463,6 @@ "searchIndexDocuments.result.expandTooltip.showMore": "表示するフィールド数を{amount}個増やす", "searchIndexDocuments.result.header.metadata.deleteDocument": "ドキュメントを削除", "searchIndexDocuments.result.header.metadata.icon.ariaLabel": "ドキュメント{id}のメタデータ", - "searchIndexDocuments.result.header.metadata.score": "スコア", "searchIndexDocuments.result.header.metadata.title": "ドキュメントメタデータ", "searchIndexDocuments.result.title.id": "ドキュメントID:{id}", "searchIndexDocuments.result.value.denseVector.copy": "ベクトルをコピー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4d8de21af735a..d1f0a075b2d21 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7307,7 +7307,6 @@ "searchIndexDocuments.result.expandTooltip.showMore": "显示多于 {amount} 个字段", "searchIndexDocuments.result.header.metadata.deleteDocument": "删除文档", "searchIndexDocuments.result.header.metadata.icon.ariaLabel": "以下文档的元数据:{id}", - "searchIndexDocuments.result.header.metadata.score": "分数", "searchIndexDocuments.result.header.metadata.title": "文档元数据", "searchIndexDocuments.result.title.id": "文档 ID:{id}", "searchIndexDocuments.result.value.denseVector.copy": "复制向量", diff --git a/x-pack/test/functional/page_objects/index_management_page.ts b/x-pack/test/functional/page_objects/index_management_page.ts index 8053293f98633..e5a2604294675 100644 --- a/x-pack/test/functional/page_objects/index_management_page.ts +++ b/x-pack/test/functional/page_objects/index_management_page.ts @@ -159,6 +159,28 @@ export function IndexManagementPageProvider({ getService }: FtrProviderContext) const url = await browser.getCurrentUrl(); expect(url).to.contain(`tab=${tabId}`); }, + async expectEditSettingsToBeEnabled() { + await testSubjects.existOrFail('indexDetailsSettingsEditModeSwitch', { timeout: 2000 }); + const isEditSettingsButtonDisabled = await testSubjects.isEnabled( + 'indexDetailsSettingsEditModeSwitch' + ); + expect(isEditSettingsButtonDisabled).to.be(true); + }, + async expectIndexDetailsMappingsAddFieldToBeEnabled() { + await testSubjects.existOrFail('indexDetailsMappingsAddField'); + const isMappingsFieldEnabled = await testSubjects.isEnabled('indexDetailsMappingsAddField'); + expect(isMappingsFieldEnabled).to.be(true); + }, + async expectTabsExists() { + await testSubjects.existOrFail('indexDetailsTab-mappings', { timeout: 2000 }); + await testSubjects.existOrFail('indexDetailsTab-overview', { timeout: 2000 }); + await testSubjects.existOrFail('indexDetailsTab-settings', { timeout: 2000 }); + }, + async changeTab( + tab: 'indexDetailsTab-mappings' | 'indexDetailsTab-overview' | 'indexDetailsTab-settings' + ) { + await testSubjects.click(tab); + }, }, async clickCreateIndexButton() { await testSubjects.click('createIndexButton'); diff --git a/x-pack/test_serverless/api_integration/test_suites/search/search_indices/status.ts b/x-pack/test_serverless/api_integration/test_suites/search/search_indices/status.ts index 33a2a438016b9..e92cc62296849 100644 --- a/x-pack/test_serverless/api_integration/test_suites/search/search_indices/status.ts +++ b/x-pack/test_serverless/api_integration/test_suites/search/search_indices/status.ts @@ -13,6 +13,7 @@ export default function ({ getService }: FtrProviderContext) { const roleScopedSupertest = getService('roleScopedSupertest'); let supertestDeveloperWithCookieCredentials: SupertestWithRoleScopeType; let supertestViewerWithCookieCredentials: SupertestWithRoleScopeType; + const testIndexName = 'search-test-index'; describe('search_indices Status APIs', function () { describe('indices status', function () { @@ -37,37 +38,41 @@ export default function ({ getService }: FtrProviderContext) { describe('developer', function () { it('returns expected privileges', async () => { const { body } = await supertestDeveloperWithCookieCredentials - .get('/internal/search_indices/start_privileges') + .get(`/internal/search_indices/start_privileges/${testIndexName}`) .expect(200); expect(body).toEqual({ privileges: { canCreateApiKeys: true, - canCreateIndex: true, + canDeleteDocuments: true, + canManageIndex: true, }, }); }); }); - describe('viewer', function () { - before(async () => { - supertestViewerWithCookieCredentials = - await roleScopedSupertest.getSupertestWithRoleScope('viewer', { - useCookieHeader: true, - withInternalHeaders: true, - }); - }); + }); + describe('viewer', function () { + before(async () => { + supertestViewerWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope( + 'viewer', + { + useCookieHeader: true, + withInternalHeaders: true, + } + ); + }); - it('returns expected privileges', async () => { - const { body } = await supertestViewerWithCookieCredentials - .get('/internal/search_indices/start_privileges') - .expect(200); + it('returns expected privileges', async () => { + const { body } = await supertestViewerWithCookieCredentials + .get(`/internal/search_indices/start_privileges/${testIndexName}`) + .expect(200); - expect(body).toEqual({ - privileges: { - canCreateApiKeys: false, - canCreateIndex: false, - }, - }); + expect(body).toEqual({ + privileges: { + canCreateApiKeys: false, + canDeleteDocuments: false, + canManageIndex: false, + }, }); }); }); diff --git a/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts b/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts index 277b4d2c7ada2..0609b2bec4aed 100644 --- a/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts +++ b/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts @@ -100,6 +100,18 @@ export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderCont async expectAPIReferenceDocLinkMissingInMoreOptions() { await testSubjects.missingOrFail('moreOptionsApiReference', { timeout: 2000 }); }, + async expectDeleteIndexButtonToBeDisabled() { + await testSubjects.existOrFail('moreOptionsDeleteIndex'); + const deleteIndexButton = await testSubjects.isEnabled('moreOptionsDeleteIndex'); + expect(deleteIndexButton).to.be(false); + await testSubjects.moveMouseTo('moreOptionsDeleteIndex'); + await testSubjects.existOrFail('moreOptionsDeleteIndexTooltip'); + }, + async expectDeleteIndexButtonToBeEnabled() { + await testSubjects.existOrFail('moreOptionsDeleteIndex'); + const deleteIndexButton = await testSubjects.isEnabled('moreOptionsDeleteIndex'); + expect(deleteIndexButton).to.be(true); + }, async expectDeleteIndexButtonExistsInMoreOptions() { await testSubjects.existOrFail('moreOptionsDeleteIndex'); }, @@ -132,11 +144,11 @@ export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderCont await testSubjects.click('reloadButton', 2000); }); }, - async expectWithDataTabsExists() { + async expectTabsExists() { await testSubjects.existOrFail('mappingsTab', { timeout: 2000 }); await testSubjects.existOrFail('dataTab', { timeout: 2000 }); }, - async withDataChangeTabs(tab: 'dataTab' | 'mappingsTab' | 'settingsTab') { + async changeTab(tab: 'dataTab' | 'mappingsTab' | 'settingsTab') { await testSubjects.click(tab); }, async expectUrlShouldChangeTo(tab: 'data' | 'mappings' | 'settings') { @@ -148,6 +160,22 @@ export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderCont async expectSettingsComponentIsVisible() { await testSubjects.existOrFail('indexDetailsSettingsEditModeSwitch', { timeout: 2000 }); }, + async expectEditSettingsIsDisabled() { + await testSubjects.existOrFail('indexDetailsSettingsEditModeSwitch', { timeout: 2000 }); + const isEditSettingsButtonDisabled = await testSubjects.isEnabled( + 'indexDetailsSettingsEditModeSwitch' + ); + expect(isEditSettingsButtonDisabled).to.be(false); + await testSubjects.moveMouseTo('indexDetailsSettingsEditModeSwitch'); + await testSubjects.existOrFail('indexDetailsSettingsEditModeSwitchToolTip'); + }, + async expectEditSettingsToBeEnabled() { + await testSubjects.existOrFail('indexDetailsSettingsEditModeSwitch', { timeout: 2000 }); + const isEditSettingsButtonDisabled = await testSubjects.isEnabled( + 'indexDetailsSettingsEditModeSwitch' + ); + expect(isEditSettingsButtonDisabled).to.be(true); + }, async expectSelectedLanguage(language: string) { await testSubjects.existOrFail('codeExampleLanguageSelect'); expect( @@ -186,12 +214,28 @@ export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderCont await testSubjects.existOrFail('deleteDocumentButton'); await testSubjects.click('deleteDocumentButton'); }, - async expectDeleteDocumentActionNotVisible() { await testSubjects.existOrFail('documentMetadataButton'); await testSubjects.click('documentMetadataButton'); await testSubjects.missingOrFail('deleteDocumentButton'); }, + async expectDeleteDocumentActionIsDisabled() { + await testSubjects.existOrFail('documentMetadataButton'); + await testSubjects.click('documentMetadataButton'); + await testSubjects.existOrFail('deleteDocumentButton'); + const isDeleteDocumentEnabled = await testSubjects.isEnabled('deleteDocumentButton'); + expect(isDeleteDocumentEnabled).to.be(false); + await testSubjects.moveMouseTo('deleteDocumentButton'); + await testSubjects.existOrFail('deleteDocumentButtonToolTip'); + }, + async expectDeleteDocumentActionToBeEnabled() { + await testSubjects.existOrFail('documentMetadataButton'); + await testSubjects.click('documentMetadataButton'); + await testSubjects.existOrFail('deleteDocumentButton'); + const isDeleteDocumentEnabled = await testSubjects.isEnabled('deleteDocumentButton'); + expect(isDeleteDocumentEnabled).to.be(true); + }, + async openIndicesDetailFromIndexManagementIndicesListTable(indexOfRow: number) { const indexList = await testSubjects.findAll('indexTableIndexNameLink'); await indexList[indexOfRow].click(); @@ -219,5 +263,19 @@ export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderCont } } }, + + async expectAddFieldToBeDisabled() { + await testSubjects.existOrFail('indexDetailsMappingsAddField'); + const isMappingsFieldEnabled = await testSubjects.isEnabled('indexDetailsMappingsAddField'); + expect(isMappingsFieldEnabled).to.be(false); + await testSubjects.moveMouseTo('indexDetailsMappingsAddField'); + await testSubjects.existOrFail('indexDetailsMappingsAddFieldTooltip'); + }, + + async expectAddFieldToBeEnabled() { + await testSubjects.existOrFail('indexDetailsMappingsAddField'); + const isMappingsFieldEnabled = await testSubjects.isEnabled('indexDetailsMappingsAddField'); + expect(isMappingsFieldEnabled).to.be(true); + }, }; } diff --git a/x-pack/test_serverless/functional/test_suites/common/management/index_management/index_detail.ts b/x-pack/test_serverless/functional/test_suites/common/management/index_management/index_detail.ts index be3b683d9903a..7330a5d162240 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/index_management/index_detail.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/index_management/index_detail.ts @@ -38,6 +38,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('index with no documents', async () => { await pageObjects.indexManagement.indexDetailsPage.openIndexDetailsPage(0); await pageObjects.indexManagement.indexDetailsPage.expectIndexDetailsPageIsLoaded(); + await pageObjects.indexManagement.indexDetailsPage.expectTabsExists(); + }); + it('can add mappings', async () => { + await pageObjects.indexManagement.indexDetailsPage.changeTab('indexDetailsTab-mappings'); + await pageObjects.indexManagement.indexDetailsPage.expectIndexDetailsMappingsAddFieldToBeEnabled(); + }); + it('can edit settings', async () => { + await pageObjects.indexManagement.indexDetailsPage.changeTab('indexDetailsTab-settings'); + await pageObjects.indexManagement.indexDetailsPage.expectEditSettingsToBeEnabled(); }); }); }); diff --git a/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts b/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts index 0070ce7e2cb43..5aa2627a3cdf4 100644 --- a/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts +++ b/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts @@ -24,210 +24,286 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const esDeleteAllIndices = getService('esDeleteAllIndices'); const indexName = 'test-my-index'; - describe('Search index detail page', function () { - before(async () => { - await pageObjects.svlCommonPage.loginWithRole('developer'); - await pageObjects.svlApiKeys.deleteAPIKeys(); - }); - after(async () => { - await esDeleteAllIndices(indexName); - }); - - describe('index details page overview', () => { + describe('index details page - search solution', function () { + describe('developer', function () { before(async () => { - await es.indices.create({ index: indexName }); - await svlSearchNavigation.navigateToIndexDetailPage(indexName); + await pageObjects.svlCommonPage.loginWithRole('developer'); + await pageObjects.svlApiKeys.deleteAPIKeys(); }); after(async () => { await esDeleteAllIndices(indexName); }); - it('can load index detail page', async () => { - await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); - await pageObjects.svlSearchIndexDetailPage.expectSearchIndexDetailsTabsExists(); - await pageObjects.svlSearchIndexDetailPage.expectAPIReferenceDocLinkExists(); - await pageObjects.svlSearchIndexDetailPage.expectAPIReferenceDocLinkMissingInMoreOptions(); - }); - it('should have embedded dev console', async () => { - await testHasEmbeddedConsole(pageObjects); - }); - it('should have connection details', async () => { - await pageObjects.svlSearchIndexDetailPage.expectConnectionDetails(); - }); - - it.skip('should show api key', async () => { - await pageObjects.svlApiKeys.deleteAPIKeys(); - await svlSearchNavigation.navigateToIndexDetailPage(indexName); - await pageObjects.svlApiKeys.expectAPIKeyAvailable(); - const apiKey = await pageObjects.svlApiKeys.getAPIKeyFromUI(); - await pageObjects.svlSearchIndexDetailPage.expectAPIKeyToBeVisibleInCodeBlock(apiKey); - }); - - it('should have quick stats', async () => { - await pageObjects.svlSearchIndexDetailPage.expectQuickStats(); - await pageObjects.svlSearchIndexDetailPage.expectQuickStatsAIMappings(); - await es.indices.putMapping({ - index: indexName, - body: { - properties: { - my_field: { - type: 'dense_vector', - dims: 3, - }, - }, - }, + describe('search index details page', () => { + before(async () => { + await es.indices.create({ index: indexName }); + await svlSearchNavigation.navigateToIndexDetailPage(indexName); + }); + after(async () => { + await esDeleteAllIndices(indexName); + }); + it('can load index detail page', async () => { + await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); + await pageObjects.svlSearchIndexDetailPage.expectSearchIndexDetailsTabsExists(); + await pageObjects.svlSearchIndexDetailPage.expectAPIReferenceDocLinkExists(); + await pageObjects.svlSearchIndexDetailPage.expectAPIReferenceDocLinkMissingInMoreOptions(); + }); + it('should have embedded dev console', async () => { + await testHasEmbeddedConsole(pageObjects); + }); + it('should have connection details', async () => { + await pageObjects.svlSearchIndexDetailPage.expectConnectionDetails(); }); - await svlSearchNavigation.navigateToIndexDetailPage(indexName); - await pageObjects.svlSearchIndexDetailPage.expectQuickStatsAIMappingsToHaveVectorFields(); - }); - - it('should have breadcrumb navigation', async () => { - await pageObjects.svlSearchIndexDetailPage.expectBreadcrumbNavigationWithIndexName( - indexName - ); - await pageObjects.svlSearchIndexDetailPage.clickOnIndexManagementBreadcrumb(); - await pageObjects.indexManagement.expectToBeOnIndicesManagement(); - await svlSearchNavigation.navigateToIndexDetailPage(indexName); - }); - it('should show code examples for adding documents', async () => { - await pageObjects.svlSearchIndexDetailPage.expectAddDocumentCodeExamples(); - await pageObjects.svlSearchIndexDetailPage.expectSelectedLanguage('python'); - await pageObjects.svlSearchIndexDetailPage.codeSampleContainsValue( - 'installCodeExample', - 'pip install' - ); - await pageObjects.svlSearchIndexDetailPage.selectCodingLanguage('javascript'); - await pageObjects.svlSearchIndexDetailPage.codeSampleContainsValue( - 'installCodeExample', - 'npm install' - ); - await pageObjects.svlSearchIndexDetailPage.selectCodingLanguage('curl'); - await pageObjects.svlSearchIndexDetailPage.openConsoleCodeExample(); - await pageObjects.embeddedConsole.expectEmbeddedConsoleToBeOpen(); - await pageObjects.embeddedConsole.clickEmbeddedConsoleControlBar(); - }); + it.skip('should show api key', async () => { + await pageObjects.svlApiKeys.deleteAPIKeys(); + await svlSearchNavigation.navigateToIndexDetailPage(indexName); + await pageObjects.svlApiKeys.expectAPIKeyAvailable(); + const apiKey = await pageObjects.svlApiKeys.getAPIKeyFromUI(); + await pageObjects.svlSearchIndexDetailPage.expectAPIKeyToBeVisibleInCodeBlock(apiKey); + }); - // FLAKY: https://github.com/elastic/kibana/issues/197144 - describe.skip('With data', () => { - before(async () => { - await es.index({ + it('should have quick stats', async () => { + await pageObjects.svlSearchIndexDetailPage.expectQuickStats(); + await pageObjects.svlSearchIndexDetailPage.expectQuickStatsAIMappings(); + await es.indices.putMapping({ index: indexName, body: { - my_field: [1, 0, 1], + properties: { + my_field: { + type: 'dense_vector', + dims: 3, + }, + }, }, }); await svlSearchNavigation.navigateToIndexDetailPage(indexName); + await pageObjects.svlSearchIndexDetailPage.expectQuickStatsAIMappingsToHaveVectorFields(); }); - it('should have index documents', async () => { - await pageObjects.svlSearchIndexDetailPage.expectHasIndexDocuments(); - }); - it('menu action item should be replaced with playground', async () => { - await pageObjects.svlSearchIndexDetailPage.expectActionItemReplacedWhenHasDocs(); - }); - it('should have link to API reference doc link in options menu', async () => { - await pageObjects.svlSearchIndexDetailPage.clickMoreOptionsActionsButton(); - await pageObjects.svlSearchIndexDetailPage.expectAPIReferenceDocLinkExistsInMoreOptions(); + + it('should have breadcrumb navigation', async () => { + await pageObjects.svlSearchIndexDetailPage.expectBreadcrumbNavigationWithIndexName( + indexName + ); + await pageObjects.svlSearchIndexDetailPage.clickOnIndexManagementBreadcrumb(); + await pageObjects.indexManagement.expectToBeOnIndicesManagement(); + await svlSearchNavigation.navigateToIndexDetailPage(indexName); }); - it('should have one document in quick stats', async () => { - await pageObjects.svlSearchIndexDetailPage.expectQuickStatsToHaveDocumentCount(1); + + it('should show code examples for adding documents', async () => { + await pageObjects.svlSearchIndexDetailPage.expectAddDocumentCodeExamples(); + await pageObjects.svlSearchIndexDetailPage.expectSelectedLanguage('python'); + await pageObjects.svlSearchIndexDetailPage.codeSampleContainsValue( + 'installCodeExample', + 'pip install' + ); + await pageObjects.svlSearchIndexDetailPage.selectCodingLanguage('javascript'); + await pageObjects.svlSearchIndexDetailPage.codeSampleContainsValue( + 'installCodeExample', + 'npm install' + ); + await pageObjects.svlSearchIndexDetailPage.selectCodingLanguage('curl'); + await pageObjects.svlSearchIndexDetailPage.openConsoleCodeExample(); + await pageObjects.embeddedConsole.expectEmbeddedConsoleToBeOpen(); + await pageObjects.embeddedConsole.clickEmbeddedConsoleControlBar(); }); - it('should have with data tabs', async () => { - await pageObjects.svlSearchIndexDetailPage.expectWithDataTabsExists(); - await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('data'); + + // FLAKY: https://github.com/elastic/kibana/issues/197144 + describe.skip('With data', () => { + before(async () => { + await es.index({ + index: indexName, + body: { + my_field: [1, 0, 1], + }, + }); + await svlSearchNavigation.navigateToIndexDetailPage(indexName); + }); + it('should have index documents', async () => { + await pageObjects.svlSearchIndexDetailPage.expectHasIndexDocuments(); + }); + it('menu action item should be replaced with playground', async () => { + await pageObjects.svlSearchIndexDetailPage.expectActionItemReplacedWhenHasDocs(); + }); + it('should have link to API reference doc link in options menu', async () => { + await pageObjects.svlSearchIndexDetailPage.clickMoreOptionsActionsButton(); + await pageObjects.svlSearchIndexDetailPage.expectAPIReferenceDocLinkExistsInMoreOptions(); + }); + it('should have one document in quick stats', async () => { + await pageObjects.svlSearchIndexDetailPage.expectQuickStatsToHaveDocumentCount(1); + }); + it('should have with data tabs', async () => { + await pageObjects.svlSearchIndexDetailPage.expectTabsExists(); + await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('data'); + }); + it('should be able to change tabs to mappings and mappings is shown', async () => { + await pageObjects.svlSearchIndexDetailPage.changeTab('mappingsTab'); + await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('mappings'); + await pageObjects.svlSearchIndexDetailPage.expectMappingsComponentIsVisible(); + }); + it('should be able to change tabs to settings and settings is shown', async () => { + await pageObjects.svlSearchIndexDetailPage.changeTab('settingsTab'); + await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('settings'); + await pageObjects.svlSearchIndexDetailPage.expectSettingsComponentIsVisible(); + }); + it('should be able to delete document', async () => { + await pageObjects.svlSearchIndexDetailPage.changeTab('dataTab'); + await pageObjects.svlSearchIndexDetailPage.clickFirstDocumentDeleteAction(); + await pageObjects.svlSearchIndexDetailPage.expectAddDocumentCodeExamples(); + await pageObjects.svlSearchIndexDetailPage.expectQuickStatsToHaveDocumentCount(0); + }); }); - it('should be able to change tabs to mappings and mappings is shown', async () => { - await pageObjects.svlSearchIndexDetailPage.withDataChangeTabs('mappingsTab'); - await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('mappings'); - await pageObjects.svlSearchIndexDetailPage.expectMappingsComponentIsVisible(); + describe('has index actions enabled', () => { + before(async () => { + await es.index({ + index: indexName, + body: { + my_field: [1, 0, 1], + }, + }); + await svlSearchNavigation.navigateToIndexDetailPage(indexName); + }); + + beforeEach(async () => { + await svlSearchNavigation.navigateToIndexDetailPage(indexName); + }); + + it('delete document button is enabled', async () => { + await pageObjects.svlSearchIndexDetailPage.expectDeleteDocumentActionToBeEnabled(); + }); + it('add field button is enabled', async () => { + await pageObjects.svlSearchIndexDetailPage.changeTab('mappingsTab'); + await pageObjects.svlSearchIndexDetailPage.expectAddFieldToBeEnabled(); + }); + it('edit settings button is enabled', async () => { + await pageObjects.svlSearchIndexDetailPage.changeTab('settingsTab'); + await pageObjects.svlSearchIndexDetailPage.expectEditSettingsToBeEnabled(); + }); + it('delete index button is enabled', async () => { + await pageObjects.svlSearchIndexDetailPage.expectMoreOptionsActionButtonExists(); + await pageObjects.svlSearchIndexDetailPage.clickMoreOptionsActionsButton(); + await pageObjects.svlSearchIndexDetailPage.expectMoreOptionsOverviewMenuIsShown(); + await pageObjects.svlSearchIndexDetailPage.expectDeleteIndexButtonExistsInMoreOptions(); + await pageObjects.svlSearchIndexDetailPage.expectDeleteIndexButtonToBeEnabled(); + }); }); - it('should be able to change tabs to settings and settings is shown', async () => { - await pageObjects.svlSearchIndexDetailPage.withDataChangeTabs('settingsTab'); - await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('settings'); - await pageObjects.svlSearchIndexDetailPage.expectSettingsComponentIsVisible(); + + describe('page loading error', () => { + before(async () => { + await svlSearchNavigation.navigateToIndexDetailPage(indexName); + await esDeleteAllIndices(indexName); + }); + it('has page load error section', async () => { + await pageObjects.svlSearchIndexDetailPage.expectPageLoadErrorExists(); + await pageObjects.svlSearchIndexDetailPage.expectIndexNotFoundErrorExists(); + }); + it('reload button shows details page again', async () => { + await es.indices.create({ index: indexName }); + await pageObjects.svlSearchIndexDetailPage.clickPageReload(); + await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); + }); }); - it('should be able to delete document', async () => { - await pageObjects.svlSearchIndexDetailPage.withDataChangeTabs('dataTab'); - await pageObjects.svlSearchIndexDetailPage.clickFirstDocumentDeleteAction(); - await pageObjects.svlSearchIndexDetailPage.expectAddDocumentCodeExamples(); - await pageObjects.svlSearchIndexDetailPage.expectQuickStatsToHaveDocumentCount(0); + describe('Index more options menu', () => { + before(async () => { + await svlSearchNavigation.navigateToIndexDetailPage(indexName); + }); + it('shows action menu in actions popover', async () => { + await pageObjects.svlSearchIndexDetailPage.expectMoreOptionsActionButtonExists(); + await pageObjects.svlSearchIndexDetailPage.clickMoreOptionsActionsButton(); + await pageObjects.svlSearchIndexDetailPage.expectMoreOptionsOverviewMenuIsShown(); + }); + it('should delete index', async () => { + await pageObjects.svlSearchIndexDetailPage.expectDeleteIndexButtonExistsInMoreOptions(); + await pageObjects.svlSearchIndexDetailPage.clickDeleteIndexButton(); + await pageObjects.svlSearchIndexDetailPage.clickConfirmingDeleteIndex(); + }); }); }); - - describe('page loading error', () => { + describe('index management index list page', () => { before(async () => { - await svlSearchNavigation.navigateToIndexDetailPage(indexName); - await esDeleteAllIndices(indexName); - }); - it('has page load error section', async () => { - await pageObjects.svlSearchIndexDetailPage.expectPageLoadErrorExists(); - await pageObjects.svlSearchIndexDetailPage.expectIndexNotFoundErrorExists(); - }); - it('reload button shows details page again', async () => { await es.indices.create({ index: indexName }); - await pageObjects.svlSearchIndexDetailPage.clickPageReload(); - await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); + await security.testUser.setRoles(['index_management_user']); }); - }); - describe('Index more options menu', () => { - before(async () => { - await svlSearchNavigation.navigateToIndexDetailPage(indexName); + beforeEach(async () => { + await pageObjects.common.navigateToApp('indexManagement'); + // Navigate to the indices tab + await pageObjects.indexManagement.changeTabs('indicesTab'); + await pageObjects.header.waitUntilLoadingHasFinished(); }); - it('shows action menu in actions popover', async () => { - await pageObjects.svlSearchIndexDetailPage.expectMoreOptionsActionButtonExists(); - await pageObjects.svlSearchIndexDetailPage.clickMoreOptionsActionsButton(); - await pageObjects.svlSearchIndexDetailPage.expectMoreOptionsOverviewMenuIsShown(); + after(async () => { + await esDeleteAllIndices(indexName); }); - it('should delete index', async () => { - await pageObjects.svlSearchIndexDetailPage.expectDeleteIndexButtonExistsInMoreOptions(); - await pageObjects.svlSearchIndexDetailPage.clickDeleteIndexButton(); - await pageObjects.svlSearchIndexDetailPage.clickConfirmingDeleteIndex(); + describe('manage index action', () => { + beforeEach(async () => { + await pageObjects.indexManagement.manageIndex(indexName); + await pageObjects.indexManagement.manageIndexContextMenuExists(); + }); + it('navigates to overview tab', async () => { + await pageObjects.indexManagement.changeManageIndexTab('showOverviewIndexMenuButton'); + await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); + await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('data'); + }); + + it('navigates to settings tab', async () => { + await pageObjects.indexManagement.changeManageIndexTab('showSettingsIndexMenuButton'); + await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); + await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('settings'); + }); + it('navigates to mappings tab', async () => { + await pageObjects.indexManagement.changeManageIndexTab('showMappingsIndexMenuButton'); + await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); + await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('mappings'); + }); + }); + describe('can view search index details', function () { + it('renders search index details with no documents', async () => { + await pageObjects.svlSearchIndexDetailPage.openIndicesDetailFromIndexManagementIndicesListTable( + 0 + ); + await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); + await pageObjects.svlSearchIndexDetailPage.expectSearchIndexDetailsTabsExists(); + await pageObjects.svlSearchIndexDetailPage.expectAPIReferenceDocLinkExists(); + }); }); }); }); - describe('index management index details', () => { + + describe('viewer', function () { before(async () => { - await es.indices.create({ index: indexName }); - await security.testUser.setRoles(['index_management_user']); - }); - beforeEach(async () => { - await pageObjects.common.navigateToApp('indexManagement'); - // Navigate to the indices tab - await pageObjects.indexManagement.changeTabs('indicesTab'); - await pageObjects.header.waitUntilLoadingHasFinished(); + await esDeleteAllIndices(indexName); + await es.index({ + index: indexName, + body: { + my_field: [1, 0, 1], + }, + }); }); after(async () => { await esDeleteAllIndices(indexName); }); - describe('manage index action', () => { + describe('search index details page', function () { + before(async () => { + await pageObjects.svlCommonPage.loginAsViewer(); + }); beforeEach(async () => { - await pageObjects.indexManagement.manageIndex(indexName); - await pageObjects.indexManagement.manageIndexContextMenuExists(); + await svlSearchNavigation.navigateToIndexDetailPage(indexName); }); - it('navigates to overview tab', async () => { - await pageObjects.indexManagement.changeManageIndexTab('showOverviewIndexMenuButton'); - await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); - await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('data'); + it('delete document button is disabled', async () => { + await pageObjects.svlSearchIndexDetailPage.expectDeleteDocumentActionIsDisabled(); }); - - it('navigates to settings tab', async () => { - await pageObjects.indexManagement.changeManageIndexTab('showSettingsIndexMenuButton'); - await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); - await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('settings'); + it('add field button is disabled', async () => { + await pageObjects.svlSearchIndexDetailPage.changeTab('mappingsTab'); + await pageObjects.svlSearchIndexDetailPage.expectAddFieldToBeDisabled(); }); - it('navigates to mappings tab', async () => { - await pageObjects.indexManagement.changeManageIndexTab('showMappingsIndexMenuButton'); - await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); - await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('mappings'); + it('edit settings button is disabled', async () => { + await pageObjects.svlSearchIndexDetailPage.changeTab('settingsTab'); + await pageObjects.svlSearchIndexDetailPage.expectEditSettingsIsDisabled(); }); - }); - describe('can view search index details', function () { - it('renders search index details with no documents', async () => { - await pageObjects.svlSearchIndexDetailPage.openIndicesDetailFromIndexManagementIndicesListTable( - 0 - ); - await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); - await pageObjects.svlSearchIndexDetailPage.expectSearchIndexDetailsTabsExists(); - await pageObjects.svlSearchIndexDetailPage.expectAPIReferenceDocLinkExists(); + it('delete index button is disabled', async () => { + await pageObjects.svlSearchIndexDetailPage.expectMoreOptionsActionButtonExists(); + await pageObjects.svlSearchIndexDetailPage.clickMoreOptionsActionsButton(); + await pageObjects.svlSearchIndexDetailPage.expectMoreOptionsOverviewMenuIsShown(); + await pageObjects.svlSearchIndexDetailPage.expectDeleteIndexButtonExistsInMoreOptions(); + await pageObjects.svlSearchIndexDetailPage.expectDeleteIndexButtonToBeDisabled(); }); }); }); From 1e29cf8af1d8a7d60f156e583ada9e62e8bb16c2 Mon Sep 17 00:00:00 2001 From: Tre Date: Mon, 18 Nov 2024 16:17:13 +0000 Subject: [PATCH 03/14] [Ownership] Assign maps test files to presentation team (#200214) ## Summary Assign maps test files to presentation team Contributes to: #192979 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Nick Peihl --- .github/CODEOWNERS | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c2450338f3e45..7b4cd85b0d8d0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1382,6 +1382,17 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /test/plugin_functional/test_suites/panel_actions @elastic/kibana-presentation /x-pack/test/functional/es_archives/canvas/logstash_lens @elastic/kibana-presentation #CC# /src/plugins/kibana_react/public/code_editor/ @elastic/kibana-presentation +/x-pack/test/upgrade/services/maps_upgrade_services.ts @elastic/kibana-presentation +/x-pack/test/stack_functional_integration/apps/maps @elastic/kibana-presentation +/x-pack/test/functional/page_objects/geo_file_upload.ts @elastic/kibana-presentation +/x-pack/test/functional/page_objects/gis_page.ts @elastic/kibana-presentation +/x-pack/test/upgrade/apps/maps @elastic/kibana-presentation +/x-pack/test/api_integration/apis/maps/ @elastic/kibana-presentation +/x-pack/test/functional/apps/maps/ @elastic/kibana-presentation +/x-pack/test/functional/es_archives/maps/ @elastic/kibana-presentation +/x-pack/plugins/stack_alerts/server/rule_types/geo_containment @elastic/kibana-presentation +/x-pack/plugins/stack_alerts/public/rule_types/geo_containment @elastic/kibana-presentation + # Machine Learning /x-pack/test/stack_functional_integration/apps/ml @elastic/ml-ui @@ -1421,15 +1432,6 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /x-pack/test/functional/services/aiops @elastic/ml-ui /x-pack/test/functional_basic/apps/transform/ @elastic/ml-ui -# Maps -#CC# /x-pack/plugins/maps/ @elastic/kibana-gis -/x-pack/test/api_integration/apis/maps/ @elastic/kibana-gis -/x-pack/test/functional/apps/maps/ @elastic/kibana-gis -/x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis -/x-pack/plugins/stack_alerts/server/rule_types/geo_containment @elastic/kibana-gis -/x-pack/plugins/stack_alerts/public/rule_types/geo_containment @elastic/kibana-gis -#CC# /x-pack/plugins/file_upload @elastic/kibana-gis - # Operations /test/package @elastic/kibana-operations /test/package/roles @elastic/kibana-operations From 5ab39e5a413f426da232723317786321a7d57fc7 Mon Sep 17 00:00:00 2001 From: Ievgen Sorokopud Date: Mon, 18 Nov 2024 17:23:05 +0100 Subject: [PATCH 04/14] [Security Solution] [Bug] Index Values are not available in dropdown under New Index Enter for Knowledge Base. (#199610) (#199990) ## Summary This PR fixes the issue described here https://github.com/elastic/kibana/issues/199610#issuecomment-2473393109 In short, we do not show all indices with `semantic_text` fields. We only show those which have `semantic_text` fields named `content`. ### Testing notes Add the index with the `semantic_text` field by running this command in dev tools: ``` PUT test_index { "mappings": { "properties": { "attachment": { "properties": { "content": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "content_length": { "type": "long" }, "content_type": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "format": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "language": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } } } }, "content2": { "type": "semantic_text", "inference_id": "elastic-security-ai-assistant-elser2" } } } } ``` You should see the `test_index` index in the `Knowledge Base > New > Index > Index (dropdown)`. ### Checklist Delete any items that are not applicable to this PR. - [ ] [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: Steph Milovic --- .../get_knowledge_base_indices.test.ts | 74 ++++++++++++++++--- .../get_knowledge_base_indices.ts | 8 +- 2 files changed, 68 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_indices.test.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_indices.test.ts index e7eaa75407248..d5e92cb8d682e 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_indices.test.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_indices.test.ts @@ -12,12 +12,15 @@ import { getGetKnowledgeBaseIndicesRequest } from '../../__mocks__/request'; const mockFieldCaps = { indices: [ - '.ds-logs-endpoint.alerts-default-2024.10.31-000001', - '.ds-metrics-endpoint.metadata-default-2024.10.31-000001', - '.internal.alerts-security.alerts-default-000001', + '.ds-.items-default-2024.11.12-000001', + '.ds-.lists-default-2024.11.12-000001', + '.ds-logs-endpoint.alerts-default-2024.11.12-000001', + '.ds-logs-endpoint.events.process-default-2024.11.12-000001', + 'gtr-1', + 'gtr-with-bug', + 'gtr-with-semantic-1', 'metrics-endpoint.metadata_current_default', - 'semantic-index-1', - 'semantic-index-2', + 'search-elastic-security-docs', ], fields: { content: { @@ -27,9 +30,12 @@ const mockFieldCaps = { searchable: false, aggregatable: false, indices: [ - '.ds-logs-endpoint.alerts-default-2024.10.31-000001', - '.ds-metrics-endpoint.metadata-default-2024.10.31-000001', - '.internal.alerts-security.alerts-default-000001', + '.ds-.items-default-2024.11.12-000001', + '.ds-.lists-default-2024.11.12-000001', + '.ds-logs-endpoint.alerts-default-2024.11.12-000001', + '.ds-logs-endpoint.events.process-default-2024.11.12-000001', + 'gtr-1', + 'gtr-with-bug', 'metrics-endpoint.metadata_current_default', ], }, @@ -38,7 +44,55 @@ const mockFieldCaps = { metadata_field: false, searchable: true, aggregatable: false, - indices: ['semantic-index-1', 'semantic-index-2'], + indices: ['gtr-with-semantic-1'], + }, + }, + ai_embeddings: { + unmapped: { + type: 'unmapped', + metadata_field: false, + searchable: false, + aggregatable: false, + indices: [ + '.ds-.items-default-2024.11.12-000001', + '.ds-.lists-default-2024.11.12-000001', + '.ds-logs-endpoint.alerts-default-2024.11.12-000001', + '.ds-logs-endpoint.events.process-default-2024.11.12-000001', + 'gtr-1', + 'gtr-with-semantic-1', + 'metrics-endpoint.metadata_current_default', + ], + }, + semantic_text: { + type: 'semantic_text', + metadata_field: false, + searchable: true, + aggregatable: false, + indices: ['gtr-with-bug', 'search-elastic-security-docs'], + }, + }, + semantic_text: { + unmapped: { + type: 'unmapped', + metadata_field: false, + searchable: false, + aggregatable: false, + indices: [ + '.ds-.items-default-2024.11.12-000001', + '.ds-.lists-default-2024.11.12-000001', + '.ds-logs-endpoint.alerts-default-2024.11.12-000001', + '.ds-logs-endpoint.events.process-default-2024.11.12-000001', + 'gtr-1', + 'gtr-with-semantic-1', + 'metrics-endpoint.metadata_current_default', + ], + }, + semantic_text: { + type: 'semantic_text', + metadata_field: false, + searchable: true, + aggregatable: false, + indices: ['search-elastic-security-docs'], }, }, }, @@ -66,7 +120,7 @@ describe('Get Knowledge Base Status Route', () => { expect(response.status).toEqual(200); expect(response.body).toEqual({ - indices: ['semantic-index-1', 'semantic-index-2'], + indices: ['gtr-with-bug', 'gtr-with-semantic-1', 'search-elastic-security-docs'], }); expect(context.core.elasticsearch.client.asCurrentUser.fieldCaps).toBeCalledWith({ index: '*', diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_indices.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_indices.ts index 18191291468de..5106c31d39e7d 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_indices.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_indices.ts @@ -53,10 +53,10 @@ export const getKnowledgeBaseIndicesRoute = (router: ElasticAssistantPluginRoute include_unmapped: true, }); - const indices = res.fields.content?.semantic_text?.indices; - if (indices) { - body.indices = Array.isArray(indices) ? indices : [indices]; - } + body.indices = Object.values(res.fields) + .flatMap((value) => value.semantic_text?.indices ?? []) + .filter((value, index, self) => self.indexOf(value) === index) + .sort(); return response.ok({ body }); } catch (err) { From aa0dcdcf0164a916d569ac269e87bb3179c467c2 Mon Sep 17 00:00:00 2001 From: Ievgen Sorokopud Date: Mon, 18 Nov 2024 17:24:04 +0100 Subject: [PATCH 05/14] [Security GenAI] Fetching Assistant Knowledge Base fails when current user's username contains a : character (#11159) (#200131) ## Summary Original bug: [internal link](https://github.com/elastic/security-team/issues/11159) **This PR fixes the next bug**: When the user is logged in with a username that contains a `:` character, fetching Knowlege Base entries fails with an error. This is preventing customers from viewing their created KB entries. This problem affects ECE customers using the SSO login option. There were a similar bugfix which inspired this one https://github.com/elastic/kibana/pull/181709 There is no easy way to reproduce this but you can try and change the line in question so that the faulty username is used instead of the one passed in. @MadameSheema Do you know a way to login locally with the username that contains a `:` character? As mentioned above this situation is possible with the ECE customers using SSO login. ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [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> --- .../knowledge_base/entries/utils.test.ts | 43 +++++++++++++++++++ .../routes/knowledge_base/entries/utils.ts | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/utils.test.ts diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/utils.test.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/utils.test.ts new file mode 100644 index 0000000000000..e718ff44630c7 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/utils.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AuthenticatedUser } from '@kbn/core-security-common'; +import { getKBUserFilter } from './utils'; + +describe('Utils', () => { + describe('getKBUserFilter', () => { + it('should return global filter when user is null', () => { + const filter = getKBUserFilter(null); + expect(filter).toEqual('(NOT users: {name:* OR id:* })'); + }); + + it('should return global filter when `username` and `profile_uid` are undefined', () => { + const filter = getKBUserFilter({} as AuthenticatedUser); + expect(filter).toEqual('(NOT users: {name:* OR id:* })'); + }); + + it('should return global filter when `username` is undefined', () => { + const filter = getKBUserFilter({ profile_uid: 'fake_user_id' } as AuthenticatedUser); + expect(filter).toEqual('(NOT users: {name:* OR id:* } OR users: {id: fake_user_id})'); + }); + + it('should return global filter when `profile_uid` is undefined', () => { + const filter = getKBUserFilter({ username: 'user1' } as AuthenticatedUser); + expect(filter).toEqual('(NOT users: {name:* OR id:* } OR users: {name: "user1"})'); + }); + + it('should return global filter when `username` has semicolon', () => { + const filter = getKBUserFilter({ + username: 'user:1', + profile_uid: 'fake_user_id', + } as AuthenticatedUser); + expect(filter).toEqual( + '(NOT users: {name:* OR id:* } OR (users: {name: "user:1"} OR users: {id: fake_user_id}))' + ); + }); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/utils.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/utils.ts index 3a548cd812539..0f5a0ab97fb29 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/utils.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/utils.ts @@ -11,7 +11,7 @@ export const getKBUserFilter = (user: AuthenticatedUser | null) => { // Only return the current users entries and all other global entries (where user[] is empty) const globalFilter = 'NOT users: {name:* OR id:* }'; - const nameFilter = user?.username ? `users: {name: ${user?.username}}` : ''; + const nameFilter = user?.username ? `users: {name: "${user?.username}"}` : ''; const idFilter = user?.profile_uid ? `users: {id: ${user?.profile_uid}}` : ''; const userFilter = user?.username && user?.profile_uid From 1607803f610cbbc97883b78578723fa8400c7b4a Mon Sep 17 00:00:00 2001 From: jennypavlova Date: Mon, 18 Nov 2024 17:27:32 +0100 Subject: [PATCH 06/14] [Inventory] Remove open in Discover button (#200574) Closes #199964 This PR removes the `open in discover` button from the inventory search bar section. | Before | After | | ------- | ----- | | image | image | --- .../inventory/e2e/cypress/e2e/home.cy.ts | 29 ------------- .../components/search_bar/discover_button.tsx | 35 ---------------- .../public/components/search_bar/index.tsx | 42 +++++++------------ .../translations/translations/fr-FR.json | 2 +- .../translations/translations/ja-JP.json | 2 +- .../translations/translations/zh-CN.json | 2 +- 6 files changed, 18 insertions(+), 94 deletions(-) delete mode 100644 x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx diff --git a/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts b/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts index 17b6cf502280a..c9d341c708965 100644 --- a/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts +++ b/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts @@ -121,35 +121,6 @@ describe('Home page', () => { cy.url().should('include', '/app/metrics/detail/host/server1'); }); - it('Navigates to discover with default filter', () => { - cy.intercept('GET', '/internal/entities/managed/enablement', { - fixture: 'eem_enabled.json', - }).as('getEEMStatus'); - cy.visitKibana('/app/inventory'); - cy.wait('@getEEMStatus'); - cy.contains('Open in discover').click(); - cy.url().should( - 'include', - "query:(language:kuery,query:'entity.definition_id%20:%20builtin*" - ); - }); - - it('Navigates to discover with kuery filter', () => { - cy.intercept('GET', '/internal/entities/managed/enablement', { - fixture: 'eem_enabled.json', - }).as('getEEMStatus'); - cy.visitKibana('/app/inventory'); - cy.wait('@getEEMStatus'); - cy.getByTestSubj('queryInput').type('service.name : foo'); - - cy.contains('Update').click(); - cy.contains('Open in discover').click(); - cy.url().should( - 'include', - "query:'service.name%20:%20foo%20AND%20entity.definition_id%20:%20builtin*'" - ); - }); - it('Navigates to infra when clicking on a container type entity', () => { cy.intercept('GET', '/internal/entities/managed/enablement', { fixture: 'eem_enabled.json', diff --git a/x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx b/x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx deleted file mode 100644 index 13477d63e5f82..0000000000000 --- a/x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx +++ /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 { EuiButton } from '@elastic/eui'; -import { DataView } from '@kbn/data-views-plugin/public'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useDiscoverRedirect } from '../../hooks/use_discover_redirect'; - -export function DiscoverButton({ dataView }: { dataView: DataView }) { - const { getDiscoverRedirectUrl } = useDiscoverRedirect(); - - const discoverLink = getDiscoverRedirectUrl(); - - if (!discoverLink) { - return null; - } - - return ( - - {i18n.translate('xpack.inventory.searchBar.discoverButton', { - defaultMessage: 'Open in discover', - })} - - ); -} diff --git a/x-pack/plugins/observability_solution/inventory/public/components/search_bar/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/search_bar/index.tsx index d1ccfd3f358e3..3464c5749dbc3 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/search_bar/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/search_bar/index.tsx @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import type { Query } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import type { SearchBarOwnProps } from '@kbn/unified-search-plugin/public/search_bar'; @@ -14,7 +13,6 @@ import { useKibana } from '../../hooks/use_kibana'; import { useUnifiedSearchContext } from '../../hooks/use_unified_search_context'; import { getKqlFieldsWithFallback } from '../../utils/get_kql_field_names_with_fallback'; import { ControlGroups } from './control_groups'; -import { DiscoverButton } from './discover_button'; export function SearchBar() { const { refreshSubject$, dataView, searchState, onQueryChange } = useUnifiedSearchContext(); @@ -73,30 +71,20 @@ export function SearchBar() { ); return ( - - - } - onQuerySubmit={handleQuerySubmit} - placeholder={i18n.translate('xpack.inventory.searchBar.placeholder', { - defaultMessage: - 'Search for your entities by name or its metadata (e.g. entity.type : service)', - })} - showDatePicker={false} - showFilterBar - showQueryInput - showQueryMenu - /> - - - {dataView ? ( - - - - ) : null} - + } + onQuerySubmit={handleQuerySubmit} + placeholder={i18n.translate('xpack.inventory.searchBar.placeholder', { + defaultMessage: + 'Search for your entities by name or its metadata (e.g. entity.type : service)', + })} + showDatePicker={false} + showFilterBar + showQueryInput + showQueryMenu + /> ); } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index fef8997584f80..49566da1f6b18 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -26205,6 +26205,7 @@ "xpack.inventory.badgeFilterWithPopover.openPopoverBadgeLabel": "Ouvrir la fenêtre contextuelle", "xpack.inventory.data_view.creation_failed": "Une erreur s'est produite lors de la création de la vue de données", "xpack.inventory.eemEnablement.errorTitle": "Erreur lors de l'activation du nouveau modèle d'entité", + "xpack.inventory.entityActions.discoverLink": "Ouvrir dans Discover", "xpack.inventory.entitiesGrid.euiDataGrid.alertsLabel": "Alertes", "xpack.inventory.entitiesGrid.euiDataGrid.alertsTooltip": "Le nombre d'alertes actives", "xpack.inventory.entitiesGrid.euiDataGrid.entityNameLabel": "Nom de l'entité", @@ -26234,7 +26235,6 @@ "xpack.inventory.noEntitiesEmptyState.description": "L'affichage de vos entités peut prendre quelques minutes. Essayez de rafraîchir à nouveau dans une minute ou deux.", "xpack.inventory.noEntitiesEmptyState.learnMore.link": "En savoir plus", "xpack.inventory.noEntitiesEmptyState.title": "Aucune entité disponible", - "xpack.inventory.searchBar.discoverButton": "Ouvrir dans Discover", "xpack.inventory.searchBar.placeholder": "Recherchez vos entités par nom ou par leurs métadonnées (par exemple entity.type : service)", "xpack.inventory.shareLink.shareButtonLabel": "Partager", "xpack.inventory.shareLink.shareToastFailureLabel": "Les URL courtes ne peuvent pas être copiées.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 78eec61c64215..f0cf7c38ac66b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -26177,6 +26177,7 @@ "xpack.inventory.badgeFilterWithPopover.openPopoverBadgeLabel": "ポップオーバーを開く", "xpack.inventory.data_view.creation_failed": "データビューの作成中にエラーが発生しました", "xpack.inventory.eemEnablement.errorTitle": "新しいエンティティモデルの有効化エラー", + "xpack.inventory.entityActions.discoverLink": "Discoverで開く", "xpack.inventory.entitiesGrid.euiDataGrid.alertsLabel": "アラート", "xpack.inventory.entitiesGrid.euiDataGrid.alertsTooltip": "アクティブなアラートの件数", "xpack.inventory.entitiesGrid.euiDataGrid.entityNameLabel": "エンティティ名", @@ -26206,7 +26207,6 @@ "xpack.inventory.noEntitiesEmptyState.description": "エンティティが表示されるまで数分かかる場合があります。1〜2分後に更新してください。", "xpack.inventory.noEntitiesEmptyState.learnMore.link": "詳細", "xpack.inventory.noEntitiesEmptyState.title": "エンティティがありません", - "xpack.inventory.searchBar.discoverButton": "Discoverで開く", "xpack.inventory.searchBar.placeholder": "エンティティを名前またはメタデータ(例:entity.type : service)で検索します。", "xpack.inventory.shareLink.shareButtonLabel": "共有", "xpack.inventory.shareLink.shareToastFailureLabel": "短縮URLをコピーできません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d1f0a075b2d21..c69512018d0f4 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -25703,6 +25703,7 @@ "xpack.inventory.badgeFilterWithPopover.openPopoverBadgeLabel": "打开弹出框", "xpack.inventory.data_view.creation_failed": "创建数据视图时出错", "xpack.inventory.eemEnablement.errorTitle": "启用新实体模型时出错", + "xpack.inventory.entityActions.discoverLink": "在 Discover 中打开", "xpack.inventory.entitiesGrid.euiDataGrid.alertsLabel": "告警", "xpack.inventory.entitiesGrid.euiDataGrid.alertsTooltip": "活动告警计数", "xpack.inventory.entitiesGrid.euiDataGrid.entityNameLabel": "实体名称", @@ -25732,7 +25733,6 @@ "xpack.inventory.noEntitiesEmptyState.description": "您的实体可能需要数分钟才能显示。请尝试在一或两分钟后刷新。", "xpack.inventory.noEntitiesEmptyState.learnMore.link": "了解详情", "xpack.inventory.noEntitiesEmptyState.title": "无可用实体", - "xpack.inventory.searchBar.discoverButton": "在 Discover 中打开", "xpack.inventory.searchBar.placeholder": "按名称或其元数据(例如,entity.type:服务)搜索您的实体", "xpack.inventory.shareLink.shareButtonLabel": "共享", "xpack.inventory.shareLink.shareToastFailureLabel": "无法复制短 URL。", From d804b38ea4211f3fb4f84b8594ee9d14f071959f Mon Sep 17 00:00:00 2001 From: Tre Date: Mon, 18 Nov 2024 16:32:46 +0000 Subject: [PATCH 07/14] [Ownership] Assign test files to platform security team (#199795) ## Summary Assign test files to platform security team Contributes to: #192979 --------- Co-authored-by: Elastic Machine Co-authored-by: Jeramy Soucy --- .github/CODEOWNERS | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7b4cd85b0d8d0..e51fceb9e2e0f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1586,6 +1586,8 @@ x-pack/test/api_integration/deployment_agnostic/services/ @elastic/appex-qa x-pack/test/**/deployment_agnostic/ @elastic/appex-qa #temporarily to monitor tests migration # Core +/test/api_integration/apis/general/*.js @elastic/kibana-core # Assigned per https://github.com/elastic/kibana/pull/199795/files/894a8ede3f9d0398c5af56bf5a82654a9bc0610b#r1846691639 +/x-pack/test/plugin_api_integration/plugins/feature_usage_test @elastic/kibana-core /test/plugin_functional/plugins/rendering_plugin @elastic/kibana-core /test/plugin_functional/plugins/session_notifications @elastic/kibana-core /x-pack/test/cloud_integration/plugins/saml_provider @elastic/kibana-core @@ -1642,6 +1644,28 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib #CC# /x-pack/plugins/translations/ @elastic/kibana-localization @elastic/kibana-core # Kibana Platform Security +# security +/x-pack/test_serverless/functional/test_suites/observability/role_management @elastic/kibana-security +/x-pack/test/functional/config_security_basic.ts @elastic/kibana-security +/x-pack/test/functional/page_objects/user_profile_page.ts @elastic/kibana-security +/x-pack/test/functional/page_objects/space_selector_page.ts @elastic/kibana-security +/x-pack/test/functional/page_objects/security_page.ts @elastic/kibana-security +/x-pack/test/functional/page_objects/role_mappings_page.ts @elastic/kibana-security +/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts @elastic/kibana-security # Assigned per https://github.com/elastic/kibana/pull/39002 +/x-pack/test/functional/page_objects/api_keys_page.ts @elastic/kibana-security +/x-pack/test/functional/page_objects/account_settings_page.ts @elastic/kibana-security +/x-pack/test/functional/apps/user_profiles @elastic/kibana-security +/x-pack/test/common/services/spaces.ts @elastic/kibana-security +/x-pack/test/api_integration/config_security_*.ts @elastic/kibana-security +/x-pack/test/functional/apps/api_keys @elastic/kibana-security +/x-pack/test/ftr_apis/security_and_spaces @elastic/kibana-security +/test/server_integration/services/supertest.js @elastic/kibana-security @elastic/kibana-core +/test/server_integration/http/ssl @elastic/kibana-security # Assigned per https://github.com/elastic/kibana/pull/53810 +/test/server_integration/http/ssl_with_p12 @elastic/kibana-security # Assigned per https://github.com/elastic/kibana/pull/199795#discussion_r1846522206 +/test/server_integration/http/ssl_with_p12_intermediate @elastic/kibana-security # Assigned per https://github.com/elastic/kibana/pull/199795#discussion_r1846522206 + +/test/server_integration/config.base.js @elastic/kibana-security @elastic/kibana-core # Assigned per https://github.com/elastic/kibana/pull/199795#discussion_r1846510782 +/test/server_integration/__fixtures__ @elastic/kibana-security # Assigned per https://github.com/elastic/kibana/pull/53810 /.github/codeql @elastic/kibana-security /.github/workflows/codeql.yml @elastic/kibana-security /.github/workflows/codeql-stats.yml @elastic/kibana-security From 5de75c0fbe2f01491864be027d22dd776fdf00fb Mon Sep 17 00:00:00 2001 From: Khristinin Nikita Date: Mon, 18 Nov 2024 17:44:49 +0100 Subject: [PATCH 08/14] Update roles in serverless for value lists (#200128) Update serverless role for value lists related to this [PR](https://github.com/elastic/elasticsearch-controller/pull/771) --- .../kibana_roles/project_controller_security_roles.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml b/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml index 2d80c9d398210..22b3fd31c423b 100644 --- a/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml +++ b/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml @@ -151,6 +151,8 @@ t1_analyst: - write - maintenance - names: + - .lists* + - .items* - apm-*-transaction* - traces-apm* - auditbeat-* @@ -275,6 +277,7 @@ t3_analyst: privileges: - read - write + - view_index_metadata - names: - metrics-endpoint.metadata_current_* - .fleet-agents* @@ -406,6 +409,7 @@ rule_author: privileges: - read - write + - view_index_metadata - names: - metrics-endpoint.metadata_current_* - .fleet-agents* @@ -475,6 +479,7 @@ soc_manager: privileges: - read - write + - view_index_metadata - names: - metrics-endpoint.metadata_current_* - .fleet-agents* From 9e4e8a5cf66c56d60179ced36d4587b36d6dd605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Mon, 18 Nov 2024 17:45:27 +0100 Subject: [PATCH 09/14] [Obs AI Assistant] Support querying `semantic_text` fields in search connectors (#200184) This adds support for querying search connectors that have `semantic_text` fields. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../server/plugin.ts | 42 +-- .../server/service/index.ts | 17 +- .../service/knowledge_base_service/index.ts | 27 +- .../recall_from_connectors.ts | 139 --------- .../recall_from_search_connectors.ts | 272 ++++++++++++++++++ .../observability_ai_assistant/tsconfig.json | 1 + .../tests/knowledge_base/helpers.ts | 8 +- 7 files changed, 297 insertions(+), 209 deletions(-) delete mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/recall_from_connectors.ts create mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/recall_from_search_connectors.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts index b2b5736fd1d6f..361d13e6d77f2 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts @@ -12,14 +12,13 @@ import { Plugin, PluginInitializerContext, } from '@kbn/core/server'; -import { mapValues, once } from 'lodash'; +import { mapValues } from 'lodash'; import { i18n } from '@kbn/i18n'; import { CONNECTOR_TOKEN_SAVED_OBJECT_TYPE, ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, } from '@kbn/actions-plugin/server/constants/saved_objects'; -import { firstValueFrom } from 'rxjs'; import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { OBSERVABILITY_AI_ASSISTANT_FEATURE_ID } from '../common/feature'; import type { ObservabilityAIAssistantConfig } from './config'; @@ -114,47 +113,10 @@ export class ObservabilityAIAssistantPlugin }; }) as ObservabilityAIAssistantRouteHandlerResources['plugins']; - // Using once to make sure the same model ID is used during service init and Knowledge base setup - const getSearchConnectorModelId = once(async () => { - const defaultModelId = '.elser_model_2'; - const [_, pluginsStart] = await core.getStartServices(); - // Wait for the license to be available so the ML plugin's guards pass once we ask for ELSER stats - const license = await firstValueFrom(pluginsStart.licensing.license$); - if (!license.hasAtLeast('enterprise')) { - return defaultModelId; - } - - try { - // Wait for the ML plugin's dependency on the internal saved objects client to be ready - const { ml } = await core.plugins.onSetup('ml'); - - if (!ml.found) { - throw new Error('Could not find ML plugin'); - } - - const elserModelDefinition = await ( - ml.contract as { - trainedModelsProvider: ( - request: {}, - soClient: {} - ) => { getELSER: () => Promise<{ model_id: string }> }; - } - ) - .trainedModelsProvider({} as any, {} as any) // request, savedObjectsClient (but we fake it to use the internal user) - .getELSER(); - - return elserModelDefinition.model_id; - } catch (error) { - this.logger.error(`Failed to resolve ELSER model definition: ${error}`); - return defaultModelId; - } - }); - const service = (this.service = new ObservabilityAIAssistantService({ logger: this.logger.get('service'), core, - getSearchConnectorModelId, - enableKnowledgeBase: this.config.enableKnowledgeBase, + config: this.config, })); registerMigrateKnowledgeBaseEntriesTask({ diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts index 6dcfbf1796501..9c26bebdd8388 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts @@ -20,6 +20,7 @@ import { conversationComponentTemplate } from './conversation_component_template import { kbComponentTemplate } from './kb_component_template'; import { KnowledgeBaseService } from './knowledge_base_service'; import type { RegistrationCallback, RespondFunctionResources } from './types'; +import { ObservabilityAIAssistantConfig } from '../config'; function getResourceName(resource: string) { return `.kibana-observability-ai-assistant-${resource}`; @@ -47,27 +48,23 @@ export const resourceNames = { export class ObservabilityAIAssistantService { private readonly core: CoreSetup; private readonly logger: Logger; - private readonly getSearchConnectorModelId: () => Promise; private kbService?: KnowledgeBaseService; - private enableKnowledgeBase: boolean; + private config: ObservabilityAIAssistantConfig; private readonly registrations: RegistrationCallback[] = []; constructor({ logger, core, - getSearchConnectorModelId, - enableKnowledgeBase, + config, }: { logger: Logger; core: CoreSetup; - getSearchConnectorModelId: () => Promise; - enableKnowledgeBase: boolean; + config: ObservabilityAIAssistantConfig; }) { this.core = core; this.logger = logger; - this.getSearchConnectorModelId = getSearchConnectorModelId; - this.enableKnowledgeBase = enableKnowledgeBase; + this.config = config; this.resetInit(); } @@ -166,12 +163,12 @@ export class ObservabilityAIAssistantService { }); this.kbService = new KnowledgeBaseService({ + core: this.core, logger: this.logger.get('kb'), + config: this.config, esClient: { asInternalUser, }, - getSearchConnectorModelId: this.getSearchConnectorModelId, - enabled: this.enableKnowledgeBase, }); this.logger.info('Successfully set up index assets'); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts index 66a49cdc29bee..a98cf6f810f2c 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts @@ -6,7 +6,7 @@ */ import { serverUnavailable } from '@hapi/boom'; -import type { ElasticsearchClient, IUiSettingsClient } from '@kbn/core/server'; +import type { CoreSetup, ElasticsearchClient, IUiSettingsClient } from '@kbn/core/server'; import type { Logger } from '@kbn/logging'; import { orderBy } from 'lodash'; import { encode } from 'gpt-tokenizer'; @@ -26,15 +26,17 @@ import { getInferenceEndpoint, isInferenceEndpointMissingOrUnavailable, } from '../inference_endpoint'; -import { recallFromConnectors } from './recall_from_connectors'; +import { recallFromSearchConnectors } from './recall_from_search_connectors'; +import { ObservabilityAIAssistantPluginStartDependencies } from '../../types'; +import { ObservabilityAIAssistantConfig } from '../../config'; interface Dependencies { + core: CoreSetup; esClient: { asInternalUser: ElasticsearchClient; }; logger: Logger; - getSearchConnectorModelId: () => Promise; - enabled: boolean; + config: ObservabilityAIAssistantConfig; } export interface RecalledEntry { @@ -141,14 +143,13 @@ export class KnowledgeBaseService { esClient: { asCurrentUser: ElasticsearchClient; asInternalUser: ElasticsearchClient }; uiSettingsClient: IUiSettingsClient; }): Promise => { - if (!this.dependencies.enabled) { + if (!this.dependencies.config.enableKnowledgeBase) { return []; } this.dependencies.logger.debug( () => `Recalling entries from KB for queries: "${JSON.stringify(queries)}"` ); - const modelId = await this.dependencies.getSearchConnectorModelId(); const [documentsFromKb, documentsFromConnectors] = await Promise.all([ this.recallFromKnowledgeBase({ @@ -162,11 +163,11 @@ export class KnowledgeBaseService { } throw error; }), - recallFromConnectors({ + recallFromSearchConnectors({ esClient, uiSettingsClient, queries, - modelId, + core: this.dependencies.core, logger: this.dependencies.logger, }).catch((error) => { this.dependencies.logger.debug('Error getting data from search indices'); @@ -214,7 +215,7 @@ export class KnowledgeBaseService { namespace: string, user?: { name: string } ): Promise> => { - if (!this.dependencies.enabled) { + if (!this.dependencies.config.enableKnowledgeBase) { return []; } try { @@ -257,7 +258,7 @@ export class KnowledgeBaseService { sortBy?: string; sortDirection?: 'asc' | 'desc'; }): Promise<{ entries: KnowledgeBaseEntry[] }> => { - if (!this.dependencies.enabled) { + if (!this.dependencies.config.enableKnowledgeBase) { return { entries: [] }; } try { @@ -330,7 +331,7 @@ export class KnowledgeBaseService { user?: { name: string; id?: string }; namespace?: string; }) => { - if (!this.dependencies.enabled) { + if (!this.dependencies.config.enableKnowledgeBase) { return null; } const res = await this.dependencies.esClient.asInternalUser.search({ @@ -393,7 +394,7 @@ export class KnowledgeBaseService { user?: { name: string; id?: string }; namespace?: string; }): Promise => { - if (!this.dependencies.enabled) { + if (!this.dependencies.config.enableKnowledgeBase) { return; } @@ -448,7 +449,7 @@ export class KnowledgeBaseService { errorMessage = error.message; }); - const enabled = this.dependencies.enabled; + const enabled = this.dependencies.config.enableKnowledgeBase; if (!endpoint) { return { ready: false, enabled, errorMessage }; } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/recall_from_connectors.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/recall_from_connectors.ts deleted file mode 100644 index 27c133e7b88d0..0000000000000 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/recall_from_connectors.ts +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -import { IUiSettingsClient } from '@kbn/core-ui-settings-server'; -import { isEmpty } from 'lodash'; -import type { Logger } from '@kbn/logging'; -import { RecalledEntry } from '.'; -import { aiAssistantSearchConnectorIndexPattern } from '../../../common'; - -export async function recallFromConnectors({ - queries, - esClient, - uiSettingsClient, - modelId, - logger, -}: { - queries: Array<{ text: string; boost?: number }>; - esClient: { asCurrentUser: ElasticsearchClient; asInternalUser: ElasticsearchClient }; - uiSettingsClient: IUiSettingsClient; - modelId: string; - logger: Logger; -}): Promise { - const ML_INFERENCE_PREFIX = 'ml.inference.'; - const connectorIndices = await getConnectorIndices(esClient, uiSettingsClient, logger); - logger.debug(`Found connector indices: ${connectorIndices}`); - - const fieldCaps = await esClient.asCurrentUser.fieldCaps({ - index: connectorIndices, - fields: `${ML_INFERENCE_PREFIX}*`, - allow_no_indices: true, - types: ['sparse_vector'], - filters: '-metadata,-parent', - }); - - const fieldsWithVectors = Object.keys(fieldCaps.fields).map((field) => - field.replace('_expanded.predicted_value', '').replace(ML_INFERENCE_PREFIX, '') - ); - - if (!fieldsWithVectors.length) { - return []; - } - - const esQueries = fieldsWithVectors.flatMap((field) => { - const vectorField = `${ML_INFERENCE_PREFIX}${field}_expanded.predicted_value`; - const modelField = `${ML_INFERENCE_PREFIX}${field}_expanded.model_id`; - - return queries.map(({ text, boost = 1 }) => { - return { - bool: { - should: [ - { - text_expansion: { - [vectorField]: { - model_text: text, - model_id: modelId, - boost, - }, - }, - }, - ], - filter: [ - { - term: { - [modelField]: modelId, - }, - }, - ], - }, - }; - }); - }); - - const response = await esClient.asCurrentUser.search({ - index: connectorIndices, - query: { - bool: { - should: esQueries, - }, - }, - size: 20, - _source: { - exclude: ['_*', 'ml*'], - }, - }); - - const results = response.hits.hits.map((hit) => ({ - text: JSON.stringify(hit._source), - score: hit._score!, - is_correction: false, - id: hit._id!, - })); - - return results; -} - -async function getConnectorIndices( - esClient: { asCurrentUser: ElasticsearchClient; asInternalUser: ElasticsearchClient }, - uiSettingsClient: IUiSettingsClient, - logger: Logger -) { - // improve performance by running this in parallel with the `uiSettingsClient` request - const responsePromise = esClient.asInternalUser.transport - .request<{ - results?: Array<{ index_name: string }>; - }>({ - method: 'GET', - path: '_connector', - querystring: { - filter_path: 'results.index_name', - }, - }) - .catch((e) => { - logger.warn(`Failed to fetch connector indices due to ${e.message}`); - return { results: [] }; - }); - - const customSearchConnectorIndex = await uiSettingsClient.get( - aiAssistantSearchConnectorIndexPattern - ); - - if (customSearchConnectorIndex) { - return customSearchConnectorIndex.split(','); - } - - const response = await responsePromise; - const connectorIndices = response.results?.map((result) => result.index_name); - - // preserve backwards compatibility with 8.14 (may not be needed in the future) - if (isEmpty(connectorIndices)) { - return ['search-*']; - } - - return connectorIndices; -} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/recall_from_search_connectors.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/recall_from_search_connectors.ts new file mode 100644 index 0000000000000..5abd6d850a8f4 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/recall_from_search_connectors.ts @@ -0,0 +1,272 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { IUiSettingsClient } from '@kbn/core-ui-settings-server'; +import { isEmpty, orderBy, compact } from 'lodash'; +import type { Logger } from '@kbn/logging'; +import { CoreSetup } from '@kbn/core-lifecycle-server'; +import { firstValueFrom } from 'rxjs'; +import { RecalledEntry } from '.'; +import { aiAssistantSearchConnectorIndexPattern } from '../../../common'; +import { ObservabilityAIAssistantPluginStartDependencies } from '../../types'; + +export async function recallFromSearchConnectors({ + queries, + esClient, + uiSettingsClient, + logger, + core, +}: { + queries: Array<{ text: string; boost?: number }>; + esClient: { asCurrentUser: ElasticsearchClient; asInternalUser: ElasticsearchClient }; + uiSettingsClient: IUiSettingsClient; + logger: Logger; + core: CoreSetup; +}): Promise { + const connectorIndices = await getConnectorIndices(esClient, uiSettingsClient, logger); + logger.debug(`Found connector indices: ${connectorIndices}`); + + const [semanticTextConnectors, legacyConnectors] = await Promise.all([ + recallFromSemanticTextConnectors({ + queries, + esClient, + uiSettingsClient, + logger, + core, + connectorIndices, + }), + + recallFromLegacyConnectors({ + queries, + esClient, + uiSettingsClient, + logger, + core, + connectorIndices, + }), + ]); + + return orderBy([...semanticTextConnectors, ...legacyConnectors], (entry) => entry.score, 'desc'); +} + +async function recallFromSemanticTextConnectors({ + queries, + esClient, + logger, + core, + connectorIndices, +}: { + queries: Array<{ text: string; boost?: number }>; + esClient: { asCurrentUser: ElasticsearchClient; asInternalUser: ElasticsearchClient }; + uiSettingsClient: IUiSettingsClient; + logger: Logger; + core: CoreSetup; + connectorIndices: string[] | undefined; +}): Promise { + const fieldCaps = await esClient.asCurrentUser.fieldCaps({ + index: connectorIndices, + fields: `*`, + allow_no_indices: true, + types: ['semantic_text'], + filters: '-metadata,-parent', + }); + + const semanticTextFields = Object.keys(fieldCaps.fields); + if (!semanticTextFields.length) { + return []; + } + logger.debug(`Semantic text field for search connectors: ${semanticTextFields}`); + + const params = { + index: connectorIndices, + size: 20, + _source: { + excludes: semanticTextFields.map((field) => `${field}.inference`), + }, + query: { + bool: { + should: semanticTextFields.flatMap((field) => { + return queries.map(({ text, boost = 1 }) => ({ + bool: { filter: [{ semantic: { field, query: text, boost } }] }, + })); + }), + minimum_should_match: 1, + }, + }, + }; + + const response = await esClient.asCurrentUser.search(params); + + const results = response.hits.hits.map((hit) => ({ + text: JSON.stringify(hit._source), + score: hit._score!, + is_correction: false, + id: hit._id!, + })); + + return results; +} + +async function recallFromLegacyConnectors({ + queries, + esClient, + logger, + core, + connectorIndices, +}: { + queries: Array<{ text: string; boost?: number }>; + esClient: { asCurrentUser: ElasticsearchClient; asInternalUser: ElasticsearchClient }; + uiSettingsClient: IUiSettingsClient; + logger: Logger; + core: CoreSetup; + connectorIndices: string[] | undefined; +}): Promise { + const ML_INFERENCE_PREFIX = 'ml.inference.'; + + const modelIdPromise = getElserModelId(core, logger); // pre-fetch modelId in parallel with fieldCaps + const fieldCaps = await esClient.asCurrentUser.fieldCaps({ + index: connectorIndices, + fields: `${ML_INFERENCE_PREFIX}*`, + allow_no_indices: true, + types: ['sparse_vector'], + filters: '-metadata,-parent', + }); + + const fieldsWithVectors = Object.keys(fieldCaps.fields).map((field) => + field.replace('_expanded.predicted_value', '').replace(ML_INFERENCE_PREFIX, '') + ); + + if (!fieldsWithVectors.length) { + return []; + } + + const modelId = await modelIdPromise; + const esQueries = fieldsWithVectors.flatMap((field) => { + const vectorField = `${ML_INFERENCE_PREFIX}${field}_expanded.predicted_value`; + const modelField = `${ML_INFERENCE_PREFIX}${field}_expanded.model_id`; + + return queries.map(({ text, boost = 1 }) => { + return { + bool: { + should: [ + { + text_expansion: { + [vectorField]: { + model_text: text, + model_id: modelId, + boost, + }, + }, + }, + ], + filter: [ + { + term: { + [modelField]: modelId, + }, + }, + ], + }, + }; + }); + }); + + const response = await esClient.asCurrentUser.search({ + index: connectorIndices, + size: 20, + _source: { + exclude: ['_*', 'ml*'], + }, + query: { + bool: { + should: esQueries, + }, + }, + }); + + const results = response.hits.hits.map((hit) => ({ + text: JSON.stringify(hit._source), + score: hit._score!, + is_correction: false, + id: hit._id!, + })); + + return results; +} + +async function getConnectorIndices( + esClient: { asCurrentUser: ElasticsearchClient; asInternalUser: ElasticsearchClient }, + uiSettingsClient: IUiSettingsClient, + logger: Logger +) { + // improve performance by running this in parallel with the `uiSettingsClient` request + const responsePromise = esClient.asInternalUser.connector + .list({ filter_path: 'results.index_name' }) + .catch((e) => { + logger.warn(`Failed to fetch connector indices due to ${e.message}`); + return { results: [] }; + }); + + const customSearchConnectorIndex = await uiSettingsClient.get( + aiAssistantSearchConnectorIndexPattern + ); + + if (customSearchConnectorIndex) { + return customSearchConnectorIndex.split(','); + } + + const response = await responsePromise; + + const connectorIndices = compact(response.results?.map((result) => result.index_name)); + + // preserve backwards compatibility with 8.14 (may not be needed in the future) + if (isEmpty(connectorIndices)) { + return ['search-*']; + } + + return connectorIndices; +} + +async function getElserModelId( + core: CoreSetup, + logger: Logger +) { + const defaultModelId = '.elser_model_2'; + const [_, pluginsStart] = await core.getStartServices(); + + // Wait for the license to be available so the ML plugin's guards pass once we ask for ELSER stats + const license = await firstValueFrom(pluginsStart.licensing.license$); + if (!license.hasAtLeast('enterprise')) { + return defaultModelId; + } + + try { + // Wait for the ML plugin's dependency on the internal saved objects client to be ready + const { ml } = await core.plugins.onSetup('ml'); + + if (!ml.found) { + throw new Error('Could not find ML plugin'); + } + + const elserModelDefinition = await ( + ml.contract as { + trainedModelsProvider: ( + request: {}, + soClient: {} + ) => { getELSER: () => Promise<{ model_id: string }> }; + } + ) + .trainedModelsProvider({} as any, {} as any) // request, savedObjectsClient (but we fake it to use the internal user) + .getELSER(); + + return elserModelDefinition.model_id; + } catch (error) { + logger.error(`Failed to resolve ELSER model definition: ${error}`); + return defaultModelId; + } +} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/tsconfig.json b/x-pack/plugins/observability_solution/observability_ai_assistant/tsconfig.json index 750bf69477653..d5acd7a365b50 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/tsconfig.json @@ -46,6 +46,7 @@ "@kbn/management-settings-ids", "@kbn/ai-assistant-common", "@kbn/inference-common", + "@kbn/core-lifecycle-server", ], "exclude": ["target/**/*"] } diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/helpers.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/helpers.ts index fa1f15ddca4cd..25bbeb183a3b6 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/helpers.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/helpers.ts @@ -63,11 +63,5 @@ export async function deleteInferenceEndpoint({ es: Client; name?: string; }) { - return es.transport.request({ - method: 'DELETE', - path: `_inference/sparse_embedding/${name}`, - querystring: { - force: true, - }, - }); + return es.inference.delete({ inference_id: name, force: true }); } From eddfe5b8a73ac636d5e75606280bd771a01b1adb Mon Sep 17 00:00:00 2001 From: Kyra Cho Date: Mon, 18 Nov 2024 08:46:33 -0800 Subject: [PATCH 10/14] [Lens] rewrite BucketNestingEditor tests to use react testing library (#199888) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Hi! This PR rewrites the `BucketNestingEditor` tests to use the react testing library. Fixes #199839 Screenshot 2024-11-12 at 3 35 06 PM ### Checklist - [n/a] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed --------- Co-authored-by: Marta Bondyra <4283304+mbondyra@users.noreply.github.com> --- .../bucket_nesting_editor.test.tsx | 107 ++++++++---------- 1 file changed, 49 insertions(+), 58 deletions(-) diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/bucket_nesting_editor.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/bucket_nesting_editor.test.tsx index 6c09849df04ac..0479162855659 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/bucket_nesting_editor.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/bucket_nesting_editor.test.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { mount } from 'enzyme'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import React from 'react'; import { BucketNestingEditor } from './bucket_nesting_editor'; import { GenericIndexPatternColumn } from '../form_based'; @@ -21,7 +22,7 @@ const getFieldByName = (name: string): IndexPatternField | undefined => fieldMap describe('BucketNestingEditor', () => { function mockCol(col: Partial = {}): GenericIndexPatternColumn { - const result = { + return { dataType: 'string', isBucketed: true, label: 'a', @@ -33,13 +34,11 @@ describe('BucketNestingEditor', () => { }, sourceField: 'a', ...col, - }; - - return result as GenericIndexPatternColumn; + } as GenericIndexPatternColumn; } it('should display the top level grouping when at the root', () => { - const component = mount( + render( { setColumns={jest.fn()} /> ); - const nestingSwitch = component.find('[data-test-subj="indexPattern-nesting-switch"]').first(); - expect(nestingSwitch.prop('checked')).toBeTruthy(); + const nestingSwitch = screen.getByTestId('indexPattern-nesting-switch'); + expect(nestingSwitch).toBeChecked(); }); it('should display the bottom level grouping when appropriate', () => { - const component = mount( + render( { setColumns={jest.fn()} /> ); - const nestingSwitch = component.find('[data-test-subj="indexPattern-nesting-switch"]').first(); - expect(nestingSwitch.prop('checked')).toBeFalsy(); + const nestingSwitch = screen.getByTestId('indexPattern-nesting-switch'); + expect(nestingSwitch).not.toBeChecked(); }); - it('should reorder the columns when toggled', () => { + it('should reorder the columns when toggled', async () => { const setColumns = jest.fn(); - const component = mount( + const { rerender } = render( { /> ); - component - .find('[data-test-subj="indexPattern-nesting-switch"] button') - .first() - .simulate('click'); - + await userEvent.click(screen.getByTestId('indexPattern-nesting-switch')); expect(setColumns).toHaveBeenCalledTimes(1); expect(setColumns).toHaveBeenCalledWith(['a', 'b', 'c']); - component.setProps({ - layer: { - columnOrder: ['a', 'b', 'c'], - columns: { - a: mockCol(), - b: mockCol(), - c: mockCol({ operationType: 'min', isBucketed: false }), - }, - indexPatternId: 'foo', - }, - }); - - component - .find('[data-test-subj="indexPattern-nesting-switch"] button') - .first() - .simulate('click'); + rerender( + + ); + await userEvent.click(screen.getByTestId('indexPattern-nesting-switch')); expect(setColumns).toHaveBeenCalledTimes(2); expect(setColumns).toHaveBeenLastCalledWith(['b', 'a', 'c']); }); it('should display nothing if there are no buckets', () => { - const component = mount( + const { container } = render( { /> ); - expect(component.children().length).toBe(0); + expect(container.firstChild).toBeNull(); }); it('should display nothing if there is one bucket', () => { - const component = mount( + const { container } = render( { /> ); - expect(component.children().length).toBe(0); + expect(container.firstChild).toBeNull(); }); it('should display a dropdown with the parent column selected if 3+ buckets', () => { - const component = mount( + render( { /> ); - const control = component.find('[data-test-subj="indexPattern-nesting-select"]').first(); - - expect(control.prop('value')).toEqual('c'); + const control = screen.getByTestId('indexPattern-nesting-select'); + expect((control as HTMLSelectElement).value).toEqual('c'); }); - it('should reorder the columns when a column is selected in the dropdown', () => { + it('should reorder the columns when a column is selected in the dropdown', async () => { const setColumns = jest.fn(); - const component = mount( + render( { /> ); - const control = component.find('[data-test-subj="indexPattern-nesting-select"] select').first(); - control.simulate('change', { - target: { value: 'b' }, - }); + const control = screen.getByTestId('indexPattern-nesting-select'); + await userEvent.selectOptions(control, 'b'); expect(setColumns).toHaveBeenCalledWith(['c', 'b', 'a']); }); - it('should move to root if the first dropdown item is selected', () => { + it('should move to root if the first dropdown item is selected', async () => { const setColumns = jest.fn(); - const component = mount( + render( { /> ); - const control = component.find('[data-test-subj="indexPattern-nesting-select"] select').first(); - control.simulate('change', { target: { value: '' } }); + const control = screen.getByTestId('indexPattern-nesting-select'); + await userEvent.selectOptions(control, ''); expect(setColumns).toHaveBeenCalledWith(['a', 'c', 'b']); }); - it('should allow the last bucket to be moved', () => { + it('should allow the last bucket to be moved', async () => { const setColumns = jest.fn(); - const component = mount( + render( { /> ); - const control = component.find('[data-test-subj="indexPattern-nesting-select"] select').first(); - control.simulate('change', { - target: { value: '' }, - }); + const control = screen.getByTestId('indexPattern-nesting-select'); + await userEvent.selectOptions(control, ''); expect(setColumns).toHaveBeenCalledWith(['b', 'c', 'a']); }); From 81919c3d77f2d061540577cb8a889ca86bd0241d Mon Sep 17 00:00:00 2001 From: ruby Date: Tue, 19 Nov 2024 01:14:32 +0800 Subject: [PATCH 11/14] [Console]: incorrect position of console tour #198389 (#198636) --- src/plugins/console/common/constants/index.ts | 2 +- .../console/common/constants/plugin.ts | 2 ++ .../components/console_tour_step.tsx | 25 ++++++++++++++++-- .../embeddable/console_resize_button.tsx | 17 ++++++++++++ .../apps/console/_onboarding_tour.ts | 26 ++++++++++++++++--- test/functional/page_objects/console_page.ts | 6 ++++- 6 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/plugins/console/common/constants/index.ts b/src/plugins/console/common/constants/index.ts index a00bcebcf38cc..b4d6a594241ce 100644 --- a/src/plugins/console/common/constants/index.ts +++ b/src/plugins/console/common/constants/index.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { MAJOR_VERSION } from './plugin'; +export { MAJOR_VERSION, WELCOME_TOUR_DELAY } from './plugin'; export { API_BASE_PATH, KIBANA_API_PREFIX } from './api'; export { DEFAULT_VARIABLES } from './variables'; export { diff --git a/src/plugins/console/common/constants/plugin.ts b/src/plugins/console/common/constants/plugin.ts index 27ddb7d5dff1d..bb87e300c138d 100644 --- a/src/plugins/console/common/constants/plugin.ts +++ b/src/plugins/console/common/constants/plugin.ts @@ -8,3 +8,5 @@ */ export const MAJOR_VERSION = '8.0.0'; + +export const WELCOME_TOUR_DELAY = 250; diff --git a/src/plugins/console/public/application/components/console_tour_step.tsx b/src/plugins/console/public/application/components/console_tour_step.tsx index 578d590bfff4a..97e999b0090aa 100644 --- a/src/plugins/console/public/application/components/console_tour_step.tsx +++ b/src/plugins/console/public/application/components/console_tour_step.tsx @@ -7,8 +7,9 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { ReactNode, ReactElement } from 'react'; +import React, { ReactNode, ReactElement, useState, useEffect } from 'react'; import { EuiTourStep, PopoverAnchorPosition } from '@elastic/eui'; +import { WELCOME_TOUR_DELAY } from '../../../common/constants'; export interface ConsoleTourStepProps { step: number; @@ -44,11 +45,31 @@ export const ConsoleTourStep = ({ tourStepProps, children }: Props) => { css, } = tourStepProps; + const [popoverVisible, setPopoverVisible] = useState(false); + + useEffect(() => { + let timeoutId: any; + + if (isStepOpen) { + timeoutId = setTimeout(() => { + setPopoverVisible(true); + }, WELCOME_TOUR_DELAY); + } else { + setPopoverVisible(false); + } + + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [isStepOpen]); + return ( { + const debouncedResize = debounce(() => { + window.dispatchEvent(new Event('resize')); + }, WELCOME_TOUR_DELAY); + + debouncedResize(); + + // Cleanup the debounce instance on unmount or dependency change + return () => { + debouncedResize.cancel(); + }; + }, [consoleHeight]); + useEffect(() => { function handleResize() { const newMaxConsoleHeight = getCurrentConsoleMaxSize(euiTheme); diff --git a/test/functional/apps/console/_onboarding_tour.ts b/test/functional/apps/console/_onboarding_tour.ts index 330498cb7b5ec..1fc47a70d14b0 100644 --- a/test/functional/apps/console/_onboarding_tour.ts +++ b/test/functional/apps/console/_onboarding_tour.ts @@ -10,6 +10,9 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +// The euiTour shows with a small delay, so with 1s we should be safe +const DELAY_FOR = 1000; + export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const browser = getService('browser'); @@ -40,22 +43,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await isTourStepOpen('filesTourStep')).to.be(false); }; + const waitUntilFinishedLoading = async () => { + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.common.sleep(DELAY_FOR); + }; + it('displays all five steps in the tour', async () => { + const andWaitFor = DELAY_FOR; + await waitUntilFinishedLoading(); + log.debug('on Shell tour step'); expect(await isTourStepOpen('shellTourStep')).to.be(true); - await PageObjects.console.clickNextTourStep(); + await PageObjects.console.clickNextTourStep(andWaitFor); log.debug('on Editor tour step'); expect(await isTourStepOpen('editorTourStep')).to.be(true); - await PageObjects.console.clickNextTourStep(); + await PageObjects.console.clickNextTourStep(andWaitFor); log.debug('on History tour step'); expect(await isTourStepOpen('historyTourStep')).to.be(true); - await PageObjects.console.clickNextTourStep(); + await PageObjects.console.clickNextTourStep(andWaitFor); log.debug('on Config tour step'); expect(await isTourStepOpen('configTourStep')).to.be(true); - await PageObjects.console.clickNextTourStep(); + await PageObjects.console.clickNextTourStep(andWaitFor); log.debug('on Files tour step'); expect(await isTourStepOpen('filesTourStep')).to.be(true); @@ -73,10 +84,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Tour should reset after clearing local storage await browser.clearLocalStorage(); await browser.refresh(); + + await waitUntilFinishedLoading(); expect(await isTourStepOpen('shellTourStep')).to.be(true); }); it('skipping the tour hides the tour steps', async () => { + await waitUntilFinishedLoading(); + expect(await isTourStepOpen('shellTourStep')).to.be(true); expect(await testSubjects.exists('consoleSkipTourButton')).to.be(true); await PageObjects.console.clickSkipTour(); @@ -90,6 +105,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('allows re-running the tour', async () => { + await waitUntilFinishedLoading(); + await PageObjects.console.skipTourIfExists(); // Verify that tour is hiddern @@ -100,6 +117,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.console.clickRerunTour(); // Verify that first tour step is visible + await waitUntilFinishedLoading(); expect(await isTourStepOpen('shellTourStep')).to.be(true); }); }); diff --git a/test/functional/page_objects/console_page.ts b/test/functional/page_objects/console_page.ts index 87308d24fd8c4..a80f3426e256e 100644 --- a/test/functional/page_objects/console_page.ts +++ b/test/functional/page_objects/console_page.ts @@ -276,8 +276,12 @@ export class ConsolePageObject extends FtrService { await this.testSubjects.click('consoleSkipTourButton'); } - public async clickNextTourStep() { + public async clickNextTourStep(andWaitFor: number = 0) { await this.testSubjects.click('consoleNextTourStepButton'); + + if (andWaitFor) { + await this.common.sleep(andWaitFor); + } } public async clickCompleteTour() { From 9d314b9fbb6dc850da7341949765243cd0bf742c Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Mon, 18 Nov 2024 18:28:49 +0100 Subject: [PATCH 12/14] [APM] Migrate time_range_metadata test to deployment agnostic (#200146) ## Summary Closes [#198994](https://github.com/elastic/kibana/issues/198994) Part of https://github.com/elastic/kibana/issues/193245 This PR contains the changes to migrate `time_range_metadata` test folder to Deployment-agnostic testing strategy. ### How to test - Serverless ``` node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts --grep="APM" ``` It's recommended to be run against [MKI](https://github.com/crespocarlos/kibana/blob/main/x-pack/test_serverless/README.md#run-tests-on-mki) - Stateful ``` node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts --grep="APM" ``` - [ ] ~(OPTIONAL, only if a test has been unskipped) Run flaky test suite~ - [x] local run for serverless - [x] local run for stateful - [x] MKI run for serverless --------- Co-authored-by: Elastic Machine Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../apis/observability/apm/index.ts | 1 + .../apm/time_range_metadata/index.ts | 15 + .../many_apm_server_versions.spec.ts | 276 +++++++++++++++++ .../time_range_metadata.spec.ts | 68 ++--- .../many_apm_server_versions.spec.ts | 278 ------------------ 5 files changed, 324 insertions(+), 314 deletions(-) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/time_range_metadata/index.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/time_range_metadata/many_apm_server_versions.spec.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/time_range_metadata/time_range_metadata.spec.ts (94%) delete mode 100644 x-pack/test/apm_api_integration/tests/time_range_metadata/many_apm_server_versions.spec.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts index dcbf8edc4a755..640bb90abe711 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts @@ -32,6 +32,7 @@ export default function apmApiIntegrationTests({ loadTestFile(require.resolve('./service_maps')); loadTestFile(require.resolve('./inspect')); loadTestFile(require.resolve('./service_groups')); + loadTestFile(require.resolve('./time_range_metadata')); loadTestFile(require.resolve('./diagnostics')); loadTestFile(require.resolve('./service_nodes')); loadTestFile(require.resolve('./span_links')); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/time_range_metadata/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/time_range_metadata/index.ts new file mode 100644 index 0000000000000..4e3c25936a2db --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/time_range_metadata/index.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 { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('time_range_metadata', () => { + loadTestFile(require.resolve('./many_apm_server_versions.spec.ts')); + loadTestFile(require.resolve('./time_range_metadata.spec.ts')); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/time_range_metadata/many_apm_server_versions.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/time_range_metadata/many_apm_server_versions.spec.ts new file mode 100644 index 0000000000000..31012e6dd6d63 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/time_range_metadata/many_apm_server_versions.spec.ts @@ -0,0 +1,276 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import { apm, timerange } from '@kbn/apm-synthtrace-client'; +import moment from 'moment'; +import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import { + TRANSACTION_DURATION_HISTOGRAM, + TRANSACTION_DURATION_SUMMARY, +} from '@kbn/apm-plugin/common/es_fields/apm'; +import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; +import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; +import { LatencyAggregationType } from '@kbn/apm-plugin/common/latency_aggregation_types'; +import { Readable } from 'stream'; +import type { ApmApiClient } from '../../../../services/apm_api'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + const es = getService('es'); + + const baseTime = new Date('2023-10-01T00:00:00.000Z').getTime(); + const startLegacy = moment(baseTime).add(0, 'minutes'); + const start = moment(baseTime).add(5, 'minutes'); + const endLegacy = moment(baseTime).add(10, 'minutes'); + const end = moment(baseTime).add(15, 'minutes'); + + describe('Time range metadata when there are multiple APM Server versions', () => { + describe('when ingesting traces from APM Server with different versions', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await generateTraceDataForService({ + serviceName: 'synth-java-legacy', + start: startLegacy, + end: endLegacy, + isLegacy: true, + synthtrace: apmSynthtraceEsClient, + }); + + await generateTraceDataForService({ + serviceName: 'synth-java', + start, + end, + isLegacy: false, + synthtrace: apmSynthtraceEsClient, + }); + }); + + after(() => { + return apmSynthtraceEsClient.clean(); + }); + + it('ingests transaction metrics with transaction.duration.summary', async () => { + const res = await es.search({ + index: 'metrics-apm*', + body: { + query: { + bool: { + filter: [ + { exists: { field: TRANSACTION_DURATION_HISTOGRAM } }, + { exists: { field: TRANSACTION_DURATION_SUMMARY } }, + ], + }, + }, + }, + }); + + // @ts-expect-error + expect(res.hits.total.value).to.be(20); + }); + + it('ingests transaction metrics without transaction.duration.summary', async () => { + const res = await es.search({ + index: 'metrics-apm*', + body: { + query: { + bool: { + filter: [{ exists: { field: TRANSACTION_DURATION_HISTOGRAM } }], + must_not: [{ exists: { field: TRANSACTION_DURATION_SUMMARY } }], + }, + }, + }, + }); + + // @ts-expect-error + expect(res.hits.total.value).to.be(10); + }); + + it('has transaction.duration.summary field for every document type', async () => { + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/time_range_metadata', + params: { + query: { + start: endLegacy.toISOString(), + end: end.toISOString(), + enableContinuousRollups: true, + enableServiceTransactionMetrics: true, + useSpanName: false, + kuery: '', + }, + }, + }); + + const allHasSummaryField = response.body.sources + .filter( + (source) => + source.documentType !== ApmDocumentType.TransactionEvent && + source.rollupInterval !== RollupInterval.SixtyMinutes // there is not enough data for 60 minutes + ) + .every((source) => { + return source.hasDurationSummaryField; + }); + + expect(allHasSummaryField).to.eql(true); + }); + + it('does not support transaction.duration.summary when the field is not supported by all APM server versions', async () => { + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/time_range_metadata', + params: { + query: { + start: startLegacy.toISOString(), + end: endLegacy.toISOString(), + enableContinuousRollups: true, + enableServiceTransactionMetrics: true, + useSpanName: false, + kuery: '', + }, + }, + }); + + const allHasSummaryField = response.body.sources.every((source) => { + return source.hasDurationSummaryField; + }); + + expect(allHasSummaryField).to.eql(false); + }); + + it('does not support transaction.duration.summary for transactionMetric 1m when not all documents within the range support it ', async () => { + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/time_range_metadata', + params: { + query: { + start: startLegacy.toISOString(), + end: end.toISOString(), + enableContinuousRollups: true, + enableServiceTransactionMetrics: true, + useSpanName: false, + kuery: '', + }, + }, + }); + + const hasDurationSummaryField = response.body.sources.find( + (source) => + source.documentType === ApmDocumentType.TransactionMetric && + source.rollupInterval === RollupInterval.OneMinute // there is not enough data for 60 minutes in the timerange defined for the tests + )?.hasDurationSummaryField; + + expect(hasDurationSummaryField).to.eql(false); + }); + + it('does not have latency data for synth-java-legacy', async () => { + const res = await getLatencyChartForService({ + serviceName: 'synth-java-legacy', + start, + end: endLegacy, + apmApiClient, + useDurationSummary: true, + }); + + expect(res.body.currentPeriod.latencyTimeseries.map(({ y }) => y)).to.eql([ + null, + null, + null, + null, + null, + null, + ]); + }); + + it('has latency data for synth-java service', async () => { + const res = await getLatencyChartForService({ + serviceName: 'synth-java', + start, + end: endLegacy, + apmApiClient, + useDurationSummary: true, + }); + + expect(res.body.currentPeriod.latencyTimeseries.map(({ y }) => y)).to.eql([ + 1000000, 1000000, 1000000, 1000000, 1000000, 1000000, + ]); + }); + }); + }); +} + +// This will retrieve latency data expecting the `transaction.duration.summary` field to be present +function getLatencyChartForService({ + serviceName, + start, + end, + apmApiClient, + useDurationSummary, +}: { + serviceName: string; + start: moment.Moment; + end: moment.Moment; + apmApiClient: ApmApiClient; + useDurationSummary: boolean; +}) { + return apmApiClient.readUser({ + endpoint: `GET /internal/apm/services/{serviceName}/transactions/charts/latency`, + params: { + path: { serviceName }, + query: { + start: start.toISOString(), + end: end.toISOString(), + environment: 'production', + latencyAggregationType: LatencyAggregationType.avg, + transactionType: 'request', + kuery: '', + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + bucketSizeInSeconds: 60, + useDurationSummary, + }, + }, + }); +} + +function generateTraceDataForService({ + serviceName, + start, + end, + isLegacy, + synthtrace, +}: { + serviceName: string; + start: moment.Moment; + end: moment.Moment; + isLegacy?: boolean; + synthtrace: ApmSynthtraceEsClient; +}) { + const instance = apm + .service({ + name: serviceName, + environment: 'production', + agentName: 'java', + }) + .instance(`instance`); + + const events = timerange(start, end) + .ratePerMinute(6) + .generator((timestamp) => + instance + .transaction({ transactionName: 'GET /order/{id}' }) + .timestamp(timestamp) + .duration(1000) + .success() + ); + + const apmPipeline = (base: Readable) => { + return synthtrace.getDefaultPipeline({ versionOverride: '8.5.0' })(base); + }; + + return synthtrace.index(events, isLegacy ? apmPipeline : undefined); +} diff --git a/x-pack/test/apm_api_integration/tests/time_range_metadata/time_range_metadata.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/time_range_metadata/time_range_metadata.spec.ts similarity index 94% rename from x-pack/test/apm_api_integration/tests/time_range_metadata/time_range_metadata.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/time_range_metadata/time_range_metadata.spec.ts index 6ea90a1b8b1d2..7ec73a692f988 100644 --- a/x-pack/test/apm_api_integration/tests/time_range_metadata/time_range_metadata.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/time_range_metadata/time_range_metadata.spec.ts @@ -11,15 +11,14 @@ import { omit, sortBy } from 'lodash'; import moment, { Moment } from 'moment'; import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { Readable } from 'stream'; import { ToolingLog } from '@kbn/tooling-log'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const es = getService('es'); const log = getService('log'); @@ -55,29 +54,28 @@ export default function ApiTest({ getService }: FtrProviderContext) { }; } - registry.when('Time range metadata without data', { config: 'basic', archives: [] }, () => { - it('handles empty state', async () => { - const response = await getTimeRangeMedata({ - start, - end, - }); + describe('Time range metadata', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + describe('without data', () => { + it('handles empty state', async () => { + const response = await getTimeRangeMedata({ + start, + end, + }); - expect(response.isUsingServiceDestinationMetrics).to.eql(false); - expect(response.sources.filter((source) => source.hasDocs)).to.eql([ - { - documentType: ApmDocumentType.TransactionEvent, - rollupInterval: RollupInterval.None, - hasDocs: true, - hasDurationSummaryField: false, - }, - ]); + expect(response.isUsingServiceDestinationMetrics).to.eql(false); + expect(response.sources.filter((source) => source.hasDocs)).to.eql([ + { + documentType: ApmDocumentType.TransactionEvent, + rollupInterval: RollupInterval.None, + hasDocs: true, + hasDurationSummaryField: false, + }, + ]); + }); }); - }); - registry.when( - 'Time range metadata when generating data with multiple APM server versions', - { config: 'basic', archives: [] }, - () => { + describe('when generating data with multiple APM server versions', () => { describe('data loaded with and without summary field', () => { const withoutSummaryFieldStart = moment('2023-04-28T00:00:00.000Z'); const withoutSummaryFieldEnd = moment(withoutSummaryFieldStart).add(2, 'hours'); @@ -86,6 +84,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const withSummaryFieldEnd = moment(withSummaryFieldStart).add(2, 'hours'); before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await getTransactionEvents({ start: withoutSummaryFieldStart, end: withoutSummaryFieldEnd, @@ -259,15 +258,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); }); - } - ); - - registry.when( - 'Time range metadata when generating data', - { config: 'basic', archives: [] }, - () => { - before(() => { + }); + + describe('when generating data', () => { + before(async () => { const instance = apm.service('my-service', 'production', 'java').instance('instance'); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); return apmSynthtraceEsClient.index( timerange(moment(start).subtract(1, 'day'), end) @@ -620,8 +616,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); }); - } - ); + }); + }); } function getTransactionEvents({ diff --git a/x-pack/test/apm_api_integration/tests/time_range_metadata/many_apm_server_versions.spec.ts b/x-pack/test/apm_api_integration/tests/time_range_metadata/many_apm_server_versions.spec.ts deleted file mode 100644 index 6031b7dd8de5b..0000000000000 --- a/x-pack/test/apm_api_integration/tests/time_range_metadata/many_apm_server_versions.spec.ts +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import expect from '@kbn/expect'; -import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import moment from 'moment'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import { - TRANSACTION_DURATION_HISTOGRAM, - TRANSACTION_DURATION_SUMMARY, -} from '@kbn/apm-plugin/common/es_fields/apm'; -import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; -import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; -import { LatencyAggregationType } from '@kbn/apm-plugin/common/latency_aggregation_types'; -import { Readable } from 'stream'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { ApmApiClient } from '../../common/config'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const synthtrace = getService('apmSynthtraceEsClient'); - const es = getService('es'); - - const baseTime = new Date('2023-10-01T00:00:00.000Z').getTime(); - const startLegacy = moment(baseTime).add(0, 'minutes'); - const start = moment(baseTime).add(5, 'minutes'); - const endLegacy = moment(baseTime).add(10, 'minutes'); - const end = moment(baseTime).add(15, 'minutes'); - - registry.when( - 'Time range metadata when there are multiple APM Server versions', - { config: 'basic', archives: [] }, - () => { - describe('when ingesting traces from APM Server with different versions', () => { - before(async () => { - await generateTraceDataForService({ - serviceName: 'synth-java-legacy', - start: startLegacy, - end: endLegacy, - isLegacy: true, - synthtrace, - }); - - await generateTraceDataForService({ - serviceName: 'synth-java', - start, - end, - isLegacy: false, - synthtrace, - }); - }); - - after(() => { - return synthtrace.clean(); - }); - - it('ingests transaction metrics with transaction.duration.summary', async () => { - const res = await es.search({ - index: 'metrics-apm*', - body: { - query: { - bool: { - filter: [ - { exists: { field: TRANSACTION_DURATION_HISTOGRAM } }, - { exists: { field: TRANSACTION_DURATION_SUMMARY } }, - ], - }, - }, - }, - }); - - // @ts-expect-error - expect(res.hits.total.value).to.be(20); - }); - - it('ingests transaction metrics without transaction.duration.summary', async () => { - const res = await es.search({ - index: 'metrics-apm*', - body: { - query: { - bool: { - filter: [{ exists: { field: TRANSACTION_DURATION_HISTOGRAM } }], - must_not: [{ exists: { field: TRANSACTION_DURATION_SUMMARY } }], - }, - }, - }, - }); - - // @ts-expect-error - expect(res.hits.total.value).to.be(10); - }); - - it('has transaction.duration.summary field for every document type', async () => { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/time_range_metadata', - params: { - query: { - start: endLegacy.toISOString(), - end: end.toISOString(), - enableContinuousRollups: true, - enableServiceTransactionMetrics: true, - useSpanName: false, - kuery: '', - }, - }, - }); - - const allHasSummaryField = response.body.sources - .filter( - (source) => - source.documentType !== ApmDocumentType.TransactionEvent && - source.rollupInterval !== RollupInterval.SixtyMinutes // there is not enough data for 60 minutes - ) - .every((source) => { - return source.hasDurationSummaryField; - }); - - expect(allHasSummaryField).to.eql(true); - }); - - it('does not support transaction.duration.summary when the field is not supported by all APM server versions', async () => { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/time_range_metadata', - params: { - query: { - start: startLegacy.toISOString(), - end: endLegacy.toISOString(), - enableContinuousRollups: true, - enableServiceTransactionMetrics: true, - useSpanName: false, - kuery: '', - }, - }, - }); - - const allHasSummaryField = response.body.sources.every((source) => { - return source.hasDurationSummaryField; - }); - - expect(allHasSummaryField).to.eql(false); - }); - - it('does not support transaction.duration.summary for transactionMetric 1m when not all documents within the range support it ', async () => { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/time_range_metadata', - params: { - query: { - start: startLegacy.toISOString(), - end: end.toISOString(), - enableContinuousRollups: true, - enableServiceTransactionMetrics: true, - useSpanName: false, - kuery: '', - }, - }, - }); - - const hasDurationSummaryField = response.body.sources.find( - (source) => - source.documentType === ApmDocumentType.TransactionMetric && - source.rollupInterval === RollupInterval.OneMinute // there is not enough data for 60 minutes in the timerange defined for the tests - )?.hasDurationSummaryField; - - expect(hasDurationSummaryField).to.eql(false); - }); - - it('does not have latency data for synth-java-legacy', async () => { - const res = await getLatencyChartForService({ - serviceName: 'synth-java-legacy', - start, - end: endLegacy, - apmApiClient, - useDurationSummary: true, - }); - - expect(res.body.currentPeriod.latencyTimeseries.map(({ y }) => y)).to.eql([ - null, - null, - null, - null, - null, - null, - ]); - }); - - it('has latency data for synth-java service', async () => { - const res = await getLatencyChartForService({ - serviceName: 'synth-java', - start, - end: endLegacy, - apmApiClient, - useDurationSummary: true, - }); - - expect(res.body.currentPeriod.latencyTimeseries.map(({ y }) => y)).to.eql([ - 1000000, 1000000, 1000000, 1000000, 1000000, 1000000, - ]); - }); - }); - } - ); -} - -// This will retrieve latency data expecting the `transaction.duration.summary` field to be present -function getLatencyChartForService({ - serviceName, - start, - end, - apmApiClient, - useDurationSummary, -}: { - serviceName: string; - start: moment.Moment; - end: moment.Moment; - apmApiClient: ApmApiClient; - useDurationSummary: boolean; -}) { - return apmApiClient.readUser({ - endpoint: `GET /internal/apm/services/{serviceName}/transactions/charts/latency`, - params: { - path: { serviceName }, - query: { - start: start.toISOString(), - end: end.toISOString(), - environment: 'production', - latencyAggregationType: LatencyAggregationType.avg, - transactionType: 'request', - kuery: '', - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.OneMinute, - bucketSizeInSeconds: 60, - useDurationSummary, - }, - }, - }); -} - -function generateTraceDataForService({ - serviceName, - start, - end, - isLegacy, - synthtrace, -}: { - serviceName: string; - start: moment.Moment; - end: moment.Moment; - isLegacy?: boolean; - synthtrace: ApmSynthtraceEsClient; -}) { - const instance = apm - .service({ - name: serviceName, - environment: 'production', - agentName: 'java', - }) - .instance(`instance`); - - const events = timerange(start, end) - .ratePerMinute(6) - .generator((timestamp) => - instance - .transaction({ transactionName: 'GET /order/{id}' }) - .timestamp(timestamp) - .duration(1000) - .success() - ); - - const apmPipeline = (base: Readable) => { - return synthtrace.getDefaultPipeline({ versionOverride: '8.5.0' })(base); - }; - - return synthtrace.index(events, isLegacy ? apmPipeline : undefined); -} From 8664415750699bad6624b19ba3acab55fa54b9f9 Mon Sep 17 00:00:00 2001 From: Milosz Marcinkowski <38698566+miloszmarcinkowski@users.noreply.github.com> Date: Mon, 18 Nov 2024 18:45:52 +0100 Subject: [PATCH 13/14] Migrate `/test/apm_api_integration/tests/suggestions` to be deployment agnostic api tests (#200556) closes #198992 closes #198993 part of https://github.com/elastic/kibana/issues/193245 ### How to test - Serverless ``` node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts --grep="APM" ``` - Stateful ``` node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts --grep="APM" ``` - [MKI](https://github.com/crespocarlos/kibana/blob/main/x-pack/test_serverless/README.md#run-tests-on-mki) ### Checklist - [x] (OPTIONAL, only if a test has been unskipped) Run flaky test suite - [x] serverless - [x] stateful - [x] MKI --------- Co-authored-by: Sergi Romeu --- .../apis/observability/apm/index.ts | 2 ++ .../apm}/suggestions/generate_data.ts | 0 .../observability/apm/suggestions/index.ts | 14 ++++++++++++++ .../apm}/suggestions/suggestions.spec.ts | 17 ++++++++++------- .../apm}/throughput/dependencies_apis.spec.ts | 18 ++++++++++-------- .../observability/apm/throughput/index.ts | 16 ++++++++++++++++ .../apm}/throughput/service_apis.spec.ts | 18 ++++++++++-------- .../apm}/throughput/service_maps.spec.ts | 19 ++++++++++--------- 8 files changed, 72 insertions(+), 32 deletions(-) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/suggestions/generate_data.ts (100%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/index.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/suggestions/suggestions.spec.ts (94%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/throughput/dependencies_apis.spec.ts (94%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/index.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/throughput/service_apis.spec.ts (92%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/throughput/service_maps.spec.ts (90%) diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts index 640bb90abe711..ab7f9e5736392 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts @@ -36,5 +36,7 @@ export default function apmApiIntegrationTests({ loadTestFile(require.resolve('./diagnostics')); loadTestFile(require.resolve('./service_nodes')); loadTestFile(require.resolve('./span_links')); + loadTestFile(require.resolve('./suggestions')); + loadTestFile(require.resolve('./throughput')); }); } diff --git a/x-pack/test/apm_api_integration/tests/suggestions/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/generate_data.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/suggestions/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/generate_data.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/index.ts new file mode 100644 index 0000000000000..9b2563c093a9d --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/index.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. + */ + +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('Suggestions', () => { + loadTestFile(require.resolve('./suggestions.spec.ts')); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/suggestions/suggestions.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/suggestions.spec.ts similarity index 94% rename from x-pack/test/apm_api_integration/tests/suggestions/suggestions.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/suggestions.spec.ts index d4d1c3b141700..a6e9342885571 100644 --- a/x-pack/test/apm_api_integration/tests/suggestions/suggestions.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/suggestions.spec.ts @@ -11,7 +11,8 @@ import { TRANSACTION_TYPE, } from '@kbn/apm-plugin/common/es_fields/apm'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { generateData } from './generate_data'; const startNumber = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -20,14 +21,16 @@ const endNumber = new Date('2021-01-01T00:05:00.000Z').getTime() - 1; const start = new Date(startNumber).toISOString(); const end = new Date(endNumber).toISOString(); -export default function suggestionsTests({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function suggestionsTests({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + describe('suggestions when data is loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; - // FLAKY: https://github.com/elastic/kibana/issues/177538 - registry.when('suggestions when data is loaded', { config: 'basic', archives: [] }, async () => { before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await generateData({ apmSynthtraceEsClient, start: startNumber, diff --git a/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/dependencies_apis.spec.ts similarity index 94% rename from x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/dependencies_apis.spec.ts index fe591631fafe7..84d293f287b2f 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/dependencies_apis.spec.ts @@ -8,13 +8,13 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { meanBy, sumBy } from 'lodash'; import { DependencyNode, ServiceNode } from '@kbn/apm-plugin/common/connections'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { roundNumber } from '../../utils'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { roundNumber } from '../utils/common'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -93,11 +93,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { let throughputValues: Awaited>; - // FLAKY: https://github.com/elastic/kibana/issues/177536 - registry.when.skip('Dependencies throughput value', { config: 'basic', archives: [] }, () => { + describe('Dependencies throughput value', () => { describe('when data is loaded', () => { const GO_PROD_RATE = 75; const JAVA_PROD_RATE = 25; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { const serviceGoProdInstance = apm .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) @@ -105,6 +106,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const serviceJavaInstance = apm .service({ name: 'synth-java', environment: 'development', agentName: 'java' }) .instance('instance-c'); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await apmSynthtraceEsClient.index([ timerange(start, end) diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/index.ts new file mode 100644 index 0000000000000..e0176b18be783 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/index.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 { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('Throughput', () => { + loadTestFile(require.resolve('./dependencies_apis.spec.ts')); + loadTestFile(require.resolve('./service_apis.spec.ts')); + loadTestFile(require.resolve('./service_maps.spec.ts')); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/service_apis.spec.ts similarity index 92% rename from x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/service_apis.spec.ts index 9d69ce74bf0ea..429d29090a1d2 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/service_apis.spec.ts @@ -11,13 +11,13 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; import { meanBy, sumBy } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { roundNumber } from '../../utils'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { roundNumber } from '../utils/common'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -141,11 +141,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { let throughputMetricValues: Awaited>; let throughputTransactionValues: Awaited>; - // FLAKY: https://github.com/elastic/kibana/issues/177535 - registry.when('Services APIs', { config: 'basic', archives: [] }, () => { + describe('Services APIs', () => { describe('when data is loaded ', () => { const GO_PROD_RATE = 80; const GO_DEV_RATE = 20; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { const serviceGoProdInstance = apm .service({ name: serviceName, environment: 'production', agentName: 'go' }) @@ -153,6 +154,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const serviceGoDevInstance = apm .service({ name: serviceName, environment: 'development', agentName: 'go' }) .instance('instance-b'); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await apmSynthtraceEsClient.index([ timerange(start, end) diff --git a/x-pack/test/apm_api_integration/tests/throughput/service_maps.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/service_maps.spec.ts similarity index 90% rename from x-pack/test/apm_api_integration/tests/throughput/service_maps.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/service_maps.spec.ts index 5ee475344e286..883e81ea24524 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/service_maps.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/service_maps.spec.ts @@ -9,13 +9,13 @@ import expect from '@kbn/expect'; import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { roundNumber } from '../../utils'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { roundNumber } from '../utils/common'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -83,10 +83,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { let throughputMetricValues: Awaited>; let throughputTransactionValues: Awaited>; - registry.when('Service Maps APIs', { config: 'trial', archives: [] }, () => { + describe('Service Maps APIs', () => { describe('when data is loaded ', () => { const GO_PROD_RATE = 80; const GO_DEV_RATE = 20; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { const serviceGoProdInstance = apm .service({ name: serviceName, environment: 'production', agentName: 'go' }) @@ -94,6 +96,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const serviceGoDevInstance = apm .service({ name: serviceName, environment: 'development', agentName: 'go' }) .instance('instance-b'); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await apmSynthtraceEsClient.index([ timerange(start, end) @@ -119,7 +122,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { after(() => apmSynthtraceEsClient.clean()); - // FLAKY: https://github.com/elastic/kibana/issues/176984 describe('compare throughput value between service inventory and service maps', () => { before(async () => { [throughputTransactionValues, throughputMetricValues] = await Promise.all([ @@ -136,7 +138,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/176987 describe('when calling service maps transactions stats api', () => { let serviceMapsNodeThroughput: number | null | undefined; before(async () => { From 598076e215bda81ba4f35166be3cf1b8edc9026a Mon Sep 17 00:00:00 2001 From: Tre Date: Mon, 18 Nov 2024 17:50:21 +0000 Subject: [PATCH 14/14] [Ownership] Assign test files to presentation team (#200209) ## Summary Assign test files to presentation team Contributes to: #192979 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine --- .github/CODEOWNERS | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e51fceb9e2e0f..760a4e0ba446e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1368,7 +1368,23 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai ### END Observability Plugins # Presentation -/x-pack/test/disable_ems @elastic/kibana-presentation +/test/interpreter_functional/snapshots @elastic/kibana-presentation # Assigned per https://github.com/elastic/kibana/pull/54342 +/test/functional/services/inspector.ts @elastic/kibana-presentation +/x-pack/test/functional/services/canvas_element.ts @elastic/kibana-presentation +/x-pack/test/functional/page_objects/canvas_page.ts @elastic/kibana-presentation +/x-pack/test/accessibility/apps/group3/canvas.ts @elastic/kibana-presentation +/x-pack/test/upgrade/apps/canvas @elastic/kibana-presentation +/x-pack/test/upgrade/apps/dashboard @elastic/kibana-presentation +/test/functional/screenshots/baseline/tsvb_dashboard.png @elastic/kibana-presentation +/test/functional/screenshots/baseline/dashboard_*.png @elastic/kibana-presentation +/test/functional/screenshots/baseline/area_chart.png @elastic/kibana-presentation +/x-pack/test/disable_ems @elastic/kibana-presentation # Assigned per https://github.com/elastic/kibana/pull/165986 +/x-pack/test/functional/fixtures/kbn_archiver/dashboard* @elastic/kibana-presentation +/test/functional/page_objects/dashboard_page* @elastic/kibana-presentation +/test/functional/firefox/dashboard.config.ts @elastic/kibana-presentation # Assigned per: https://github.com/elastic/kibana/issues/15023 +/test/functional/fixtures/es_archiver/dashboard @elastic/kibana-presentation # Assigned per: https://github.com/elastic/kibana/issues/15023 +/test/accessibility/apps/dashboard.ts @elastic/kibana-presentation +/test/accessibility/apps/filter_panel.ts @elastic/kibana-presentation /x-pack/test/functional/apps/dashboard @elastic/kibana-presentation /x-pack/test/accessibility/apps/group3/maps.ts @elastic/kibana-presentation /x-pack/test/accessibility/apps/group1/dashboard_panel_options.ts @elastic/kibana-presentation