[Discover] Add support for context dependent profiles (#189229)

## Summary

This PR adds support for passing higher level context objects to profile
providers as part of the resolution process to support profiles which
are context dependent (e.g. an `oblt-logs-data-source` profile which
should only become active when viewing a logs data source _and_ in the
`oblt` project type).

### 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
- [ ] [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)

---------

Co-authored-by: Julia Rechkunova <julia.rechkunova@elastic.co>
This commit is contained in:
Davis McPhee 2024-07-29 15:40:07 -03:00 committed by GitHub
parent 3783e82490
commit 69de10f81f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 59 additions and 9 deletions

View file

@ -8,7 +8,13 @@
import { buildDataTableRecord } from '@kbn/discover-utils';
import { DocViewsRegistry } from '@kbn/unified-doc-viewer';
import { DocumentType } from '../../profiles';
import {
DataSourceCategory,
DataSourceContext,
DocumentType,
RootContext,
SolutionType,
} from '../../profiles';
import { createContextAwarenessMocks } from '../../__mocks__';
import { createLogDocumentProfileProvider } from './profile';
@ -16,6 +22,8 @@ const mockServices = createContextAwarenessMocks().profileProviderServices;
describe('logDocumentProfileProvider', () => {
const logDocumentProfileProvider = createLogDocumentProfileProvider(mockServices);
const ROOT_CONTEXT: RootContext = { solutionType: SolutionType.Default };
const DATA_SOURCE_CONTEXT: DataSourceContext = { category: DataSourceCategory.Logs };
const RESOLUTION_MATCH = {
isMatch: true,
context: {
@ -29,6 +37,8 @@ describe('logDocumentProfileProvider', () => {
it('matches records with the correct data stream type', () => {
expect(
logDocumentProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSourceContext: DATA_SOURCE_CONTEXT,
record: buildMockRecord('logs-2000-01-01', {
'data_stream.type': ['logs'],
}),
@ -39,6 +49,8 @@ describe('logDocumentProfileProvider', () => {
it('matches records with fields prefixed with "log."', () => {
expect(
logDocumentProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSourceContext: DATA_SOURCE_CONTEXT,
record: buildMockRecord('logs-2000-01-01', {
'log.level': ['INFO'],
}),
@ -49,11 +61,15 @@ describe('logDocumentProfileProvider', () => {
it('matches records with indices matching the allowed pattern', () => {
expect(
logDocumentProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSourceContext: DATA_SOURCE_CONTEXT,
record: buildMockRecord('logs-2000-01-01'),
})
).toEqual(RESOLUTION_MATCH);
expect(
logDocumentProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSourceContext: DATA_SOURCE_CONTEXT,
record: buildMockRecord('remote_cluster:filebeat'),
})
).toEqual(RESOLUTION_MATCH);
@ -62,6 +78,8 @@ describe('logDocumentProfileProvider', () => {
it('does not match records with neither characteristic', () => {
expect(
logDocumentProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSourceContext: DATA_SOURCE_CONTEXT,
record: buildMockRecord('another-index'),
})
).toEqual(RESOLUTION_MISMATCH);

View file

@ -10,7 +10,7 @@ import { buildDataTableRecord } from '@kbn/discover-utils';
import type { EuiThemeComputed } from '@elastic/eui';
import { createStubIndexPattern } from '@kbn/data-views-plugin/common/data_view.stub';
import { createDataViewDataSource, createEsqlDataSource } from '../../../../common/data_sources';
import { DataSourceCategory } from '../../profiles';
import { DataSourceCategory, RootContext, SolutionType } from '../../profiles';
import { createContextAwarenessMocks } from '../../__mocks__';
import { createLogsDataSourceProfileProvider } from './profile';
@ -21,7 +21,7 @@ describe('logsDataSourceProfileProvider', () => {
const VALID_INDEX_PATTERN = 'logs-nginx.access-*';
const MIXED_INDEX_PATTERN = 'logs-nginx.access-*,metrics-*';
const INVALID_INDEX_PATTERN = 'my_source-access-*';
const ROOT_CONTEXT: RootContext = { solutionType: SolutionType.Default };
const RESOLUTION_MATCH = {
isMatch: true,
context: { category: DataSourceCategory.Logs },
@ -33,6 +33,7 @@ describe('logsDataSourceProfileProvider', () => {
it('should match ES|QL sources with an allowed index pattern in its query', () => {
expect(
logsDataSourceProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSource: createEsqlDataSource(),
query: { esql: `from ${VALID_INDEX_PATTERN}` },
})
@ -42,12 +43,14 @@ describe('logsDataSourceProfileProvider', () => {
it('should NOT match ES|QL sources with a mixed or not allowed index pattern in its query', () => {
expect(
logsDataSourceProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSource: createEsqlDataSource(),
query: { esql: `from ${INVALID_INDEX_PATTERN}` },
})
).toEqual(RESOLUTION_MISMATCH);
expect(
logsDataSourceProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSource: createEsqlDataSource(),
query: { esql: `from ${MIXED_INDEX_PATTERN}` },
})
@ -57,6 +60,7 @@ describe('logsDataSourceProfileProvider', () => {
it('should match data view sources with an allowed index pattern', () => {
expect(
logsDataSourceProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSource: createDataViewDataSource({ dataViewId: VALID_INDEX_PATTERN }),
dataView: createStubIndexPattern({ spec: { title: VALID_INDEX_PATTERN } }),
})
@ -66,12 +70,14 @@ describe('logsDataSourceProfileProvider', () => {
it('should NOT match data view sources with a mixed or not allowed index pattern', () => {
expect(
logsDataSourceProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSource: createDataViewDataSource({ dataViewId: INVALID_INDEX_PATTERN }),
dataView: createStubIndexPattern({ spec: { title: INVALID_INDEX_PATTERN } }),
})
).toEqual(RESOLUTION_MISMATCH);
expect(
logsDataSourceProfileProvider.resolve({
rootContext: ROOT_CONTEXT,
dataSource: createDataViewDataSource({ dataViewId: MIXED_INDEX_PATTERN }),
dataView: createStubIndexPattern({ spec: { title: MIXED_INDEX_PATTERN } }),
})

View file

@ -62,10 +62,13 @@ describe('registerProfileProviders', () => {
});
const rootContext = await rootProfileServiceMock.resolve({ solutionNavId: null });
const dataSourceContext = await dataSourceProfileServiceMock.resolve({
rootContext,
dataSource: createEsqlDataSource(),
query: { esql: 'from my-example-logs' },
});
const documentContext = documentProfileServiceMock.resolve({
rootContext,
dataSourceContext,
record: {
id: 'test',
flattened: { 'data_stream.type': 'example' },
@ -94,10 +97,13 @@ describe('registerProfileProviders', () => {
});
const rootContext = await rootProfileServiceMock.resolve({ solutionNavId: null });
const dataSourceContext = await dataSourceProfileServiceMock.resolve({
rootContext,
dataSource: createEsqlDataSource(),
query: { esql: 'from my-example-logs' },
});
const documentContext = documentProfileServiceMock.resolve({
rootContext,
dataSourceContext,
record: {
id: 'test',
flattened: { 'data_stream.type': 'example' },

View file

@ -10,7 +10,8 @@ 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 { AsyncProfileProvider, AsyncProfileService } from '../profile_service';
import { Profile } from '../types';
import type { Profile } from '../types';
import type { RootContext } from './root_profile';
export enum DataSourceCategory {
Logs = 'logs',
@ -20,6 +21,7 @@ export enum DataSourceCategory {
export type DataSourceProfile = Profile;
export interface DataSourceProfileProviderParams {
rootContext: RootContext;
dataSource?: DiscoverDataSource;
dataView?: DataView;
query?: Query | AggregateQuery;

View file

@ -9,6 +9,8 @@
import type { DataTableRecord } from '@kbn/discover-utils';
import type { Profile } from '../types';
import { ProfileProvider, ProfileService } from '../profile_service';
import type { RootContext } from './root_profile';
import type { DataSourceContext } from './data_source_profile';
export enum DocumentType {
Log = 'log',
@ -18,6 +20,8 @@ export enum DocumentType {
export type DocumentProfile = Omit<Profile, 'getCellRenderers'>;
export interface DocumentProfileProviderParams {
rootContext: RootContext;
dataSourceContext: DataSourceContext;
record: DataTableRecord;
}

View file

@ -9,6 +9,7 @@
import { firstValueFrom, Subject } from 'rxjs';
import { createEsqlDataSource } from '../../common/data_sources';
import { addLog } from '../utils/add_log';
import { SolutionType } from './profiles/root_profile';
import { createContextAwarenessMocks } from './__mocks__';
jest.mock('../utils/add_log');
@ -220,10 +221,12 @@ describe('ProfilesManager', () => {
it('should cancel existing data source profile resolution when another is triggered', async () => {
const context = await mocks.dataSourceProfileProviderMock.resolve({
rootContext: { solutionType: SolutionType.Default },
dataSource: createEsqlDataSource(),
query: { esql: 'from *' },
});
const newContext = await mocks.dataSourceProfileProviderMock.resolve({
rootContext: { solutionType: SolutionType.Default },
dataSource: createEsqlDataSource(),
query: { esql: 'from logs-*' },
});

View file

@ -87,7 +87,9 @@ export class ProfilesManager {
this.prevRootProfileParams = serializedParams;
}
public async resolveDataSourceProfile(params: DataSourceProfileProviderParams) {
public async resolveDataSourceProfile(
params: Omit<DataSourceProfileProviderParams, 'rootContext'>
) {
const serializedParams = serializeDataSourceProfileParams(params);
if (isEqual(this.prevDataSourceProfileParams, serializedParams)) {
@ -101,7 +103,10 @@ export class ProfilesManager {
let context = this.dataSourceProfileService.defaultContext;
try {
context = await this.dataSourceProfileService.resolve(params);
context = await this.dataSourceProfileService.resolve({
...params,
rootContext: this.rootContext$.getValue(),
});
} catch (e) {
logResolutionError(ContextType.DataSource, serializedParams, e);
}
@ -114,7 +119,9 @@ export class ProfilesManager {
this.prevDataSourceProfileParams = serializedParams;
}
public resolveDocumentProfile(params: DocumentProfileProviderParams) {
public resolveDocumentProfile(
params: Omit<DocumentProfileProviderParams, 'rootContext' | 'dataSourceContext'>
) {
let context: ContextWithProfileId<DocumentContext> | undefined;
return new Proxy(params.record, {
@ -126,7 +133,11 @@ export class ProfilesManager {
if (!context) {
try {
context = this.documentProfileService.resolve(params);
context = this.documentProfileService.resolve({
...params,
rootContext: this.rootContext$.getValue(),
dataSourceContext: this.dataSourceContext$.getValue(),
});
} catch (e) {
logResolutionError(ContextType.Document, { recordId: params.record.id }, e);
context = this.documentProfileService.defaultContext;
@ -164,7 +175,7 @@ const serializeRootProfileParams = (
};
const serializeDataSourceProfileParams = (
params: DataSourceProfileProviderParams
params: Omit<DataSourceProfileProviderParams, 'rootContext'>
): SerializedDataSourceProfileParams => {
return {
dataViewId: isDataSourceType(params.dataSource, DataSourceType.DataView)