[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)
This commit is contained in:
Davis McPhee 2024-06-19 14:17:26 -03:00 committed by GitHub
parent 06f773934a
commit 57891ff353
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 1073 additions and 204 deletions

View file

@ -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

View file

@ -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<typeof configSchema>;
export type ExperimentalFeatures = ConfigSchema['experimental'];
export type ExperimentalFeatures = NonNullable<ConfigSchema['experimental']>;

View file

@ -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,

View file

@ -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';

View file

@ -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 (
<span
css={{ color: euiThemeVars.euiTextSubduedColor }}
data-test-subj="exampleDataSourceProfileLogLevelEmpty"
>
(None)
</span>
);
}
const levelMap: Record<string, string> = {
info: 'primary',
debug: 'default',
error: 'danger',
};
return (
<EuiBadge
color={levelMap[level]}
title={level}
data-test-subj="exampleDataSourceProfileLogLevel"
>
{capitalize(level)}
</EuiBadge>
);
},
}),
},
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;
};

View file

@ -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';

View file

@ -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;
};

View file

@ -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';

View file

@ -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 (
<EuiBadge color="hollow" title={timestamp} data-test-subj="exampleRootProfileTimestamp">
{timestamp}
</EuiBadge>
);
},
}),
},
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;
};

View file

@ -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';

View file

@ -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

View file

@ -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

View file

@ -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
);
});
});

View file

@ -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<TProvider, {}>
>({
profileService,
availableProviders,
enabledProfileIds,
}: {
profileService: TService;
availableProviders: TProvider[];
enabledProfileIds: string[];
}) => {
for (const profile of availableProviders) {
if (enabledProfileIds.includes(profile.profileId)) {
profileService.registerProvider(profile);
}
}
};

View file

@ -15,38 +15,33 @@ export type ResolveProfileResult<TContext> =
| { isMatch: true; context: TContext }
| { isMatch: false };
export type ProfileProviderMode = 'sync' | 'async';
export type ContextWithProfileId<TContext> = TContext & { profileId: string };
export interface ProfileProvider<
TProfile extends PartialProfile,
TParams,
TContext,
TMode extends ProfileProviderMode
> {
export interface BaseProfileProvider<TProfile extends PartialProfile> {
profileId: string;
profile: ComposableProfile<TProfile>;
resolve: (
params: TParams
) => TMode extends 'sync'
? ResolveProfileResult<TContext>
: ResolveProfileResult<TContext> | Promise<ResolveProfileResult<TContext>>;
}
export type ContextWithProfileId<TContext> = TContext & { profileId: string };
export interface ProfileProvider<TProfile extends PartialProfile, TParams, TContext>
extends BaseProfileProvider<TProfile> {
resolve: (params: TParams) => ResolveProfileResult<TContext>;
}
export interface AsyncProfileProvider<TProfile extends PartialProfile, TParams, TContext>
extends BaseProfileProvider<TProfile> {
resolve: (
params: TParams
) => ResolveProfileResult<TContext> | Promise<ResolveProfileResult<TContext>>;
}
const EMPTY_PROFILE = {};
abstract class BaseProfileService<
TProfile extends PartialProfile,
TParams,
TContext,
TMode extends ProfileProviderMode
> {
protected readonly providers: Array<ProfileProvider<TProfile, TParams, TContext, TMode>> = [];
export abstract class BaseProfileService<TProvider extends BaseProfileProvider<{}>, TContext> {
protected readonly providers: TProvider[] = [];
protected constructor(public readonly defaultContext: ContextWithProfileId<TContext>) {}
public registerProvider(provider: ProfileProvider<TProfile, TParams, TContext, TMode>) {
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<TContext>
: Promise<ContextWithProfileId<TContext>>;
}
export class ProfileService<
TProfile extends PartialProfile,
TParams,
TContext
> extends BaseProfileService<TProfile, TParams, TContext, 'sync'> {
> extends BaseProfileService<ProfileProvider<TProfile, TParams, TContext>, 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<TProfile, TParams, TContext, 'async'> {
> extends BaseProfileService<AsyncProfileProvider<TProfile, TParams, TContext>, TContext> {
public async resolve(params: TParams) {
for (const provider of this.providers) {
const result = await provider.resolve(params);

View file

@ -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<DataSourceProfileService['registerProvider']>[0];

View file

@ -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<Profile, 'getCellRenderers'>;
export interface DocumentProfileProviderParams {
record: DataTableRecord;
}
@ -23,7 +25,11 @@ export interface DocumentContext {
type: DocumentType;
}
export type DocumentProfile = Omit<Profile, 'getCellRenderers'>;
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<DocumentProfileService['registerProvider']>[0];

View file

@ -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 (
<EuiBadge color="hollow" title={timestamp}>
{timestamp}
</EuiBadge>
);
},
'log.level': (props) => {
const level = getFieldValue(props.row, 'log.level');
if (!level) {
return <span css={{ color: euiThemeVars.euiTextSubduedColor }}>(None)</span>;
}
const levelMap: Record<string, string> = {
info: 'primary',
debug: 'default',
error: 'danger',
};
return (
<EuiBadge color={levelMap[level]} title={level}>
{capitalize(level)}
</EuiBadge>
);
},
message: (props) => {
const { value } = getMessageFieldWithFallbacks(
props.row.flattened as unknown as LogDocumentOverview
);
return value || <span css={{ color: euiThemeVars.euiTextSubduedColor }}>(None)</span>;
},
}),
},
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;
};

View file

@ -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<RootProfileService['registerProvider']>[0];

View file

@ -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<DiscoverSetup, DiscoverStart, DiscoverSetupPlugins, DiscoverStartPlugins>
{
private readonly rootProfileService = new RootProfileService();
private readonly dataSourceProfileService = new DataSourceProfileService();
private readonly documentProfileService = new DocumentProfileService();
private readonly appStateUpdater = new BehaviorSubject<AppUpdater>(() => ({}));
private readonly historyService = new HistoryService();
private readonly inlineTopNav: Map<string | null, DiscoverCustomizationContext['inlineTopNav']> =
new Map([[null, defaultCustomizationContext.inlineTopNav]]);
private readonly experimentalFeatures: ExperimentalFeatures = {
ruleFormV2Enabled: false,
};
private readonly experimentalFeatures: ExperimentalFeatures;
private scopedHistory?: ScopedHistory<unknown>;
private urlTracker?: UrlTracker;
@ -87,8 +80,12 @@ export class DiscoverPlugin
private singleDocLocator?: DiscoverSingleDocLocator;
constructor(private readonly initializerContext: PluginInitializerContext<ConfigSchema>) {
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 ?? [];
this.dataSourceProfileService.registerProvider(
createLogsDataSourceProfileProvider(providerServices)
);
this.documentProfileService.registerProvider(
createLogDocumentProfileProvider(providerServices)
);
}
registerProfileProviders({
rootProfileService,
dataSourceProfileService,
documentProfileService,
experimentalProfileIds,
});
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);

View file

@ -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');
});
});
});
});
}

