diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c2450338f3e45..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 @@ -1382,6 +1398,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 +1448,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 @@ -1584,6 +1602,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 @@ -1640,6 +1660,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 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()); 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/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() { 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/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 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) { 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/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']); }); 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/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/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..49566da1f6b18 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", @@ -26206,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é", @@ -26235,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 fffed2d59a462..f0cf7c38ac66b 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": "ベクトルをコピー", @@ -26178,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": "エンティティ名", @@ -26207,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 4d8de21af735a..c69512018d0f4 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": "复制向量", @@ -25704,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": "实体名称", @@ -25733,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。", 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..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 @@ -32,8 +32,11 @@ 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')); + 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 () => { 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); -} 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/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 }); } 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(); }); }); }); 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*