From 57891ff3534781bd558b680bc511427646c774c2 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 19 Jun 2024 14:17:26 -0300 Subject: [PATCH] [Discover] Add support for contextual awareness functional tests (#185905) ## Summary This PR adds functional test support for the Discover contextual awareness framework, and adds tests for the initial `getCellRenderers` extension point using example profiles. To support this, this PR introduces a new YAML setting called `discover.experimental.enabledProfiles` which can be used to selectively enable profiles both for functional testing and demoing WIP profiles that aren't yet ready for GA. Example usage: ```yml discover.experimental.enabledProfiles: ['example-root-profile', 'example-data-source-profile', 'example-document-profile'] ``` Flaky test runs: - Stateful x50: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6304 - Serverless Observability x50: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6305 - Serverless Search x50: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6306 - Serverless Security x50: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6307 Resolves #184699. ### Checklist - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .buildkite/ftr_configs.yml | 4 + src/plugins/discover/common/config.ts | 3 +- .../context_awareness/__mocks__/index.ts | 21 +-- .../example_data_source_profile/index.ts | 9 ++ .../example_data_source_profile/profile.tsx | 83 ++++++++++++ .../example_document_profile/index.ts | 9 ++ .../example_document_profile/profile.ts | 32 +++++ .../example_root_pofile/index.ts | 9 ++ .../example_root_pofile/profile.tsx | 42 ++++++ .../profile_providers/index.ts | 9 ++ .../log_document_profile/profile.ts | 2 +- .../logs_data_source_profile/profile.ts | 2 +- .../profile_provider_services.ts | 0 .../register_profile_providers.test.ts | 117 +++++++++++++++++ .../register_profile_providers.ts | 85 ++++++++++++ .../context_awareness/profile_service.ts | 45 +++---- .../profiles/data_source_profile.ts | 12 +- .../profiles/document_profile.ts | 12 +- .../profiles/example_profiles.tsx | 123 ------------------ .../profiles/root_profile.ts | 12 +- src/plugins/discover/public/plugin.tsx | 63 ++++----- .../context_awareness/_data_source_profile.ts | 97 ++++++++++++++ .../context_awareness/_root_profile.ts | 52 ++++++++ .../apps/discover/context_awareness/config.ts | 26 ++++ .../apps/discover/context_awareness/index.ts | 39 ++++++ .../discover/context_awareness/data.json.gz | Bin 0 -> 351 bytes .../discover/context_awareness/mappings.json | 76 +++++++++++ .../discover/context_awareness.json | 49 +++++++ .../test_suites/core_plugins/rendering.ts | 1 + .../context_awareness/_data_source_profile.ts | 88 +++++++++++++ .../context_awareness/_root_profile.ts | 47 +++++++ .../discover/context_awareness/index.ts | 40 ++++++ .../observability/config.context_awareness.ts | 22 ++++ .../search/config.context_awareness.ts | 22 ++++ .../security/config.context_awareness.ts | 22 ++++ 35 files changed, 1072 insertions(+), 203 deletions(-) create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/example_data_source_profile/index.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/example_data_source_profile/profile.tsx create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/example_document_profile/index.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/example_document_profile/profile.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/example_root_pofile/index.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/example_root_pofile/profile.tsx create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/index.ts rename src/plugins/discover/public/context_awareness/{profiles => profile_providers}/profile_provider_services.ts (100%) create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.test.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.ts delete mode 100644 src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx create mode 100644 test/functional/apps/discover/context_awareness/_data_source_profile.ts create mode 100644 test/functional/apps/discover/context_awareness/_root_profile.ts create mode 100644 test/functional/apps/discover/context_awareness/config.ts create mode 100644 test/functional/apps/discover/context_awareness/index.ts create mode 100644 test/functional/fixtures/es_archiver/discover/context_awareness/data.json.gz create mode 100644 test/functional/fixtures/es_archiver/discover/context_awareness/mappings.json create mode 100644 test/functional/fixtures/kbn_archiver/discover/context_awareness.json create mode 100644 x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/_data_source_profile.ts create mode 100644 x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/_root_profile.ts create mode 100644 x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/index.ts create mode 100644 x-pack/test_serverless/functional/test_suites/observability/config.context_awareness.ts create mode 100644 x-pack/test_serverless/functional/test_suites/search/config.context_awareness.ts create mode 100644 x-pack/test_serverless/functional/test_suites/security/config.context_awareness.ts diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index 55f23715e3468..8e558458feb04 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -127,6 +127,7 @@ enabled: - test/functional/apps/discover/group6/config.ts - test/functional/apps/discover/group7/config.ts - test/functional/apps/discover/group8/config.ts + - test/functional/apps/discover/context_awareness/config.ts - test/functional/apps/getting_started/config.ts - test/functional/apps/home/config.ts - test/functional/apps/kibana_overview/config.ts @@ -426,6 +427,7 @@ enabled: - x-pack/test_serverless/functional/test_suites/observability/config.ts - x-pack/test_serverless/functional/test_suites/observability/config.examples.ts - x-pack/test_serverless/functional/test_suites/observability/config.saved_objects_management.ts + - x-pack/test_serverless/functional/test_suites/observability/config.context_awareness.ts - x-pack/test_serverless/functional/test_suites/observability/common_configs/config.group1.ts - x-pack/test_serverless/functional/test_suites/observability/common_configs/config.group2.ts - x-pack/test_serverless/functional/test_suites/observability/common_configs/config.group3.ts @@ -438,6 +440,7 @@ enabled: - x-pack/test_serverless/functional/test_suites/search/config.examples.ts - x-pack/test_serverless/functional/test_suites/search/config.screenshots.ts - x-pack/test_serverless/functional/test_suites/search/config.saved_objects_management.ts + - x-pack/test_serverless/functional/test_suites/search/config.context_awareness.ts - x-pack/test_serverless/functional/test_suites/search/common_configs/config.group1.ts - x-pack/test_serverless/functional/test_suites/search/common_configs/config.group2.ts - x-pack/test_serverless/functional/test_suites/search/common_configs/config.group3.ts @@ -449,6 +452,7 @@ enabled: - x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.basic.ts - x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.essentials.ts - x-pack/test_serverless/functional/test_suites/security/config.saved_objects_management.ts + - x-pack/test_serverless/functional/test_suites/security/config.context_awareness.ts - x-pack/test_serverless/functional/test_suites/security/common_configs/config.group1.ts - x-pack/test_serverless/functional/test_suites/security/common_configs/config.group2.ts - x-pack/test_serverless/functional/test_suites/security/common_configs/config.group3.ts diff --git a/src/plugins/discover/common/config.ts b/src/plugins/discover/common/config.ts index f3afc0ebf08be..272f81b77d5a8 100644 --- a/src/plugins/discover/common/config.ts +++ b/src/plugins/discover/common/config.ts @@ -13,9 +13,10 @@ export const configSchema = schema.object({ experimental: schema.maybe( schema.object({ ruleFormV2Enabled: schema.maybe(schema.boolean({ defaultValue: false })), + enabledProfiles: schema.maybe(schema.arrayOf(schema.string(), { defaultValue: [] })), }) ), }); export type ConfigSchema = TypeOf; -export type ExperimentalFeatures = ConfigSchema['experimental']; +export type ExperimentalFeatures = NonNullable; diff --git a/src/plugins/discover/public/context_awareness/__mocks__/index.ts b/src/plugins/discover/public/context_awareness/__mocks__/index.ts index c13dd80676148..fbdb13a2e5648 100644 --- a/src/plugins/discover/public/context_awareness/__mocks__/index.ts +++ b/src/plugins/discover/public/context_awareness/__mocks__/index.ts @@ -19,10 +19,12 @@ import { RootProfileService, SolutionType, } from '../profiles'; -import { createProfileProviderServices } from '../profiles/profile_provider_services'; +import { createProfileProviderServices } from '../profile_providers/profile_provider_services'; import { ProfilesManager } from '../profiles_manager'; -export const createContextAwarenessMocks = () => { +export const createContextAwarenessMocks = ({ + shouldRegisterProviders = true, +}: { shouldRegisterProviders?: boolean } = {}) => { const rootProfileProviderMock: RootProfileProvider = { profileId: 'root-profile', profile: { @@ -92,15 +94,15 @@ export const createContextAwarenessMocks = () => { const records = getDataTableRecords(dataViewWithTimefieldMock); const contextRecordMock = records[0]; const contextRecordMock2 = records[1]; - const rootProfileServiceMock = new RootProfileService(); - rootProfileServiceMock.registerProvider(rootProfileProviderMock); - const dataSourceProfileServiceMock = new DataSourceProfileService(); - dataSourceProfileServiceMock.registerProvider(dataSourceProfileProviderMock); - const documentProfileServiceMock = new DocumentProfileService(); - documentProfileServiceMock.registerProvider(documentProfileProviderMock); + + if (shouldRegisterProviders) { + rootProfileServiceMock.registerProvider(rootProfileProviderMock); + dataSourceProfileServiceMock.registerProvider(dataSourceProfileProviderMock); + documentProfileServiceMock.registerProvider(documentProfileProviderMock); + } const profilesManagerMock = new ProfilesManager( rootProfileServiceMock, @@ -114,6 +116,9 @@ export const createContextAwarenessMocks = () => { rootProfileProviderMock, dataSourceProfileProviderMock, documentProfileProviderMock, + rootProfileServiceMock, + dataSourceProfileServiceMock, + documentProfileServiceMock, contextRecordMock, contextRecordMock2, profilesManagerMock, diff --git a/src/plugins/discover/public/context_awareness/profile_providers/example_data_source_profile/index.ts b/src/plugins/discover/public/context_awareness/profile_providers/example_data_source_profile/index.ts new file mode 100644 index 0000000000000..032da55d4f6d0 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/example_data_source_profile/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { exampleDataSourceProfileProvider } from './profile'; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/example_data_source_profile/profile.tsx b/src/plugins/discover/public/context_awareness/profile_providers/example_data_source_profile/profile.tsx new file mode 100644 index 0000000000000..080c2ffed222b --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/example_data_source_profile/profile.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EuiBadge } from '@elastic/eui'; +import type { DataTableRecord } from '@kbn/discover-utils'; +import { isOfAggregateQueryType } from '@kbn/es-query'; +import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { capitalize } from 'lodash'; +import React from 'react'; +import { DataSourceType, isDataSourceType } from '../../../../common/data_sources'; +import { DataSourceCategory, DataSourceProfileProvider } from '../../profiles'; + +export const exampleDataSourceProfileProvider: DataSourceProfileProvider = { + profileId: 'example-data-source-profile', + profile: { + getCellRenderers: (prev) => () => ({ + ...prev(), + 'log.level': (props) => { + const level = getFieldValue(props.row, 'log.level'); + + if (!level) { + return ( + + (None) + + ); + } + + const levelMap: Record = { + info: 'primary', + debug: 'default', + error: 'danger', + }; + + return ( + + {capitalize(level)} + + ); + }, + }), + }, + resolve: (params) => { + let indexPattern: string | undefined; + + if (isDataSourceType(params.dataSource, DataSourceType.Esql)) { + if (!isOfAggregateQueryType(params.query)) { + return { isMatch: false }; + } + + indexPattern = getIndexPatternFromESQLQuery(params.query.esql); + } else if (isDataSourceType(params.dataSource, DataSourceType.DataView) && params.dataView) { + indexPattern = params.dataView.getIndexPattern(); + } + + if (indexPattern !== 'my-example-logs') { + return { isMatch: false }; + } + + return { + isMatch: true, + context: { category: DataSourceCategory.Logs }, + }; + }, +}; + +const getFieldValue = (record: DataTableRecord, field: string) => { + const value = record.flattened[field]; + return Array.isArray(value) ? value[0] : value; +}; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/example_document_profile/index.ts b/src/plugins/discover/public/context_awareness/profile_providers/example_document_profile/index.ts new file mode 100644 index 0000000000000..dab5364d8af3e --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/example_document_profile/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { exampleDocumentProfileProvider } from './profile'; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/example_document_profile/profile.ts b/src/plugins/discover/public/context_awareness/profile_providers/example_document_profile/profile.ts new file mode 100644 index 0000000000000..303efa103e327 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/example_document_profile/profile.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { DataTableRecord } from '@kbn/discover-utils'; +import { DocumentProfileProvider, DocumentType } from '../../profiles'; + +export const exampleDocumentProfileProvider: DocumentProfileProvider = { + profileId: 'example-document-profile', + profile: {}, + resolve: (params) => { + if (getFieldValue(params.record, 'data_stream.type') !== 'logs') { + return { isMatch: false }; + } + + return { + isMatch: true, + context: { + type: DocumentType.Log, + }, + }; + }, +}; + +const getFieldValue = (record: DataTableRecord, field: string) => { + const value = record.flattened[field]; + return Array.isArray(value) ? value[0] : value; +}; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/example_root_pofile/index.ts b/src/plugins/discover/public/context_awareness/profile_providers/example_root_pofile/index.ts new file mode 100644 index 0000000000000..53f4cdc955db4 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/example_root_pofile/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { exampleRootProfileProvider } from './profile'; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/example_root_pofile/profile.tsx b/src/plugins/discover/public/context_awareness/profile_providers/example_root_pofile/profile.tsx new file mode 100644 index 0000000000000..389059c518217 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/example_root_pofile/profile.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EuiBadge } from '@elastic/eui'; +import type { DataTableRecord } from '@kbn/discover-utils'; +import React from 'react'; +import { RootProfileProvider, SolutionType } from '../../profiles'; + +export const exampleRootProfileProvider: RootProfileProvider = { + profileId: 'example-root-profile', + profile: { + getCellRenderers: (prev) => () => ({ + ...prev(), + '@timestamp': (props) => { + const timestamp = getFieldValue(props.row, '@timestamp'); + + return ( + + {timestamp} + + ); + }, + }), + }, + resolve: (params) => { + if (params.solutionNavId != null) { + return { isMatch: false }; + } + + return { isMatch: true, context: { solutionType: SolutionType.Default } }; + }, +}; + +const getFieldValue = (record: DataTableRecord, field: string) => { + const value = record.flattened[field]; + return Array.isArray(value) ? value[0] : value; +}; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/index.ts b/src/plugins/discover/public/context_awareness/profile_providers/index.ts new file mode 100644 index 0000000000000..1a5979f5a37f8 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { registerProfileProviders } from './register_profile_providers'; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/log_document_profile/profile.ts b/src/plugins/discover/public/context_awareness/profile_providers/log_document_profile/profile.ts index 7c1da0cef45cd..2d03c9e35398a 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/log_document_profile/profile.ts +++ b/src/plugins/discover/public/context_awareness/profile_providers/log_document_profile/profile.ts @@ -8,7 +8,7 @@ import { DataTableRecord } from '@kbn/discover-utils'; import { DocumentProfileProvider, DocumentType } from '../../profiles'; -import { ProfileProviderServices } from '../../profiles/profile_provider_services'; +import { ProfileProviderServices } from '../profile_provider_services'; export const createLogDocumentProfileProvider = ( services: ProfileProviderServices diff --git a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/profile.ts b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/profile.ts index 1ce42c6d1ffd1..e4a12aa9e4baf 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/profile.ts +++ b/src/plugins/discover/public/context_awareness/profile_providers/logs_data_source_profile/profile.ts @@ -14,7 +14,7 @@ import { DataSourceProfileProvider, DataSourceProfileProviderParams, } from '../../profiles'; -import { ProfileProviderServices } from '../../profiles/profile_provider_services'; +import { ProfileProviderServices } from '../profile_provider_services'; export const createLogsDataSourceProfileProvider = ( services: ProfileProviderServices diff --git a/src/plugins/discover/public/context_awareness/profiles/profile_provider_services.ts b/src/plugins/discover/public/context_awareness/profile_providers/profile_provider_services.ts similarity index 100% rename from src/plugins/discover/public/context_awareness/profiles/profile_provider_services.ts rename to src/plugins/discover/public/context_awareness/profile_providers/profile_provider_services.ts diff --git a/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.test.ts b/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.test.ts new file mode 100644 index 0000000000000..c58726ecc1c11 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.test.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { createEsqlDataSource } from '../../../common/data_sources'; +import { createContextAwarenessMocks } from '../__mocks__'; +import { exampleDataSourceProfileProvider } from './example_data_source_profile'; +import { exampleDocumentProfileProvider } from './example_document_profile'; +import { exampleRootProfileProvider } from './example_root_pofile'; +import { + registerEnabledProfileProviders, + registerProfileProviders, +} from './register_profile_providers'; + +describe('registerEnabledProfileProviders', () => { + it('should register enabled profile providers', async () => { + const { rootProfileServiceMock, rootProfileProviderMock } = createContextAwarenessMocks({ + shouldRegisterProviders: false, + }); + registerEnabledProfileProviders({ + profileService: rootProfileServiceMock, + availableProviders: [rootProfileProviderMock], + enabledProfileIds: ['root-profile'], + }); + const context = await rootProfileServiceMock.resolve({ solutionNavId: null }); + expect(rootProfileServiceMock.getProfile(context)).toBe(rootProfileProviderMock.profile); + }); + + it('should not register disabled profile providers', async () => { + const { rootProfileServiceMock, rootProfileProviderMock } = createContextAwarenessMocks({ + shouldRegisterProviders: false, + }); + registerEnabledProfileProviders({ + profileService: rootProfileServiceMock, + availableProviders: [rootProfileProviderMock], + enabledProfileIds: [], + }); + const context = await rootProfileServiceMock.resolve({ solutionNavId: null }); + expect(rootProfileServiceMock.getProfile(context)).not.toBe(rootProfileProviderMock.profile); + }); +}); + +describe('registerProfileProviders', () => { + it('should register enabled experimental profile providers', async () => { + const { rootProfileServiceMock, dataSourceProfileServiceMock, documentProfileServiceMock } = + createContextAwarenessMocks({ + shouldRegisterProviders: false, + }); + registerProfileProviders({ + rootProfileService: rootProfileServiceMock, + dataSourceProfileService: dataSourceProfileServiceMock, + documentProfileService: documentProfileServiceMock, + experimentalProfileIds: [ + exampleRootProfileProvider.profileId, + exampleDataSourceProfileProvider.profileId, + exampleDocumentProfileProvider.profileId, + ], + }); + const rootContext = await rootProfileServiceMock.resolve({ solutionNavId: null }); + const dataSourceContext = await dataSourceProfileServiceMock.resolve({ + dataSource: createEsqlDataSource(), + query: { esql: 'from my-example-logs' }, + }); + const documentContext = documentProfileServiceMock.resolve({ + record: { + id: 'test', + flattened: { 'data_stream.type': 'logs' }, + raw: {}, + }, + }); + expect(rootProfileServiceMock.getProfile(rootContext)).toBe(exampleRootProfileProvider.profile); + expect(dataSourceProfileServiceMock.getProfile(dataSourceContext)).toBe( + exampleDataSourceProfileProvider.profile + ); + expect(documentProfileServiceMock.getProfile(documentContext)).toBe( + exampleDocumentProfileProvider.profile + ); + }); + + it('should not register disabled experimental profile providers', async () => { + const { rootProfileServiceMock, dataSourceProfileServiceMock, documentProfileServiceMock } = + createContextAwarenessMocks({ + shouldRegisterProviders: false, + }); + registerProfileProviders({ + rootProfileService: rootProfileServiceMock, + dataSourceProfileService: dataSourceProfileServiceMock, + documentProfileService: documentProfileServiceMock, + experimentalProfileIds: [], + }); + const rootContext = await rootProfileServiceMock.resolve({ solutionNavId: null }); + const dataSourceContext = await dataSourceProfileServiceMock.resolve({ + dataSource: createEsqlDataSource(), + query: { esql: 'from my-example-logs' }, + }); + const documentContext = documentProfileServiceMock.resolve({ + record: { + id: 'test', + flattened: { 'data_stream.type': 'logs' }, + raw: {}, + }, + }); + expect(rootProfileServiceMock.getProfile(rootContext)).not.toBe( + exampleRootProfileProvider.profile + ); + expect(dataSourceProfileServiceMock.getProfile(dataSourceContext)).not.toBe( + exampleDataSourceProfileProvider.profile + ); + expect(documentProfileServiceMock.getProfile(documentContext)).not.toBe( + exampleDocumentProfileProvider.profile + ); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.ts b/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.ts new file mode 100644 index 0000000000000..1f4f2fbb7d931 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { uniq } from 'lodash'; +import type { + DataSourceProfileService, + DocumentProfileService, + RootProfileService, +} from '../profiles'; +import type { BaseProfileProvider, BaseProfileService } from '../profile_service'; +import { exampleDataSourceProfileProvider } from './example_data_source_profile'; +import { exampleDocumentProfileProvider } from './example_document_profile'; +import { exampleRootProfileProvider } from './example_root_pofile'; +import { createLogsDataSourceProfileProvider } from './logs_data_source_profile'; +import { createLogDocumentProfileProvider } from './log_document_profile'; +import { createProfileProviderServices } from './profile_provider_services'; + +export const registerProfileProviders = ({ + rootProfileService, + dataSourceProfileService, + documentProfileService, + experimentalProfileIds, +}: { + rootProfileService: RootProfileService; + dataSourceProfileService: DataSourceProfileService; + documentProfileService: DocumentProfileService; + experimentalProfileIds: string[]; +}) => { + const providerServices = createProfileProviderServices(); + const logsDataSourceProfileProvider = createLogsDataSourceProfileProvider(providerServices); + const logsDocumentProfileProvider = createLogDocumentProfileProvider(providerServices); + const rootProfileProviders = [exampleRootProfileProvider]; + const dataSourceProfileProviders = [ + exampleDataSourceProfileProvider, + logsDataSourceProfileProvider, + ]; + const documentProfileProviders = [exampleDocumentProfileProvider, logsDocumentProfileProvider]; + const enabledProfileIds = uniq([ + logsDataSourceProfileProvider.profileId, + logsDocumentProfileProvider.profileId, + ...experimentalProfileIds, + ]); + + registerEnabledProfileProviders({ + profileService: rootProfileService, + availableProviders: rootProfileProviders, + enabledProfileIds, + }); + + registerEnabledProfileProviders({ + profileService: dataSourceProfileService, + availableProviders: dataSourceProfileProviders, + enabledProfileIds, + }); + + registerEnabledProfileProviders({ + profileService: documentProfileService, + availableProviders: documentProfileProviders, + enabledProfileIds, + }); +}; + +export const registerEnabledProfileProviders = < + TProvider extends BaseProfileProvider<{}>, + TService extends BaseProfileService +>({ + profileService, + availableProviders, + enabledProfileIds, +}: { + profileService: TService; + availableProviders: TProvider[]; + enabledProfileIds: string[]; +}) => { + for (const profile of availableProviders) { + if (enabledProfileIds.includes(profile.profileId)) { + profileService.registerProvider(profile); + } + } +}; diff --git a/src/plugins/discover/public/context_awareness/profile_service.ts b/src/plugins/discover/public/context_awareness/profile_service.ts index 2b43595761d19..efc143e222414 100644 --- a/src/plugins/discover/public/context_awareness/profile_service.ts +++ b/src/plugins/discover/public/context_awareness/profile_service.ts @@ -15,38 +15,33 @@ export type ResolveProfileResult = | { isMatch: true; context: TContext } | { isMatch: false }; -export type ProfileProviderMode = 'sync' | 'async'; +export type ContextWithProfileId = TContext & { profileId: string }; -export interface ProfileProvider< - TProfile extends PartialProfile, - TParams, - TContext, - TMode extends ProfileProviderMode -> { +export interface BaseProfileProvider { profileId: string; profile: ComposableProfile; +} + +export interface ProfileProvider + extends BaseProfileProvider { + resolve: (params: TParams) => ResolveProfileResult; +} + +export interface AsyncProfileProvider + extends BaseProfileProvider { resolve: ( params: TParams - ) => TMode extends 'sync' - ? ResolveProfileResult - : ResolveProfileResult | Promise>; + ) => ResolveProfileResult | Promise>; } -export type ContextWithProfileId = TContext & { profileId: string }; - const EMPTY_PROFILE = {}; -abstract class BaseProfileService< - TProfile extends PartialProfile, - TParams, - TContext, - TMode extends ProfileProviderMode -> { - protected readonly providers: Array> = []; +export abstract class BaseProfileService, TContext> { + protected readonly providers: TProvider[] = []; protected constructor(public readonly defaultContext: ContextWithProfileId) {} - public registerProvider(provider: ProfileProvider) { + public registerProvider(provider: TProvider) { this.providers.push(provider); } @@ -54,19 +49,13 @@ abstract class BaseProfileService< const provider = this.providers.find((current) => current.profileId === context.profileId); return provider?.profile ?? EMPTY_PROFILE; } - - public abstract resolve( - params: TParams - ): TMode extends 'sync' - ? ContextWithProfileId - : Promise>; } export class ProfileService< TProfile extends PartialProfile, TParams, TContext -> extends BaseProfileService { +> extends BaseProfileService, TContext> { public resolve(params: TParams) { for (const provider of this.providers) { const result = provider.resolve(params); @@ -87,7 +76,7 @@ export class AsyncProfileService< TProfile extends PartialProfile, TParams, TContext -> extends BaseProfileService { +> extends BaseProfileService, TContext> { public async resolve(params: TParams) { for (const provider of this.providers) { const result = await provider.resolve(params); diff --git a/src/plugins/discover/public/context_awareness/profiles/data_source_profile.ts b/src/plugins/discover/public/context_awareness/profiles/data_source_profile.ts index f616fef913259..1dbb3c20d8b51 100644 --- a/src/plugins/discover/public/context_awareness/profiles/data_source_profile.ts +++ b/src/plugins/discover/public/context_awareness/profiles/data_source_profile.ts @@ -9,7 +9,7 @@ import type { DataView } from '@kbn/data-views-plugin/common'; import type { AggregateQuery, Query } from '@kbn/es-query'; import type { DiscoverDataSource } from '../../../common/data_sources'; -import { AsyncProfileService } from '../profile_service'; +import { AsyncProfileProvider, AsyncProfileService } from '../profile_service'; import { Profile } from '../types'; export enum DataSourceCategory { @@ -17,6 +17,8 @@ export enum DataSourceCategory { Default = 'default', } +export type DataSourceProfile = Profile; + export interface DataSourceProfileProviderParams { dataSource?: DiscoverDataSource; dataView?: DataView; @@ -27,7 +29,11 @@ export interface DataSourceContext { category: DataSourceCategory; } -export type DataSourceProfile = Profile; +export type DataSourceProfileProvider = AsyncProfileProvider< + DataSourceProfile, + DataSourceProfileProviderParams, + DataSourceContext +>; export class DataSourceProfileService extends AsyncProfileService< DataSourceProfile, @@ -41,5 +47,3 @@ export class DataSourceProfileService extends AsyncProfileService< }); } } - -export type DataSourceProfileProvider = Parameters[0]; diff --git a/src/plugins/discover/public/context_awareness/profiles/document_profile.ts b/src/plugins/discover/public/context_awareness/profiles/document_profile.ts index 70b134da452e4..c6fd19b8f2014 100644 --- a/src/plugins/discover/public/context_awareness/profiles/document_profile.ts +++ b/src/plugins/discover/public/context_awareness/profiles/document_profile.ts @@ -8,13 +8,15 @@ import type { DataTableRecord } from '@kbn/discover-utils'; import type { Profile } from '../types'; -import { ProfileService } from '../profile_service'; +import { ProfileProvider, ProfileService } from '../profile_service'; export enum DocumentType { Log = 'log', Default = 'default', } +export type DocumentProfile = Omit; + export interface DocumentProfileProviderParams { record: DataTableRecord; } @@ -23,7 +25,11 @@ export interface DocumentContext { type: DocumentType; } -export type DocumentProfile = Omit; +export type DocumentProfileProvider = ProfileProvider< + DocumentProfile, + DocumentProfileProviderParams, + DocumentContext +>; export class DocumentProfileService extends ProfileService< DocumentProfile, @@ -37,5 +43,3 @@ export class DocumentProfileService extends ProfileService< }); } } - -export type DocumentProfileProvider = Parameters[0]; diff --git a/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx deleted file mode 100644 index 3835337b25304..0000000000000 --- a/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { EuiBadge } from '@elastic/eui'; -import { - DataTableRecord, - getMessageFieldWithFallbacks, - LogDocumentOverview, -} from '@kbn/discover-utils'; -import { isOfAggregateQueryType } from '@kbn/es-query'; -import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; -import { euiThemeVars } from '@kbn/ui-theme'; -import { capitalize } from 'lodash'; -import React from 'react'; -import { DataSourceType, isDataSourceType } from '../../../common/data_sources'; -import { DataSourceCategory, DataSourceProfileProvider } from './data_source_profile'; -import { DocumentProfileProvider, DocumentType } from './document_profile'; -import { RootProfileProvider, SolutionType } from './root_profile'; - -export const o11yRootProfileProvider: RootProfileProvider = { - profileId: 'o11y-root-profile', - profile: {}, - resolve: (params) => { - if (params.solutionNavId === 'oblt') { - return { - isMatch: true, - context: { - solutionType: SolutionType.Observability, - }, - }; - } - - return { isMatch: false }; - }, -}; - -export const logsDataSourceProfileProvider: DataSourceProfileProvider = { - profileId: 'logs-data-source-profile', - profile: { - getCellRenderers: (prev) => () => ({ - ...prev(), - '@timestamp': (props) => { - const timestamp = getFieldValue(props.row, '@timestamp'); - return ( - - {timestamp} - - ); - }, - 'log.level': (props) => { - const level = getFieldValue(props.row, 'log.level'); - if (!level) { - return (None); - } - const levelMap: Record = { - info: 'primary', - debug: 'default', - error: 'danger', - }; - return ( - - {capitalize(level)} - - ); - }, - message: (props) => { - const { value } = getMessageFieldWithFallbacks( - props.row.flattened as unknown as LogDocumentOverview - ); - return value || (None); - }, - }), - }, - resolve: (params) => { - let indices: string[] = []; - - if (isDataSourceType(params.dataSource, DataSourceType.Esql)) { - if (!isOfAggregateQueryType(params.query)) { - return { isMatch: false }; - } - - indices = getIndexPatternFromESQLQuery(params.query.esql).split(','); - } else if (isDataSourceType(params.dataSource, DataSourceType.DataView) && params.dataView) { - indices = params.dataView.getIndexPattern().split(','); - } - - if (indices.every((index) => index.startsWith('logs-'))) { - return { - isMatch: true, - context: { category: DataSourceCategory.Logs }, - }; - } - - return { isMatch: false }; - }, -}; - -export const logDocumentProfileProvider: DocumentProfileProvider = { - profileId: 'log-document-profile', - profile: {}, - resolve: (params) => { - if (getFieldValue(params.record, 'data_stream.type') === 'logs') { - return { - isMatch: true, - context: { - type: DocumentType.Log, - }, - }; - } - - return { isMatch: false }; - }, -}; - -const getFieldValue = (record: DataTableRecord, field: string) => { - const value = record.flattened[field]; - return Array.isArray(value) ? value[0] : value; -}; diff --git a/src/plugins/discover/public/context_awareness/profiles/root_profile.ts b/src/plugins/discover/public/context_awareness/profiles/root_profile.ts index 42497fe680c5c..77bf9d9d63b0b 100644 --- a/src/plugins/discover/public/context_awareness/profiles/root_profile.ts +++ b/src/plugins/discover/public/context_awareness/profiles/root_profile.ts @@ -7,7 +7,7 @@ */ import type { Profile } from '../types'; -import { AsyncProfileService } from '../profile_service'; +import { AsyncProfileProvider, AsyncProfileService } from '../profile_service'; export enum SolutionType { Observability = 'oblt', @@ -16,6 +16,8 @@ export enum SolutionType { Default = 'default', } +export type RootProfile = Profile; + export interface RootProfileProviderParams { solutionNavId?: string | null; } @@ -24,7 +26,11 @@ export interface RootContext { solutionType: SolutionType; } -export type RootProfile = Profile; +export type RootProfileProvider = AsyncProfileProvider< + RootProfile, + RootProfileProviderParams, + RootContext +>; export class RootProfileService extends AsyncProfileService< RootProfile, @@ -38,5 +44,3 @@ export class RootProfileService extends AsyncProfileService< }); } } - -export type RootProfileProvider = Parameters[0]; diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 4f1ae2a32634e..78da5820f1ddb 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -21,6 +21,7 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core/public'; import { ENABLE_ESQL } from '@kbn/esql-utils'; import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; import { TRUNCATE_MAX_HEIGHT } from '@kbn/discover-utils'; +import { once } from 'lodash'; import { PLUGIN_ID } from '../common'; import { registerFeature } from './register_feature'; import { buildServices, UrlTracker } from './build_services'; @@ -56,10 +57,7 @@ import { ProfilesManager, RootProfileService, } from './context_awareness'; -import { createProfileProviderServices } from './context_awareness/profiles/profile_provider_services'; import { DiscoverSetup, DiscoverSetupPlugins, DiscoverStart, DiscoverStartPlugins } from './types'; -import { createLogsDataSourceProfileProvider } from './context_awareness/profile_providers/logs_data_source_profile'; -import { createLogDocumentProfileProvider } from './context_awareness/profile_providers/log_document_profile'; /** * Contains Discover, one of the oldest parts of Kibana @@ -68,16 +66,11 @@ import { createLogDocumentProfileProvider } from './context_awareness/profile_pr export class DiscoverPlugin implements Plugin { - private readonly rootProfileService = new RootProfileService(); - private readonly dataSourceProfileService = new DataSourceProfileService(); - private readonly documentProfileService = new DocumentProfileService(); private readonly appStateUpdater = new BehaviorSubject(() => ({})); private readonly historyService = new HistoryService(); private readonly inlineTopNav: Map = new Map([[null, defaultCustomizationContext.inlineTopNav]]); - private readonly experimentalFeatures: ExperimentalFeatures = { - ruleFormV2Enabled: false, - }; + private readonly experimentalFeatures: ExperimentalFeatures; private scopedHistory?: ScopedHistory; private urlTracker?: UrlTracker; @@ -87,8 +80,12 @@ export class DiscoverPlugin private singleDocLocator?: DiscoverSingleDocLocator; constructor(private readonly initializerContext: PluginInitializerContext) { - this.experimentalFeatures = - initializerContext.config.get().experimental ?? this.experimentalFeatures; + const experimental = this.initializerContext.config.get().experimental; + + this.experimentalFeatures = { + ruleFormV2Enabled: experimental?.ruleFormV2Enabled ?? false, + enabledProfiles: experimental?.enabledProfiles ?? [], + }; } setup( @@ -184,7 +181,7 @@ export class DiscoverPlugin history: this.historyService.getHistory(), scopedHistory: this.scopedHistory, urlTracker: this.urlTracker!, - profilesManager: this.createProfilesManager(), + profilesManager: await this.createProfilesManager(), setHeaderActionMenu: params.setHeaderActionMenu, }); @@ -267,8 +264,6 @@ export class DiscoverPlugin } start(core: CoreStart, plugins: DiscoverStartPlugins): DiscoverStart { - this.registerProfiles(); - const viewSavedSearchAction = new ViewSavedSearchAction(core.application, this.locator!); plugins.uiActions.addTriggerAction('CONTEXT_MENU_TRIGGER', viewSavedSearchAction); @@ -304,22 +299,31 @@ export class DiscoverPlugin } } - private registerProfiles() { - const providerServices = createProfileProviderServices(); + private createProfileServices = once(async () => { + const { registerProfileProviders } = await import('./context_awareness/profile_providers'); + const rootProfileService = new RootProfileService(); + const dataSourceProfileService = new DataSourceProfileService(); + const documentProfileService = new DocumentProfileService(); + const experimentalProfileIds = this.experimentalFeatures.enabledProfiles ?? []; + + registerProfileProviders({ + rootProfileService, + dataSourceProfileService, + documentProfileService, + experimentalProfileIds, + }); - this.dataSourceProfileService.registerProvider( - createLogsDataSourceProfileProvider(providerServices) - ); - this.documentProfileService.registerProvider( - createLogDocumentProfileProvider(providerServices) - ); - } + return { rootProfileService, dataSourceProfileService, documentProfileService }; + }); + + private async createProfilesManager() { + const { rootProfileService, dataSourceProfileService, documentProfileService } = + await this.createProfileServices(); - private createProfilesManager() { return new ProfilesManager( - this.rootProfileService, - this.dataSourceProfileService, - this.documentProfileService + rootProfileService, + dataSourceProfileService, + documentProfileService ); } @@ -334,7 +338,7 @@ export class DiscoverPlugin private getDiscoverServices = ( core: CoreStart, plugins: DiscoverStartPlugins, - profilesManager = this.createProfilesManager() + profilesManager: ProfilesManager ) => { return buildServices({ core, @@ -360,7 +364,8 @@ export class DiscoverPlugin const getDiscoverServicesInternal = async () => { const [coreStart, deps] = await core.getStartServices(); - return this.getDiscoverServices(coreStart, deps); + const profilesManager = await this.createProfilesManager(); + return this.getDiscoverServices(coreStart, deps, profilesManager); }; const factory = new SearchEmbeddableFactory(getStartServices, getDiscoverServicesInternal); diff --git a/test/functional/apps/discover/context_awareness/_data_source_profile.ts b/test/functional/apps/discover/context_awareness/_data_source_profile.ts new file mode 100644 index 0000000000000..d203f33c887e8 --- /dev/null +++ b/test/functional/apps/discover/context_awareness/_data_source_profile.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import kbnRison from '@kbn/rison'; +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'timePicker', 'discover', 'unifiedFieldList']); + const testSubjects = getService('testSubjects'); + const dataViews = getService('dataViews'); + + describe('data source profile', () => { + describe('ES|QL mode', () => { + describe('cell renderers', () => { + it('should render custom @timestamp but not custom log.level', async () => { + const state = kbnRison.encode({ + dataSource: { type: 'esql' }, + query: { esql: 'from my-example-* | sort @timestamp desc' }, + }); + await PageObjects.common.navigateToApp('discover', { + hash: `/?_a=${state}`, + }); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp'); + await PageObjects.unifiedFieldList.clickFieldListItemAdd('log.level'); + const timestamps = await testSubjects.findAll('exampleRootProfileTimestamp'); + expect(timestamps).to.have.length(6); + expect(await timestamps[0].getVisibleText()).to.be('2024-06-10T16:30:00.000Z'); + expect(await timestamps[5].getVisibleText()).to.be('2024-06-10T14:00:00.000Z'); + const logLevels = await testSubjects.findAll('exampleDataSourceProfileLogLevel', 2500); + expect(logLevels).to.have.length(0); + }); + + it('should render custom @timestamp and custom log.level', async () => { + const state = kbnRison.encode({ + dataSource: { type: 'esql' }, + query: { esql: 'from my-example-logs | sort @timestamp desc' }, + }); + await PageObjects.common.navigateToApp('discover', { + hash: `/?_a=${state}`, + }); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp'); + await PageObjects.unifiedFieldList.clickFieldListItemAdd('log.level'); + const timestamps = await testSubjects.findAll('exampleRootProfileTimestamp'); + expect(timestamps).to.have.length(3); + expect(await timestamps[0].getVisibleText()).to.be('2024-06-10T16:00:00.000Z'); + expect(await timestamps[2].getVisibleText()).to.be('2024-06-10T14:00:00.000Z'); + const logLevels = await testSubjects.findAll('exampleDataSourceProfileLogLevel'); + expect(logLevels).to.have.length(3); + expect(await logLevels[0].getVisibleText()).to.be('Debug'); + expect(await logLevels[2].getVisibleText()).to.be('Info'); + }); + }); + }); + + describe('data view mode', () => { + describe('cell renderers', () => { + it('should render custom @timestamp but not custom log.level', async () => { + await PageObjects.common.navigateToApp('discover'); + await dataViews.switchTo('my-example-*'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp'); + await PageObjects.unifiedFieldList.clickFieldListItemAdd('log.level'); + const timestamps = await testSubjects.findAll('exampleRootProfileTimestamp'); + expect(timestamps).to.have.length(6); + expect(await timestamps[0].getVisibleText()).to.be('2024-06-10T16:30:00.000Z'); + expect(await timestamps[5].getVisibleText()).to.be('2024-06-10T14:00:00.000Z'); + const logLevels = await testSubjects.findAll('exampleDataSourceProfileLogLevel', 2500); + expect(logLevels).to.have.length(0); + }); + + it('should render custom @timestamp and custom log.level', async () => { + await PageObjects.common.navigateToApp('discover'); + await dataViews.switchTo('my-example-logs'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp'); + await PageObjects.unifiedFieldList.clickFieldListItemAdd('log.level'); + const timestamps = await testSubjects.findAll('exampleRootProfileTimestamp'); + expect(timestamps).to.have.length(3); + expect(await timestamps[0].getVisibleText()).to.be('2024-06-10T16:00:00.000Z'); + expect(await timestamps[2].getVisibleText()).to.be('2024-06-10T14:00:00.000Z'); + const logLevels = await testSubjects.findAll('exampleDataSourceProfileLogLevel'); + expect(logLevels).to.have.length(3); + expect(await logLevels[0].getVisibleText()).to.be('Debug'); + expect(await logLevels[2].getVisibleText()).to.be('Info'); + }); + }); + }); + }); +} diff --git a/test/functional/apps/discover/context_awareness/_root_profile.ts b/test/functional/apps/discover/context_awareness/_root_profile.ts new file mode 100644 index 0000000000000..c0bb4885699f8 --- /dev/null +++ b/test/functional/apps/discover/context_awareness/_root_profile.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import kbnRison from '@kbn/rison'; +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); + const testSubjects = getService('testSubjects'); + const dataViews = getService('dataViews'); + + describe('root profile', () => { + describe('ES|QL mode', () => { + describe('cell renderers', () => { + it('should render custom @timestamp', async () => { + const state = kbnRison.encode({ + dataSource: { type: 'esql' }, + query: { esql: 'from my-example-* | sort @timestamp desc' }, + }); + await PageObjects.common.navigateToApp('discover', { + hash: `/?_a=${state}`, + }); + await PageObjects.discover.waitUntilSearchingHasFinished(); + const timestamps = await testSubjects.findAll('exampleRootProfileTimestamp'); + expect(timestamps).to.have.length(6); + expect(await timestamps[0].getVisibleText()).to.be('2024-06-10T16:30:00.000Z'); + expect(await timestamps[5].getVisibleText()).to.be('2024-06-10T14:00:00.000Z'); + }); + }); + }); + + describe('data view mode', () => { + describe('cell renderers', () => { + it('should render custom @timestamp', async () => { + await PageObjects.common.navigateToApp('discover'); + await dataViews.switchTo('my-example-*'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + const timestamps = await testSubjects.findAll('exampleRootProfileTimestamp'); + expect(timestamps).to.have.length(6); + expect(await timestamps[0].getVisibleText()).to.be('2024-06-10T16:30:00.000Z'); + expect(await timestamps[5].getVisibleText()).to.be('2024-06-10T14:00:00.000Z'); + }); + }); + }); + }); +} diff --git a/test/functional/apps/discover/context_awareness/config.ts b/test/functional/apps/discover/context_awareness/config.ts new file mode 100644 index 0000000000000..c8e43a6bb2c9d --- /dev/null +++ b/test/functional/apps/discover/context_awareness/config.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + const baseConfig = functionalConfig.getAll(); + + return { + ...baseConfig, + testFiles: [require.resolve('.')], + kbnTestServer: { + ...baseConfig.kbnTestServer, + serverArgs: [ + ...baseConfig.kbnTestServer.serverArgs, + '--discover.experimental.enabledProfiles=["example-root-profile","example-data-source-profile","example-document-profile"]', + ], + }, + }; +} diff --git a/test/functional/apps/discover/context_awareness/index.ts b/test/functional/apps/discover/context_awareness/index.ts new file mode 100644 index 0000000000000..f280024ec7c80 --- /dev/null +++ b/test/functional/apps/discover/context_awareness/index.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['timePicker']); + const from = '2024-06-10T14:00:00.000Z'; + const to = '2024-06-10T16:30:00.000Z'; + + describe('discover/context_awareness', () => { + before(async () => { + await esArchiver.load('test/functional/fixtures/es_archiver/discover/context_awareness'); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/discover/context_awareness' + ); + await kibanaServer.uiSettings.update({ + 'timepicker:timeDefaults': `{ "from": "${from}", "to": "${to}"}`, + }); + }); + + after(async () => { + await esArchiver.unload('test/functional/fixtures/es_archiver/discover/context_awareness'); + await kibanaServer.importExport.unload( + 'test/functional/fixtures/kbn_archiver/discover/context_awareness' + ); + await PageObjects.timePicker.resetDefaultAbsoluteRangeViaUiSettings(); + }); + + loadTestFile(require.resolve('./_root_profile')); + loadTestFile(require.resolve('./_data_source_profile')); + }); +} diff --git a/test/functional/fixtures/es_archiver/discover/context_awareness/data.json.gz b/test/functional/fixtures/es_archiver/discover/context_awareness/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..cd1f670958d7d47b36a6868efc537ec09bb76939 GIT binary patch literal 351 zcmV-l0igaLiwFP!000026Sb2;Z-Ous$M1ZKm!1cyC@gZ?=G@d66BDzsU6x8KW1%gh z#psOR-IQpQi`x`PIMDZhEx#X+jRk=8I%6mRL}i3-%)*kSg<08H001bVrW#JB{m1P7 zVe$MUUV5#$KNyKJZW4mBl?m~+%~phGDYH`Tr^Xh5T(!Oy~lyIi=B)ahTY5&nm^$-BWmP}}e7CF(06es}K_i%Y# x4E{E$f`7M%?S{bpI>9l { + describe('ES|QL mode', () => { + describe('cell renderers', () => { + it('should not render custom @timestamp or log.level', async () => { + const state = kbnRison.encode({ + dataSource: { type: 'esql' }, + query: { esql: 'from my-example-* | sort @timestamp desc' }, + }); + await PageObjects.common.navigateToApp('discover', { + hash: `/?_a=${state}`, + }); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp'); + await PageObjects.unifiedFieldList.clickFieldListItemAdd('log.level'); + const timestamps = await testSubjects.findAll('exampleRootProfileTimestamp', 2500); + expect(timestamps).to.have.length(0); + const logLevels = await testSubjects.findAll('exampleDataSourceProfileLogLevel', 2500); + expect(logLevels).to.have.length(0); + }); + + it('should not render custom @timestamp but should render custom log.level', async () => { + const state = kbnRison.encode({ + dataSource: { type: 'esql' }, + query: { esql: 'from my-example-logs | sort @timestamp desc' }, + }); + await PageObjects.common.navigateToApp('discover', { + hash: `/?_a=${state}`, + }); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp'); + await PageObjects.unifiedFieldList.clickFieldListItemAdd('log.level'); + const timestamps = await testSubjects.findAll('exampleRootProfileTimestamp', 2500); + expect(timestamps).to.have.length(0); + const logLevels = await testSubjects.findAll('exampleDataSourceProfileLogLevel'); + expect(logLevels).to.have.length(3); + expect(await logLevels[0].getVisibleText()).to.be('Debug'); + expect(await logLevels[2].getVisibleText()).to.be('Info'); + }); + }); + }); + + describe('data view mode', () => { + describe('cell renderers', () => { + it('should not render custom @timestamp or log.level', async () => { + await PageObjects.common.navigateToApp('discover'); + await dataViews.switchTo('my-example-*'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp'); + await PageObjects.unifiedFieldList.clickFieldListItemAdd('log.level'); + const timestamps = await testSubjects.findAll('exampleRootProfileTimestamp', 2500); + expect(timestamps).to.have.length(0); + const logLevels = await testSubjects.findAll('exampleDataSourceProfileLogLevel', 2500); + expect(logLevels).to.have.length(0); + }); + + it('should not render custom @timestamp but should render custom log.level', async () => { + await PageObjects.common.navigateToApp('discover'); + await dataViews.switchTo('my-example-logs'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.unifiedFieldList.clickFieldListItemAdd('@timestamp'); + await PageObjects.unifiedFieldList.clickFieldListItemAdd('log.level'); + const timestamps = await testSubjects.findAll('exampleRootProfileTimestamp', 2500); + expect(timestamps).to.have.length(0); + const logLevels = await testSubjects.findAll('exampleDataSourceProfileLogLevel'); + expect(logLevels).to.have.length(3); + expect(await logLevels[0].getVisibleText()).to.be('Debug'); + expect(await logLevels[2].getVisibleText()).to.be('Info'); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/_root_profile.ts b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/_root_profile.ts new file mode 100644 index 0000000000000..649d0a67ee22f --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/_root_profile.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import kbnRison from '@kbn/rison'; +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); + const testSubjects = getService('testSubjects'); + const dataViews = getService('dataViews'); + + describe('root profile', () => { + describe('ES|QL mode', () => { + describe('cell renderers', () => { + it('should not render custom @timestamp', async () => { + const state = kbnRison.encode({ + dataSource: { type: 'esql' }, + query: { esql: 'from my-example-* | sort @timestamp desc' }, + }); + await PageObjects.common.navigateToApp('discover', { + hash: `/?_a=${state}`, + }); + await PageObjects.discover.waitUntilSearchingHasFinished(); + const timestamps = await testSubjects.findAll('exampleRootProfileTimestamp', 2500); + expect(timestamps).to.have.length(0); + }); + }); + }); + + describe('data view mode', () => { + describe('cell renderers', () => { + it('should not render custom @timestamp', async () => { + await PageObjects.common.navigateToApp('discover'); + await dataViews.switchTo('my-example-*'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + const timestamps = await testSubjects.findAll('exampleRootProfileTimestamp', 2500); + expect(timestamps).to.have.length(0); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/index.ts b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/index.ts new file mode 100644 index 0000000000000..e6e128e4b6b59 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/index.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['timePicker', 'svlCommonPage']); + const from = '2024-06-10T14:00:00.000Z'; + const to = '2024-06-10T16:30:00.000Z'; + + describe('discover/context_awareness', () => { + before(async () => { + await esArchiver.load('test/functional/fixtures/es_archiver/discover/context_awareness'); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/discover/context_awareness' + ); + await kibanaServer.uiSettings.update({ + 'timepicker:timeDefaults': `{ "from": "${from}", "to": "${to}"}`, + }); + await PageObjects.svlCommonPage.login(); + }); + + after(async () => { + await esArchiver.unload('test/functional/fixtures/es_archiver/discover/context_awareness'); + await kibanaServer.importExport.unload( + 'test/functional/fixtures/kbn_archiver/discover/context_awareness' + ); + await PageObjects.timePicker.resetDefaultAbsoluteRangeViaUiSettings(); + }); + + loadTestFile(require.resolve('./_root_profile')); + loadTestFile(require.resolve('./_data_source_profile')); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/observability/config.context_awareness.ts b/x-pack/test_serverless/functional/test_suites/observability/config.context_awareness.ts new file mode 100644 index 0000000000000..76362cc111e6f --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/config.context_awareness.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createTestConfig } from '../../config.base'; + +export default createTestConfig({ + serverlessProject: 'oblt', + testFiles: [require.resolve('../common/discover/context_awareness')], + junit: { + reportName: 'Serverless Observability Discover Context Awareness Functional Tests', + }, + kbnServerArgs: [ + '--discover.experimental.enabledProfiles=["example-root-profile","example-data-source-profile","example-document-profile"]', + ], + // include settings from project controller + // https://github.com/elastic/project-controller/blob/main/internal/project/observability/config/elasticsearch.yml + esServerArgs: ['xpack.ml.dfa.enabled=false'], +}); diff --git a/x-pack/test_serverless/functional/test_suites/search/config.context_awareness.ts b/x-pack/test_serverless/functional/test_suites/search/config.context_awareness.ts new file mode 100644 index 0000000000000..7b608c29c9f3a --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/search/config.context_awareness.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createTestConfig } from '../../config.base'; + +export default createTestConfig({ + serverlessProject: 'es', + testFiles: [require.resolve('../common/discover/context_awareness')], + junit: { + reportName: 'Serverless Search Discover Context Awareness Functional Tests', + }, + kbnServerArgs: [ + '--discover.experimental.enabledProfiles=["example-root-profile","example-data-source-profile","example-document-profile"]', + ], + // include settings from project controller + // https://github.com/elastic/project-controller/blob/main/internal/project/observability/config/elasticsearch.yml + esServerArgs: ['xpack.ml.dfa.enabled=false'], +}); diff --git a/x-pack/test_serverless/functional/test_suites/security/config.context_awareness.ts b/x-pack/test_serverless/functional/test_suites/security/config.context_awareness.ts new file mode 100644 index 0000000000000..6276922df83f4 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/security/config.context_awareness.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createTestConfig } from '../../config.base'; + +export default createTestConfig({ + serverlessProject: 'security', + testFiles: [require.resolve('../common/discover/context_awareness')], + junit: { + reportName: 'Serverless Security Discover Context Awareness Functional Tests', + }, + kbnServerArgs: [ + '--discover.experimental.enabledProfiles=["example-root-profile","example-data-source-profile","example-document-profile"]', + ], + // include settings from project controller + // https://github.com/elastic/project-controller/blob/main/internal/project/observability/config/elasticsearch.yml + esServerArgs: ['xpack.ml.dfa.enabled=false'], +});