View file

@ -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');
});
});
});
});
}

View file

@ -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"]',
],
},
};
}

View file

@ -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'));
});
}

View file

@ -0,0 +1,76 @@
{
"type": "index",
"value": {
"aliases": {
},
"index": "my-example-logs",
"mappings": {
"properties": {
"@timestamp": {
"type": "date"
},
"data_stream": {
"properties": {
"type": {
"type": "constant_keyword",
"value": "logs"
}
}
},
"log": {
"properties": {
"level": {
"type": "keyword"
}
}
},
"message": {
"type": "match_only_text"
}
}
},
"settings": {
"index": {
"number_of_replicas": "1",
"number_of_shards": "1"
}
}
}
}
{
"type": "index",
"value": {
"aliases": {
},
"index": "my-example-metrics",
"mappings": {
"properties": {
"@timestamp": {
"type": "date"
},
"data_stream": {
"properties": {
"type": {
"type": "constant_keyword",
"value": "metrics"
}
}
},
"event": {
"properties": {
"duration": {
"type": "long"
}
}
}
}
},
"settings": {
"index": {
"number_of_replicas": "1",
"number_of_shards": "1"
}
}
}
}

View file

@ -0,0 +1,49 @@
{
"attributes": {
"allowHidden": false,
"fieldAttrs": "{}",
"fieldFormatMap": "{}",
"fields": "[]",
"name": "my-example-*",
"runtimeFieldMap": "{}",
"sourceFilters": "[]",
"timeFieldName": "@timestamp",
"title": "my-example-*"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2024-06-12T22:23:09.061Z",
"created_by": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0",
"id": "47fbfd7b-a51d-442b-86ff-b1529cf5b935",
"managed": false,
"references": [],
"type": "index-pattern",
"typeMigrationVersion": "8.0.0",
"updated_at": "2024-06-12T22:23:09.061Z",
"updated_by": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0",
"version": "WzgsMV0="
}
{
"attributes": {
"allowHidden": false,
"fieldAttrs": "{}",
"fieldFormatMap": "{}",
"fields": "[]",
"name": "my-example-logs",
"runtimeFieldMap": "{}",
"sourceFilters": "[]",
"timeFieldName": "@timestamp",
"title": "my-example-logs"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2024-06-12T22:23:21.331Z",
"created_by": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0",
"id": "795df528-add1-491a-8e25-72a862c4bf8c",
"managed": false,
"references": [],
"type": "index-pattern",
"typeMigrationVersion": "8.0.0",
"updated_at": "2024-06-12T22:23:21.331Z",
"updated_by": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0",
"version": "WzEwLDFd"
}

View file

@ -126,6 +126,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'data_views.scriptedFieldsEnabled (any)', // It's a boolean (any because schema.conditional)
'data_visualizer.resultLinks.fileBeat.enabled (boolean)',
'dev_tools.deeplinks.navLinkStatus (string)',
'discover.experimental.enabledProfiles (array)',
'enterpriseSearch.canDeployEntSearch (boolean)',
'enterpriseSearch.host (string)',
'enterpriseSearch.ui.enabled (boolean)',

View file

@ -0,0 +1,88 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import 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 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');
});
});
});
});
}

View file

@ -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);
});
});
});
});
}

View file

@ -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'));
});
}

View file

@ -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'],
});

View file

@ -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'],
});

View file

@ -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'],
});