From a0785b9ef06b5c11973b16973ede8e1e99625fa5 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 13 May 2024 16:00:16 -0300 Subject: [PATCH 01/52] Initial pass at context provider services --- .../public/context_awareness/index.ts | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 src/plugins/discover/public/context_awareness/index.ts diff --git a/src/plugins/discover/public/context_awareness/index.ts b/src/plugins/discover/public/context_awareness/index.ts new file mode 100644 index 0000000000000..01f790603f4d8 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/index.ts @@ -0,0 +1,201 @@ +/* + * 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. + */ + +/* eslint-disable max-classes-per-file */ + +import type { DataView } from '@kbn/data-views-plugin/common'; +import type { DataTableRecord } from '@kbn/discover-utils'; +import { AggregateQuery, isOfAggregateQueryType, Query } from '@kbn/es-query'; +import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; +import { DataSourceType, DiscoverDataSource, isDataSourceType } from '../../common/data_sources'; + +export interface ContextProvider { + order: number; + resolve: (params: TParams) => TContext | undefined; +} + +export abstract class ContextService { + private providers: Array> = []; + + public registerProvider(provider: ContextProvider) { + this.providers.push(provider); + this.providers.sort((a, b) => a.order - b.order); + } + + public resolve(params: TParams): TContext { + for (const provider of this.providers) { + const context = provider.resolve(params); + + if (context) { + return context; + } + } + + return this.getDefaultContext(); + } + + protected abstract getDefaultContext(): TContext; +} + +export enum SolutionType { + Observability = 'oblt', + Security = 'security', + Search = 'search', + Default = 'default', +} + +export interface RootContext { + solutionType: SolutionType; +} + +export interface RootContextProviderParams { + solutionNavId: string; +} + +export type RootContextProvider = ContextProvider; + +export class RootContextService extends ContextService { + protected getDefaultContext(): RootContext { + return { + solutionType: SolutionType.Default, + }; + } +} + +export const rootContextService = new RootContextService(); + +rootContextService.registerProvider({ + order: 0, + resolve: (params) => { + if (params.solutionNavId === 'search') { + return { + solutionType: SolutionType.Search, + }; + } + }, +}); + +rootContextService.registerProvider({ + order: 100, + resolve: (params) => { + if (params.solutionNavId === 'oblt') { + return { + solutionType: SolutionType.Observability, + }; + } + }, +}); + +rootContextService.registerProvider({ + order: 200, + resolve: (params) => { + if (params.solutionNavId === 'security') { + return { + solutionType: SolutionType.Observability, + }; + } + }, +}); + +export enum DataSourceCategory { + Logs = 'logs', + Default = 'default', +} + +export interface DataSourceContext { + category: DataSourceCategory; +} + +export interface DataSourceContextProviderParams { + dataSource: DiscoverDataSource; + dataView: DataView; + query: Query | AggregateQuery | undefined; +} + +export type DataSourceContextProvider = ContextProvider< + DataSourceContextProviderParams, + DataSourceContext +>; + +export class DataSourceContextService extends ContextService< + DataSourceContextProviderParams, + DataSourceContext +> { + protected getDefaultContext(): DataSourceContext { + return { + category: DataSourceCategory.Default, + }; + } +} + +export const dataSourceContextService = new DataSourceContextService(); + +dataSourceContextService.registerProvider({ + order: 0, + resolve: (params) => { + let indices: string[] = []; + + if (isDataSourceType(params.dataSource, DataSourceType.Esql)) { + if (!isOfAggregateQueryType(params.query)) { + return; + } + + indices = getIndexPatternFromESQLQuery(params.query.esql).split(','); + } else if (isDataSourceType(params.dataSource, DataSourceType.DataView)) { + indices = params.dataView.getIndexPattern().split(','); + } + + if (indices.every((index) => index.startsWith('logs-'))) { + return { + category: DataSourceCategory.Logs, + }; + } + }, +}); + +export enum DocumentType { + Log = 'log', + Default = 'default', +} + +export interface DocumentContext { + type: DocumentType; +} + +export interface DocumentContextProviderParams { + record: DataTableRecord; +} + +export type DocumentContextProvider = ContextProvider< + DocumentContextProviderParams, + DocumentContext +>; + +export class DocumentContextService extends ContextService< + DocumentContextProviderParams, + DocumentContext +> { + protected getDefaultContext(): DocumentContext { + return { + type: DocumentType.Default, + }; + } +} + +export const documentContextService = new DocumentContextService(); + +documentContextService.registerProvider({ + order: 0, + resolve: (params) => { + if ('message' in params.record.flattened) { + return { + type: DocumentType.Log, + }; + } + }, +}); From 386a18cb1879055b7cfdaa523e35d3a5218bd736 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 14 May 2024 20:05:26 -0300 Subject: [PATCH 02/52] Initial context awareness implementation --- .../components/layout/discover_documents.tsx | 24 ++- .../discover_state_provider.tsx | 2 + .../context_awareness/context_service.ts | 35 ++++ .../context_types/data_source_context.ts | 69 +++++++ .../context_types/document_context.ts | 52 +++++ .../context_awareness/context_types/index.ts | 11 + .../context_types/root_context.ts | 69 +++++++ .../public/context_awareness/index.ts | 194 +----------------- .../use_context_awareness.tsx | 46 +++++ 9 files changed, 308 insertions(+), 194 deletions(-) create mode 100644 src/plugins/discover/public/context_awareness/context_service.ts create mode 100644 src/plugins/discover/public/context_awareness/context_types/data_source_context.ts create mode 100644 src/plugins/discover/public/context_awareness/context_types/document_context.ts create mode 100644 src/plugins/discover/public/context_awareness/context_types/index.ts create mode 100644 src/plugins/discover/public/context_awareness/context_types/root_context.ts create mode 100644 src/plugins/discover/public/context_awareness/use_context_awareness.tsx diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index ad46b7f3db658..356f0720f50f4 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -68,6 +68,7 @@ import { onResizeGridColumn } from '../../../../utils/on_resize_grid_column'; import { useContextualGridCustomisations } from '../../hooks/grid_customisations'; import { useIsEsqlMode } from '../../hooks/use_is_esql_mode'; import { useAdditionalFieldGroups } from '../../hooks/sidebar/use_additional_field_groups'; +import { useContextAwareness } from '../../../../context_awareness'; const containerStyles = css` position: relative; @@ -319,6 +320,8 @@ function DiscoverDocumentsComponent({ [viewModeToggle, callouts, gridAnnouncementCallout, loadingIndicator] ); + const { rootContext, dataSourceContext, documentContexts } = useContextAwareness(); + if (isDataViewLoading || (isEmptyDataResult && isDataLoading)) { return (
@@ -374,6 +377,14 @@ function DiscoverDocumentsComponent({ )} {!isLegacy && ( <> +
+ + Solution: {rootContext.solutionType} + {' '} + + Data source: {dataSourceContext.category} + +
{ + return ( + <> + {documentContexts.get(props.row.id)?.type ?? 'unknown'} +
+
{JSON.stringify(props.row.flattened)}
+ + ); + }, + }} customGridColumnsConfiguration={customGridColumnsConfiguration} customControlColumnsConfiguration={customControlColumnsConfiguration} additionalFieldGroups={additionalFieldGroups} diff --git a/src/plugins/discover/public/application/main/state_management/discover_state_provider.tsx b/src/plugins/discover/public/application/main/state_management/discover_state_provider.tsx index 17d9cfbe24bce..003d559499553 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_state_provider.tsx +++ b/src/plugins/discover/public/application/main/state_management/discover_state_provider.tsx @@ -40,6 +40,7 @@ function createStateHelpers() { return { Provider: context.Provider, + useContainer, useSavedSearch, useSavedSearchInitial, useSavedSearchHasChanged, @@ -48,6 +49,7 @@ function createStateHelpers() { export const { Provider: DiscoverStateProvider, + useContainer, useSavedSearchInitial, useSavedSearch, useSavedSearchHasChanged, diff --git a/src/plugins/discover/public/context_awareness/context_service.ts b/src/plugins/discover/public/context_awareness/context_service.ts new file mode 100644 index 0000000000000..54d8ca9596c15 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/context_service.ts @@ -0,0 +1,35 @@ +/* + * 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 interface ContextProvider { + order: number; + resolve: (params: TParams) => TContext | undefined; +} + +export abstract class ContextService { + private providers: Array> = []; + + public registerProvider(provider: ContextProvider) { + this.providers.push(provider); + this.providers.sort((a, b) => a.order - b.order); + } + + public resolve(params: TParams): TContext { + for (const provider of this.providers) { + const context = provider.resolve(params); + + if (context) { + return context; + } + } + + return this.getDefaultContext(); + } + + protected abstract getDefaultContext(): TContext; +} diff --git a/src/plugins/discover/public/context_awareness/context_types/data_source_context.ts b/src/plugins/discover/public/context_awareness/context_types/data_source_context.ts new file mode 100644 index 0000000000000..1385019c1805d --- /dev/null +++ b/src/plugins/discover/public/context_awareness/context_types/data_source_context.ts @@ -0,0 +1,69 @@ +/* + * 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 { DataView } from '@kbn/data-views-plugin/common'; +import { AggregateQuery, isOfAggregateQueryType, Query } from '@kbn/es-query'; +import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; +import { DataSourceType, DiscoverDataSource, isDataSourceType } from '../../../common/data_sources'; +import { ContextProvider, ContextService } from '../context_service'; + +export enum DataSourceCategory { + Logs = 'logs', + Default = 'default', +} + +export interface DataSourceContext { + category: DataSourceCategory; +} + +export interface DataSourceContextProviderParams { + dataSource?: DiscoverDataSource; + dataView?: DataView; + query?: Query | AggregateQuery; +} + +export type DataSourceContextProvider = ContextProvider< + DataSourceContextProviderParams, + DataSourceContext +>; + +export class DataSourceContextService extends ContextService< + DataSourceContextProviderParams, + DataSourceContext +> { + protected getDefaultContext(): DataSourceContext { + return { + category: DataSourceCategory.Default, + }; + } +} + +export const dataSourceContextService = new DataSourceContextService(); + +dataSourceContextService.registerProvider({ + order: 0, + resolve: (params) => { + let indices: string[] = []; + + if (isDataSourceType(params.dataSource, DataSourceType.Esql)) { + if (!isOfAggregateQueryType(params.query)) { + return; + } + + 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.includes('logs'))) { + return { + category: DataSourceCategory.Logs, + }; + } + }, +}); diff --git a/src/plugins/discover/public/context_awareness/context_types/document_context.ts b/src/plugins/discover/public/context_awareness/context_types/document_context.ts new file mode 100644 index 0000000000000..1e028f853501e --- /dev/null +++ b/src/plugins/discover/public/context_awareness/context_types/document_context.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 { DataTableRecord } from '@kbn/discover-utils'; +import { ContextProvider, ContextService } from '../context_service'; + +export enum DocumentType { + Log = 'log', + Default = 'default', +} + +export interface DocumentContext { + type: DocumentType; +} + +export interface DocumentContextProviderParams { + record: DataTableRecord; +} + +export type DocumentContextProvider = ContextProvider< + DocumentContextProviderParams, + DocumentContext +>; + +export class DocumentContextService extends ContextService< + DocumentContextProviderParams, + DocumentContext +> { + protected getDefaultContext(): DocumentContext { + return { + type: DocumentType.Default, + }; + } +} + +export const documentContextService = new DocumentContextService(); + +documentContextService.registerProvider({ + order: 0, + resolve: (params) => { + if ('message' in params.record.flattened) { + return { + type: DocumentType.Log, + }; + } + }, +}); diff --git a/src/plugins/discover/public/context_awareness/context_types/index.ts b/src/plugins/discover/public/context_awareness/context_types/index.ts new file mode 100644 index 0000000000000..362179200ff8b --- /dev/null +++ b/src/plugins/discover/public/context_awareness/context_types/index.ts @@ -0,0 +1,11 @@ +/* + * 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 * from './root_context'; +export * from './data_source_context'; +export * from './document_context'; diff --git a/src/plugins/discover/public/context_awareness/context_types/root_context.ts b/src/plugins/discover/public/context_awareness/context_types/root_context.ts new file mode 100644 index 0000000000000..073c10cc53bd1 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/context_types/root_context.ts @@ -0,0 +1,69 @@ +/* + * 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 { ContextProvider, ContextService } from '../context_service'; + +export enum SolutionType { + Observability = 'oblt', + Security = 'security', + Search = 'search', + Default = 'default', +} + +export interface RootContext { + solutionType: SolutionType; +} + +export interface RootContextProviderParams { + solutionNavId?: string | null; +} + +export type RootContextProvider = ContextProvider; + +export class RootContextService extends ContextService { + protected getDefaultContext(): RootContext { + return { + solutionType: SolutionType.Default, + }; + } +} + +export const rootContextService = new RootContextService(); + +rootContextService.registerProvider({ + order: 0, + resolve: (params) => { + if (params.solutionNavId === 'es') { + return { + solutionType: SolutionType.Search, + }; + } + }, +}); + +rootContextService.registerProvider({ + order: 100, + resolve: (params) => { + if (params.solutionNavId === 'oblt') { + return { + solutionType: SolutionType.Observability, + }; + } + }, +}); + +rootContextService.registerProvider({ + order: 200, + resolve: (params) => { + if (params.solutionNavId === 'security') { + return { + solutionType: SolutionType.Security, + }; + } + }, +}); diff --git a/src/plugins/discover/public/context_awareness/index.ts b/src/plugins/discover/public/context_awareness/index.ts index 01f790603f4d8..d96417fe03c46 100644 --- a/src/plugins/discover/public/context_awareness/index.ts +++ b/src/plugins/discover/public/context_awareness/index.ts @@ -6,196 +6,4 @@ * Side Public License, v 1. */ -/* eslint-disable max-classes-per-file */ - -import type { DataView } from '@kbn/data-views-plugin/common'; -import type { DataTableRecord } from '@kbn/discover-utils'; -import { AggregateQuery, isOfAggregateQueryType, Query } from '@kbn/es-query'; -import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; -import { DataSourceType, DiscoverDataSource, isDataSourceType } from '../../common/data_sources'; - -export interface ContextProvider { - order: number; - resolve: (params: TParams) => TContext | undefined; -} - -export abstract class ContextService { - private providers: Array> = []; - - public registerProvider(provider: ContextProvider) { - this.providers.push(provider); - this.providers.sort((a, b) => a.order - b.order); - } - - public resolve(params: TParams): TContext { - for (const provider of this.providers) { - const context = provider.resolve(params); - - if (context) { - return context; - } - } - - return this.getDefaultContext(); - } - - protected abstract getDefaultContext(): TContext; -} - -export enum SolutionType { - Observability = 'oblt', - Security = 'security', - Search = 'search', - Default = 'default', -} - -export interface RootContext { - solutionType: SolutionType; -} - -export interface RootContextProviderParams { - solutionNavId: string; -} - -export type RootContextProvider = ContextProvider; - -export class RootContextService extends ContextService { - protected getDefaultContext(): RootContext { - return { - solutionType: SolutionType.Default, - }; - } -} - -export const rootContextService = new RootContextService(); - -rootContextService.registerProvider({ - order: 0, - resolve: (params) => { - if (params.solutionNavId === 'search') { - return { - solutionType: SolutionType.Search, - }; - } - }, -}); - -rootContextService.registerProvider({ - order: 100, - resolve: (params) => { - if (params.solutionNavId === 'oblt') { - return { - solutionType: SolutionType.Observability, - }; - } - }, -}); - -rootContextService.registerProvider({ - order: 200, - resolve: (params) => { - if (params.solutionNavId === 'security') { - return { - solutionType: SolutionType.Observability, - }; - } - }, -}); - -export enum DataSourceCategory { - Logs = 'logs', - Default = 'default', -} - -export interface DataSourceContext { - category: DataSourceCategory; -} - -export interface DataSourceContextProviderParams { - dataSource: DiscoverDataSource; - dataView: DataView; - query: Query | AggregateQuery | undefined; -} - -export type DataSourceContextProvider = ContextProvider< - DataSourceContextProviderParams, - DataSourceContext ->; - -export class DataSourceContextService extends ContextService< - DataSourceContextProviderParams, - DataSourceContext -> { - protected getDefaultContext(): DataSourceContext { - return { - category: DataSourceCategory.Default, - }; - } -} - -export const dataSourceContextService = new DataSourceContextService(); - -dataSourceContextService.registerProvider({ - order: 0, - resolve: (params) => { - let indices: string[] = []; - - if (isDataSourceType(params.dataSource, DataSourceType.Esql)) { - if (!isOfAggregateQueryType(params.query)) { - return; - } - - indices = getIndexPatternFromESQLQuery(params.query.esql).split(','); - } else if (isDataSourceType(params.dataSource, DataSourceType.DataView)) { - indices = params.dataView.getIndexPattern().split(','); - } - - if (indices.every((index) => index.startsWith('logs-'))) { - return { - category: DataSourceCategory.Logs, - }; - } - }, -}); - -export enum DocumentType { - Log = 'log', - Default = 'default', -} - -export interface DocumentContext { - type: DocumentType; -} - -export interface DocumentContextProviderParams { - record: DataTableRecord; -} - -export type DocumentContextProvider = ContextProvider< - DocumentContextProviderParams, - DocumentContext ->; - -export class DocumentContextService extends ContextService< - DocumentContextProviderParams, - DocumentContext -> { - protected getDefaultContext(): DocumentContext { - return { - type: DocumentType.Default, - }; - } -} - -export const documentContextService = new DocumentContextService(); - -documentContextService.registerProvider({ - order: 0, - resolve: (params) => { - if ('message' in params.record.flattened) { - return { - type: DocumentType.Log, - }; - } - }, -}); +export { useContextAwareness } from './use_context_awareness'; diff --git a/src/plugins/discover/public/context_awareness/use_context_awareness.tsx b/src/plugins/discover/public/context_awareness/use_context_awareness.tsx new file mode 100644 index 0000000000000..6603222824a8e --- /dev/null +++ b/src/plugins/discover/public/context_awareness/use_context_awareness.tsx @@ -0,0 +1,46 @@ +/* + * 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 { useMemo, useState } from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { useAppStateSelector } from '../application/main/state_management/discover_app_state_container'; +import { useInternalStateSelector } from '../application/main/state_management/discover_internal_state_container'; +import { useContainer } from '../application/main/state_management/discover_state_provider'; +import { useDiscoverServices } from '../hooks/use_discover_services'; +import { + dataSourceContextService, + DocumentContext, + documentContextService, + rootContextService, +} from './context_types'; + +export const useContextAwareness = () => { + const { chrome } = useDiscoverServices(); + const [solutionNavId$] = useState(() => chrome.getActiveSolutionNavId$()); + const solutionNavId = useObservable(solutionNavId$); + const rootContext = useMemo(() => rootContextService.resolve({ solutionNavId }), [solutionNavId]); + const dataSource = useAppStateSelector((state) => state.dataSource); + const dataView = useInternalStateSelector((state) => state.dataView); + const query = useAppStateSelector((state) => state.query); + const dataSourceContext = useMemo( + () => dataSourceContextService.resolve({ dataSource, dataView, query }), + [dataSource, dataView, query] + ); + const stateContainer = useContainer(); + const documents = useObservable(stateContainer?.dataState.data$.documents$!); + const documentContexts = useMemo( + () => + (documents?.result ?? []).reduce( + (map, record) => map.set(record.id, documentContextService.resolve({ record })), + new Map() + ), + [documents] + ); + + return { rootContext, dataSourceContext, documentContexts }; +}; From 89e2a6262a07eb094f69f002ccafd2d93286d0e4 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 14 May 2024 22:15:51 -0300 Subject: [PATCH 03/52] Add profile service --- .../context_awareness/composable_profile.ts | 53 ++++++++++++++ .../context_awareness/context_service.ts | 35 ---------- .../context_types/data_source_context.ts | 69 ------------------- .../context_types/document_context.ts | 52 -------------- .../context_types/root_context.ts | 69 ------------------- .../public/context_awareness/index.ts | 18 +++++ .../context_awareness/profile_service.ts | 37 ++++++++++ .../profiles/data_source_profile.ts | 34 +++++++++ .../profiles/document_profile.ts | 32 +++++++++ .../{context_types => profiles}/index.ts | 6 +- .../profiles/root_profile.ts | 31 +++++++++ 11 files changed, 208 insertions(+), 228 deletions(-) create mode 100644 src/plugins/discover/public/context_awareness/composable_profile.ts delete mode 100644 src/plugins/discover/public/context_awareness/context_service.ts delete mode 100644 src/plugins/discover/public/context_awareness/context_types/data_source_context.ts delete mode 100644 src/plugins/discover/public/context_awareness/context_types/document_context.ts delete mode 100644 src/plugins/discover/public/context_awareness/context_types/root_context.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_service.ts create mode 100644 src/plugins/discover/public/context_awareness/profiles/data_source_profile.ts create mode 100644 src/plugins/discover/public/context_awareness/profiles/document_profile.ts rename src/plugins/discover/public/context_awareness/{context_types => profiles}/index.ts (76%) create mode 100644 src/plugins/discover/public/context_awareness/profiles/root_profile.ts diff --git a/src/plugins/discover/public/context_awareness/composable_profile.ts b/src/plugins/discover/public/context_awareness/composable_profile.ts new file mode 100644 index 0000000000000..8ea22a3fed347 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/composable_profile.ts @@ -0,0 +1,53 @@ +/* + * 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 interface Profile { + getTopNavItems: (count: number) => TopNavItem[]; + getDefaultColumns: () => Column[]; + getFlyout: () => FlyoutComponent; +} + +export type PartialProfile = Partial; + +export type ComposableAccessor = (getPrevious: T) => T; + +export type ComposableProfile = { + [TKey in keyof TProfile]?: ComposableAccessor; +}; + +export const getMergedAccessor = < + TProfile extends PartialProfile, + TKey extends keyof ComposableProfile +>( + key: TKey, + baseProfile: TProfile +) => { + return (profiles: Array>) => { + return profiles.reduce((nextAccessor, profile) => { + const currentAccessor = profile[key]; + return currentAccessor ? currentAccessor(nextAccessor) : nextAccessor; + }, baseProfile[key]); + }; +}; + +// placeholders + +interface TopNavItem { + __brand: 'TopNavItem'; + name: string; +} + +interface Column { + __brand: 'Columns'; + name: string; +} + +interface FlyoutComponent { + __brand: 'FlyoutComponent'; + name: string; +} diff --git a/src/plugins/discover/public/context_awareness/context_service.ts b/src/plugins/discover/public/context_awareness/context_service.ts deleted file mode 100644 index 54d8ca9596c15..0000000000000 --- a/src/plugins/discover/public/context_awareness/context_service.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 interface ContextProvider { - order: number; - resolve: (params: TParams) => TContext | undefined; -} - -export abstract class ContextService { - private providers: Array> = []; - - public registerProvider(provider: ContextProvider) { - this.providers.push(provider); - this.providers.sort((a, b) => a.order - b.order); - } - - public resolve(params: TParams): TContext { - for (const provider of this.providers) { - const context = provider.resolve(params); - - if (context) { - return context; - } - } - - return this.getDefaultContext(); - } - - protected abstract getDefaultContext(): TContext; -} diff --git a/src/plugins/discover/public/context_awareness/context_types/data_source_context.ts b/src/plugins/discover/public/context_awareness/context_types/data_source_context.ts deleted file mode 100644 index 1385019c1805d..0000000000000 --- a/src/plugins/discover/public/context_awareness/context_types/data_source_context.ts +++ /dev/null @@ -1,69 +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 { DataView } from '@kbn/data-views-plugin/common'; -import { AggregateQuery, isOfAggregateQueryType, Query } from '@kbn/es-query'; -import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; -import { DataSourceType, DiscoverDataSource, isDataSourceType } from '../../../common/data_sources'; -import { ContextProvider, ContextService } from '../context_service'; - -export enum DataSourceCategory { - Logs = 'logs', - Default = 'default', -} - -export interface DataSourceContext { - category: DataSourceCategory; -} - -export interface DataSourceContextProviderParams { - dataSource?: DiscoverDataSource; - dataView?: DataView; - query?: Query | AggregateQuery; -} - -export type DataSourceContextProvider = ContextProvider< - DataSourceContextProviderParams, - DataSourceContext ->; - -export class DataSourceContextService extends ContextService< - DataSourceContextProviderParams, - DataSourceContext -> { - protected getDefaultContext(): DataSourceContext { - return { - category: DataSourceCategory.Default, - }; - } -} - -export const dataSourceContextService = new DataSourceContextService(); - -dataSourceContextService.registerProvider({ - order: 0, - resolve: (params) => { - let indices: string[] = []; - - if (isDataSourceType(params.dataSource, DataSourceType.Esql)) { - if (!isOfAggregateQueryType(params.query)) { - return; - } - - 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.includes('logs'))) { - return { - category: DataSourceCategory.Logs, - }; - } - }, -}); diff --git a/src/plugins/discover/public/context_awareness/context_types/document_context.ts b/src/plugins/discover/public/context_awareness/context_types/document_context.ts deleted file mode 100644 index 1e028f853501e..0000000000000 --- a/src/plugins/discover/public/context_awareness/context_types/document_context.ts +++ /dev/null @@ -1,52 +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 { DataTableRecord } from '@kbn/discover-utils'; -import { ContextProvider, ContextService } from '../context_service'; - -export enum DocumentType { - Log = 'log', - Default = 'default', -} - -export interface DocumentContext { - type: DocumentType; -} - -export interface DocumentContextProviderParams { - record: DataTableRecord; -} - -export type DocumentContextProvider = ContextProvider< - DocumentContextProviderParams, - DocumentContext ->; - -export class DocumentContextService extends ContextService< - DocumentContextProviderParams, - DocumentContext -> { - protected getDefaultContext(): DocumentContext { - return { - type: DocumentType.Default, - }; - } -} - -export const documentContextService = new DocumentContextService(); - -documentContextService.registerProvider({ - order: 0, - resolve: (params) => { - if ('message' in params.record.flattened) { - return { - type: DocumentType.Log, - }; - } - }, -}); diff --git a/src/plugins/discover/public/context_awareness/context_types/root_context.ts b/src/plugins/discover/public/context_awareness/context_types/root_context.ts deleted file mode 100644 index 073c10cc53bd1..0000000000000 --- a/src/plugins/discover/public/context_awareness/context_types/root_context.ts +++ /dev/null @@ -1,69 +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 { ContextProvider, ContextService } from '../context_service'; - -export enum SolutionType { - Observability = 'oblt', - Security = 'security', - Search = 'search', - Default = 'default', -} - -export interface RootContext { - solutionType: SolutionType; -} - -export interface RootContextProviderParams { - solutionNavId?: string | null; -} - -export type RootContextProvider = ContextProvider; - -export class RootContextService extends ContextService { - protected getDefaultContext(): RootContext { - return { - solutionType: SolutionType.Default, - }; - } -} - -export const rootContextService = new RootContextService(); - -rootContextService.registerProvider({ - order: 0, - resolve: (params) => { - if (params.solutionNavId === 'es') { - return { - solutionType: SolutionType.Search, - }; - } - }, -}); - -rootContextService.registerProvider({ - order: 100, - resolve: (params) => { - if (params.solutionNavId === 'oblt') { - return { - solutionType: SolutionType.Observability, - }; - } - }, -}); - -rootContextService.registerProvider({ - order: 200, - resolve: (params) => { - if (params.solutionNavId === 'security') { - return { - solutionType: SolutionType.Security, - }; - } - }, -}); diff --git a/src/plugins/discover/public/context_awareness/index.ts b/src/plugins/discover/public/context_awareness/index.ts index d96417fe03c46..96cf09d0dca85 100644 --- a/src/plugins/discover/public/context_awareness/index.ts +++ b/src/plugins/discover/public/context_awareness/index.ts @@ -6,4 +6,22 @@ * Side Public License, v 1. */ +import { getMergedAccessor, Profile } from './composable_profile'; +import { documentProfileService, rootProfileService } from './profiles'; + export { useContextAwareness } from './use_context_awareness'; + +const rootProfile = rootProfileService.resolve(); +const dataSourceProfile = rootProfileService.resolve(); +const documentProfile = documentProfileService.resolve(); + +const baseProfile: Profile = { + getTopNavItems: (count: number) => [], + getDefaultColumns: () => [], + getFlyout: () => null, +}; + +const res = getMergedAccessor( + 'getFlyout', + baseProfile +)([rootProfile, dataSourceProfile, documentProfile]); diff --git a/src/plugins/discover/public/context_awareness/profile_service.ts b/src/plugins/discover/public/context_awareness/profile_service.ts new file mode 100644 index 0000000000000..61a94aa846b9a --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_service.ts @@ -0,0 +1,37 @@ +/* + * 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 { ComposableProfile, PartialProfile } from './composable_profile'; + +export interface ProfileProvider { + order: number; + profile: ComposableProfile; + resolve: (params: TParams) => { isMatch: true; context: TContext } | { isMatch: false }; +} + +export class ProfileService { + private static readonly EMPTY_PROFILE = {}; + private readonly providers: Array> = []; + + public registerProvider(provider: ProfileProvider) { + this.providers.push(provider); + this.providers.sort((a, b) => a.order - b.order); + } + + public resolve(params: TParams): ComposableProfile { + for (const provider of this.providers) { + const result = provider.resolve(params); + + if (result.isMatch) { + return provider.profile; + } + } + + return ProfileService.EMPTY_PROFILE; + } +} 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 new file mode 100644 index 0000000000000..66285e1053ef7 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profiles/data_source_profile.ts @@ -0,0 +1,34 @@ +/* + * 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 { DataView } from '@kbn/data-views-plugin/common'; +import { AggregateQuery, Query } from '@kbn/es-query'; +import { DiscoverDataSource } from '../../../common/data_sources'; +import { Profile } from '../composable_profile'; +import { ProfileService } from '../profile_service'; + +export enum DataSourceCategory { + Logs = 'logs', + Default = 'default', +} + +export interface DataSourceProfileProviderParams { + dataSource?: DiscoverDataSource; + dataView?: DataView; + query?: Query | AggregateQuery; +} + +export interface DataSourceContext { + category: DataSourceCategory; +} + +export const dataSourceProfileService = new ProfileService< + Profile, + DataSourceProfileProviderParams, + DataSourceContext +>(); diff --git a/src/plugins/discover/public/context_awareness/profiles/document_profile.ts b/src/plugins/discover/public/context_awareness/profiles/document_profile.ts new file mode 100644 index 0000000000000..b7c0816afdc06 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profiles/document_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 { DataTableRecord } from '@kbn/discover-utils'; +import { Profile } from '../composable_profile'; +import { ProfileService } from '../profile_service'; + +export enum DocumentType { + Log = 'log', + Default = 'default', +} + +export interface DocumentProfileProviderParams { + record: DataTableRecord; +} + +export interface DocumentContext { + type: DocumentType; +} + +export type DocumentProfile = Pick; + +export const documentProfileService = new ProfileService< + DocumentProfile, + DocumentProfileProviderParams, + DocumentContext +>(); diff --git a/src/plugins/discover/public/context_awareness/context_types/index.ts b/src/plugins/discover/public/context_awareness/profiles/index.ts similarity index 76% rename from src/plugins/discover/public/context_awareness/context_types/index.ts rename to src/plugins/discover/public/context_awareness/profiles/index.ts index 362179200ff8b..f661276b4a04c 100644 --- a/src/plugins/discover/public/context_awareness/context_types/index.ts +++ b/src/plugins/discover/public/context_awareness/profiles/index.ts @@ -6,6 +6,6 @@ * Side Public License, v 1. */ -export * from './root_context'; -export * from './data_source_context'; -export * from './document_context'; +export * from './root_profile'; +export * from './data_source_profile'; +export * from './document_profile'; diff --git a/src/plugins/discover/public/context_awareness/profiles/root_profile.ts b/src/plugins/discover/public/context_awareness/profiles/root_profile.ts new file mode 100644 index 0000000000000..0333d2d1b26ac --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profiles/root_profile.ts @@ -0,0 +1,31 @@ +/* + * 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 { Profile } from '../composable_profile'; +import { ProfileService } from '../profile_service'; + +export enum SolutionType { + Observability = 'oblt', + Security = 'security', + Search = 'search', + Default = 'default', +} + +export interface RootProfileProviderParams { + solutionNavId?: string | null; +} + +export interface RootContext { + solutionType: SolutionType; +} + +export const rootProfileService = new ProfileService< + Profile, + RootProfileProviderParams, + RootContext +>(); From 3cd00c507c0c44ed14f604624669d1bdbea820e7 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 14 May 2024 23:49:58 -0300 Subject: [PATCH 04/52] Add use_profile_accessor --- .../components/layout/discover_documents.tsx | 138 ++++++++---------- .../components/top_nav/use_discover_topnav.ts | 8 +- .../context_awareness/composable_profile.ts | 24 ++- .../public/context_awareness/index.ts | 20 +-- .../context_awareness/profile_service.ts | 11 +- .../profiles/data_source_profile.ts | 44 +++++- .../profiles/root_profile.ts | 29 ++++ ..._awareness.tsx => use_profile_accessor.ts} | 39 +++-- 8 files changed, 178 insertions(+), 135 deletions(-) rename src/plugins/discover/public/context_awareness/{use_context_awareness.tsx => use_profile_accessor.ts} (57%) diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index 356f0720f50f4..c73e2b334195f 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -68,7 +68,6 @@ import { onResizeGridColumn } from '../../../../utils/on_resize_grid_column'; import { useContextualGridCustomisations } from '../../hooks/grid_customisations'; import { useIsEsqlMode } from '../../hooks/use_is_esql_mode'; import { useAdditionalFieldGroups } from '../../hooks/sidebar/use_additional_field_groups'; -import { useContextAwareness } from '../../../../context_awareness'; const containerStyles = css` position: relative; @@ -320,8 +319,6 @@ function DiscoverDocumentsComponent({ [viewModeToggle, callouts, gridAnnouncementCallout, loadingIndicator] ); - const { rootContext, dataSourceContext, documentContexts } = useContextAwareness(); - if (isDataViewLoading || (isEmptyDataResult && isDataLoading)) { return (
@@ -376,85 +373,64 @@ function DiscoverDocumentsComponent({ )} {!isLegacy && ( - <> -
- - Solution: {rootContext.solutionType} - {' '} - - Data source: {dataSourceContext.category} - -
-
- - { - return ( - <> - {documentContexts.get(props.row.id)?.type ?? 'unknown'} -
-
{JSON.stringify(props.row.flattened)}
- - ); - }, - }} - customGridColumnsConfiguration={customGridColumnsConfiguration} - customControlColumnsConfiguration={customControlColumnsConfiguration} +
+ + - -
- + /> +
+
)} diff --git a/src/plugins/discover/public/application/main/components/top_nav/use_discover_topnav.ts b/src/plugins/discover/public/application/main/components/top_nav/use_discover_topnav.ts index 06efd3205d1be..54634e4dc7940 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/use_discover_topnav.ts +++ b/src/plugins/discover/public/application/main/components/top_nav/use_discover_topnav.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import useObservable from 'react-use/lib/useObservable'; +import { useProfileAccessor } from '../../../../context_awareness'; import { useDiscoverCustomization } from '../../../../customizations'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { useInspector } from '../../hooks/use_inspector'; @@ -48,7 +49,7 @@ export const useDiscoverTopNav = ({ stateContainer, }); - const topNavMenu = useMemo( + const baseGetTopNavMenu = useCallback( () => getTopNavLinks({ dataView, @@ -70,6 +71,9 @@ export const useDiscoverTopNav = ({ ] ); + const getTopNavMenu = useProfileAccessor('getTopNavItems', baseGetTopNavMenu); + const topNavMenu = useMemo(() => getTopNavMenu(), [getTopNavMenu]); + return { topNavMenu, topNavBadges, diff --git a/src/plugins/discover/public/context_awareness/composable_profile.ts b/src/plugins/discover/public/context_awareness/composable_profile.ts index 8ea22a3fed347..e48c84e72e597 100644 --- a/src/plugins/discover/public/context_awareness/composable_profile.ts +++ b/src/plugins/discover/public/context_awareness/composable_profile.ts @@ -6,8 +6,10 @@ * Side Public License, v 1. */ +import { TopNavMenuData } from '@kbn/navigation-plugin/public'; + export interface Profile { - getTopNavItems: (count: number) => TopNavItem[]; + getTopNavItems: () => TopNavMenuData[]; getDefaultColumns: () => Column[]; getFlyout: () => FlyoutComponent; } @@ -16,23 +18,19 @@ export type PartialProfile = Partial; export type ComposableAccessor = (getPrevious: T) => T; -export type ComposableProfile = { +export type ComposableProfile = { [TKey in keyof TProfile]?: ComposableAccessor; }; -export const getMergedAccessor = < - TProfile extends PartialProfile, - TKey extends keyof ComposableProfile ->( +export const getMergedAccessor = ( + profiles: ComposableProfile[], key: TKey, - baseProfile: TProfile + defaultImplementation: Profile[TKey] ) => { - return (profiles: Array>) => { - return profiles.reduce((nextAccessor, profile) => { - const currentAccessor = profile[key]; - return currentAccessor ? currentAccessor(nextAccessor) : nextAccessor; - }, baseProfile[key]); - }; + return profiles.reduce((nextAccessor, profile) => { + const currentAccessor = profile[key]; + return currentAccessor ? currentAccessor(nextAccessor) : nextAccessor; + }, defaultImplementation); }; // placeholders diff --git a/src/plugins/discover/public/context_awareness/index.ts b/src/plugins/discover/public/context_awareness/index.ts index 96cf09d0dca85..15ed90b922f28 100644 --- a/src/plugins/discover/public/context_awareness/index.ts +++ b/src/plugins/discover/public/context_awareness/index.ts @@ -6,22 +6,4 @@ * Side Public License, v 1. */ -import { getMergedAccessor, Profile } from './composable_profile'; -import { documentProfileService, rootProfileService } from './profiles'; - -export { useContextAwareness } from './use_context_awareness'; - -const rootProfile = rootProfileService.resolve(); -const dataSourceProfile = rootProfileService.resolve(); -const documentProfile = documentProfileService.resolve(); - -const baseProfile: Profile = { - getTopNavItems: (count: number) => [], - getDefaultColumns: () => [], - getFlyout: () => null, -}; - -const res = getMergedAccessor( - 'getFlyout', - baseProfile -)([rootProfile, dataSourceProfile, documentProfile]); +export { useProfileAccessor } from './use_profile_accessor'; diff --git a/src/plugins/discover/public/context_awareness/profile_service.ts b/src/plugins/discover/public/context_awareness/profile_service.ts index 61a94aa846b9a..620e497063c7d 100644 --- a/src/plugins/discover/public/context_awareness/profile_service.ts +++ b/src/plugins/discover/public/context_awareness/profile_service.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ComposableProfile, PartialProfile } from './composable_profile'; +import { ComposableProfile, PartialProfile, Profile } from './composable_profile'; export interface ProfileProvider { order: number; @@ -14,8 +14,9 @@ export interface ProfileProvider { isMatch: true; context: TContext } | { isMatch: false }; } +const EMPTY_PROFILE = {}; + export class ProfileService { - private static readonly EMPTY_PROFILE = {}; private readonly providers: Array> = []; public registerProvider(provider: ProfileProvider) { @@ -23,15 +24,15 @@ export class ProfileService this.providers.sort((a, b) => a.order - b.order); } - public resolve(params: TParams): ComposableProfile { + public resolve(params: TParams): ComposableProfile { for (const provider of this.providers) { const result = provider.resolve(params); if (result.isMatch) { - return provider.profile; + return provider.profile as ComposableProfile; } } - return ProfileService.EMPTY_PROFILE; + return EMPTY_PROFILE; } } 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 66285e1053ef7..c1d5476d2b7c0 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 @@ -7,8 +7,9 @@ */ import { DataView } from '@kbn/data-views-plugin/common'; -import { AggregateQuery, Query } from '@kbn/es-query'; -import { DiscoverDataSource } from '../../../common/data_sources'; +import { AggregateQuery, isOfAggregateQueryType, Query } from '@kbn/es-query'; +import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; +import { DataSourceType, DiscoverDataSource, isDataSourceType } from '../../../common/data_sources'; import { Profile } from '../composable_profile'; import { ProfileService } from '../profile_service'; @@ -32,3 +33,42 @@ export const dataSourceProfileService = new ProfileService< DataSourceProfileProviderParams, DataSourceContext >(); + +dataSourceProfileService.registerProvider({ + order: 0, + profile: { + getTopNavItems: (prev) => () => + [ + { + id: 'logs-data-source-entry', + label: 'Logs data source entry', + run: () => { + alert('HELLO WORLD'); + }, + }, + ...prev(), + ], + }, + 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.includes('logs'))) { + return { + isMatch: true, + context: { category: DataSourceCategory.Logs }, + }; + } + + return { isMatch: false }; + }, +}); 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 0333d2d1b26ac..0307beb770d35 100644 --- a/src/plugins/discover/public/context_awareness/profiles/root_profile.ts +++ b/src/plugins/discover/public/context_awareness/profiles/root_profile.ts @@ -29,3 +29,32 @@ export const rootProfileService = new ProfileService< RootProfileProviderParams, RootContext >(); + +rootProfileService.registerProvider({ + order: 0, + profile: { + getTopNavItems: (prev) => () => + [ + { + id: 'o11y-root-entry', + label: 'O11y project entry', + run: () => { + alert('HELLO WORLD'); + }, + }, + ...prev(), + ], + }, + resolve: (params) => { + if (params.solutionNavId === 'oblt') { + return { + isMatch: true, + context: { + solutionType: SolutionType.Observability, + }, + }; + } + + return { isMatch: false }; + }, +}); diff --git a/src/plugins/discover/public/context_awareness/use_context_awareness.tsx b/src/plugins/discover/public/context_awareness/use_profile_accessor.ts similarity index 57% rename from src/plugins/discover/public/context_awareness/use_context_awareness.tsx rename to src/plugins/discover/public/context_awareness/use_profile_accessor.ts index 6603222824a8e..729a3893530eb 100644 --- a/src/plugins/discover/public/context_awareness/use_context_awareness.tsx +++ b/src/plugins/discover/public/context_awareness/use_profile_accessor.ts @@ -6,29 +6,30 @@ * Side Public License, v 1. */ +import { DataTableRecord } from '@kbn/discover-utils'; import { useMemo, useState } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { useAppStateSelector } from '../application/main/state_management/discover_app_state_container'; import { useInternalStateSelector } from '../application/main/state_management/discover_internal_state_container'; import { useContainer } from '../application/main/state_management/discover_state_provider'; import { useDiscoverServices } from '../hooks/use_discover_services'; -import { - dataSourceContextService, - DocumentContext, - documentContextService, - rootContextService, -} from './context_types'; +import { ComposableProfile, getMergedAccessor, Profile } from './composable_profile'; +import { dataSourceProfileService, documentProfileService, rootProfileService } from './profiles'; -export const useContextAwareness = () => { +export const useProfileAccessor = ( + key: TKey, + defaultImplementation: Profile[TKey], + { record }: { record?: DataTableRecord } = {} +) => { const { chrome } = useDiscoverServices(); const [solutionNavId$] = useState(() => chrome.getActiveSolutionNavId$()); const solutionNavId = useObservable(solutionNavId$); - const rootContext = useMemo(() => rootContextService.resolve({ solutionNavId }), [solutionNavId]); + const rootProfile = useMemo(() => rootProfileService.resolve({ solutionNavId }), [solutionNavId]); const dataSource = useAppStateSelector((state) => state.dataSource); const dataView = useInternalStateSelector((state) => state.dataView); const query = useAppStateSelector((state) => state.query); - const dataSourceContext = useMemo( - () => dataSourceContextService.resolve({ dataSource, dataView, query }), + const dataSourceProfile = useMemo( + () => dataSourceProfileService.resolve({ dataSource, dataView, query }), [dataSource, dataView, query] ); const stateContainer = useContainer(); @@ -36,11 +37,23 @@ export const useContextAwareness = () => { const documentContexts = useMemo( () => (documents?.result ?? []).reduce( - (map, record) => map.set(record.id, documentContextService.resolve({ record })), - new Map() + (map, curr) => map.set(curr.id, documentProfileService.resolve({ record: curr })), + new Map() ), [documents] ); - return { rootContext, dataSourceContext, documentContexts }; + return useMemo(() => { + const profiles = [rootProfile, dataSourceProfile]; + + if (record) { + const documentContext = documentContexts.get(record.id); + + if (documentContext) { + profiles.push(documentContext); + } + } + + return getMergedAccessor(profiles, key, defaultImplementation); + }, [rootProfile, dataSourceProfile, record, key, defaultImplementation, documentContexts]); }; From b9519d4f5dc9e0850bf9271b0d8a98a641b8d6c5 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 15 May 2024 01:06:45 -0300 Subject: [PATCH 05/52] Add custom cell renderers and grid flyout --- .../components/layout/discover_documents.tsx | 11 ++++- .../discover_grid_flyout.tsx | 31 +++++++++++--- .../context_awareness/composable_profile.ts | 27 +++--------- ...rce_profile.ts => data_source_profile.tsx} | 41 +++++++++++++++++++ ...cument_profile.ts => document_profile.tsx} | 24 ++++++++++- .../context_awareness/use_profile_accessor.ts | 6 +-- 6 files changed, 108 insertions(+), 32 deletions(-) rename src/plugins/discover/public/context_awareness/profiles/{data_source_profile.ts => data_source_profile.tsx} (60%) rename src/plugins/discover/public/context_awareness/profiles/{document_profile.ts => document_profile.tsx} (62%) diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index c73e2b334195f..d8b23f8537b5f 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -68,6 +68,7 @@ import { onResizeGridColumn } from '../../../../utils/on_resize_grid_column'; import { useContextualGridCustomisations } from '../../hooks/grid_customisations'; import { useIsEsqlMode } from '../../hooks/use_is_esql_mode'; import { useAdditionalFieldGroups } from '../../hooks/sidebar/use_additional_field_groups'; +import { useProfileAccessor } from '../../../../context_awareness'; const containerStyles = css` position: relative; @@ -263,6 +264,14 @@ function DiscoverDocumentsComponent({ useContextualGridCustomisations() || {}; const additionalFieldGroups = useAdditionalFieldGroups(); + const baseGetCellRenderers = useCallback( + () => externalCustomRenderers ?? {}, + [externalCustomRenderers] + ); + + const getCellRenderers = useProfileAccessor('getCellRenderers', baseGetCellRenderers); + const cellRenderers = useMemo(() => getCellRenderers(), [getCellRenderers]); + const documents = useObservable(stateContainer.dataState.data$.documents$); const callouts = useMemo( @@ -424,7 +433,7 @@ function DiscoverDocumentsComponent({ totalHits={totalHits} onFetchMoreRecords={onFetchMoreRecords} componentsTourSteps={TOUR_STEPS} - externalCustomRenderers={externalCustomRenderers} + externalCustomRenderers={cellRenderers} customGridColumnsConfiguration={customGridColumnsConfiguration} customControlColumnsConfiguration={customControlColumnsConfiguration} additionalFieldGroups={additionalFieldGroups} diff --git a/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.tsx b/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.tsx index d6273d7669391..b0b8c3878035e 100644 --- a/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.tsx +++ b/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.tsx @@ -33,10 +33,12 @@ import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; import type { DataTableColumnsMeta } from '@kbn/unified-data-table'; import { UnifiedDocViewer } from '@kbn/unified-doc-viewer-plugin/public'; import useLocalStorage from 'react-use/lib/useLocalStorage'; +import { DocViewsRegistry } from '@kbn/unified-doc-viewer'; import { useDiscoverServices } from '../../hooks/use_discover_services'; import { useFlyoutActions } from './use_flyout_actions'; import { useDiscoverCustomization } from '../../customizations'; import { DiscoverGridFlyoutActions } from './discover_grid_flyout_actions'; +import { useProfileAccessor } from '../../context_awareness'; export interface DiscoverGridFlyoutProps { savedSearchId?: string; @@ -161,6 +163,23 @@ export function DiscoverGridFlyout({ [onRemoveColumn, services.toastNotifications] ); + const baseGetDocViewsRegistry = useCallback( + (registry: DocViewsRegistry) => { + return typeof flyoutCustomization?.docViewsRegistry === 'function' + ? flyoutCustomization.docViewsRegistry(registry) + : registry; + }, + [flyoutCustomization] + ); + + const getDocViewsRegistry = useProfileAccessor('getDocViewsRegistry', baseGetDocViewsRegistry, { + record: actualHit, + }); + const docViewsRegistry = useCallback( + (registry: DocViewsRegistry) => getDocViewsRegistry(registry), + [getDocViewsRegistry] + ); + const renderDefaultContent = useCallback( () => ( ), [ - actualHit, - addColumn, columns, columnsMeta, dataView, - hits, - isEsqlQuery, onFilter, + actualHit, + addColumn, removeColumn, - flyoutCustomization?.docViewsRegistry, + isEsqlQuery, + hits, + docViewsRegistry, ] ); diff --git a/src/plugins/discover/public/context_awareness/composable_profile.ts b/src/plugins/discover/public/context_awareness/composable_profile.ts index e48c84e72e597..731aa64f55a4b 100644 --- a/src/plugins/discover/public/context_awareness/composable_profile.ts +++ b/src/plugins/discover/public/context_awareness/composable_profile.ts @@ -7,11 +7,13 @@ */ import { TopNavMenuData } from '@kbn/navigation-plugin/public'; +import { CustomCellRenderer } from '@kbn/unified-data-table'; +import { DocViewsRegistry } from '@kbn/unified-doc-viewer'; export interface Profile { getTopNavItems: () => TopNavMenuData[]; - getDefaultColumns: () => Column[]; - getFlyout: () => FlyoutComponent; + getCellRenderers: () => CustomCellRenderer; + getDocViewsRegistry: (prevRegistry: DocViewsRegistry) => DocViewsRegistry; } export type PartialProfile = Partial; @@ -25,27 +27,10 @@ export type ComposableProfile = { export const getMergedAccessor = ( profiles: ComposableProfile[], key: TKey, - defaultImplementation: Profile[TKey] + baseImpl: Profile[TKey] ) => { return profiles.reduce((nextAccessor, profile) => { const currentAccessor = profile[key]; return currentAccessor ? currentAccessor(nextAccessor) : nextAccessor; - }, defaultImplementation); + }, baseImpl); }; - -// placeholders - -interface TopNavItem { - __brand: 'TopNavItem'; - name: string; -} - -interface Column { - __brand: 'Columns'; - name: string; -} - -interface FlyoutComponent { - __brand: 'FlyoutComponent'; - name: string; -} 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.tsx similarity index 60% rename from src/plugins/discover/public/context_awareness/profiles/data_source_profile.ts rename to src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx index c1d5476d2b7c0..0bee77c945d8b 100644 --- a/src/plugins/discover/public/context_awareness/profiles/data_source_profile.ts +++ b/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx @@ -9,6 +9,7 @@ import { DataView } from '@kbn/data-views-plugin/common'; import { AggregateQuery, isOfAggregateQueryType, Query } from '@kbn/es-query'; import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; +import React from 'react'; import { DataSourceType, DiscoverDataSource, isDataSourceType } from '../../../common/data_sources'; import { Profile } from '../composable_profile'; import { ProfileService } from '../profile_service'; @@ -48,6 +49,46 @@ dataSourceProfileService.registerProvider({ }, ...prev(), ], + getCellRenderers: (prev) => () => ({ + ...prev(), + ['@timestamp']: (props) => { + const date = new Date((props.row.flattened['@timestamp'] as string[])[0]); + + return ( + <> + {date.getFullYear()}- + {date.getMonth() + 1}- + {date.getDate()}{' '} + + {date.getHours()}:{date.getMinutes()}:{date.getSeconds()} + + + ); + }, + timestamp: (props) => { + const date = new Date((props.row.flattened['@timestamp'] as string[])[0]); + + return ( + <> + {date.getFullYear()}- + {date.getMonth() + 1}- + {date.getDate()}{' '} + + {date.getHours()}:{date.getMinutes()}:{date.getSeconds()} + + + ); + }, + message: (props) => { + const message = (props.row.flattened.message as string[])[0]; + + return ( +
+ {message} +
+ ); + }, + }), }, resolve: (params) => { let indices: string[] = []; diff --git a/src/plugins/discover/public/context_awareness/profiles/document_profile.ts b/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx similarity index 62% rename from src/plugins/discover/public/context_awareness/profiles/document_profile.ts rename to src/plugins/discover/public/context_awareness/profiles/document_profile.tsx index b7c0816afdc06..932b8a92a12e9 100644 --- a/src/plugins/discover/public/context_awareness/profiles/document_profile.ts +++ b/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx @@ -23,10 +23,32 @@ export interface DocumentContext { type: DocumentType; } -export type DocumentProfile = Pick; +export type DocumentProfile = Pick; export const documentProfileService = new ProfileService< DocumentProfile, DocumentProfileProviderParams, DocumentContext >(); + +documentProfileService.registerProvider({ + order: 0, + profile: { + getDocViewsRegistry: (prev) => (registry) => { + registry.enableById('doc_view_logs_overview'); + return prev(registry); + }, + }, + resolve: (params) => { + if ('message' in params.record.flattened) { + return { + isMatch: true, + context: { + type: DocumentType.Log, + }, + }; + } + + return { isMatch: false }; + }, +}); diff --git a/src/plugins/discover/public/context_awareness/use_profile_accessor.ts b/src/plugins/discover/public/context_awareness/use_profile_accessor.ts index 729a3893530eb..a5a8af09a8ce5 100644 --- a/src/plugins/discover/public/context_awareness/use_profile_accessor.ts +++ b/src/plugins/discover/public/context_awareness/use_profile_accessor.ts @@ -18,7 +18,7 @@ import { dataSourceProfileService, documentProfileService, rootProfileService } export const useProfileAccessor = ( key: TKey, - defaultImplementation: Profile[TKey], + baseImpl: Profile[TKey], { record }: { record?: DataTableRecord } = {} ) => { const { chrome } = useDiscoverServices(); @@ -54,6 +54,6 @@ export const useProfileAccessor = ( } } - return getMergedAccessor(profiles, key, defaultImplementation); - }, [rootProfile, dataSourceProfile, record, key, defaultImplementation, documentContexts]); + return getMergedAccessor(profiles, key, baseImpl); + }, [rootProfile, dataSourceProfile, record, key, baseImpl, documentContexts]); }; From ac6f2e55abc83ae0b5fef03c4a44bcfad096bccc Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 15 May 2024 22:18:40 -0300 Subject: [PATCH 06/52] Attach document profiles to data table records --- .../src/utils/build_data_record.ts | 10 +++++--- .../main/data_fetching/fetch_documents.ts | 12 ++++++++- .../main/data_fetching/fetch_esql.ts | 24 ++++++++++++------ .../discover_state_provider.tsx | 2 -- .../public/context_awareness/index.ts | 2 ++ .../profiles/document_profile.tsx | 12 ++++++++- .../public/context_awareness/types.ts | 15 +++++++++++ .../context_awareness/use_profile_accessor.ts | 25 ++++--------------- .../log_ai_assistant/log_ai_assistant.tsx | 3 ++- 9 files changed, 70 insertions(+), 35 deletions(-) create mode 100644 src/plugins/discover/public/context_awareness/types.ts diff --git a/packages/kbn-discover-utils/src/utils/build_data_record.ts b/packages/kbn-discover-utils/src/utils/build_data_record.ts index 43adf7b9c8b66..9769201e94aa4 100644 --- a/packages/kbn-discover-utils/src/utils/build_data_record.ts +++ b/packages/kbn-discover-utils/src/utils/build_data_record.ts @@ -35,9 +35,13 @@ export function buildDataTableRecord( * @param docs Array of documents returned from Elasticsearch * @param dataView this current data view */ -export function buildDataTableRecordList( +export function buildDataTableRecordList( docs: EsHitRecord[], - dataView?: DataView + dataView?: DataView, + { processRecord }: { processRecord?: (record: DataTableRecord) => T } = {} ): DataTableRecord[] { - return docs.map((doc) => buildDataTableRecord(doc, dataView)); + return docs.map((doc) => { + const record = buildDataTableRecord(doc, dataView); + return processRecord ? processRecord(record) : record; + }); } diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts index 414d4b3a36587..615604991199c 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts @@ -16,6 +16,7 @@ import { DataViewType } from '@kbn/data-views-plugin/public'; import type { RecordsFetchResponse } from '../../types'; import { getAllowedSampleSize } from '../../../utils/get_allowed_sample_size'; import { FetchDeps } from './fetch_all'; +import { DataTableRecordWithProfile, documentProfileService } from '../../../context_awareness'; /** * Requests the documents for Discover. This will return a promise that will resolve @@ -67,7 +68,16 @@ export const fetchDocuments = ( .pipe( filter((res) => !isRunningResponse(res)), map((res) => { - return buildDataTableRecordList(res.rawResponse.hits.hits as EsHitRecord[], dataView); + return buildDataTableRecordList( + res.rawResponse.hits.hits as EsHitRecord[], + dataView, + { + processRecord: (record) => { + const profile = documentProfileService.resolve({ record }); + return { ...record, profile }; + }, + } + ); }) ); diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts index 3aba795d26920..341207ae4bbb3 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts @@ -15,8 +15,9 @@ import type { ExpressionsStart } from '@kbn/expressions-plugin/public'; import type { Datatable } from '@kbn/expressions-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; import { textBasedQueryStateToAstWithValidation } from '@kbn/data-plugin/common'; -import type { DataTableRecord } from '@kbn/discover-utils/types'; +import { DataTableRecord } from '@kbn/discover-utils'; import type { RecordsFetchResponse } from '../../types'; +import { DataTableRecordWithProfile, documentProfileService } from '../../../context_awareness'; interface EsqlErrorResponse { error: { @@ -56,7 +57,7 @@ export function fetchEsql( }); abortSignal?.addEventListener('abort', contract.cancel); const execution = contract.getData(); - let finalData: DataTableRecord[] = []; + let finalData: DataTableRecordWithProfile[] = []; let esqlQueryColumns: Datatable['columns'] | undefined; let error: string | undefined; let esqlHeaderWarning: string | undefined; @@ -69,12 +70,21 @@ export function fetchEsql( const rows = table?.rows ?? []; esqlQueryColumns = table?.columns ?? undefined; esqlHeaderWarning = table.warning ?? undefined; - finalData = rows.map((row: Record, idx: number) => { - return { + finalData = rows.map((row, idx) => { + const record: DataTableRecord = { id: String(idx), - raw: row, + raw: { + _id: '', + _index: '', + ...row, + }, flattened: row, - } as unknown as DataTableRecord; + }; + + return { + ...record, + profile: documentProfileService.resolve({ record }), + }; }); } }); @@ -91,7 +101,7 @@ export function fetchEsql( }); } return { - records: [] as DataTableRecord[], + records: [], esqlQueryColumns: [], esqlHeaderWarning: undefined, }; diff --git a/src/plugins/discover/public/application/main/state_management/discover_state_provider.tsx b/src/plugins/discover/public/application/main/state_management/discover_state_provider.tsx index 003d559499553..17d9cfbe24bce 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_state_provider.tsx +++ b/src/plugins/discover/public/application/main/state_management/discover_state_provider.tsx @@ -40,7 +40,6 @@ function createStateHelpers() { return { Provider: context.Provider, - useContainer, useSavedSearch, useSavedSearchInitial, useSavedSearchHasChanged, @@ -49,7 +48,6 @@ function createStateHelpers() { export const { Provider: DiscoverStateProvider, - useContainer, useSavedSearchInitial, useSavedSearch, useSavedSearchHasChanged, diff --git a/src/plugins/discover/public/context_awareness/index.ts b/src/plugins/discover/public/context_awareness/index.ts index 15ed90b922f28..3451d0498f2ed 100644 --- a/src/plugins/discover/public/context_awareness/index.ts +++ b/src/plugins/discover/public/context_awareness/index.ts @@ -6,4 +6,6 @@ * Side Public License, v 1. */ +export * from './types'; +export * from './profiles'; export { useProfileAccessor } from './use_profile_accessor'; diff --git a/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx b/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx index 932b8a92a12e9..9f3fbaf06831c 100644 --- a/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx +++ b/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx @@ -9,6 +9,7 @@ import { DataTableRecord } from '@kbn/discover-utils'; import { Profile } from '../composable_profile'; import { ProfileService } from '../profile_service'; +import { DataTableRecordWithProfile } from '../types'; export enum DocumentType { Log = 'log', @@ -31,6 +32,12 @@ export const documentProfileService = new ProfileService< DocumentContext >(); +export const recordHasProfile = ( + record?: DataTableRecord +): record is DataTableRecordWithProfile => { + return Boolean(record && 'profile' in record); +}; + documentProfileService.registerProvider({ order: 0, profile: { @@ -40,7 +47,10 @@ documentProfileService.registerProvider({ }, }, resolve: (params) => { - if ('message' in params.record.flattened) { + if ( + 'message' in params.record.flattened && + typeof params.record.flattened.message === 'string' + ) { return { isMatch: true, context: { diff --git a/src/plugins/discover/public/context_awareness/types.ts b/src/plugins/discover/public/context_awareness/types.ts new file mode 100644 index 0000000000000..9e80861dd0741 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/types.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 type { ComposableProfile } from './composable_profile'; +import type { DocumentProfile } from './profiles'; + +export interface DataTableRecordWithProfile extends DataTableRecord { + profile: ComposableProfile; +} diff --git a/src/plugins/discover/public/context_awareness/use_profile_accessor.ts b/src/plugins/discover/public/context_awareness/use_profile_accessor.ts index a5a8af09a8ce5..cda0d40851b06 100644 --- a/src/plugins/discover/public/context_awareness/use_profile_accessor.ts +++ b/src/plugins/discover/public/context_awareness/use_profile_accessor.ts @@ -11,10 +11,9 @@ import { useMemo, useState } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { useAppStateSelector } from '../application/main/state_management/discover_app_state_container'; import { useInternalStateSelector } from '../application/main/state_management/discover_internal_state_container'; -import { useContainer } from '../application/main/state_management/discover_state_provider'; import { useDiscoverServices } from '../hooks/use_discover_services'; -import { ComposableProfile, getMergedAccessor, Profile } from './composable_profile'; -import { dataSourceProfileService, documentProfileService, rootProfileService } from './profiles'; +import { getMergedAccessor, Profile } from './composable_profile'; +import { dataSourceProfileService, recordHasProfile, rootProfileService } from './profiles'; export const useProfileAccessor = ( key: TKey, @@ -32,28 +31,14 @@ export const useProfileAccessor = ( () => dataSourceProfileService.resolve({ dataSource, dataView, query }), [dataSource, dataView, query] ); - const stateContainer = useContainer(); - const documents = useObservable(stateContainer?.dataState.data$.documents$!); - const documentContexts = useMemo( - () => - (documents?.result ?? []).reduce( - (map, curr) => map.set(curr.id, documentProfileService.resolve({ record: curr })), - new Map() - ), - [documents] - ); return useMemo(() => { const profiles = [rootProfile, dataSourceProfile]; - if (record) { - const documentContext = documentContexts.get(record.id); - - if (documentContext) { - profiles.push(documentContext); - } + if (recordHasProfile(record)) { + profiles.push(record.profile); } return getMergedAccessor(profiles, key, baseImpl); - }, [rootProfile, dataSourceProfile, record, key, baseImpl, documentContexts]); + }, [rootProfile, dataSourceProfile, record, key, baseImpl]); }; diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/log_ai_assistant/log_ai_assistant.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/log_ai_assistant/log_ai_assistant.tsx index 3e1b6fced3337..49d4fbc01c2e4 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/components/log_ai_assistant/log_ai_assistant.tsx +++ b/x-pack/plugins/observability_solution/logs_shared/public/components/log_ai_assistant/log_ai_assistant.tsx @@ -51,7 +51,8 @@ export const LogAIAssistant = ({ return undefined; } - const message = doc.fields.find((field) => field.field === 'message')?.value[0]; + const messageValue = doc.fields.find((field) => field.field === 'message')?.value; + const message = Array.isArray(messageValue) ? messageValue[0] : messageValue; return getContextualInsightMessages({ message: `I'm looking at a log entry. Can you construct a Kibana KQL query that I can enter in the search bar that gives me similar log entries, based on the message field?`, From de7c2761622dcaea07dad39369949eeef33d77b0 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Thu, 16 May 2024 00:07:20 -0300 Subject: [PATCH 07/52] Make root profile service async and add profiles provider --- .../application/main/discover_main_route.tsx | 35 ++++++++-- .../public/context_awareness/index.ts | 1 + .../context_awareness/profile_service.ts | 64 +++++++++++++++++-- .../profiles/data_source_profile.tsx | 8 ++- .../profiles/document_profile.tsx | 2 + .../profiles/root_profile.ts | 10 ++- .../context_awareness/profiles_provider.tsx | 41 ++++++++++++ .../context_awareness/use_profile_accessor.ts | 20 +++--- .../__mocks__/customization_context.ts | 10 +-- .../public/customizations/defaults.ts | 1 + .../discover/public/customizations/types.ts | 4 ++ src/plugins/discover/public/plugin.tsx | 5 +- 12 files changed, 161 insertions(+), 40 deletions(-) create mode 100644 src/plugins/discover/public/context_awareness/profiles_provider.tsx diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx index 560f4cb03535e..0a0b5a511fc29 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.tsx @@ -40,6 +40,8 @@ import { import { DiscoverTopNavInline } from './components/top_nav/discover_topnav_inline'; import { DiscoverStateContainer, LoadParams } from './state_management/discover_state'; import { DataSourceType, isDataSourceType } from '../../../common/data_sources'; +import { ProfilesProvider, rootProfileService } from '../../context_awareness'; +import { ComposableProfile } from '../../context_awareness/composable_profile'; const DiscoverMainAppMemoized = memo(DiscoverMainApp); @@ -338,22 +340,41 @@ export function DiscoverMainRoute({ stateContainer, ]); + const { solutionNavId } = customizationContext; + const [rootProfile, setRootProfile] = useState(); + + useEffect(() => { + let aborted = false; + + rootProfileService.resolve({ solutionNavId }).then((profile) => { + if (!aborted) { + setRootProfile(profile); + } + }); + + return () => { + aborted = true; + }; + }, [solutionNavId]); + if (error) { return ; } - if (!customizationService) { + if (!customizationService || !rootProfile) { return loadingIndicator; } return ( - - <> - - {mainContent} - - + + + <> + + {mainContent} + + + ); } diff --git a/src/plugins/discover/public/context_awareness/index.ts b/src/plugins/discover/public/context_awareness/index.ts index 3451d0498f2ed..7ee5bc91c7cbb 100644 --- a/src/plugins/discover/public/context_awareness/index.ts +++ b/src/plugins/discover/public/context_awareness/index.ts @@ -9,3 +9,4 @@ export * from './types'; export * from './profiles'; export { useProfileAccessor } from './use_profile_accessor'; +export { ProfilesProvider, useProfiles } from './profiles_provider'; diff --git a/src/plugins/discover/public/context_awareness/profile_service.ts b/src/plugins/discover/public/context_awareness/profile_service.ts index 620e497063c7d..8e245f905c5f9 100644 --- a/src/plugins/discover/public/context_awareness/profile_service.ts +++ b/src/plugins/discover/public/context_awareness/profile_service.ts @@ -6,24 +6,56 @@ * Side Public License, v 1. */ +/* eslint-disable max-classes-per-file */ + import { ComposableProfile, PartialProfile, Profile } from './composable_profile'; -export interface ProfileProvider { +export type ResolveProfileResult = + | { isMatch: true; context: TContext } + | { isMatch: false }; + +export type ProfileProviderMode = 'sync' | 'async'; + +export interface ProfileProvider< + TProfile extends PartialProfile, + TParams, + TContext, + TMode extends ProfileProviderMode +> { order: number; profile: ComposableProfile; - resolve: (params: TParams) => { isMatch: true; context: TContext } | { isMatch: false }; + resolve: ( + params: TParams + ) => TMode extends 'sync' + ? ResolveProfileResult + : ResolveProfileResult | Promise>; } -const EMPTY_PROFILE = {}; - -export class ProfileService { - private readonly providers: Array> = []; +abstract class BaseProfileService< + TProfile extends PartialProfile, + TParams, + TContext, + TMode extends ProfileProviderMode +> { + protected readonly providers: Array> = []; - public registerProvider(provider: ProfileProvider) { + public registerProvider(provider: ProfileProvider) { this.providers.push(provider); this.providers.sort((a, b) => a.order - b.order); } + public abstract resolve( + params: TParams + ): TMode extends 'sync' ? ComposableProfile : Promise>; +} + +const EMPTY_PROFILE = {}; + +export class ProfileService< + TProfile extends PartialProfile, + TParams, + TContext +> extends BaseProfileService { public resolve(params: TParams): ComposableProfile { for (const provider of this.providers) { const result = provider.resolve(params); @@ -36,3 +68,21 @@ export class ProfileService return EMPTY_PROFILE; } } + +export class AsyncProfileService< + TProfile extends PartialProfile, + TParams, + TContext +> extends BaseProfileService { + public async resolve(params: TParams): Promise> { + for (const provider of this.providers) { + const result = await provider.resolve(params); + + if (result.isMatch) { + return provider.profile as ComposableProfile; + } + } + + return EMPTY_PROFILE; + } +} diff --git a/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx b/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx index 0bee77c945d8b..e69b83a97cf50 100644 --- a/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx +++ b/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx @@ -29,12 +29,18 @@ export interface DataSourceContext { category: DataSourceCategory; } +export type DataSourceProfile = Profile; + export const dataSourceProfileService = new ProfileService< - Profile, + DataSourceProfile, DataSourceProfileProviderParams, DataSourceContext >(); +export type DataSourceProfileProvider = Parameters< + typeof dataSourceProfileService.registerProvider +>[0]; + dataSourceProfileService.registerProvider({ order: 0, profile: { diff --git a/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx b/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx index 9f3fbaf06831c..be652d8828432 100644 --- a/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx +++ b/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx @@ -32,6 +32,8 @@ export const documentProfileService = new ProfileService< DocumentContext >(); +export type DocumentProfileProvider = Parameters[0]; + export const recordHasProfile = ( record?: DataTableRecord ): record is DataTableRecordWithProfile => { 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 0307beb770d35..aa3304b5c607f 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 { Profile } from '../composable_profile'; -import { ProfileService } from '../profile_service'; +import { AsyncProfileService } from '../profile_service'; export enum SolutionType { Observability = 'oblt', @@ -24,12 +24,16 @@ export interface RootContext { solutionType: SolutionType; } -export const rootProfileService = new ProfileService< - Profile, +export type RootProfile = Profile; + +export const rootProfileService = new AsyncProfileService< + RootProfile, RootProfileProviderParams, RootContext >(); +export type RootProfileProvider = Parameters[0]; + rootProfileService.registerProvider({ order: 0, profile: { diff --git a/src/plugins/discover/public/context_awareness/profiles_provider.tsx b/src/plugins/discover/public/context_awareness/profiles_provider.tsx new file mode 100644 index 0000000000000..f2c4d6f99aa29 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profiles_provider.tsx @@ -0,0 +1,41 @@ +/* + * 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 React, { createContext, FC, useContext, useMemo, useState } from 'react'; +import { ComposableProfile } from './composable_profile'; + +const profilesContext = createContext<{ + profiles: ComposableProfile[]; + setDataSourceProfile: (profile: ComposableProfile) => void; +}>({ + profiles: [], + setDataSourceProfile: () => {}, +}); + +export const ProfilesProvider: FC<{ rootProfile: ComposableProfile }> = ({ + rootProfile, + children, +}) => { + const [dataSourceProfile, setDataSourceProfile] = useState(); + const profiles = useMemo( + () => [rootProfile, dataSourceProfile].filter(profileExists), + [dataSourceProfile, rootProfile] + ); + + return ( + + {children} + + ); +}; + +export const useProfiles = () => useContext(profilesContext); + +const profileExists = (profile?: ComposableProfile): profile is ComposableProfile => { + return profile !== undefined; +}; diff --git a/src/plugins/discover/public/context_awareness/use_profile_accessor.ts b/src/plugins/discover/public/context_awareness/use_profile_accessor.ts index cda0d40851b06..ed1f28fdba50a 100644 --- a/src/plugins/discover/public/context_awareness/use_profile_accessor.ts +++ b/src/plugins/discover/public/context_awareness/use_profile_accessor.ts @@ -7,23 +7,18 @@ */ import { DataTableRecord } from '@kbn/discover-utils'; -import { useMemo, useState } from 'react'; -import useObservable from 'react-use/lib/useObservable'; +import { useMemo } from 'react'; import { useAppStateSelector } from '../application/main/state_management/discover_app_state_container'; import { useInternalStateSelector } from '../application/main/state_management/discover_internal_state_container'; -import { useDiscoverServices } from '../hooks/use_discover_services'; import { getMergedAccessor, Profile } from './composable_profile'; -import { dataSourceProfileService, recordHasProfile, rootProfileService } from './profiles'; +import { dataSourceProfileService, recordHasProfile } from './profiles'; +import { useProfiles } from './profiles_provider'; export const useProfileAccessor = ( key: TKey, baseImpl: Profile[TKey], { record }: { record?: DataTableRecord } = {} ) => { - const { chrome } = useDiscoverServices(); - const [solutionNavId$] = useState(() => chrome.getActiveSolutionNavId$()); - const solutionNavId = useObservable(solutionNavId$); - const rootProfile = useMemo(() => rootProfileService.resolve({ solutionNavId }), [solutionNavId]); const dataSource = useAppStateSelector((state) => state.dataSource); const dataView = useInternalStateSelector((state) => state.dataView); const query = useAppStateSelector((state) => state.query); @@ -31,14 +26,15 @@ export const useProfileAccessor = ( () => dataSourceProfileService.resolve({ dataSource, dataView, query }), [dataSource, dataView, query] ); + const { profiles } = useProfiles(); return useMemo(() => { - const profiles = [rootProfile, dataSourceProfile]; + const allProfiles = [...profiles, dataSourceProfile]; if (recordHasProfile(record)) { - profiles.push(record.profile); + allProfiles.push(record.profile); } - return getMergedAccessor(profiles, key, baseImpl); - }, [rootProfile, dataSourceProfile, record, key, baseImpl]); + return getMergedAccessor(allProfiles, key, baseImpl); + }, [baseImpl, dataSourceProfile, key, profiles, record]); }; diff --git a/src/plugins/discover/public/customizations/__mocks__/customization_context.ts b/src/plugins/discover/public/customizations/__mocks__/customization_context.ts index 6ede54673cda9..1fabe661dd20e 100644 --- a/src/plugins/discover/public/customizations/__mocks__/customization_context.ts +++ b/src/plugins/discover/public/customizations/__mocks__/customization_context.ts @@ -6,12 +6,6 @@ * Side Public License, v 1. */ -import type { DiscoverCustomizationContext } from '../types'; +import { defaultCustomizationContext } from '../defaults'; -export const mockCustomizationContext: DiscoverCustomizationContext = { - displayMode: 'standalone', - inlineTopNav: { - enabled: false, - showLogsExplorerTabs: false, - }, -}; +export const mockCustomizationContext = defaultCustomizationContext; diff --git a/src/plugins/discover/public/customizations/defaults.ts b/src/plugins/discover/public/customizations/defaults.ts index a9dc60ac356ff..034e7be2b5dc6 100644 --- a/src/plugins/discover/public/customizations/defaults.ts +++ b/src/plugins/discover/public/customizations/defaults.ts @@ -9,6 +9,7 @@ import { DiscoverCustomizationContext } from './types'; export const defaultCustomizationContext: DiscoverCustomizationContext = { + solutionNavId: null, displayMode: 'standalone', inlineTopNav: { enabled: false, diff --git a/src/plugins/discover/public/customizations/types.ts b/src/plugins/discover/public/customizations/types.ts index 079cde37da716..21419da709946 100644 --- a/src/plugins/discover/public/customizations/types.ts +++ b/src/plugins/discover/public/customizations/types.ts @@ -21,6 +21,10 @@ export type CustomizationCallback = ( export type DiscoverDisplayMode = 'embedded' | 'standalone'; export interface DiscoverCustomizationContext { + /** + * The current solution nav ID + */ + solutionNavId: string | null; /* * Display mode in which discover is running */ diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 2eb34b20345e4..c040769ab1a78 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -344,10 +344,11 @@ export class DiscoverPlugin const customizationContext$: Observable = services.chrome .getActiveSolutionNavId$() .pipe( - map((navId) => ({ + map((solutionNavId) => ({ ...defaultCustomizationContext, + solutionNavId, inlineTopNav: - this.inlineTopNav.get(navId) ?? + this.inlineTopNav.get(solutionNavId) ?? this.inlineTopNav.get(null) ?? defaultCustomizationContext.inlineTopNav, })) From 00669f76f91ee89f84700816a91d313b6a87eb85 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Thu, 16 May 2024 01:17:53 -0300 Subject: [PATCH 08/52] Make data source profile service async and set it via profiles provider --- .../application/main/discover_main_route.tsx | 4 ++- .../discover_data_state_container.ts | 19 +++++++++++--- .../main/state_management/discover_state.ts | 4 +++ .../profiles/data_source_profile.tsx | 4 +-- .../profiles/document_profile.tsx | 5 +--- .../context_awareness/profiles_provider.tsx | 25 ++++++------------- .../context_awareness/use_profile_accessor.ts | 20 ++++++--------- 7 files changed, 40 insertions(+), 41 deletions(-) diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx index 0a0b5a511fc29..a2e1eeebef734 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.tsx @@ -73,11 +73,13 @@ export function DiscoverMainRoute({ getScopedHistory, } = services; const { id: savedSearchId } = useParams(); + const [dataSourceProfile, setDataSourceProfile] = useState(); const [stateContainer, { reset: resetStateContainer }] = useDiscoverStateContainer({ history, services, customizationContext, stateStorageContainer, + setDataSourceProfile, }); const { customizationService, isInitialized: isCustomizationServiceInitialized } = useDiscoverCustomizationService({ @@ -367,7 +369,7 @@ export function DiscoverMainRoute({ return ( - + <> diff --git a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts index 71ad2ed87e79b..a387715c73177 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts @@ -28,6 +28,8 @@ import { fetchAll, fetchMoreDocuments } from '../data_fetching/fetch_all'; import { sendResetMsg } from '../hooks/use_saved_search_messages'; import { getFetch$ } from '../data_fetching/get_fetch_observable'; import { InternalState } from './discover_internal_state_container'; +import { ComposableProfile } from '../../../context_awareness/composable_profile'; +import { dataSourceProfileService } from '../../../context_awareness'; export interface SavedSearchData { main$: DataMain$; @@ -144,6 +146,7 @@ export function getDataStateContainer({ getInternalState, getSavedSearch, setDataView, + setDataSourceProfile, }: { services: DiscoverServices; searchSessionManager: DiscoverSearchSessionManager; @@ -151,6 +154,7 @@ export function getDataStateContainer({ getInternalState: () => InternalState; getSavedSearch: () => SavedSearch; setDataView: (dataView: DataView) => void; + setDataSourceProfile: (profile: ComposableProfile) => void; }): DiscoverDataStateContainer { const { data, uiSettings, toastNotifications } = services; const { timefilter } = data.query.timefilter; @@ -284,15 +288,24 @@ export function getDataStateContainer({ const fetchQuery = async (resetQuery?: boolean) => { const query = getAppState().query; - const currentDataView = getSavedSearch().searchSource.getField('index'); + let dataView = getSavedSearch().searchSource.getField('index'); if (isOfAggregateQueryType(query)) { - const nextDataView = await getEsqlDataView(query, currentDataView, services); - if (nextDataView !== currentDataView) { + const nextDataView = await getEsqlDataView(query, dataView, services); + if (nextDataView !== dataView) { setDataView(nextDataView); + dataView = nextDataView; } } + const dataSourceProfile = await dataSourceProfileService.resolve({ + dataSource: getAppState().dataSource, + dataView, + query, + }); + + setDataSourceProfile(dataSourceProfile); + if (resetQuery) { refetch$.next('reset'); } else { diff --git a/src/plugins/discover/public/application/main/state_management/discover_state.ts b/src/plugins/discover/public/application/main/state_management/discover_state.ts index 169e596a2cf93..93f1d94b621a9 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_state.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_state.ts @@ -59,6 +59,7 @@ import { DataSourceType, isDataSourceType, } from '../../../../common/data_sources'; +import { ComposableProfile } from '../../../context_awareness/composable_profile'; export interface DiscoverStateContainerParams { /** @@ -81,6 +82,7 @@ export interface DiscoverStateContainerParams { * a custom url state storage */ stateStorageContainer?: IKbnUrlStateStorage; + setDataSourceProfile: (profile: ComposableProfile) => void; } export interface LoadParams { @@ -219,6 +221,7 @@ export function getDiscoverStateContainer({ services, customizationContext, stateStorageContainer, + setDataSourceProfile, }: DiscoverStateContainerParams): DiscoverStateContainer { const storeInSessionStorage = services.uiSettings.get('state:storeInSessionStorage'); const toasts = services.core.notifications.toasts; @@ -294,6 +297,7 @@ export function getDiscoverStateContainer({ getInternalState: internalStateContainer.getState, getSavedSearch: savedSearchContainer.getState, setDataView, + setDataSourceProfile, }); const loadDataViewList = async () => { diff --git a/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx b/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx index e69b83a97cf50..c64acf367b587 100644 --- a/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx +++ b/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx @@ -12,7 +12,7 @@ import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; import React from 'react'; import { DataSourceType, DiscoverDataSource, isDataSourceType } from '../../../common/data_sources'; import { Profile } from '../composable_profile'; -import { ProfileService } from '../profile_service'; +import { AsyncProfileService } from '../profile_service'; export enum DataSourceCategory { Logs = 'logs', @@ -31,7 +31,7 @@ export interface DataSourceContext { export type DataSourceProfile = Profile; -export const dataSourceProfileService = new ProfileService< +export const dataSourceProfileService = new AsyncProfileService< DataSourceProfile, DataSourceProfileProviderParams, DataSourceContext diff --git a/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx b/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx index be652d8828432..a199a974a6a71 100644 --- a/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx +++ b/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx @@ -49,10 +49,7 @@ documentProfileService.registerProvider({ }, }, resolve: (params) => { - if ( - 'message' in params.record.flattened && - typeof params.record.flattened.message === 'string' - ) { + if ('message' in params.record.flattened && params.record.flattened.message != null) { return { isMatch: true, context: { diff --git a/src/plugins/discover/public/context_awareness/profiles_provider.tsx b/src/plugins/discover/public/context_awareness/profiles_provider.tsx index f2c4d6f99aa29..b1b4f155fdb4f 100644 --- a/src/plugins/discover/public/context_awareness/profiles_provider.tsx +++ b/src/plugins/discover/public/context_awareness/profiles_provider.tsx @@ -6,32 +6,21 @@ * Side Public License, v 1. */ -import React, { createContext, FC, useContext, useMemo, useState } from 'react'; +import React, { createContext, FC, useContext, useMemo } from 'react'; import { ComposableProfile } from './composable_profile'; -const profilesContext = createContext<{ - profiles: ComposableProfile[]; - setDataSourceProfile: (profile: ComposableProfile) => void; -}>({ - profiles: [], - setDataSourceProfile: () => {}, -}); +const profilesContext = createContext([]); -export const ProfilesProvider: FC<{ rootProfile: ComposableProfile }> = ({ - rootProfile, - children, -}) => { - const [dataSourceProfile, setDataSourceProfile] = useState(); +export const ProfilesProvider: FC<{ + rootProfile: ComposableProfile; + dataSourceProfile: ComposableProfile | undefined; +}> = ({ rootProfile, dataSourceProfile, children }) => { const profiles = useMemo( () => [rootProfile, dataSourceProfile].filter(profileExists), [dataSourceProfile, rootProfile] ); - return ( - - {children} - - ); + return {children}; }; export const useProfiles = () => useContext(profilesContext); diff --git a/src/plugins/discover/public/context_awareness/use_profile_accessor.ts b/src/plugins/discover/public/context_awareness/use_profile_accessor.ts index ed1f28fdba50a..1e88fef55eadc 100644 --- a/src/plugins/discover/public/context_awareness/use_profile_accessor.ts +++ b/src/plugins/discover/public/context_awareness/use_profile_accessor.ts @@ -9,9 +9,8 @@ import { DataTableRecord } from '@kbn/discover-utils'; import { useMemo } from 'react'; import { useAppStateSelector } from '../application/main/state_management/discover_app_state_container'; -import { useInternalStateSelector } from '../application/main/state_management/discover_internal_state_container'; import { getMergedAccessor, Profile } from './composable_profile'; -import { dataSourceProfileService, recordHasProfile } from './profiles'; +import { recordHasProfile } from './profiles'; import { useProfiles } from './profiles_provider'; export const useProfileAccessor = ( @@ -19,22 +18,17 @@ export const useProfileAccessor = ( baseImpl: Profile[TKey], { record }: { record?: DataTableRecord } = {} ) => { - const dataSource = useAppStateSelector((state) => state.dataSource); - const dataView = useInternalStateSelector((state) => state.dataView); - const query = useAppStateSelector((state) => state.query); - const dataSourceProfile = useMemo( - () => dataSourceProfileService.resolve({ dataSource, dataView, query }), - [dataSource, dataView, query] - ); - const { profiles } = useProfiles(); + // Why does this fail to build without useAppStateSelector? + useAppStateSelector((state) => state.dataSource); + const profiles = useProfiles(); return useMemo(() => { - const allProfiles = [...profiles, dataSourceProfile]; + let allProfiles = profiles; if (recordHasProfile(record)) { - allProfiles.push(record.profile); + allProfiles = [...profiles, record.profile]; } return getMergedAccessor(allProfiles, key, baseImpl); - }, [baseImpl, dataSourceProfile, key, profiles, record]); + }, [baseImpl, key, profiles, record]); }; From b19bd7ef03a13c74f9a212f6f9f84d4406a81db9 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Thu, 16 May 2024 01:28:16 -0300 Subject: [PATCH 09/52] Move example profiles to separate file --- .../profiles/data_source_profile.tsx | 85 +--------- .../profiles/document_profile.tsx | 22 --- .../profiles/example_profiles.tsx | 153 ++++++++++++++++++ .../context_awareness/profiles/index.ts | 2 + .../profiles/root_profile.ts | 29 ---- 5 files changed, 157 insertions(+), 134 deletions(-) create mode 100644 src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx diff --git a/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx b/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx index c64acf367b587..c9d65a70acc33 100644 --- a/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx +++ b/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx @@ -7,10 +7,8 @@ */ import { DataView } from '@kbn/data-views-plugin/common'; -import { AggregateQuery, isOfAggregateQueryType, Query } from '@kbn/es-query'; -import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; -import React from 'react'; -import { DataSourceType, DiscoverDataSource, isDataSourceType } from '../../../common/data_sources'; +import { AggregateQuery, Query } from '@kbn/es-query'; +import { DiscoverDataSource } from '../../../common/data_sources'; import { Profile } from '../composable_profile'; import { AsyncProfileService } from '../profile_service'; @@ -40,82 +38,3 @@ export const dataSourceProfileService = new AsyncProfileService< export type DataSourceProfileProvider = Parameters< typeof dataSourceProfileService.registerProvider >[0]; - -dataSourceProfileService.registerProvider({ - order: 0, - profile: { - getTopNavItems: (prev) => () => - [ - { - id: 'logs-data-source-entry', - label: 'Logs data source entry', - run: () => { - alert('HELLO WORLD'); - }, - }, - ...prev(), - ], - getCellRenderers: (prev) => () => ({ - ...prev(), - ['@timestamp']: (props) => { - const date = new Date((props.row.flattened['@timestamp'] as string[])[0]); - - return ( - <> - {date.getFullYear()}- - {date.getMonth() + 1}- - {date.getDate()}{' '} - - {date.getHours()}:{date.getMinutes()}:{date.getSeconds()} - - - ); - }, - timestamp: (props) => { - const date = new Date((props.row.flattened['@timestamp'] as string[])[0]); - - return ( - <> - {date.getFullYear()}- - {date.getMonth() + 1}- - {date.getDate()}{' '} - - {date.getHours()}:{date.getMinutes()}:{date.getSeconds()} - - - ); - }, - message: (props) => { - const message = (props.row.flattened.message as string[])[0]; - - return ( -
- {message} -
- ); - }, - }), - }, - 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.includes('logs'))) { - return { - isMatch: true, - context: { category: DataSourceCategory.Logs }, - }; - } - - return { isMatch: false }; - }, -}); diff --git a/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx b/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx index a199a974a6a71..18a78e95671db 100644 --- a/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx +++ b/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx @@ -39,25 +39,3 @@ export const recordHasProfile = ( ): record is DataTableRecordWithProfile => { return Boolean(record && 'profile' in record); }; - -documentProfileService.registerProvider({ - order: 0, - profile: { - getDocViewsRegistry: (prev) => (registry) => { - registry.enableById('doc_view_logs_overview'); - return prev(registry); - }, - }, - resolve: (params) => { - if ('message' in params.record.flattened && params.record.flattened.message != null) { - return { - isMatch: true, - context: { - type: DocumentType.Log, - }, - }; - } - - return { isMatch: false }; - }, -}); diff --git a/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx new file mode 100644 index 0000000000000..c2ae046c842c0 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx @@ -0,0 +1,153 @@ +/* + * 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 { isOfAggregateQueryType } from '@kbn/es-query'; +import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; +import React from 'react'; +import { DataSourceType, isDataSourceType } from '../../../common/data_sources'; +import { + DataSourceCategory, + DataSourceProfileProvider, + dataSourceProfileService, +} from './data_source_profile'; +import { DocumentProfileProvider, documentProfileService, DocumentType } from './document_profile'; +import { RootProfileProvider, rootProfileService, SolutionType } from './root_profile'; + +export const o11yRootProfileProvider: RootProfileProvider = { + order: 0, + profile: { + getTopNavItems: (prev) => () => + [ + { + id: 'o11y-root-entry', + label: 'O11y project entry', + run: () => { + alert('HELLO WORLD'); + }, + }, + ...prev(), + ], + }, + resolve: (params) => { + if (params.solutionNavId === 'oblt') { + return { + isMatch: true, + context: { + solutionType: SolutionType.Observability, + }, + }; + } + + return { isMatch: false }; + }, +}; + +export const logsDataSourceProfileProvider: DataSourceProfileProvider = { + order: 0, + profile: { + getTopNavItems: (prev) => () => + [ + { + id: 'logs-data-source-entry', + label: 'Logs data source entry', + run: () => { + alert('HELLO WORLD'); + }, + }, + ...prev(), + ], + getCellRenderers: (prev) => () => ({ + ...prev(), + ['@timestamp']: (props) => { + const date = new Date((props.row.flattened['@timestamp'] as string[])[0]); + + return ( + <> + {date.getFullYear()}- + {date.getMonth() + 1}- + {date.getDate()}{' '} + + {date.getHours()}:{date.getMinutes()}:{date.getSeconds()} + + + ); + }, + timestamp: (props) => { + const date = new Date((props.row.flattened['@timestamp'] as string[])[0]); + + return ( + <> + {date.getFullYear()}- + {date.getMonth() + 1}- + {date.getDate()}{' '} + + {date.getHours()}:{date.getMinutes()}:{date.getSeconds()} + + + ); + }, + message: (props) => { + const message = (props.row.flattened.message as string[])[0]; + + return ( +
+ {message} +
+ ); + }, + }), + }, + 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.includes('logs'))) { + return { + isMatch: true, + context: { category: DataSourceCategory.Logs }, + }; + } + + return { isMatch: false }; + }, +}; + +export const logDocumentProfileProvider: DocumentProfileProvider = { + order: 0, + profile: { + getDocViewsRegistry: (prev) => (registry) => { + registry.enableById('doc_view_logs_overview'); + return prev(registry); + }, + }, + resolve: (params) => { + if ('message' in params.record.flattened && params.record.flattened.message != null) { + return { + isMatch: true, + context: { + type: DocumentType.Log, + }, + }; + } + + return { isMatch: false }; + }, +}; + +rootProfileService.registerProvider(o11yRootProfileProvider); +dataSourceProfileService.registerProvider(logsDataSourceProfileProvider); +documentProfileService.registerProvider(logDocumentProfileProvider); diff --git a/src/plugins/discover/public/context_awareness/profiles/index.ts b/src/plugins/discover/public/context_awareness/profiles/index.ts index f661276b4a04c..9174e78979726 100644 --- a/src/plugins/discover/public/context_awareness/profiles/index.ts +++ b/src/plugins/discover/public/context_awareness/profiles/index.ts @@ -9,3 +9,5 @@ export * from './root_profile'; export * from './data_source_profile'; export * from './document_profile'; + +import('./example_profiles'); 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 aa3304b5c607f..762449f5028dd 100644 --- a/src/plugins/discover/public/context_awareness/profiles/root_profile.ts +++ b/src/plugins/discover/public/context_awareness/profiles/root_profile.ts @@ -33,32 +33,3 @@ export const rootProfileService = new AsyncProfileService< >(); export type RootProfileProvider = Parameters[0]; - -rootProfileService.registerProvider({ - order: 0, - profile: { - getTopNavItems: (prev) => () => - [ - { - id: 'o11y-root-entry', - label: 'O11y project entry', - run: () => { - alert('HELLO WORLD'); - }, - }, - ...prev(), - ], - }, - resolve: (params) => { - if (params.solutionNavId === 'oblt') { - return { - isMatch: true, - context: { - solutionType: SolutionType.Observability, - }, - }; - } - - return { isMatch: false }; - }, -}); From 5e4318f11552f3dc549afe7640622914954c93a9 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Thu, 16 May 2024 22:15:34 -0300 Subject: [PATCH 10/52] Improve example profiles --- .../profiles/example_profiles.tsx | 69 ++++++++++++------- .../context_awareness/use_profile_accessor.ts | 3 - 2 files changed, 44 insertions(+), 28 deletions(-) diff --git a/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx index c2ae046c842c0..7da6f23c418be 100644 --- a/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx +++ b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx @@ -6,8 +6,16 @@ * 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 { @@ -64,40 +72,46 @@ export const logsDataSourceProfileProvider: DataSourceProfileProvider = { getCellRenderers: (prev) => () => ({ ...prev(), ['@timestamp']: (props) => { - const date = new Date((props.row.flattened['@timestamp'] as string[])[0]); + const timestamp = getFieldValue(props.row, '@timestamp'); return ( - <> - {date.getFullYear()}- - {date.getMonth() + 1}- - {date.getDate()}{' '} - - {date.getHours()}:{date.getMinutes()}:{date.getSeconds()} - - + + {timestamp} + ); }, - timestamp: (props) => { - const date = new Date((props.row.flattened['@timestamp'] as string[])[0]); + ['log.level']: (props) => { + const level = getFieldValue(props.row, 'log.level'); + + if (!level) { + return (None); + } + + const levelMap: Record = { + info: 'primary', + debug: 'default', + error: 'danger', + }; return ( - <> - {date.getFullYear()}- - {date.getMonth() + 1}- - {date.getDate()}{' '} - - {date.getHours()}:{date.getMinutes()}:{date.getSeconds()} - - + + {capitalize(level)} + ); }, message: (props) => { - const message = (props.row.flattened.message as string[])[0]; + const { field, value } = getMessageFieldWithFallbacks( + props.row.flattened as unknown as LogDocumentOverview + ); + + if (!value) { + return (None); + } return ( -
- {message} -
+ <> + {field}: {value} + ); }, }), @@ -115,7 +129,7 @@ export const logsDataSourceProfileProvider: DataSourceProfileProvider = { indices = params.dataView.getIndexPattern().split(','); } - if (indices.every((index) => index.includes('logs'))) { + if (indices.every((index) => index.startsWith('logs-'))) { return { isMatch: true, context: { category: DataSourceCategory.Logs }, @@ -135,7 +149,7 @@ export const logDocumentProfileProvider: DocumentProfileProvider = { }, }, resolve: (params) => { - if ('message' in params.record.flattened && params.record.flattened.message != null) { + if (getFieldValue(params.record, 'data_stream.type') === 'logs') { return { isMatch: true, context: { @@ -151,3 +165,8 @@ export const logDocumentProfileProvider: DocumentProfileProvider = { rootProfileService.registerProvider(o11yRootProfileProvider); dataSourceProfileService.registerProvider(logsDataSourceProfileProvider); documentProfileService.registerProvider(logDocumentProfileProvider); + +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/use_profile_accessor.ts b/src/plugins/discover/public/context_awareness/use_profile_accessor.ts index 1e88fef55eadc..11b98a2145af0 100644 --- a/src/plugins/discover/public/context_awareness/use_profile_accessor.ts +++ b/src/plugins/discover/public/context_awareness/use_profile_accessor.ts @@ -8,7 +8,6 @@ import { DataTableRecord } from '@kbn/discover-utils'; import { useMemo } from 'react'; -import { useAppStateSelector } from '../application/main/state_management/discover_app_state_container'; import { getMergedAccessor, Profile } from './composable_profile'; import { recordHasProfile } from './profiles'; import { useProfiles } from './profiles_provider'; @@ -18,8 +17,6 @@ export const useProfileAccessor = ( baseImpl: Profile[TKey], { record }: { record?: DataTableRecord } = {} ) => { - // Why does this fail to build without useAppStateSelector? - useAppStateSelector((state) => state.dataSource); const profiles = useProfiles(); return useMemo(() => { From 70d43dd6a5f4cbae463afa1af9c6553b22cfc24e Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Thu, 16 May 2024 23:17:50 -0300 Subject: [PATCH 11/52] Add default columns extension --- .../discover_data_state_container.ts | 30 +++++++++++++++++-- .../main/state_management/discover_state.ts | 3 +- .../context_awareness/composable_profile.ts | 10 +------ .../public/context_awareness/index.ts | 3 +- .../context_awareness/profile_service.ts | 3 +- .../profiles/data_source_profile.tsx | 8 ++--- .../profiles/document_profile.tsx | 5 ++-- .../profiles/example_profiles.tsx | 12 ++++++-- .../profiles/root_profile.ts | 2 +- .../context_awareness/profiles_provider.tsx | 2 +- .../public/context_awareness/types.ts | 13 ++++++++ .../context_awareness/use_profile_accessor.ts | 5 ++-- 12 files changed, 68 insertions(+), 28 deletions(-) diff --git a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts index a387715c73177..9215fce5ed019 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts @@ -19,7 +19,7 @@ import type { SearchResponseWarning } from '@kbn/search-response-warnings'; import type { DataTableRecord } from '@kbn/discover-utils/types'; import { SEARCH_FIELDS_FROM_SOURCE, SEARCH_ON_PAGE_LOAD_SETTING } from '@kbn/discover-utils'; import { getEsqlDataView } from './utils/get_esql_data_view'; -import { DiscoverAppState } from './discover_app_state_container'; +import { DiscoverAppState, DiscoverAppStateContainer } from './discover_app_state_container'; import { DiscoverServices } from '../../../build_services'; import { DiscoverSearchSessionManager } from './discover_search_session'; import { FetchStatus } from '../../types'; @@ -28,8 +28,11 @@ import { fetchAll, fetchMoreDocuments } from '../data_fetching/fetch_all'; import { sendResetMsg } from '../hooks/use_saved_search_messages'; import { getFetch$ } from '../data_fetching/get_fetch_observable'; import { InternalState } from './discover_internal_state_container'; -import { ComposableProfile } from '../../../context_awareness/composable_profile'; -import { dataSourceProfileService } from '../../../context_awareness'; +import { + ComposableProfile, + dataSourceProfileService, + getMergedAccessor, +} from '../../../context_awareness'; export interface SavedSearchData { main$: DataMain$; @@ -147,6 +150,7 @@ export function getDataStateContainer({ getSavedSearch, setDataView, setDataSourceProfile, + uppdateAppState, }: { services: DiscoverServices; searchSessionManager: DiscoverSearchSessionManager; @@ -155,6 +159,7 @@ export function getDataStateContainer({ getSavedSearch: () => SavedSearch; setDataView: (dataView: DataView) => void; setDataSourceProfile: (profile: ComposableProfile) => void; + uppdateAppState: DiscoverAppStateContainer['update']; }): DiscoverDataStateContainer { const { data, uiSettings, toastNotifications } = services; const { timefilter } = data.query.timefilter; @@ -306,6 +311,25 @@ export function getDataStateContainer({ setDataSourceProfile(dataSourceProfile); + const defaultColumns = getMergedAccessor( + [dataSourceProfile], + 'getDefaultColumns', + () => undefined + )(); + + if (defaultColumns) { + uppdateAppState( + { + columns: defaultColumns.columns, + grid: { + ...getAppState().grid, + columns: defaultColumns.settings, + }, + }, + true + ); + } + if (resetQuery) { refetch$.next('reset'); } else { diff --git a/src/plugins/discover/public/application/main/state_management/discover_state.ts b/src/plugins/discover/public/application/main/state_management/discover_state.ts index 93f1d94b621a9..ab9d7a27d4dbd 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_state.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_state.ts @@ -59,7 +59,7 @@ import { DataSourceType, isDataSourceType, } from '../../../../common/data_sources'; -import { ComposableProfile } from '../../../context_awareness/composable_profile'; +import { ComposableProfile } from '../../../context_awareness'; export interface DiscoverStateContainerParams { /** @@ -298,6 +298,7 @@ export function getDiscoverStateContainer({ getSavedSearch: savedSearchContainer.getState, setDataView, setDataSourceProfile, + uppdateAppState: appStateContainer.update, }); const loadDataViewList = async () => { diff --git a/src/plugins/discover/public/context_awareness/composable_profile.ts b/src/plugins/discover/public/context_awareness/composable_profile.ts index 731aa64f55a4b..c2211dee3f370 100644 --- a/src/plugins/discover/public/context_awareness/composable_profile.ts +++ b/src/plugins/discover/public/context_awareness/composable_profile.ts @@ -6,15 +6,7 @@ * Side Public License, v 1. */ -import { TopNavMenuData } from '@kbn/navigation-plugin/public'; -import { CustomCellRenderer } from '@kbn/unified-data-table'; -import { DocViewsRegistry } from '@kbn/unified-doc-viewer'; - -export interface Profile { - getTopNavItems: () => TopNavMenuData[]; - getCellRenderers: () => CustomCellRenderer; - getDocViewsRegistry: (prevRegistry: DocViewsRegistry) => DocViewsRegistry; -} +import type { Profile } from './types'; export type PartialProfile = Partial; diff --git a/src/plugins/discover/public/context_awareness/index.ts b/src/plugins/discover/public/context_awareness/index.ts index 7ee5bc91c7cbb..628341e3e1423 100644 --- a/src/plugins/discover/public/context_awareness/index.ts +++ b/src/plugins/discover/public/context_awareness/index.ts @@ -8,5 +8,6 @@ export * from './types'; export * from './profiles'; -export { useProfileAccessor } from './use_profile_accessor'; +export { type ComposableProfile, getMergedAccessor } from './composable_profile'; export { ProfilesProvider, useProfiles } from './profiles_provider'; +export { useProfileAccessor } from './use_profile_accessor'; diff --git a/src/plugins/discover/public/context_awareness/profile_service.ts b/src/plugins/discover/public/context_awareness/profile_service.ts index 8e245f905c5f9..1c0d811447887 100644 --- a/src/plugins/discover/public/context_awareness/profile_service.ts +++ b/src/plugins/discover/public/context_awareness/profile_service.ts @@ -8,7 +8,8 @@ /* eslint-disable max-classes-per-file */ -import { ComposableProfile, PartialProfile, Profile } from './composable_profile'; +import type { ComposableProfile, PartialProfile } from './composable_profile'; +import type { Profile } from './types'; export type ResolveProfileResult = | { isMatch: true; context: TContext } diff --git a/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx b/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx index c9d65a70acc33..8115c4ea7b136 100644 --- a/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx +++ b/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import { DataView } from '@kbn/data-views-plugin/common'; -import { AggregateQuery, Query } from '@kbn/es-query'; -import { DiscoverDataSource } from '../../../common/data_sources'; -import { Profile } from '../composable_profile'; +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 { Profile } from '../types'; export enum DataSourceCategory { Logs = 'logs', diff --git a/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx b/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx index 18a78e95671db..a433d7565a3b6 100644 --- a/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx +++ b/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx @@ -6,10 +6,9 @@ * Side Public License, v 1. */ -import { DataTableRecord } from '@kbn/discover-utils'; -import { Profile } from '../composable_profile'; +import type { DataTableRecord } from '@kbn/discover-utils'; +import type { DataTableRecordWithProfile, Profile } from '../types'; import { ProfileService } from '../profile_service'; -import { DataTableRecordWithProfile } from '../types'; export enum DocumentType { Log = 'log', diff --git a/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx index 7da6f23c418be..8e94eb644ca91 100644 --- a/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx +++ b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx @@ -69,9 +69,17 @@ export const logsDataSourceProfileProvider: DataSourceProfileProvider = { }, ...prev(), ], + getDefaultColumns: () => () => ({ + columns: ['@timestamp', 'log.level', 'message'], + settings: { + 'log.level': { + width: 120, + }, + }, + }), getCellRenderers: (prev) => () => ({ ...prev(), - ['@timestamp']: (props) => { + '@timestamp': (props) => { const timestamp = getFieldValue(props.row, '@timestamp'); return ( @@ -80,7 +88,7 @@ export const logsDataSourceProfileProvider: DataSourceProfileProvider = { ); }, - ['log.level']: (props) => { + 'log.level': (props) => { const level = getFieldValue(props.row, 'log.level'); if (!level) { 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 762449f5028dd..c8f250d8fd3cd 100644 --- a/src/plugins/discover/public/context_awareness/profiles/root_profile.ts +++ b/src/plugins/discover/public/context_awareness/profiles/root_profile.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Profile } from '../composable_profile'; +import type { Profile } from '../types'; import { AsyncProfileService } from '../profile_service'; export enum SolutionType { diff --git a/src/plugins/discover/public/context_awareness/profiles_provider.tsx b/src/plugins/discover/public/context_awareness/profiles_provider.tsx index b1b4f155fdb4f..5cda69d88fa48 100644 --- a/src/plugins/discover/public/context_awareness/profiles_provider.tsx +++ b/src/plugins/discover/public/context_awareness/profiles_provider.tsx @@ -7,7 +7,7 @@ */ import React, { createContext, FC, useContext, useMemo } from 'react'; -import { ComposableProfile } from './composable_profile'; +import type { ComposableProfile } from './composable_profile'; const profilesContext = createContext([]); diff --git a/src/plugins/discover/public/context_awareness/types.ts b/src/plugins/discover/public/context_awareness/types.ts index 9e80861dd0741..7ff323c94f205 100644 --- a/src/plugins/discover/public/context_awareness/types.ts +++ b/src/plugins/discover/public/context_awareness/types.ts @@ -7,9 +7,22 @@ */ import type { DataTableRecord } from '@kbn/discover-utils'; +import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; +import type { DiscoverGridSettings } from '@kbn/saved-search-plugin/common'; +import type { CustomCellRenderer } from '@kbn/unified-data-table'; +import type { DocViewsRegistry } from '@kbn/unified-doc-viewer'; import type { ComposableProfile } from './composable_profile'; import type { DocumentProfile } from './profiles'; +export interface Profile { + getTopNavItems: () => TopNavMenuData[]; + getDefaultColumns: () => + | { columns: string[]; settings?: DiscoverGridSettings['columns'] } + | undefined; + getCellRenderers: () => CustomCellRenderer; + getDocViewsRegistry: (prevRegistry: DocViewsRegistry) => DocViewsRegistry; +} + export interface DataTableRecordWithProfile extends DataTableRecord { profile: ComposableProfile; } diff --git a/src/plugins/discover/public/context_awareness/use_profile_accessor.ts b/src/plugins/discover/public/context_awareness/use_profile_accessor.ts index 11b98a2145af0..52f66c35e5e06 100644 --- a/src/plugins/discover/public/context_awareness/use_profile_accessor.ts +++ b/src/plugins/discover/public/context_awareness/use_profile_accessor.ts @@ -6,11 +6,12 @@ * Side Public License, v 1. */ -import { DataTableRecord } from '@kbn/discover-utils'; +import type { DataTableRecord } from '@kbn/discover-utils'; import { useMemo } from 'react'; -import { getMergedAccessor, Profile } from './composable_profile'; +import { getMergedAccessor } from './composable_profile'; import { recordHasProfile } from './profiles'; import { useProfiles } from './profiles_provider'; +import type { Profile } from './types'; export const useProfileAccessor = ( key: TKey, From d3eee0898a458edc58b9f0675b47515cdc591f02 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Fri, 17 May 2024 21:10:08 -0300 Subject: [PATCH 12/52] Don't display message field in cell renderer --- .../public/application/main/discover_main_route.tsx | 3 +-- .../context_awareness/profiles/example_profiles.tsx | 12 ++---------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx index a2e1eeebef734..6a6ac73d4bd70 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.tsx @@ -40,8 +40,7 @@ import { import { DiscoverTopNavInline } from './components/top_nav/discover_topnav_inline'; import { DiscoverStateContainer, LoadParams } from './state_management/discover_state'; import { DataSourceType, isDataSourceType } from '../../../common/data_sources'; -import { ProfilesProvider, rootProfileService } from '../../context_awareness'; -import { ComposableProfile } from '../../context_awareness/composable_profile'; +import { ComposableProfile, ProfilesProvider, rootProfileService } from '../../context_awareness'; const DiscoverMainAppMemoized = memo(DiscoverMainApp); diff --git a/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx index 8e94eb644ca91..472149a292032 100644 --- a/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx +++ b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx @@ -108,19 +108,11 @@ export const logsDataSourceProfileProvider: DataSourceProfileProvider = { ); }, message: (props) => { - const { field, value } = getMessageFieldWithFallbacks( + const { value } = getMessageFieldWithFallbacks( props.row.flattened as unknown as LogDocumentOverview ); - if (!value) { - return (None); - } - - return ( - <> - {field}: {value} - - ); + return value || (None); }, }), }, From d2eee9c76e0a952c391700082c1e09b777329575 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Sun, 19 May 2024 01:52:51 -0300 Subject: [PATCH 13/52] Add ProfilesManager --- .../profiles/data_source_profile.tsx | 6 +- .../profiles/document_profile.tsx | 8 +- .../profiles/root_profile.ts | 8 +- .../context_awareness/profiles_manager.ts | 115 ++++++++++++++++++ .../context_awareness/profiles_provider.tsx | 9 +- 5 files changed, 137 insertions(+), 9 deletions(-) create mode 100644 src/plugins/discover/public/context_awareness/profiles_manager.ts diff --git a/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx b/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx index 8115c4ea7b136..70cbbdededb88 100644 --- a/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx +++ b/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx @@ -29,11 +29,13 @@ export interface DataSourceContext { export type DataSourceProfile = Profile; -export const dataSourceProfileService = new AsyncProfileService< +export class DataSourceProfileService extends AsyncProfileService< DataSourceProfile, DataSourceProfileProviderParams, DataSourceContext ->(); +> {} + +export const dataSourceProfileService = new DataSourceProfileService(); export type DataSourceProfileProvider = Parameters< typeof dataSourceProfileService.registerProvider diff --git a/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx b/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx index a433d7565a3b6..dee2d551e1033 100644 --- a/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx +++ b/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx @@ -25,13 +25,15 @@ export interface DocumentContext { export type DocumentProfile = Pick; -export const documentProfileService = new ProfileService< +export class DocumentProfileService extends ProfileService< DocumentProfile, DocumentProfileProviderParams, DocumentContext ->(); +> {} -export type DocumentProfileProvider = Parameters[0]; +export type DocumentProfileProvider = Parameters[0]; + +export const documentProfileService = new DocumentProfileService(); export const recordHasProfile = ( record?: DataTableRecord 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 c8f250d8fd3cd..6461b445ac2a8 100644 --- a/src/plugins/discover/public/context_awareness/profiles/root_profile.ts +++ b/src/plugins/discover/public/context_awareness/profiles/root_profile.ts @@ -26,10 +26,12 @@ export interface RootContext { export type RootProfile = Profile; -export const rootProfileService = new AsyncProfileService< +export class RootProfileService extends AsyncProfileService< RootProfile, RootProfileProviderParams, RootContext ->(); +> {} -export type RootProfileProvider = Parameters[0]; +export type RootProfileProvider = Parameters[0]; + +export const rootProfileService = new RootProfileService(); diff --git a/src/plugins/discover/public/context_awareness/profiles_manager.ts b/src/plugins/discover/public/context_awareness/profiles_manager.ts new file mode 100644 index 0000000000000..efab19c573a65 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profiles_manager.ts @@ -0,0 +1,115 @@ +/* + * 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 { DataTableRecord } from '@kbn/discover-utils'; +import { isOfAggregateQueryType } from '@kbn/es-query'; +import { isEqual } from 'lodash'; +import { DataSourceType, isDataSourceType } from '../../common/data_sources'; +import type { ComposableProfile } from './composable_profile'; +import type { + RootProfile, + DataSourceProfile, + DocumentProfile, + RootProfileService, + DataSourceProfileService, + DocumentProfileService, + RootProfileProviderParams, + DataSourceProfileProviderParams, + DocumentProfileProviderParams, +} from './profiles'; + +interface SerializedRootProfileParams { + solutionNavId: RootProfileProviderParams['solutionNavId']; +} + +interface SerializedDataSourceProfileParams { + dataViewId: string | undefined; + esqlQuery: string | undefined; +} + +export class ProfilesManager { + private rootProfile?: ComposableProfile; + private dataSourceProfile?: ComposableProfile; + private documentProfiles?: Map>; + private prevRootProfileParams?: SerializedRootProfileParams; + private prevDataSourceProfileParams?: SerializedDataSourceProfileParams; + + constructor( + private readonly rootProfileService: RootProfileService, + private readonly dataSourceProfileService: DataSourceProfileService, + private readonly documentProfileService: DocumentProfileService + ) {} + + public async resolveRootContext(params: RootProfileProviderParams) { + const serializedParams = this.parseRootProfileParams(params); + + if (this.rootProfile && isEqual(this.prevRootProfileParams, serializedParams)) { + return; + } + + this.rootProfile = await this.rootProfileService.resolve(params); + this.prevRootProfileParams = serializedParams; + } + + public async resolveDataSourceContext(params: DataSourceProfileProviderParams) { + const serializedParams = this.parseDataSourceProfileParams(params); + + if (this.dataSourceProfile && isEqual(this.prevDataSourceProfileParams, serializedParams)) { + return; + } + + this.dataSourceProfile = await this.dataSourceProfileService.resolve(params); + this.prevDataSourceProfileParams = serializedParams; + } + + public createDocumentContextCollector() { + const documentProfiles = new Map>(); + + return { + collect: (params: DocumentProfileProviderParams) => { + documentProfiles.set(params.record.id, this.documentProfileService.resolve(params)); + }, + finalize: () => { + this.documentProfiles = documentProfiles; + }, + }; + } + + public getProfiles({ record }: { record?: DataTableRecord } = {}) { + return [ + this.rootProfile, + this.dataSourceProfile, + record ? this.documentProfiles?.get(record.id) : undefined, + ].filter(profileExists); + } + + private parseRootProfileParams(params: RootProfileProviderParams): SerializedRootProfileParams { + return { + solutionNavId: params.solutionNavId, + }; + } + + private parseDataSourceProfileParams( + params: DataSourceProfileProviderParams + ): SerializedDataSourceProfileParams { + return { + dataViewId: isDataSourceType(params.dataSource, DataSourceType.DataView) + ? params.dataSource.dataViewId + : undefined, + esqlQuery: + isDataSourceType(params.dataSource, DataSourceType.Esql) && + isOfAggregateQueryType(params.query) + ? params.query.esql + : undefined, + }; + } +} + +const profileExists = (profile?: ComposableProfile): profile is ComposableProfile => { + return profile !== undefined; +}; diff --git a/src/plugins/discover/public/context_awareness/profiles_provider.tsx b/src/plugins/discover/public/context_awareness/profiles_provider.tsx index 5cda69d88fa48..64b0524dbe6d3 100644 --- a/src/plugins/discover/public/context_awareness/profiles_provider.tsx +++ b/src/plugins/discover/public/context_awareness/profiles_provider.tsx @@ -6,8 +6,12 @@ * Side Public License, v 1. */ -import React, { createContext, FC, useContext, useMemo } from 'react'; +import React, { createContext, FC, useContext, useMemo, useState } from 'react'; import type { ComposableProfile } from './composable_profile'; +import { dataSourceProfileService } from './profiles/data_source_profile'; +import { documentProfileService } from './profiles/document_profile'; +import { rootProfileService } from './profiles/root_profile'; +import { ProfilesManager } from './profiles_manager'; const profilesContext = createContext([]); @@ -15,6 +19,9 @@ export const ProfilesProvider: FC<{ rootProfile: ComposableProfile; dataSourceProfile: ComposableProfile | undefined; }> = ({ rootProfile, dataSourceProfile, children }) => { + const [manager] = useState( + () => new ProfilesManager(rootProfileService, dataSourceProfileService, documentProfileService) + ); const profiles = useMemo( () => [rootProfile, dataSourceProfile].filter(profileExists), [dataSourceProfile, rootProfile] From a96bc15205f0c3bedf29bde81b378114d958cdf4 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Sun, 19 May 2024 03:50:31 -0300 Subject: [PATCH 14/52] Use ProfilesManager for accessing and setting profiles --- .../components/layout/discover_documents.tsx | 6 +- .../main/data_fetching/fetch_all.ts | 4 + .../main/data_fetching/fetch_documents.ts | 33 +++++--- .../main/data_fetching/fetch_esql.ts | 15 ++-- .../application/main/discover_main_route.tsx | 35 ++++---- .../discover_data_state_container.ts | 71 ++++++++-------- .../main/state_management/discover_state.ts | 10 +-- .../profiles/data_source_profile.tsx | 6 +- .../profiles/document_profile.tsx | 8 +- .../context_awareness/profiles_manager.ts | 83 ++++++++++++++----- .../context_awareness/profiles_provider.tsx | 41 ++++----- .../public/context_awareness/types.ts | 7 -- .../context_awareness/use_profile_accessor.ts | 18 +--- .../context_awareness/use_root_profile.ts | 38 +++++++++ 14 files changed, 215 insertions(+), 160 deletions(-) create mode 100644 src/plugins/discover/public/context_awareness/use_root_profile.ts diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index d8b23f8537b5f..651760782e901 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -264,11 +264,7 @@ function DiscoverDocumentsComponent({ useContextualGridCustomisations() || {}; const additionalFieldGroups = useAdditionalFieldGroups(); - const baseGetCellRenderers = useCallback( - () => externalCustomRenderers ?? {}, - [externalCustomRenderers] - ); - + const baseGetCellRenderers = useCallback(() => customCellRenderer ?? {}, [customCellRenderer]); const getCellRenderers = useProfileAccessor('getCellRenderers', baseGetCellRenderers); const cellRenderers = useMemo(() => getCellRenderers(), [getCellRenderers]); diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts index 410d1d468275d..fd3d278cf0b97 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts @@ -30,6 +30,7 @@ import { DataMsg, SavedSearchData } from '../state_management/discover_data_stat import { DiscoverServices } from '../../../build_services'; import { fetchEsql } from './fetch_esql'; import { InternalState } from '../state_management/discover_internal_state_container'; +import { ProfilesManager } from '../../../context_awareness/profiles_manager'; export interface FetchDeps { abortController: AbortController; @@ -41,6 +42,7 @@ export interface FetchDeps { searchSessionId: string; services: DiscoverServices; useNewFieldsApi: boolean; + profilesManager: ProfilesManager; } /** @@ -63,6 +65,7 @@ export function fetchAll( inspectorAdapters, savedSearch, abortController, + profilesManager, } = fetchDeps; const { data } = services; const searchSource = savedSearch.searchSource.createChild(); @@ -106,6 +109,7 @@ export function fetchAll( data, services.expressions, inspectorAdapters, + profilesManager, abortController.signal ) : fetchDocuments(searchSource, fetchDeps); diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts index 615604991199c..4d92c0e00e5a6 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import { i18n } from '@kbn/i18n'; -import { filter, map } from 'rxjs'; +import { filter, map, tap } from 'rxjs'; import { lastValueFrom } from 'rxjs'; import { isRunningResponse, ISearchSource } from '@kbn/data-plugin/public'; import { buildDataTableRecordList } from '@kbn/discover-utils'; @@ -16,7 +16,6 @@ import { DataViewType } from '@kbn/data-views-plugin/public'; import type { RecordsFetchResponse } from '../../types'; import { getAllowedSampleSize } from '../../../utils/get_allowed_sample_size'; import { FetchDeps } from './fetch_all'; -import { DataTableRecordWithProfile, documentProfileService } from '../../../context_awareness'; /** * Requests the documents for Discover. This will return a promise that will resolve @@ -24,7 +23,14 @@ import { DataTableRecordWithProfile, documentProfileService } from '../../../con */ export const fetchDocuments = ( searchSource: ISearchSource, - { abortController, inspectorAdapters, searchSessionId, services, getAppState }: FetchDeps + { + abortController, + inspectorAdapters, + searchSessionId, + services, + getAppState, + profilesManager, + }: FetchDeps ): Promise => { const sampleSize = getAppState().sampleSize; searchSource.setField('size', getAllowedSampleSize(sampleSize, services.uiSettings)); @@ -45,6 +51,8 @@ export const fetchDocuments = ( description: isFetchingMore ? 'fetch more documents' : 'fetch documents', }; + const contextCollector = profilesManager.createDocumentContextCollector(); + const fetch$ = searchSource .fetch$({ abortSignal: abortController.signal, @@ -68,16 +76,15 @@ export const fetchDocuments = ( .pipe( filter((res) => !isRunningResponse(res)), map((res) => { - return buildDataTableRecordList( - res.rawResponse.hits.hits as EsHitRecord[], - dataView, - { - processRecord: (record) => { - const profile = documentProfileService.resolve({ record }); - return { ...record, profile }; - }, - } - ); + return buildDataTableRecordList(res.rawResponse.hits.hits as EsHitRecord[], dataView, { + processRecord: (record) => { + contextCollector.collect({ record }); + return record; + }, + }); + }), + tap(() => { + contextCollector.finalize(!isFetchingMore); }) ); diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts index 341207ae4bbb3..1addf739706b9 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts @@ -17,7 +17,7 @@ import type { DataView } from '@kbn/data-views-plugin/common'; import { textBasedQueryStateToAstWithValidation } from '@kbn/data-plugin/common'; import { DataTableRecord } from '@kbn/discover-utils'; import type { RecordsFetchResponse } from '../../types'; -import { DataTableRecordWithProfile, documentProfileService } from '../../../context_awareness'; +import { ProfilesManager } from '../../../context_awareness/profiles_manager'; interface EsqlErrorResponse { error: { @@ -32,6 +32,7 @@ export function fetchEsql( data: DataPublicPluginStart, expressions: ExpressionsStart, inspectorAdapters: Adapters, + profilesManager: ProfilesManager, abortSignal?: AbortSignal, filters?: Filter[], inputQuery?: Query @@ -57,10 +58,11 @@ export function fetchEsql( }); abortSignal?.addEventListener('abort', contract.cancel); const execution = contract.getData(); - let finalData: DataTableRecordWithProfile[] = []; + let finalData: DataTableRecord[] = []; let esqlQueryColumns: Datatable['columns'] | undefined; let error: string | undefined; let esqlHeaderWarning: string | undefined; + const contextCollector = profilesManager.createDocumentContextCollector(); execution.pipe(pluck('result')).subscribe((resp) => { const response = resp as Datatable | EsqlErrorResponse; if (response.type === 'error') { @@ -81,10 +83,9 @@ export function fetchEsql( flattened: row, }; - return { - ...record, - profile: documentProfileService.resolve({ record }), - }; + contextCollector.collect({ record }); + + return record; }); } }); @@ -92,6 +93,8 @@ export function fetchEsql( if (error) { throw new Error(error); } else { + contextCollector.finalize(true); + return { records: finalData || [], esqlQueryColumns, diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx index 6a6ac73d4bd70..f63ecd5e9bd17 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.tsx @@ -40,7 +40,14 @@ import { import { DiscoverTopNavInline } from './components/top_nav/discover_topnav_inline'; import { DiscoverStateContainer, LoadParams } from './state_management/discover_state'; import { DataSourceType, isDataSourceType } from '../../../common/data_sources'; -import { ComposableProfile, ProfilesProvider, rootProfileService } from '../../context_awareness'; +import { + dataSourceProfileService, + documentProfileService, + ProfilesProvider, + rootProfileService, +} from '../../context_awareness'; +import { ProfilesManager } from '../../context_awareness/profiles_manager'; +import { useRootProfile } from '../../context_awareness/use_root_profile'; const DiscoverMainAppMemoized = memo(DiscoverMainApp); @@ -72,13 +79,15 @@ export function DiscoverMainRoute({ getScopedHistory, } = services; const { id: savedSearchId } = useParams(); - const [dataSourceProfile, setDataSourceProfile] = useState(); + const [profilesManager] = useState( + () => new ProfilesManager(rootProfileService, dataSourceProfileService, documentProfileService) + ); const [stateContainer, { reset: resetStateContainer }] = useDiscoverStateContainer({ history, services, customizationContext, stateStorageContainer, - setDataSourceProfile, + profilesManager, }); const { customizationService, isInitialized: isCustomizationServiceInitialized } = useDiscoverCustomizationService({ @@ -342,33 +351,19 @@ export function DiscoverMainRoute({ ]); const { solutionNavId } = customizationContext; - const [rootProfile, setRootProfile] = useState(); - - useEffect(() => { - let aborted = false; - - rootProfileService.resolve({ solutionNavId }).then((profile) => { - if (!aborted) { - setRootProfile(profile); - } - }); - - return () => { - aborted = true; - }; - }, [solutionNavId]); + const { rootProfileLoading } = useRootProfile({ profilesManager, solutionNavId }); if (error) { return ; } - if (!customizationService || !rootProfile) { + if (!customizationService || rootProfileLoading) { return loadingIndicator; } return ( - + <> diff --git a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts index 9215fce5ed019..d4963e7962648 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts @@ -28,11 +28,8 @@ import { fetchAll, fetchMoreDocuments } from '../data_fetching/fetch_all'; import { sendResetMsg } from '../hooks/use_saved_search_messages'; import { getFetch$ } from '../data_fetching/get_fetch_observable'; import { InternalState } from './discover_internal_state_container'; -import { - ComposableProfile, - dataSourceProfileService, - getMergedAccessor, -} from '../../../context_awareness'; +import { getMergedAccessor } from '../../../context_awareness'; +import { ProfilesManager } from '../../../context_awareness/profiles_manager'; export interface SavedSearchData { main$: DataMain$; @@ -145,21 +142,21 @@ export interface DiscoverDataStateContainer { export function getDataStateContainer({ services, searchSessionManager, + profilesManager, getAppState, getInternalState, getSavedSearch, setDataView, - setDataSourceProfile, - uppdateAppState, + updateAppState, }: { services: DiscoverServices; searchSessionManager: DiscoverSearchSessionManager; + profilesManager: ProfilesManager; getAppState: () => DiscoverAppState; getInternalState: () => InternalState; getSavedSearch: () => SavedSearch; setDataView: (dataView: DataView) => void; - setDataSourceProfile: (profile: ComposableProfile) => void; - uppdateAppState: DiscoverAppStateContainer['update']; + updateAppState: DiscoverAppStateContainer['update']; }): DiscoverDataStateContainer { const { data, uiSettings, toastNotifications } = services; const { timefilter } = data.query.timefilter; @@ -237,6 +234,7 @@ export function getDataStateContainer({ getInternalState, savedSearch: getSavedSearch(), useNewFieldsApi: !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), + profilesManager, }; abortController?.abort(); @@ -280,6 +278,34 @@ export function getDataStateContainer({ autoRefreshDone?.(); autoRefreshDone = undefined; } + + await profilesManager.resolveDataSourceContext( + { + dataSource: getAppState().dataSource, + dataView: getSavedSearch().searchSource.getField('index'), + query: getAppState().query, + }, + abortController.signal + ); + + const defaultColumns = getMergedAccessor( + profilesManager.getProfiles(), + 'getDefaultColumns', + () => undefined + )(); + + if (defaultColumns) { + updateAppState( + { + columns: defaultColumns.columns, + grid: { + ...getAppState().grid, + columns: defaultColumns.settings, + }, + }, + true + ); + } }) ) .subscribe(); @@ -303,33 +329,6 @@ export function getDataStateContainer({ } } - const dataSourceProfile = await dataSourceProfileService.resolve({ - dataSource: getAppState().dataSource, - dataView, - query, - }); - - setDataSourceProfile(dataSourceProfile); - - const defaultColumns = getMergedAccessor( - [dataSourceProfile], - 'getDefaultColumns', - () => undefined - )(); - - if (defaultColumns) { - uppdateAppState( - { - columns: defaultColumns.columns, - grid: { - ...getAppState().grid, - columns: defaultColumns.settings, - }, - }, - true - ); - } - if (resetQuery) { refetch$.next('reset'); } else { diff --git a/src/plugins/discover/public/application/main/state_management/discover_state.ts b/src/plugins/discover/public/application/main/state_management/discover_state.ts index ab9d7a27d4dbd..4c822dee4f566 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_state.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_state.ts @@ -59,7 +59,7 @@ import { DataSourceType, isDataSourceType, } from '../../../../common/data_sources'; -import { ComposableProfile } from '../../../context_awareness'; +import { ProfilesManager } from '../../../context_awareness/profiles_manager'; export interface DiscoverStateContainerParams { /** @@ -82,7 +82,7 @@ export interface DiscoverStateContainerParams { * a custom url state storage */ stateStorageContainer?: IKbnUrlStateStorage; - setDataSourceProfile: (profile: ComposableProfile) => void; + profilesManager: ProfilesManager; } export interface LoadParams { @@ -221,7 +221,7 @@ export function getDiscoverStateContainer({ services, customizationContext, stateStorageContainer, - setDataSourceProfile, + profilesManager, }: DiscoverStateContainerParams): DiscoverStateContainer { const storeInSessionStorage = services.uiSettings.get('state:storeInSessionStorage'); const toasts = services.core.notifications.toasts; @@ -293,12 +293,12 @@ export function getDiscoverStateContainer({ const dataStateContainer = getDataStateContainer({ services, searchSessionManager, + profilesManager, getAppState: appStateContainer.getState, getInternalState: internalStateContainer.getState, getSavedSearch: savedSearchContainer.getState, setDataView, - setDataSourceProfile, - uppdateAppState: appStateContainer.update, + updateAppState: appStateContainer.update, }); const loadDataViewList = async () => { diff --git a/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx b/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx index 70cbbdededb88..0fbade6123bb2 100644 --- a/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx +++ b/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx @@ -35,8 +35,6 @@ export class DataSourceProfileService extends AsyncProfileService< DataSourceContext > {} -export const dataSourceProfileService = new DataSourceProfileService(); +export type DataSourceProfileProvider = Parameters[0]; -export type DataSourceProfileProvider = Parameters< - typeof dataSourceProfileService.registerProvider ->[0]; +export const dataSourceProfileService = new DataSourceProfileService(); diff --git a/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx b/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx index dee2d551e1033..42702f4504c94 100644 --- a/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx +++ b/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx @@ -7,7 +7,7 @@ */ import type { DataTableRecord } from '@kbn/discover-utils'; -import type { DataTableRecordWithProfile, Profile } from '../types'; +import type { Profile } from '../types'; import { ProfileService } from '../profile_service'; export enum DocumentType { @@ -34,9 +34,3 @@ export class DocumentProfileService extends ProfileService< export type DocumentProfileProvider = Parameters[0]; export const documentProfileService = new DocumentProfileService(); - -export const recordHasProfile = ( - record?: DataTableRecord -): record is DataTableRecordWithProfile => { - return Boolean(record && 'profile' in record); -}; diff --git a/src/plugins/discover/public/context_awareness/profiles_manager.ts b/src/plugins/discover/public/context_awareness/profiles_manager.ts index efab19c573a65..e1885b5a9ea00 100644 --- a/src/plugins/discover/public/context_awareness/profiles_manager.ts +++ b/src/plugins/discover/public/context_awareness/profiles_manager.ts @@ -9,6 +9,7 @@ import { DataTableRecord } from '@kbn/discover-utils'; import { isOfAggregateQueryType } from '@kbn/es-query'; import { isEqual } from 'lodash'; +import { BehaviorSubject, combineLatest, map } from 'rxjs'; import { DataSourceType, isDataSourceType } from '../../common/data_sources'; import type { ComposableProfile } from './composable_profile'; import type { @@ -32,10 +33,18 @@ interface SerializedDataSourceProfileParams { esqlQuery: string | undefined; } +export interface GetProfilesOptions { + record?: DataTableRecord; +} + export class ProfilesManager { - private rootProfile?: ComposableProfile; - private dataSourceProfile?: ComposableProfile; - private documentProfiles?: Map>; + private rootProfile$ = new BehaviorSubject | undefined>(undefined); + private dataSourceProfile$ = new BehaviorSubject< + ComposableProfile | undefined + >(undefined); + private documentProfiles$ = new BehaviorSubject>>( + new Map() + ); private prevRootProfileParams?: SerializedRootProfileParams; private prevDataSourceProfileParams?: SerializedDataSourceProfileParams; @@ -45,25 +54,43 @@ export class ProfilesManager { private readonly documentProfileService: DocumentProfileService ) {} - public async resolveRootContext(params: RootProfileProviderParams) { - const serializedParams = this.parseRootProfileParams(params); + public async resolveRootContext(params: RootProfileProviderParams, abortSignal: AbortSignal) { + const serializedParams = this.serializeRootProfileParams(params); + + if (this.rootProfile$.getValue() && isEqual(this.prevRootProfileParams, serializedParams)) { + return; + } + + const profile = await this.rootProfileService.resolve(params); - if (this.rootProfile && isEqual(this.prevRootProfileParams, serializedParams)) { + if (abortSignal.aborted) { return; } - this.rootProfile = await this.rootProfileService.resolve(params); + this.rootProfile$.next(profile); this.prevRootProfileParams = serializedParams; } - public async resolveDataSourceContext(params: DataSourceProfileProviderParams) { - const serializedParams = this.parseDataSourceProfileParams(params); + public async resolveDataSourceContext( + params: DataSourceProfileProviderParams, + abortSignal: AbortSignal + ) { + const serializedParams = this.serializeDataSourceProfileParams(params); - if (this.dataSourceProfile && isEqual(this.prevDataSourceProfileParams, serializedParams)) { + if ( + this.dataSourceProfile$.getValue() && + isEqual(this.prevDataSourceProfileParams, serializedParams) + ) { return; } - this.dataSourceProfile = await this.dataSourceProfileService.resolve(params); + const profile = await this.dataSourceProfileService.resolve(params); + + if (abortSignal.aborted) { + return; + } + + this.dataSourceProfile$.next(profile); this.prevDataSourceProfileParams = serializedParams; } @@ -74,27 +101,45 @@ export class ProfilesManager { collect: (params: DocumentProfileProviderParams) => { documentProfiles.set(params.record.id, this.documentProfileService.resolve(params)); }, - finalize: () => { - this.documentProfiles = documentProfiles; + finalize: (clearExisting: boolean) => { + if (clearExisting) { + this.documentProfiles$.next(documentProfiles); + } else { + const existingProfiles = this.documentProfiles$.getValue(); + + documentProfiles.forEach((profile, id) => { + existingProfiles.set(id, profile); + }); + + this.documentProfiles$.next(existingProfiles); + } }, }; } - public getProfiles({ record }: { record?: DataTableRecord } = {}) { + public getProfiles({ record }: GetProfilesOptions = {}) { return [ - this.rootProfile, - this.dataSourceProfile, - record ? this.documentProfiles?.get(record.id) : undefined, + this.rootProfile$.getValue(), + this.dataSourceProfile$.getValue(), + record ? this.documentProfiles$.getValue().get(record.id) : undefined, ].filter(profileExists); } - private parseRootProfileParams(params: RootProfileProviderParams): SerializedRootProfileParams { + public getProfiles$(options: GetProfilesOptions = {}) { + return combineLatest([this.rootProfile$, this.dataSourceProfile$, this.documentProfiles$]).pipe( + map(() => this.getProfiles(options)) + ); + } + + private serializeRootProfileParams( + params: RootProfileProviderParams + ): SerializedRootProfileParams { return { solutionNavId: params.solutionNavId, }; } - private parseDataSourceProfileParams( + private serializeDataSourceProfileParams( params: DataSourceProfileProviderParams ): SerializedDataSourceProfileParams { return { diff --git a/src/plugins/discover/public/context_awareness/profiles_provider.tsx b/src/plugins/discover/public/context_awareness/profiles_provider.tsx index 64b0524dbe6d3..ca905ba42db71 100644 --- a/src/plugins/discover/public/context_awareness/profiles_provider.tsx +++ b/src/plugins/discover/public/context_awareness/profiles_provider.tsx @@ -6,32 +6,25 @@ * Side Public License, v 1. */ -import React, { createContext, FC, useContext, useMemo, useState } from 'react'; -import type { ComposableProfile } from './composable_profile'; -import { dataSourceProfileService } from './profiles/data_source_profile'; -import { documentProfileService } from './profiles/document_profile'; -import { rootProfileService } from './profiles/root_profile'; -import { ProfilesManager } from './profiles_manager'; +import { createContext, useContext, useMemo } from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { DataSourceProfileService, DocumentProfileService, RootProfileService } from './profiles'; +import { GetProfilesOptions, ProfilesManager } from './profiles_manager'; -const profilesContext = createContext([]); +const profilesContext = createContext( + new ProfilesManager( + new RootProfileService(), + new DataSourceProfileService(), + new DocumentProfileService() + ) +); -export const ProfilesProvider: FC<{ - rootProfile: ComposableProfile; - dataSourceProfile: ComposableProfile | undefined; -}> = ({ rootProfile, dataSourceProfile, children }) => { - const [manager] = useState( - () => new ProfilesManager(rootProfileService, dataSourceProfileService, documentProfileService) - ); - const profiles = useMemo( - () => [rootProfile, dataSourceProfile].filter(profileExists), - [dataSourceProfile, rootProfile] - ); +export const ProfilesProvider = profilesContext.Provider; - return {children}; -}; - -export const useProfiles = () => useContext(profilesContext); +export const useProfiles = ({ record }: GetProfilesOptions = {}) => { + const manager = useContext(profilesContext); + const profiles$ = useMemo(() => manager.getProfiles$({ record }), [manager, record]); + const profiles = useMemo(() => manager.getProfiles({ record }), [manager, record]); -const profileExists = (profile?: ComposableProfile): profile is ComposableProfile => { - return profile !== undefined; + return useObservable(profiles$, profiles); }; diff --git a/src/plugins/discover/public/context_awareness/types.ts b/src/plugins/discover/public/context_awareness/types.ts index 7ff323c94f205..99e34e2cd2603 100644 --- a/src/plugins/discover/public/context_awareness/types.ts +++ b/src/plugins/discover/public/context_awareness/types.ts @@ -6,13 +6,10 @@ * Side Public License, v 1. */ -import type { DataTableRecord } from '@kbn/discover-utils'; import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; import type { DiscoverGridSettings } from '@kbn/saved-search-plugin/common'; import type { CustomCellRenderer } from '@kbn/unified-data-table'; import type { DocViewsRegistry } from '@kbn/unified-doc-viewer'; -import type { ComposableProfile } from './composable_profile'; -import type { DocumentProfile } from './profiles'; export interface Profile { getTopNavItems: () => TopNavMenuData[]; @@ -22,7 +19,3 @@ export interface Profile { getCellRenderers: () => CustomCellRenderer; getDocViewsRegistry: (prevRegistry: DocViewsRegistry) => DocViewsRegistry; } - -export interface DataTableRecordWithProfile extends DataTableRecord { - profile: ComposableProfile; -} diff --git a/src/plugins/discover/public/context_awareness/use_profile_accessor.ts b/src/plugins/discover/public/context_awareness/use_profile_accessor.ts index 52f66c35e5e06..ec508a4699d9b 100644 --- a/src/plugins/discover/public/context_awareness/use_profile_accessor.ts +++ b/src/plugins/discover/public/context_awareness/use_profile_accessor.ts @@ -6,27 +6,17 @@ * Side Public License, v 1. */ -import type { DataTableRecord } from '@kbn/discover-utils'; import { useMemo } from 'react'; import { getMergedAccessor } from './composable_profile'; -import { recordHasProfile } from './profiles'; +import { GetProfilesOptions } from './profiles_manager'; import { useProfiles } from './profiles_provider'; import type { Profile } from './types'; export const useProfileAccessor = ( key: TKey, baseImpl: Profile[TKey], - { record }: { record?: DataTableRecord } = {} + options: GetProfilesOptions = {} ) => { - const profiles = useProfiles(); - - return useMemo(() => { - let allProfiles = profiles; - - if (recordHasProfile(record)) { - allProfiles = [...profiles, record.profile]; - } - - return getMergedAccessor(allProfiles, key, baseImpl); - }, [baseImpl, key, profiles, record]); + const profiles = useProfiles(options); + return useMemo(() => getMergedAccessor(profiles, key, baseImpl), [baseImpl, key, profiles]); }; diff --git a/src/plugins/discover/public/context_awareness/use_root_profile.ts b/src/plugins/discover/public/context_awareness/use_root_profile.ts new file mode 100644 index 0000000000000..e5a7d6e65d7a5 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/use_root_profile.ts @@ -0,0 +1,38 @@ +/* + * 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 { useEffect, useState } from 'react'; +import type { ProfilesManager } from './profiles_manager'; + +export const useRootProfile = ({ + profilesManager, + solutionNavId, +}: { + profilesManager: ProfilesManager; + solutionNavId: string | null; +}) => { + const [rootProfileLoading, setRootProfileLoading] = useState(true); + + useEffect(() => { + const abortController = new AbortController(); + + setRootProfileLoading(true); + + profilesManager.resolveRootContext({ solutionNavId }, abortController.signal).then(() => { + if (!abortController.signal.aborted) { + setRootProfileLoading(false); + } + }); + + return () => { + abortController.abort(); + }; + }, [profilesManager, solutionNavId]); + + return { rootProfileLoading }; +}; From b6984cebdab775684b7d3a5ba5e2241683709c52 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 21 May 2024 18:07:22 -0300 Subject: [PATCH 15/52] Change tsx files to ts --- .../profiles/{data_source_profile.tsx => data_source_profile.ts} | 0 .../profiles/{document_profile.tsx => document_profile.ts} | 0 .../{profiles_provider.tsx => profiles_provider.ts} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename src/plugins/discover/public/context_awareness/profiles/{data_source_profile.tsx => data_source_profile.ts} (100%) rename src/plugins/discover/public/context_awareness/profiles/{document_profile.tsx => document_profile.ts} (100%) rename src/plugins/discover/public/context_awareness/{profiles_provider.tsx => profiles_provider.ts} (100%) diff --git a/src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx b/src/plugins/discover/public/context_awareness/profiles/data_source_profile.ts similarity index 100% rename from src/plugins/discover/public/context_awareness/profiles/data_source_profile.tsx rename to src/plugins/discover/public/context_awareness/profiles/data_source_profile.ts diff --git a/src/plugins/discover/public/context_awareness/profiles/document_profile.tsx b/src/plugins/discover/public/context_awareness/profiles/document_profile.ts similarity index 100% rename from src/plugins/discover/public/context_awareness/profiles/document_profile.tsx rename to src/plugins/discover/public/context_awareness/profiles/document_profile.ts diff --git a/src/plugins/discover/public/context_awareness/profiles_provider.tsx b/src/plugins/discover/public/context_awareness/profiles_provider.ts similarity index 100% rename from src/plugins/discover/public/context_awareness/profiles_provider.tsx rename to src/plugins/discover/public/context_awareness/profiles_provider.ts From 0e5f2e1f146957a1b6141e186e1bda91ef01d341 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 21 May 2024 18:14:31 -0300 Subject: [PATCH 16/52] Handle ProfilesManager abort controllers internally --- .../discover_data_state_container.ts | 13 +++++------- .../context_awareness/profiles_manager.ts | 21 ++++++++++++------- .../context_awareness/use_root_profile.ts | 8 +++---- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts index d4963e7962648..31bf79e5c0717 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts @@ -279,14 +279,11 @@ export function getDataStateContainer({ autoRefreshDone = undefined; } - await profilesManager.resolveDataSourceContext( - { - dataSource: getAppState().dataSource, - dataView: getSavedSearch().searchSource.getField('index'), - query: getAppState().query, - }, - abortController.signal - ); + await profilesManager.resolveDataSourceContext({ + dataSource: getAppState().dataSource, + dataView: getSavedSearch().searchSource.getField('index'), + query: getAppState().query, + }); const defaultColumns = getMergedAccessor( profilesManager.getProfiles(), diff --git a/src/plugins/discover/public/context_awareness/profiles_manager.ts b/src/plugins/discover/public/context_awareness/profiles_manager.ts index e1885b5a9ea00..d26acf6af9082 100644 --- a/src/plugins/discover/public/context_awareness/profiles_manager.ts +++ b/src/plugins/discover/public/context_awareness/profiles_manager.ts @@ -47,6 +47,8 @@ export class ProfilesManager { ); private prevRootProfileParams?: SerializedRootProfileParams; private prevDataSourceProfileParams?: SerializedDataSourceProfileParams; + private rootProfileAbortController?: AbortController; + private dataSourceProfileAbortController?: AbortController; constructor( private readonly rootProfileService: RootProfileService, @@ -54,16 +56,20 @@ export class ProfilesManager { private readonly documentProfileService: DocumentProfileService ) {} - public async resolveRootContext(params: RootProfileProviderParams, abortSignal: AbortSignal) { + public async resolveRootContext(params: RootProfileProviderParams) { const serializedParams = this.serializeRootProfileParams(params); if (this.rootProfile$.getValue() && isEqual(this.prevRootProfileParams, serializedParams)) { return; } + const abortController = new AbortController(); + this.rootProfileAbortController?.abort(); + this.rootProfileAbortController = abortController; + const profile = await this.rootProfileService.resolve(params); - if (abortSignal.aborted) { + if (abortController.signal.aborted) { return; } @@ -71,10 +77,7 @@ export class ProfilesManager { this.prevRootProfileParams = serializedParams; } - public async resolveDataSourceContext( - params: DataSourceProfileProviderParams, - abortSignal: AbortSignal - ) { + public async resolveDataSourceContext(params: DataSourceProfileProviderParams) { const serializedParams = this.serializeDataSourceProfileParams(params); if ( @@ -84,9 +87,13 @@ export class ProfilesManager { return; } + const abortController = new AbortController(); + this.dataSourceProfileAbortController?.abort(); + this.dataSourceProfileAbortController = abortController; + const profile = await this.dataSourceProfileService.resolve(params); - if (abortSignal.aborted) { + if (abortController.signal.aborted) { return; } diff --git a/src/plugins/discover/public/context_awareness/use_root_profile.ts b/src/plugins/discover/public/context_awareness/use_root_profile.ts index e5a7d6e65d7a5..af73bdb71d98e 100644 --- a/src/plugins/discover/public/context_awareness/use_root_profile.ts +++ b/src/plugins/discover/public/context_awareness/use_root_profile.ts @@ -19,18 +19,18 @@ export const useRootProfile = ({ const [rootProfileLoading, setRootProfileLoading] = useState(true); useEffect(() => { - const abortController = new AbortController(); + let aborted = false; setRootProfileLoading(true); - profilesManager.resolveRootContext({ solutionNavId }, abortController.signal).then(() => { - if (!abortController.signal.aborted) { + profilesManager.resolveRootContext({ solutionNavId }).then(() => { + if (!aborted) { setRootProfileLoading(false); } }); return () => { - abortController.abort(); + aborted = true; }; }, [profilesManager, solutionNavId]); From ff90e2b197ea892911933b9968689473e5506415 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 21 May 2024 18:18:50 -0300 Subject: [PATCH 17/52] Rename ProfilesManager methods --- .../application/main/data_fetching/fetch_documents.ts | 2 +- .../public/application/main/data_fetching/fetch_esql.ts | 2 +- .../main/state_management/discover_data_state_container.ts | 2 +- .../discover/public/context_awareness/profiles_manager.ts | 6 +++--- .../discover/public/context_awareness/use_root_profile.ts | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts index 4d92c0e00e5a6..a9b60f94b62ce 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts @@ -51,7 +51,7 @@ export const fetchDocuments = ( description: isFetchingMore ? 'fetch more documents' : 'fetch documents', }; - const contextCollector = profilesManager.createDocumentContextCollector(); + const contextCollector = profilesManager.createDocumentProfilesCollector(); const fetch$ = searchSource .fetch$({ diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts index 1addf739706b9..a784b922fd34a 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts @@ -62,7 +62,7 @@ export function fetchEsql( let esqlQueryColumns: Datatable['columns'] | undefined; let error: string | undefined; let esqlHeaderWarning: string | undefined; - const contextCollector = profilesManager.createDocumentContextCollector(); + const contextCollector = profilesManager.createDocumentProfilesCollector(); execution.pipe(pluck('result')).subscribe((resp) => { const response = resp as Datatable | EsqlErrorResponse; if (response.type === 'error') { diff --git a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts index 31bf79e5c0717..ce048a6630d7a 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts @@ -279,7 +279,7 @@ export function getDataStateContainer({ autoRefreshDone = undefined; } - await profilesManager.resolveDataSourceContext({ + await profilesManager.resolveDataSourceProfile({ dataSource: getAppState().dataSource, dataView: getSavedSearch().searchSource.getField('index'), query: getAppState().query, diff --git a/src/plugins/discover/public/context_awareness/profiles_manager.ts b/src/plugins/discover/public/context_awareness/profiles_manager.ts index d26acf6af9082..2ad5c486d19a2 100644 --- a/src/plugins/discover/public/context_awareness/profiles_manager.ts +++ b/src/plugins/discover/public/context_awareness/profiles_manager.ts @@ -56,7 +56,7 @@ export class ProfilesManager { private readonly documentProfileService: DocumentProfileService ) {} - public async resolveRootContext(params: RootProfileProviderParams) { + public async resolveRootProfile(params: RootProfileProviderParams) { const serializedParams = this.serializeRootProfileParams(params); if (this.rootProfile$.getValue() && isEqual(this.prevRootProfileParams, serializedParams)) { @@ -77,7 +77,7 @@ export class ProfilesManager { this.prevRootProfileParams = serializedParams; } - public async resolveDataSourceContext(params: DataSourceProfileProviderParams) { + public async resolveDataSourceProfile(params: DataSourceProfileProviderParams) { const serializedParams = this.serializeDataSourceProfileParams(params); if ( @@ -101,7 +101,7 @@ export class ProfilesManager { this.prevDataSourceProfileParams = serializedParams; } - public createDocumentContextCollector() { + public createDocumentProfilesCollector() { const documentProfiles = new Map>(); return { diff --git a/src/plugins/discover/public/context_awareness/use_root_profile.ts b/src/plugins/discover/public/context_awareness/use_root_profile.ts index af73bdb71d98e..57a8485efba14 100644 --- a/src/plugins/discover/public/context_awareness/use_root_profile.ts +++ b/src/plugins/discover/public/context_awareness/use_root_profile.ts @@ -23,7 +23,7 @@ export const useRootProfile = ({ setRootProfileLoading(true); - profilesManager.resolveRootContext({ solutionNavId }).then(() => { + profilesManager.resolveRootProfile({ solutionNavId }).then(() => { if (!aborted) { setRootProfileLoading(false); } From 1de88495ddf21b03368425638be920838ff3dfc0 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 21 May 2024 22:36:17 -0300 Subject: [PATCH 18/52] Remove static profile services and instead instantiate them in plugin class --- .../application/main/discover_main_route.tsx | 27 +++----- src/plugins/discover/public/build_services.ts | 5 ++ .../public/context_awareness/index.ts | 4 +- .../profiles/data_source_profile.ts | 2 - .../profiles/document_profile.ts | 2 - .../profiles/example_profiles.tsx | 14 +---- .../profiles/root_profile.ts | 2 - .../context_awareness/profiles_manager.ts | 13 ++-- .../context_awareness/profiles_provider.ts | 30 --------- .../context_awareness/use_profile_accessor.ts | 2 +- .../public/context_awareness/use_profiles.ts | 28 +++++++++ src/plugins/discover/public/plugin.tsx | 61 +++++++++++++------ 12 files changed, 100 insertions(+), 90 deletions(-) delete mode 100644 src/plugins/discover/public/context_awareness/profiles_provider.ts create mode 100644 src/plugins/discover/public/context_awareness/use_profiles.ts diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx index f63ecd5e9bd17..bfca62ffa27b5 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.tsx @@ -40,14 +40,7 @@ import { import { DiscoverTopNavInline } from './components/top_nav/discover_topnav_inline'; import { DiscoverStateContainer, LoadParams } from './state_management/discover_state'; import { DataSourceType, isDataSourceType } from '../../../common/data_sources'; -import { - dataSourceProfileService, - documentProfileService, - ProfilesProvider, - rootProfileService, -} from '../../context_awareness'; -import { ProfilesManager } from '../../context_awareness/profiles_manager'; -import { useRootProfile } from '../../context_awareness/use_root_profile'; +import { useRootProfile } from '../../context_awareness'; const DiscoverMainAppMemoized = memo(DiscoverMainApp); @@ -76,12 +69,10 @@ export function DiscoverMainRoute({ http: { basePath }, dataViewEditor, share, + profilesManager, getScopedHistory, } = services; const { id: savedSearchId } = useParams(); - const [profilesManager] = useState( - () => new ProfilesManager(rootProfileService, dataSourceProfileService, documentProfileService) - ); const [stateContainer, { reset: resetStateContainer }] = useDiscoverStateContainer({ history, services, @@ -363,14 +354,12 @@ export function DiscoverMainRoute({ return ( - - - <> - - {mainContent} - - - + + <> + + {mainContent} + + ); } diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index e3524dcdf115c..dad20f16a14fd 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -63,6 +63,7 @@ import { DiscoverStartPlugins } from './plugin'; import { DiscoverContextAppLocator } from './application/context/services/locator'; import { DiscoverSingleDocLocator } from './application/doc/locator'; import { DiscoverAppLocator } from '../common'; +import { ProfilesManager } from './context_awareness'; /** * Location state of internal Discover history instance @@ -129,6 +130,7 @@ export interface DiscoverServices { contentClient: ContentClient; noDataPage?: NoDataPagePluginStart; observabilityAIAssistant?: ObservabilityAIAssistantPublicStart; + profilesManager: ProfilesManager; } export const buildServices = memoize( @@ -142,6 +144,7 @@ export const buildServices = memoize( history, scopedHistory, urlTracker, + profilesManager, setHeaderActionMenu = noop, }: { core: CoreStart; @@ -153,6 +156,7 @@ export const buildServices = memoize( history: History; scopedHistory?: ScopedHistory; urlTracker: UrlTracker; + profilesManager: ProfilesManager; setHeaderActionMenu?: AppMountParameters['setHeaderActionMenu']; }): DiscoverServices => { const { usageCollection } = plugins; @@ -212,6 +216,7 @@ export const buildServices = memoize( contentClient: plugins.contentManagement.client, noDataPage: plugins.noDataPage, observabilityAIAssistant: plugins.observabilityAIAssistant, + profilesManager, }; } ); diff --git a/src/plugins/discover/public/context_awareness/index.ts b/src/plugins/discover/public/context_awareness/index.ts index 628341e3e1423..67c53addfcf8d 100644 --- a/src/plugins/discover/public/context_awareness/index.ts +++ b/src/plugins/discover/public/context_awareness/index.ts @@ -9,5 +9,7 @@ export * from './types'; export * from './profiles'; export { type ComposableProfile, getMergedAccessor } from './composable_profile'; -export { ProfilesProvider, useProfiles } from './profiles_provider'; +export { type GetProfilesOptions, ProfilesManager } from './profiles_manager'; +export { useProfiles } from './use_profiles'; export { useProfileAccessor } from './use_profile_accessor'; +export { useRootProfile } from './use_root_profile'; 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 0fbade6123bb2..168e2658c22fd 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 @@ -36,5 +36,3 @@ export class DataSourceProfileService extends AsyncProfileService< > {} export type DataSourceProfileProvider = Parameters[0]; - -export const dataSourceProfileService = new DataSourceProfileService(); 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 42702f4504c94..2fb0b5f36b88e 100644 --- a/src/plugins/discover/public/context_awareness/profiles/document_profile.ts +++ b/src/plugins/discover/public/context_awareness/profiles/document_profile.ts @@ -32,5 +32,3 @@ export class DocumentProfileService extends ProfileService< > {} export type DocumentProfileProvider = Parameters[0]; - -export const documentProfileService = new DocumentProfileService(); diff --git a/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx index 472149a292032..d0ba68ffd64bb 100644 --- a/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx +++ b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx @@ -18,13 +18,9 @@ import { euiThemeVars } from '@kbn/ui-theme'; import { capitalize } from 'lodash'; import React from 'react'; import { DataSourceType, isDataSourceType } from '../../../common/data_sources'; -import { - DataSourceCategory, - DataSourceProfileProvider, - dataSourceProfileService, -} from './data_source_profile'; -import { DocumentProfileProvider, documentProfileService, DocumentType } from './document_profile'; -import { RootProfileProvider, rootProfileService, SolutionType } from './root_profile'; +import { DataSourceCategory, DataSourceProfileProvider } from './data_source_profile'; +import { DocumentProfileProvider, DocumentType } from './document_profile'; +import { RootProfileProvider, SolutionType } from './root_profile'; export const o11yRootProfileProvider: RootProfileProvider = { order: 0, @@ -162,10 +158,6 @@ export const logDocumentProfileProvider: DocumentProfileProvider = { }, }; -rootProfileService.registerProvider(o11yRootProfileProvider); -dataSourceProfileService.registerProvider(logsDataSourceProfileProvider); -documentProfileService.registerProvider(logDocumentProfileProvider); - 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 6461b445ac2a8..4b0df08b0fcfa 100644 --- a/src/plugins/discover/public/context_awareness/profiles/root_profile.ts +++ b/src/plugins/discover/public/context_awareness/profiles/root_profile.ts @@ -33,5 +33,3 @@ export class RootProfileService extends AsyncProfileService< > {} export type RootProfileProvider = Parameters[0]; - -export const rootProfileService = new RootProfileService(); diff --git a/src/plugins/discover/public/context_awareness/profiles_manager.ts b/src/plugins/discover/public/context_awareness/profiles_manager.ts index 2ad5c486d19a2..a6dc213dbcd22 100644 --- a/src/plugins/discover/public/context_awareness/profiles_manager.ts +++ b/src/plugins/discover/public/context_awareness/profiles_manager.ts @@ -38,13 +38,16 @@ export interface GetProfilesOptions { } export class ProfilesManager { - private rootProfile$ = new BehaviorSubject | undefined>(undefined); - private dataSourceProfile$ = new BehaviorSubject< + private readonly rootProfile$ = new BehaviorSubject | undefined>( + undefined + ); + private readonly dataSourceProfile$ = new BehaviorSubject< ComposableProfile | undefined >(undefined); - private documentProfiles$ = new BehaviorSubject>>( - new Map() - ); + private readonly documentProfiles$ = new BehaviorSubject< + Map> + >(new Map()); + private prevRootProfileParams?: SerializedRootProfileParams; private prevDataSourceProfileParams?: SerializedDataSourceProfileParams; private rootProfileAbortController?: AbortController; diff --git a/src/plugins/discover/public/context_awareness/profiles_provider.ts b/src/plugins/discover/public/context_awareness/profiles_provider.ts deleted file mode 100644 index ca905ba42db71..0000000000000 --- a/src/plugins/discover/public/context_awareness/profiles_provider.ts +++ /dev/null @@ -1,30 +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 { createContext, useContext, useMemo } from 'react'; -import useObservable from 'react-use/lib/useObservable'; -import { DataSourceProfileService, DocumentProfileService, RootProfileService } from './profiles'; -import { GetProfilesOptions, ProfilesManager } from './profiles_manager'; - -const profilesContext = createContext( - new ProfilesManager( - new RootProfileService(), - new DataSourceProfileService(), - new DocumentProfileService() - ) -); - -export const ProfilesProvider = profilesContext.Provider; - -export const useProfiles = ({ record }: GetProfilesOptions = {}) => { - const manager = useContext(profilesContext); - const profiles$ = useMemo(() => manager.getProfiles$({ record }), [manager, record]); - const profiles = useMemo(() => manager.getProfiles({ record }), [manager, record]); - - return useObservable(profiles$, profiles); -}; diff --git a/src/plugins/discover/public/context_awareness/use_profile_accessor.ts b/src/plugins/discover/public/context_awareness/use_profile_accessor.ts index ec508a4699d9b..588da946d25af 100644 --- a/src/plugins/discover/public/context_awareness/use_profile_accessor.ts +++ b/src/plugins/discover/public/context_awareness/use_profile_accessor.ts @@ -9,7 +9,7 @@ import { useMemo } from 'react'; import { getMergedAccessor } from './composable_profile'; import { GetProfilesOptions } from './profiles_manager'; -import { useProfiles } from './profiles_provider'; +import { useProfiles } from './use_profiles'; import type { Profile } from './types'; export const useProfileAccessor = ( diff --git a/src/plugins/discover/public/context_awareness/use_profiles.ts b/src/plugins/discover/public/context_awareness/use_profiles.ts new file mode 100644 index 0000000000000..240d8747d6e27 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/use_profiles.ts @@ -0,0 +1,28 @@ +/* + * 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 { useMemo } from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { useDiscoverServices } from '../hooks/use_discover_services'; +import { GetProfilesOptions } from './profiles_manager'; + +export const useProfiles = ({ record }: GetProfilesOptions = {}) => { + const { profilesManager } = useDiscoverServices(); + + const profiles$ = useMemo( + () => profilesManager.getProfiles$({ record }), + [profilesManager, record] + ); + + const profiles = useMemo( + () => profilesManager.getProfiles({ record }), + [profilesManager, record] + ); + + return useObservable(profiles$, profiles); +}; diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index c040769ab1a78..88fc94d4c9f15 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -82,6 +82,17 @@ import { import { getESQLSearchProvider } from './global_search/search_provider'; import { HistoryService } from './history_service'; import { ConfigSchema, ExperimentalFeatures } from '../common/config'; +import { + DataSourceProfileService, + DocumentProfileService, + ProfilesManager, + RootProfileService, +} from './context_awareness'; +import { + logDocumentProfileProvider, + logsDataSourceProfileProvider, + o11yRootProfileProvider, +} from './context_awareness/profiles/example_profiles'; /** * @public @@ -218,25 +229,28 @@ export type StartRenderServices = Pick { - constructor(private readonly initializerContext: PluginInitializerContext) { - this.experimentalFeatures = - initializerContext.config.get().experimental ?? this.experimentalFeatures; - } + 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 appStateUpdater = new BehaviorSubject(() => ({})); - private historyService = new HistoryService(); private scopedHistory?: ScopedHistory; private urlTracker?: UrlTracker; - private stopUrlTracking: (() => void) | undefined = undefined; + private stopUrlTracking?: () => void; private locator?: DiscoverAppLocator; private contextLocator?: DiscoverContextAppLocator; private singleDocLocator?: DiscoverSingleDocLocator; - private inlineTopNav: Map = new Map([ - [null, defaultCustomizationContext.inlineTopNav], - ]); - private experimentalFeatures: ExperimentalFeatures = { - ruleFormV2Enabled: false, - }; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.experimentalFeatures = + initializerContext.config.get().experimental ?? this.experimentalFeatures; + } setup( core: CoreSetup, @@ -331,6 +345,7 @@ export class DiscoverPlugin history: this.historyService.getHistory(), scopedHistory: this.scopedHistory, urlTracker: this.urlTracker!, + profilesManager: this.createProfilesManager(), setHeaderActionMenu: params.setHeaderActionMenu, }); @@ -413,10 +428,7 @@ export class DiscoverPlugin } start(core: CoreStart, plugins: DiscoverStartPlugins): DiscoverStart { - // we need to register the application service at setup, but to render it - // there are some start dependencies necessary, for this reason - // initializeServices are assigned at start and used - // when the application/embeddable is mounted + this.registerProfiles(); const viewSavedSearchAction = new ViewSavedSearchAction(core.application, this.locator!); @@ -450,6 +462,20 @@ export class DiscoverPlugin } } + private registerProfiles() { + this.rootProfileService.registerProvider(o11yRootProfileProvider); + this.dataSourceProfileService.registerProvider(logsDataSourceProfileProvider); + this.documentProfileService.registerProvider(logDocumentProfileProvider); + } + + private createProfilesManager() { + return new ProfilesManager( + this.rootProfileService, + this.dataSourceProfileService, + this.documentProfileService + ); + } + private getDiscoverServices = (core: CoreStart, plugins: DiscoverStartPlugins) => { return buildServices({ core, @@ -460,6 +486,7 @@ export class DiscoverPlugin singleDocLocator: this.singleDocLocator!, history: this.historyService.getHistory(), urlTracker: this.urlTracker!, + profilesManager: this.createProfilesManager(), }); }; From 807ee46e918e6e886690c7a926e1e2ee472bc059 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 21 May 2024 22:41:16 -0300 Subject: [PATCH 19/52] Don't export useProfiles --- src/plugins/discover/public/context_awareness/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/discover/public/context_awareness/index.ts b/src/plugins/discover/public/context_awareness/index.ts index 67c53addfcf8d..0c18b9462d73a 100644 --- a/src/plugins/discover/public/context_awareness/index.ts +++ b/src/plugins/discover/public/context_awareness/index.ts @@ -10,6 +10,5 @@ export * from './types'; export * from './profiles'; export { type ComposableProfile, getMergedAccessor } from './composable_profile'; export { type GetProfilesOptions, ProfilesManager } from './profiles_manager'; -export { useProfiles } from './use_profiles'; export { useProfileAccessor } from './use_profile_accessor'; export { useRootProfile } from './use_root_profile'; From 3829c285f765d2a6fb68bed529d59878998b9d4a Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 21 May 2024 22:58:49 -0300 Subject: [PATCH 20/52] Retrieve ProfilesManager from services --- .../main/data_fetching/fetch_all.ts | 5 +-- .../main/data_fetching/fetch_documents.ts | 11 +----- .../main/data_fetching/fetch_esql.ts | 12 ++---- .../application/main/discover_main_route.tsx | 4 +- .../discover_data_state_container.ts | 6 +-- .../main/state_management/discover_state.ts | 4 -- src/plugins/discover/public/build_services.ts | 39 +++++++++---------- .../{ => hooks}/use_profile_accessor.ts | 6 +-- .../{ => hooks}/use_profiles.ts | 4 +- .../{ => hooks}/use_root_profile.ts | 11 ++---- .../public/context_awareness/index.ts | 4 +- 11 files changed, 37 insertions(+), 69 deletions(-) rename src/plugins/discover/public/context_awareness/{ => hooks}/use_profile_accessor.ts (82%) rename src/plugins/discover/public/context_awareness/{ => hooks}/use_profiles.ts (86%) rename src/plugins/discover/public/context_awareness/{ => hooks}/use_root_profile.ts (79%) diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts index fd3d278cf0b97..268adb87afc42 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts @@ -30,7 +30,6 @@ import { DataMsg, SavedSearchData } from '../state_management/discover_data_stat import { DiscoverServices } from '../../../build_services'; import { fetchEsql } from './fetch_esql'; import { InternalState } from '../state_management/discover_internal_state_container'; -import { ProfilesManager } from '../../../context_awareness/profiles_manager'; export interface FetchDeps { abortController: AbortController; @@ -42,7 +41,6 @@ export interface FetchDeps { searchSessionId: string; services: DiscoverServices; useNewFieldsApi: boolean; - profilesManager: ProfilesManager; } /** @@ -65,7 +63,6 @@ export function fetchAll( inspectorAdapters, savedSearch, abortController, - profilesManager, } = fetchDeps; const { data } = services; const searchSource = savedSearch.searchSource.createChild(); @@ -109,7 +106,7 @@ export function fetchAll( data, services.expressions, inspectorAdapters, - profilesManager, + services.profilesManager, abortController.signal ) : fetchDocuments(searchSource, fetchDeps); diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts index a9b60f94b62ce..818f802019f17 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts @@ -23,14 +23,7 @@ import { FetchDeps } from './fetch_all'; */ export const fetchDocuments = ( searchSource: ISearchSource, - { - abortController, - inspectorAdapters, - searchSessionId, - services, - getAppState, - profilesManager, - }: FetchDeps + { abortController, inspectorAdapters, searchSessionId, services, getAppState }: FetchDeps ): Promise => { const sampleSize = getAppState().sampleSize; searchSource.setField('size', getAllowedSampleSize(sampleSize, services.uiSettings)); @@ -51,7 +44,7 @@ export const fetchDocuments = ( description: isFetchingMore ? 'fetch more documents' : 'fetch documents', }; - const contextCollector = profilesManager.createDocumentProfilesCollector(); + const contextCollector = services.profilesManager.createDocumentProfilesCollector(); const fetch$ = searchSource .fetch$({ diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts index a784b922fd34a..edf3fbbbdef88 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts @@ -8,16 +8,16 @@ import { pluck } from 'rxjs'; import { lastValueFrom } from 'rxjs'; import { i18n } from '@kbn/i18n'; -import { Query, AggregateQuery, Filter } from '@kbn/es-query'; +import type { Query, AggregateQuery, Filter } from '@kbn/es-query'; import type { Adapters } from '@kbn/inspector-plugin/common'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { ExpressionsStart } from '@kbn/expressions-plugin/public'; import type { Datatable } from '@kbn/expressions-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; import { textBasedQueryStateToAstWithValidation } from '@kbn/data-plugin/common'; -import { DataTableRecord } from '@kbn/discover-utils'; +import type { DataTableRecord, EsHitRecord } from '@kbn/discover-utils'; import type { RecordsFetchResponse } from '../../types'; -import { ProfilesManager } from '../../../context_awareness/profiles_manager'; +import type { ProfilesManager } from '../../../context_awareness'; interface EsqlErrorResponse { error: { @@ -75,11 +75,7 @@ export function fetchEsql( finalData = rows.map((row, idx) => { const record: DataTableRecord = { id: String(idx), - raw: { - _id: '', - _index: '', - ...row, - }, + raw: row as EsHitRecord, flattened: row, }; diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx index bfca62ffa27b5..f37487b6b93b7 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.tsx @@ -69,7 +69,6 @@ export function DiscoverMainRoute({ http: { basePath }, dataViewEditor, share, - profilesManager, getScopedHistory, } = services; const { id: savedSearchId } = useParams(); @@ -78,7 +77,6 @@ export function DiscoverMainRoute({ services, customizationContext, stateStorageContainer, - profilesManager, }); const { customizationService, isInitialized: isCustomizationServiceInitialized } = useDiscoverCustomizationService({ @@ -342,7 +340,7 @@ export function DiscoverMainRoute({ ]); const { solutionNavId } = customizationContext; - const { rootProfileLoading } = useRootProfile({ profilesManager, solutionNavId }); + const { rootProfileLoading } = useRootProfile({ solutionNavId }); if (error) { return ; diff --git a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts index ce048a6630d7a..d0e798c9d0896 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts @@ -29,7 +29,6 @@ import { sendResetMsg } from '../hooks/use_saved_search_messages'; import { getFetch$ } from '../data_fetching/get_fetch_observable'; import { InternalState } from './discover_internal_state_container'; import { getMergedAccessor } from '../../../context_awareness'; -import { ProfilesManager } from '../../../context_awareness/profiles_manager'; export interface SavedSearchData { main$: DataMain$; @@ -142,7 +141,6 @@ export interface DiscoverDataStateContainer { export function getDataStateContainer({ services, searchSessionManager, - profilesManager, getAppState, getInternalState, getSavedSearch, @@ -151,14 +149,13 @@ export function getDataStateContainer({ }: { services: DiscoverServices; searchSessionManager: DiscoverSearchSessionManager; - profilesManager: ProfilesManager; getAppState: () => DiscoverAppState; getInternalState: () => InternalState; getSavedSearch: () => SavedSearch; setDataView: (dataView: DataView) => void; updateAppState: DiscoverAppStateContainer['update']; }): DiscoverDataStateContainer { - const { data, uiSettings, toastNotifications } = services; + const { data, uiSettings, toastNotifications, profilesManager } = services; const { timefilter } = data.query.timefilter; const inspectorAdapters = { requests: new RequestAdapter() }; @@ -234,7 +231,6 @@ export function getDataStateContainer({ getInternalState, savedSearch: getSavedSearch(), useNewFieldsApi: !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), - profilesManager, }; abortController?.abort(); diff --git a/src/plugins/discover/public/application/main/state_management/discover_state.ts b/src/plugins/discover/public/application/main/state_management/discover_state.ts index 4c822dee4f566..4597fa49d8d11 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_state.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_state.ts @@ -59,7 +59,6 @@ import { DataSourceType, isDataSourceType, } from '../../../../common/data_sources'; -import { ProfilesManager } from '../../../context_awareness/profiles_manager'; export interface DiscoverStateContainerParams { /** @@ -82,7 +81,6 @@ export interface DiscoverStateContainerParams { * a custom url state storage */ stateStorageContainer?: IKbnUrlStateStorage; - profilesManager: ProfilesManager; } export interface LoadParams { @@ -221,7 +219,6 @@ export function getDiscoverStateContainer({ services, customizationContext, stateStorageContainer, - profilesManager, }: DiscoverStateContainerParams): DiscoverStateContainer { const storeInSessionStorage = services.uiSettings.get('state:storeInSessionStorage'); const toasts = services.core.notifications.toasts; @@ -293,7 +290,6 @@ export function getDiscoverStateContainer({ const dataStateContainer = getDataStateContainer({ services, searchSessionManager, - profilesManager, getAppState: appStateContainer.getState, getInternalState: internalStateContainer.getState, getSavedSearch: savedSearchContainer.getState, diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index dad20f16a14fd..519d6a36fb528 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -7,8 +7,7 @@ */ import { History } from 'history'; - -import { +import type { Capabilities, ChromeStart, CoreStart, @@ -24,28 +23,26 @@ import { AppMountParameters, ScopedHistory, } from '@kbn/core/public'; -import { +import type { FilterManager, TimefilterContract, DataViewsContract, DataPublicPluginStart, } from '@kbn/data-plugin/public'; import type { ExpressionsStart } from '@kbn/expressions-plugin/public'; -import { Start as InspectorPublicPluginStart } from '@kbn/inspector-plugin/public'; -import { SharePluginStart } from '@kbn/share-plugin/public'; -import { ChartsPluginStart } from '@kbn/charts-plugin/public'; -import { UiCounterMetricType } from '@kbn/analytics'; +import type { Start as InspectorPublicPluginStart } from '@kbn/inspector-plugin/public'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; +import type { ChartsPluginStart } from '@kbn/charts-plugin/public'; +import type { UiCounterMetricType } from '@kbn/analytics'; import { Storage } from '@kbn/kibana-utils-plugin/public'; - -import { UrlForwardingStart } from '@kbn/url-forwarding-plugin/public'; -import { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; -import { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; -import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -import { EmbeddableStart } from '@kbn/embeddable-plugin/public'; -import { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public'; - +import type { UrlForwardingStart } from '@kbn/url-forwarding-plugin/public'; +import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; +import type { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; +import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import type { EmbeddableStart } from '@kbn/embeddable-plugin/public'; +import type { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public'; import type { SpacesApi } from '@kbn/spaces-plugin/public'; -import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; +import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; import type { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public'; import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; @@ -59,11 +56,11 @@ import { memoize, noop } from 'lodash'; import type { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public'; import type { AiopsPluginStart } from '@kbn/aiops-plugin/public'; import type { DataVisualizerPluginStart } from '@kbn/data-visualizer-plugin/public'; -import { DiscoverStartPlugins } from './plugin'; -import { DiscoverContextAppLocator } from './application/context/services/locator'; -import { DiscoverSingleDocLocator } from './application/doc/locator'; -import { DiscoverAppLocator } from '../common'; -import { ProfilesManager } from './context_awareness'; +import type { DiscoverStartPlugins } from './plugin'; +import type { DiscoverContextAppLocator } from './application/context/services/locator'; +import type { DiscoverSingleDocLocator } from './application/doc/locator'; +import type { DiscoverAppLocator } from '../common'; +import type { ProfilesManager } from './context_awareness'; /** * Location state of internal Discover history instance diff --git a/src/plugins/discover/public/context_awareness/use_profile_accessor.ts b/src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.ts similarity index 82% rename from src/plugins/discover/public/context_awareness/use_profile_accessor.ts rename to src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.ts index 588da946d25af..dd1fff2240bcf 100644 --- a/src/plugins/discover/public/context_awareness/use_profile_accessor.ts +++ b/src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.ts @@ -7,10 +7,10 @@ */ import { useMemo } from 'react'; -import { getMergedAccessor } from './composable_profile'; -import { GetProfilesOptions } from './profiles_manager'; +import { getMergedAccessor } from '../composable_profile'; +import { GetProfilesOptions } from '../profiles_manager'; import { useProfiles } from './use_profiles'; -import type { Profile } from './types'; +import type { Profile } from '../types'; export const useProfileAccessor = ( key: TKey, diff --git a/src/plugins/discover/public/context_awareness/use_profiles.ts b/src/plugins/discover/public/context_awareness/hooks/use_profiles.ts similarity index 86% rename from src/plugins/discover/public/context_awareness/use_profiles.ts rename to src/plugins/discover/public/context_awareness/hooks/use_profiles.ts index 240d8747d6e27..0fda1398bc111 100644 --- a/src/plugins/discover/public/context_awareness/use_profiles.ts +++ b/src/plugins/discover/public/context_awareness/hooks/use_profiles.ts @@ -8,8 +8,8 @@ import { useMemo } from 'react'; import useObservable from 'react-use/lib/useObservable'; -import { useDiscoverServices } from '../hooks/use_discover_services'; -import { GetProfilesOptions } from './profiles_manager'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; +import { GetProfilesOptions } from '../profiles_manager'; export const useProfiles = ({ record }: GetProfilesOptions = {}) => { const { profilesManager } = useDiscoverServices(); diff --git a/src/plugins/discover/public/context_awareness/use_root_profile.ts b/src/plugins/discover/public/context_awareness/hooks/use_root_profile.ts similarity index 79% rename from src/plugins/discover/public/context_awareness/use_root_profile.ts rename to src/plugins/discover/public/context_awareness/hooks/use_root_profile.ts index 57a8485efba14..ff2d7edbcefb8 100644 --- a/src/plugins/discover/public/context_awareness/use_root_profile.ts +++ b/src/plugins/discover/public/context_awareness/hooks/use_root_profile.ts @@ -7,15 +7,10 @@ */ import { useEffect, useState } from 'react'; -import type { ProfilesManager } from './profiles_manager'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; -export const useRootProfile = ({ - profilesManager, - solutionNavId, -}: { - profilesManager: ProfilesManager; - solutionNavId: string | null; -}) => { +export const useRootProfile = ({ solutionNavId }: { solutionNavId: string | null }) => { + const { profilesManager } = useDiscoverServices(); const [rootProfileLoading, setRootProfileLoading] = useState(true); useEffect(() => { diff --git a/src/plugins/discover/public/context_awareness/index.ts b/src/plugins/discover/public/context_awareness/index.ts index 0c18b9462d73a..f91983ffe0039 100644 --- a/src/plugins/discover/public/context_awareness/index.ts +++ b/src/plugins/discover/public/context_awareness/index.ts @@ -10,5 +10,5 @@ export * from './types'; export * from './profiles'; export { type ComposableProfile, getMergedAccessor } from './composable_profile'; export { type GetProfilesOptions, ProfilesManager } from './profiles_manager'; -export { useProfileAccessor } from './use_profile_accessor'; -export { useRootProfile } from './use_root_profile'; +export { useProfileAccessor } from './hooks/use_profile_accessor'; +export { useRootProfile } from './hooks/use_root_profile'; From f753702d458ca816ba9cf747dde35d620e4b123b Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 21 May 2024 23:00:15 -0300 Subject: [PATCH 21/52] Remove show_confirm_panel since it's no longer used --- .../public/hooks/show_confirm_panel.tsx | 71 ------------------- src/plugins/discover/public/plugin.tsx | 2 - 2 files changed, 73 deletions(-) delete mode 100644 src/plugins/discover/public/hooks/show_confirm_panel.tsx diff --git a/src/plugins/discover/public/hooks/show_confirm_panel.tsx b/src/plugins/discover/public/hooks/show_confirm_panel.tsx deleted file mode 100644 index 79d2524c93161..0000000000000 --- a/src/plugins/discover/public/hooks/show_confirm_panel.tsx +++ /dev/null @@ -1,71 +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 React from 'react'; -import ReactDOM from 'react-dom'; -import { EuiConfirmModal } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; -import { StartRenderServices } from '../plugin'; - -let isOpenConfirmPanel = false; - -export const showConfirmPanel = ({ - onConfirm, - onCancel, - startServices, -}: { - onConfirm: () => void; - onCancel: () => void; - startServices: StartRenderServices; -}) => { - if (isOpenConfirmPanel) { - return; - } - - isOpenConfirmPanel = true; - const container = document.createElement('div'); - const onClose = () => { - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - isOpenConfirmPanel = false; - }; - - document.body.appendChild(container); - const element = ( - - { - onClose(); - onCancel(); - }} - onConfirm={() => { - onClose(); - onConfirm(); - }} - cancelButtonText={i18n.translate('discover.confirmDataViewSave.cancel', { - defaultMessage: 'Cancel', - })} - confirmButtonText={i18n.translate('discover.confirmDataViewSave.saveAndContinue', { - defaultMessage: 'Save and continue', - })} - defaultFocusedButton="confirm" - > -

- {i18n.translate('discover.confirmDataViewSave.message', { - defaultMessage: 'The action you chose requires a saved data view.', - })} -

-
-
- ); - ReactDOM.render(element, container); -}; diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 88fc94d4c9f15..97286c91a313e 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -220,8 +220,6 @@ export interface DiscoverStartPlugins { observabilityAIAssistant?: ObservabilityAIAssistantPublicStart; } -export type StartRenderServices = Pick; - /** * Contains Discover, one of the oldest parts of Kibana * Discover provides embeddables for Dashboards From 8d7edd650bd2454b431b8bdde8e756c8a61af237 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 21 May 2024 23:02:59 -0300 Subject: [PATCH 22/52] Rename contextCollector to proflesCollector --- .../application/main/data_fetching/fetch_documents.ts | 6 +++--- .../public/application/main/data_fetching/fetch_esql.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts index 818f802019f17..f46d571c81c8b 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts @@ -44,7 +44,7 @@ export const fetchDocuments = ( description: isFetchingMore ? 'fetch more documents' : 'fetch documents', }; - const contextCollector = services.profilesManager.createDocumentProfilesCollector(); + const profilesCollector = services.profilesManager.createDocumentProfilesCollector(); const fetch$ = searchSource .fetch$({ @@ -71,13 +71,13 @@ export const fetchDocuments = ( map((res) => { return buildDataTableRecordList(res.rawResponse.hits.hits as EsHitRecord[], dataView, { processRecord: (record) => { - contextCollector.collect({ record }); + profilesCollector.collect({ record }); return record; }, }); }), tap(() => { - contextCollector.finalize(!isFetchingMore); + profilesCollector.finalize(!isFetchingMore); }) ); diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts index edf3fbbbdef88..a4130dfa65ff8 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts @@ -62,7 +62,7 @@ export function fetchEsql( let esqlQueryColumns: Datatable['columns'] | undefined; let error: string | undefined; let esqlHeaderWarning: string | undefined; - const contextCollector = profilesManager.createDocumentProfilesCollector(); + const profilesCollector = profilesManager.createDocumentProfilesCollector(); execution.pipe(pluck('result')).subscribe((resp) => { const response = resp as Datatable | EsqlErrorResponse; if (response.type === 'error') { @@ -79,7 +79,7 @@ export function fetchEsql( flattened: row, }; - contextCollector.collect({ record }); + profilesCollector.collect({ record }); return record; }); @@ -89,7 +89,7 @@ export function fetchEsql( if (error) { throw new Error(error); } else { - contextCollector.finalize(true); + profilesCollector.finalize(true); return { records: finalData || [], From 2939c99d99baf81c6f65d48d5f53f05a5a09e9d0 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 21 May 2024 23:09:22 -0300 Subject: [PATCH 23/52] Revert discover_data_state_container change --- .../main/state_management/discover_data_state_container.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts index d0e798c9d0896..bfbdeb0f1ac1b 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts @@ -312,13 +312,12 @@ export function getDataStateContainer({ const fetchQuery = async (resetQuery?: boolean) => { const query = getAppState().query; - let dataView = getSavedSearch().searchSource.getField('index'); + const currentDataView = getSavedSearch().searchSource.getField('index'); if (isOfAggregateQueryType(query)) { - const nextDataView = await getEsqlDataView(query, dataView, services); - if (nextDataView !== dataView) { + const nextDataView = await getEsqlDataView(query, currentDataView, services); + if (nextDataView !== currentDataView) { setDataView(nextDataView); - dataView = nextDataView; } } From d66c1d8391eb35c27b83c00520c34a7160bdc14f Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 22 May 2024 00:14:10 -0300 Subject: [PATCH 24/52] Fix broken types, Jest tests, and translations --- src/plugins/discover/public/__mocks__/services.ts | 11 +++++++++++ .../application/main/discover_main_route.test.tsx | 10 ++++++++++ .../discover_container/discover_container.tsx | 1 + .../discover_grid_flyout.test.tsx | 2 ++ .../public/context_awareness/profiles/index.ts | 2 -- .../public/embeddable/saved_search_embeddable.tsx | 1 + x-pack/plugins/translations/translations/fr-FR.json | 4 ---- x-pack/plugins/translations/translations/ja-JP.json | 4 ---- x-pack/plugins/translations/translations/zh-CN.json | 4 ---- 9 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts index e4e2b71de8e74..f59cb2392f3f8 100644 --- a/src/plugins/discover/public/__mocks__/services.ts +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -41,6 +41,12 @@ import { SearchSourceDependencies } from '@kbn/data-plugin/common'; import { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { urlTrackerMock } from './url_tracker.mock'; import { createElement } from 'react'; +import { + DataSourceProfileService, + DocumentProfileService, + ProfilesManager, + RootProfileService, +} from '../context_awareness'; export function createDiscoverServicesMock(): DiscoverServices { const dataPlugin = dataPluginMock.createStartContract(); @@ -236,6 +242,11 @@ export function createDiscoverServicesMock(): DiscoverServices { contextLocator: { getRedirectUrl: jest.fn(() => '') }, singleDocLocator: { getRedirectUrl: jest.fn(() => '') }, urlTracker: urlTrackerMock, + profilesManager: new ProfilesManager( + new RootProfileService(), + new DataSourceProfileService(), + new DocumentProfileService() + ), setHeaderActionMenu: jest.fn(), } as unknown as DiscoverServices; } diff --git a/src/plugins/discover/public/application/main/discover_main_route.test.tsx b/src/plugins/discover/public/application/main/discover_main_route.test.tsx index 496bb91f92cf1..07d87efce1d59 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.test.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.test.tsx @@ -40,6 +40,16 @@ jest.mock('./discover_main_app', () => { }; }); +jest.mock('../../context_awareness', () => { + const originalModule = jest.requireActual('../../context_awareness'); + return { + ...originalModule, + useRootProfile: () => ({ + rootProfileLoading: false, + }), + }; +}); + describe('DiscoverMainRoute', () => { beforeEach(() => { mockCustomizationService = createCustomizationService(); diff --git a/src/plugins/discover/public/components/discover_container/discover_container.tsx b/src/plugins/discover/public/components/discover_container/discover_container.tsx index 4f768554e1e54..c253760fff2ac 100644 --- a/src/plugins/discover/public/components/discover_container/discover_container.tsx +++ b/src/plugins/discover/public/components/discover_container/discover_container.tsx @@ -44,6 +44,7 @@ const discoverContainerWrapperCss = css` `; const customizationContext: DiscoverCustomizationContext = { + solutionNavId: null, displayMode: 'embedded', inlineTopNav: { enabled: false, diff --git a/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.test.tsx b/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.test.tsx index 3907bc4999232..cb02e3b736663 100644 --- a/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.test.tsx +++ b/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.test.tsx @@ -25,6 +25,7 @@ import { ReactWrapper } from 'enzyme'; import { setUnifiedDocViewerServices } from '@kbn/unified-doc-viewer-plugin/public/plugin'; import { mockUnifiedDocViewerServices } from '@kbn/unified-doc-viewer-plugin/public/__mocks__'; import { FlyoutCustomization, useDiscoverCustomization } from '../../customizations'; +import { discoverServiceMock } from '../../__mocks__/services'; const mockFlyoutCustomization: FlyoutCustomization = { id: 'flyout', @@ -76,6 +77,7 @@ describe('Discover flyout', function () { }) => { const onClose = jest.fn(); const services = { + ...discoverServiceMock, filterManager: createFilterManagerMock(), addBasePath: (path: string) => `/base${path}`, history: () => ({ location: {} }), diff --git a/src/plugins/discover/public/context_awareness/profiles/index.ts b/src/plugins/discover/public/context_awareness/profiles/index.ts index 9174e78979726..f661276b4a04c 100644 --- a/src/plugins/discover/public/context_awareness/profiles/index.ts +++ b/src/plugins/discover/public/context_awareness/profiles/index.ts @@ -9,5 +9,3 @@ export * from './root_profile'; export * from './data_source_profile'; export * from './document_profile'; - -import('./example_profiles'); diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx index 06aeaa42c1376..906dba1935180 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx @@ -313,6 +313,7 @@ export class SavedSearchEmbeddable this.services.data, this.services.expressions, this.services.inspector, + this.services.profilesManager, this.abortController.signal, this.input.filters, this.input.query diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index b5074f2d79934..e093a2826ae4a 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2323,10 +2323,6 @@ "discover.backToTopLinkText": "Revenir en haut de la page.", "discover.badge.readOnly.text": "Lecture seule", "discover.badge.readOnly.tooltip": "Impossible d’enregistrer les recherches", - "discover.confirmDataViewSave.cancel": "Annuler", - "discover.confirmDataViewSave.message": "L'action que vous avez choisie requiert une vue de données enregistrée.", - "discover.confirmDataViewSave.saveAndContinue": "Enregistrer et continuer", - "discover.confirmDataViewSave.title": "Enregistrer la vue de données", "discover.context.breadcrumb": "Documents relatifs", "discover.context.failedToLoadAnchorDocumentDescription": "Échec de chargement du document ancré", "discover.context.failedToLoadAnchorDocumentErrorDescription": "Le document ancré n’a pas pu être chargé.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d9ff0f4932c25..d7da772d215c6 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2320,10 +2320,6 @@ "discover.backToTopLinkText": "最上部へ戻る。", "discover.badge.readOnly.text": "読み取り専用", "discover.badge.readOnly.tooltip": "検索を保存できません", - "discover.confirmDataViewSave.cancel": "キャンセル", - "discover.confirmDataViewSave.message": "選択したアクションでは、保存されたデータビューが必要です。", - "discover.confirmDataViewSave.saveAndContinue": "保存して続行", - "discover.confirmDataViewSave.title": "データビューを保存", "discover.context.breadcrumb": "周りのドキュメント", "discover.context.failedToLoadAnchorDocumentDescription": "アンカードキュメントの読み込みに失敗しました", "discover.context.failedToLoadAnchorDocumentErrorDescription": "アンカードキュメントの読み込みに失敗しました。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5ddb8e643a359..031cf92e1117a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2324,10 +2324,6 @@ "discover.backToTopLinkText": "返回顶部。", "discover.badge.readOnly.text": "只读", "discover.badge.readOnly.tooltip": "无法保存搜索", - "discover.confirmDataViewSave.cancel": "取消", - "discover.confirmDataViewSave.message": "您选择的操作需要已保存的数据视图。", - "discover.confirmDataViewSave.saveAndContinue": "保存并继续", - "discover.confirmDataViewSave.title": "保存数据视图。", "discover.context.breadcrumb": "周围文档", "discover.context.failedToLoadAnchorDocumentDescription": "无法加载定位点文档", "discover.context.failedToLoadAnchorDocumentErrorDescription": "无法加载定位点文档。", From f887d882a27fe07f6f788ee2f6a57b97ace1c2d9 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 27 May 2024 20:58:54 -0300 Subject: [PATCH 25/52] Simplify useProfileAccessor --- .../components/layout/discover_documents.tsx | 8 +++-- .../components/top_nav/use_discover_topnav.ts | 35 ++++++++++--------- .../discover_grid_flyout.tsx | 25 ++++++------- .../hooks/use_profile_accessor.ts | 11 +++--- 4 files changed, 41 insertions(+), 38 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index 651760782e901..c90cced254a87 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -264,9 +264,11 @@ function DiscoverDocumentsComponent({ useContextualGridCustomisations() || {}; const additionalFieldGroups = useAdditionalFieldGroups(); - const baseGetCellRenderers = useCallback(() => customCellRenderer ?? {}, [customCellRenderer]); - const getCellRenderers = useProfileAccessor('getCellRenderers', baseGetCellRenderers); - const cellRenderers = useMemo(() => getCellRenderers(), [getCellRenderers]); + const getCellRenderersAccessor = useProfileAccessor('getCellRenderers'); + const cellRenderers = useMemo(() => { + const getCellRenderers = getCellRenderersAccessor(() => customCellRenderer ?? {}); + return getCellRenderers(); + }, [customCellRenderer, getCellRenderersAccessor]); const documents = useObservable(stateContainer.dataState.data$.documents$); diff --git a/src/plugins/discover/public/application/main/components/top_nav/use_discover_topnav.ts b/src/plugins/discover/public/application/main/components/top_nav/use_discover_topnav.ts index 54634e4dc7940..5dbdfed75598d 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/use_discover_topnav.ts +++ b/src/plugins/discover/public/application/main/components/top_nav/use_discover_topnav.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { useCallback, useMemo } from 'react'; +import { useMemo } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { useProfileAccessor } from '../../../../context_awareness'; import { useDiscoverCustomization } from '../../../../customizations'; @@ -49,9 +49,10 @@ export const useDiscoverTopNav = ({ stateContainer, }); - const baseGetTopNavMenu = useCallback( - () => - getTopNavLinks({ + const getTopNavMenuAccessor = useProfileAccessor('getTopNavItems'); + const topNavMenu = useMemo(() => { + const getTopNavMenu = getTopNavMenuAccessor(() => { + return getTopNavLinks({ dataView, services, state: stateContainer, @@ -59,20 +60,20 @@ export const useDiscoverTopNav = ({ isEsqlMode, adHocDataViews, topNavCustomization, - }), - [ - adHocDataViews, - dataView, - isEsqlMode, - onOpenInspector, - services, - stateContainer, - topNavCustomization, - ] - ); + }); + }); - const getTopNavMenu = useProfileAccessor('getTopNavItems', baseGetTopNavMenu); - const topNavMenu = useMemo(() => getTopNavMenu(), [getTopNavMenu]); + return getTopNavMenu(); + }, [ + adHocDataViews, + dataView, + getTopNavMenuAccessor, + isEsqlMode, + onOpenInspector, + services, + stateContainer, + topNavCustomization, + ]); return { topNavMenu, diff --git a/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.tsx b/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.tsx index b0b8c3878035e..4b81a09721b59 100644 --- a/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.tsx +++ b/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.tsx @@ -163,22 +163,19 @@ export function DiscoverGridFlyout({ [onRemoveColumn, services.toastNotifications] ); - const baseGetDocViewsRegistry = useCallback( - (registry: DocViewsRegistry) => { - return typeof flyoutCustomization?.docViewsRegistry === 'function' - ? flyoutCustomization.docViewsRegistry(registry) - : registry; - }, - [flyoutCustomization] - ); - - const getDocViewsRegistry = useProfileAccessor('getDocViewsRegistry', baseGetDocViewsRegistry, { + const getDocViewsRegistryAccessor = useProfileAccessor('getDocViewsRegistry', { record: actualHit, }); - const docViewsRegistry = useCallback( - (registry: DocViewsRegistry) => getDocViewsRegistry(registry), - [getDocViewsRegistry] - ); + + const docViewsRegistry = useMemo(() => { + const getDocViewsRegistry = getDocViewsRegistryAccessor((registry) => + typeof flyoutCustomization?.docViewsRegistry === 'function' + ? flyoutCustomization.docViewsRegistry(registry) + : registry + ); + + return (registry: DocViewsRegistry) => getDocViewsRegistry(registry); + }, [flyoutCustomization, getDocViewsRegistryAccessor]); const renderDefaultContent = useCallback( () => ( diff --git a/src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.ts b/src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.ts index dd1fff2240bcf..58c5a316f86cf 100644 --- a/src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.ts +++ b/src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.ts @@ -6,17 +6,20 @@ * Side Public License, v 1. */ -import { useMemo } from 'react'; +import { useCallback } from 'react'; import { getMergedAccessor } from '../composable_profile'; -import { GetProfilesOptions } from '../profiles_manager'; +import type { GetProfilesOptions } from '../profiles_manager'; import { useProfiles } from './use_profiles'; import type { Profile } from '../types'; export const useProfileAccessor = ( key: TKey, - baseImpl: Profile[TKey], options: GetProfilesOptions = {} ) => { const profiles = useProfiles(options); - return useMemo(() => getMergedAccessor(profiles, key, baseImpl), [baseImpl, key, profiles]); + + return useCallback( + (baseImpl: Profile[TKey]) => getMergedAccessor(profiles, key, baseImpl), + [key, profiles] + ); }; From f0d8bc1a2441e2e38cf1388c76e52ea8c71518dd Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 28 May 2024 00:23:54 -0300 Subject: [PATCH 26/52] Attach context with profile ID to data table records --- .../main/data_fetching/fetch_documents.ts | 9 +- .../main/data_fetching/fetch_esql.ts | 5 +- .../public/context_awareness/index.ts | 4 +- .../context_awareness/profile_service.ts | 36 ++++++-- .../profiles/data_source_profile.ts | 9 +- .../profiles/document_profile.ts | 9 +- .../profiles/example_profiles.tsx | 3 + .../profiles/root_profile.ts | 9 +- .../context_awareness/profiles_manager.ts | 90 ++++++++----------- 9 files changed, 96 insertions(+), 78 deletions(-) diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts index f46d571c81c8b..18e6cd2bc0a30 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import { i18n } from '@kbn/i18n'; -import { filter, map, tap } from 'rxjs'; +import { filter, map } from 'rxjs'; import { lastValueFrom } from 'rxjs'; import { isRunningResponse, ISearchSource } from '@kbn/data-plugin/public'; import { buildDataTableRecordList } from '@kbn/discover-utils'; @@ -44,8 +44,6 @@ export const fetchDocuments = ( description: isFetchingMore ? 'fetch more documents' : 'fetch documents', }; - const profilesCollector = services.profilesManager.createDocumentProfilesCollector(); - const fetch$ = searchSource .fetch$({ abortSignal: abortController.signal, @@ -71,13 +69,10 @@ export const fetchDocuments = ( map((res) => { return buildDataTableRecordList(res.rawResponse.hits.hits as EsHitRecord[], dataView, { processRecord: (record) => { - profilesCollector.collect({ record }); + services.profilesManager.resolveDocumentProfile({ record }); return record; }, }); - }), - tap(() => { - profilesCollector.finalize(!isFetchingMore); }) ); diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts index a4130dfa65ff8..c50f8e045aa09 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts @@ -62,7 +62,6 @@ export function fetchEsql( let esqlQueryColumns: Datatable['columns'] | undefined; let error: string | undefined; let esqlHeaderWarning: string | undefined; - const profilesCollector = profilesManager.createDocumentProfilesCollector(); execution.pipe(pluck('result')).subscribe((resp) => { const response = resp as Datatable | EsqlErrorResponse; if (response.type === 'error') { @@ -79,7 +78,7 @@ export function fetchEsql( flattened: row, }; - profilesCollector.collect({ record }); + profilesManager.resolveDocumentProfile({ record }); return record; }); @@ -89,8 +88,6 @@ export function fetchEsql( if (error) { throw new Error(error); } else { - profilesCollector.finalize(true); - return { records: finalData || [], esqlQueryColumns, diff --git a/src/plugins/discover/public/context_awareness/index.ts b/src/plugins/discover/public/context_awareness/index.ts index f91983ffe0039..27245ccbdf123 100644 --- a/src/plugins/discover/public/context_awareness/index.ts +++ b/src/plugins/discover/public/context_awareness/index.ts @@ -8,7 +8,7 @@ export * from './types'; export * from './profiles'; -export { type ComposableProfile, getMergedAccessor } from './composable_profile'; -export { type GetProfilesOptions, ProfilesManager } from './profiles_manager'; +export { getMergedAccessor } from './composable_profile'; +export { ProfilesManager } from './profiles_manager'; export { useProfileAccessor } from './hooks/use_profile_accessor'; export { useRootProfile } from './hooks/use_root_profile'; diff --git a/src/plugins/discover/public/context_awareness/profile_service.ts b/src/plugins/discover/public/context_awareness/profile_service.ts index 1c0d811447887..d86935d4629ae 100644 --- a/src/plugins/discover/public/context_awareness/profile_service.ts +++ b/src/plugins/discover/public/context_awareness/profile_service.ts @@ -24,6 +24,7 @@ export interface ProfileProvider< TMode extends ProfileProviderMode > { order: number; + profileId: string; profile: ComposableProfile; resolve: ( params: TParams @@ -32,6 +33,10 @@ export interface ProfileProvider< : ResolveProfileResult | Promise>; } +export type ContextWithProfileId = TContext & { profileId: string }; + +const EMPTY_PROFILE = {}; + abstract class BaseProfileService< TProfile extends PartialProfile, TParams, @@ -40,33 +45,43 @@ abstract class BaseProfileService< > { protected readonly providers: Array> = []; + protected constructor(public readonly defaultContext: ContextWithProfileId) {} + public registerProvider(provider: ProfileProvider) { this.providers.push(provider); this.providers.sort((a, b) => a.order - b.order); } + public getProfile(context: ContextWithProfileId): ComposableProfile { + const provider = this.providers.find((current) => current.profileId === context.profileId); + return provider?.profile ?? EMPTY_PROFILE; + } + public abstract resolve( params: TParams - ): TMode extends 'sync' ? ComposableProfile : Promise>; + ): TMode extends 'sync' + ? ContextWithProfileId + : Promise>; } -const EMPTY_PROFILE = {}; - export class ProfileService< TProfile extends PartialProfile, TParams, TContext > extends BaseProfileService { - public resolve(params: TParams): ComposableProfile { + public resolve(params: TParams) { for (const provider of this.providers) { const result = provider.resolve(params); if (result.isMatch) { - return provider.profile as ComposableProfile; + return { + ...result.context, + profileId: provider.profileId, + }; } } - return EMPTY_PROFILE; + return this.defaultContext; } } @@ -75,15 +90,18 @@ export class AsyncProfileService< TParams, TContext > extends BaseProfileService { - public async resolve(params: TParams): Promise> { + public async resolve(params: TParams) { for (const provider of this.providers) { const result = await provider.resolve(params); if (result.isMatch) { - return provider.profile as ComposableProfile; + return { + ...result.context, + profileId: provider.profileId, + }; } } - return EMPTY_PROFILE; + return this.defaultContext; } } 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 168e2658c22fd..f616fef913259 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 @@ -33,6 +33,13 @@ export class DataSourceProfileService extends AsyncProfileService< DataSourceProfile, DataSourceProfileProviderParams, DataSourceContext -> {} +> { + constructor() { + super({ + profileId: 'default-data-source-profile', + category: DataSourceCategory.Default, + }); + } +} 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 2fb0b5f36b88e..0faa5c4c6f976 100644 --- a/src/plugins/discover/public/context_awareness/profiles/document_profile.ts +++ b/src/plugins/discover/public/context_awareness/profiles/document_profile.ts @@ -29,6 +29,13 @@ export class DocumentProfileService extends ProfileService< DocumentProfile, DocumentProfileProviderParams, DocumentContext -> {} +> { + constructor() { + super({ + profileId: 'default-document-profile', + type: DocumentType.Default, + }); + } +} 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 index d0ba68ffd64bb..586475aa2cccd 100644 --- a/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx +++ b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx @@ -24,6 +24,7 @@ import { RootProfileProvider, SolutionType } from './root_profile'; export const o11yRootProfileProvider: RootProfileProvider = { order: 0, + profileId: 'o11y-root-profile', profile: { getTopNavItems: (prev) => () => [ @@ -53,6 +54,7 @@ export const o11yRootProfileProvider: RootProfileProvider = { export const logsDataSourceProfileProvider: DataSourceProfileProvider = { order: 0, + profileId: 'logs-data-source-profile', profile: { getTopNavItems: (prev) => () => [ @@ -138,6 +140,7 @@ export const logsDataSourceProfileProvider: DataSourceProfileProvider = { export const logDocumentProfileProvider: DocumentProfileProvider = { order: 0, + profileId: 'log-document-profile', profile: { getDocViewsRegistry: (prev) => (registry) => { registry.enableById('doc_view_logs_overview'); 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 4b0df08b0fcfa..42497fe680c5c 100644 --- a/src/plugins/discover/public/context_awareness/profiles/root_profile.ts +++ b/src/plugins/discover/public/context_awareness/profiles/root_profile.ts @@ -30,6 +30,13 @@ export class RootProfileService extends AsyncProfileService< RootProfile, RootProfileProviderParams, RootContext -> {} +> { + constructor() { + super({ + profileId: 'default-root-profile', + solutionType: SolutionType.Default, + }); + } +} export type RootProfileProvider = Parameters[0]; diff --git a/src/plugins/discover/public/context_awareness/profiles_manager.ts b/src/plugins/discover/public/context_awareness/profiles_manager.ts index a6dc213dbcd22..f2db1a6a79ae6 100644 --- a/src/plugins/discover/public/context_awareness/profiles_manager.ts +++ b/src/plugins/discover/public/context_awareness/profiles_manager.ts @@ -6,23 +6,23 @@ * Side Public License, v 1. */ -import { DataTableRecord } from '@kbn/discover-utils'; +import type { DataTableRecord } from '@kbn/discover-utils'; import { isOfAggregateQueryType } from '@kbn/es-query'; -import { isEqual } from 'lodash'; +import { isEqual, memoize } from 'lodash'; import { BehaviorSubject, combineLatest, map } from 'rxjs'; import { DataSourceType, isDataSourceType } from '../../common/data_sources'; -import type { ComposableProfile } from './composable_profile'; import type { - RootProfile, - DataSourceProfile, - DocumentProfile, RootProfileService, DataSourceProfileService, DocumentProfileService, RootProfileProviderParams, DataSourceProfileProviderParams, DocumentProfileProviderParams, + RootContext, + DataSourceContext, + DocumentContext, } from './profiles'; +import type { ContextWithProfileId } from './profile_service'; interface SerializedRootProfileParams { solutionNavId: RootProfileProviderParams['solutionNavId']; @@ -33,20 +33,17 @@ interface SerializedDataSourceProfileParams { esqlQuery: string | undefined; } +interface DataTableRecordWithContext extends DataTableRecord { + context: ContextWithProfileId; +} + export interface GetProfilesOptions { record?: DataTableRecord; } export class ProfilesManager { - private readonly rootProfile$ = new BehaviorSubject | undefined>( - undefined - ); - private readonly dataSourceProfile$ = new BehaviorSubject< - ComposableProfile | undefined - >(undefined); - private readonly documentProfiles$ = new BehaviorSubject< - Map> - >(new Map()); + private readonly rootContext$: BehaviorSubject>; + private readonly dataSourceContext$: BehaviorSubject>; private prevRootProfileParams?: SerializedRootProfileParams; private prevDataSourceProfileParams?: SerializedDataSourceProfileParams; @@ -57,12 +54,15 @@ export class ProfilesManager { private readonly rootProfileService: RootProfileService, private readonly dataSourceProfileService: DataSourceProfileService, private readonly documentProfileService: DocumentProfileService - ) {} + ) { + this.rootContext$ = new BehaviorSubject(rootProfileService.defaultContext); + this.dataSourceContext$ = new BehaviorSubject(dataSourceProfileService.defaultContext); + } public async resolveRootProfile(params: RootProfileProviderParams) { const serializedParams = this.serializeRootProfileParams(params); - if (this.rootProfile$.getValue() && isEqual(this.prevRootProfileParams, serializedParams)) { + if (isEqual(this.prevRootProfileParams, serializedParams)) { return; } @@ -70,23 +70,20 @@ export class ProfilesManager { this.rootProfileAbortController?.abort(); this.rootProfileAbortController = abortController; - const profile = await this.rootProfileService.resolve(params); + const context = await this.rootProfileService.resolve(params); if (abortController.signal.aborted) { return; } - this.rootProfile$.next(profile); + this.rootContext$.next(context); this.prevRootProfileParams = serializedParams; } public async resolveDataSourceProfile(params: DataSourceProfileProviderParams) { const serializedParams = this.serializeDataSourceProfileParams(params); - if ( - this.dataSourceProfile$.getValue() && - isEqual(this.prevDataSourceProfileParams, serializedParams) - ) { + if (isEqual(this.prevDataSourceProfileParams, serializedParams)) { return; } @@ -94,49 +91,34 @@ export class ProfilesManager { this.dataSourceProfileAbortController?.abort(); this.dataSourceProfileAbortController = abortController; - const profile = await this.dataSourceProfileService.resolve(params); + const context = await this.dataSourceProfileService.resolve(params); if (abortController.signal.aborted) { return; } - this.dataSourceProfile$.next(profile); + this.dataSourceContext$.next(context); this.prevDataSourceProfileParams = serializedParams; } - public createDocumentProfilesCollector() { - const documentProfiles = new Map>(); - - return { - collect: (params: DocumentProfileProviderParams) => { - documentProfiles.set(params.record.id, this.documentProfileService.resolve(params)); - }, - finalize: (clearExisting: boolean) => { - if (clearExisting) { - this.documentProfiles$.next(documentProfiles); - } else { - const existingProfiles = this.documentProfiles$.getValue(); - - documentProfiles.forEach((profile, id) => { - existingProfiles.set(id, profile); - }); - - this.documentProfiles$.next(existingProfiles); - } - }, - }; + public resolveDocumentProfile(params: DocumentProfileProviderParams) { + Object.defineProperty(params.record, 'context', { + get: memoize(() => this.documentProfileService.resolve(params)), + }); } public getProfiles({ record }: GetProfilesOptions = {}) { return [ - this.rootProfile$.getValue(), - this.dataSourceProfile$.getValue(), - record ? this.documentProfiles$.getValue().get(record.id) : undefined, - ].filter(profileExists); + this.rootProfileService.getProfile(this.rootContext$.getValue()), + this.dataSourceProfileService.getProfile(this.dataSourceContext$.getValue()), + this.documentProfileService.getProfile( + recordHasContext(record) ? record.context : this.documentProfileService.defaultContext + ), + ]; } public getProfiles$(options: GetProfilesOptions = {}) { - return combineLatest([this.rootProfile$, this.dataSourceProfile$, this.documentProfiles$]).pipe( + return combineLatest([this.rootContext$, this.dataSourceContext$]).pipe( map(() => this.getProfiles(options)) ); } @@ -165,6 +147,8 @@ export class ProfilesManager { } } -const profileExists = (profile?: ComposableProfile): profile is ComposableProfile => { - return profile !== undefined; +const recordHasContext = ( + record: DataTableRecord | undefined +): record is DataTableRecordWithContext => { + return Boolean(record && 'context' in record); }; From 3597c90a849bff6afddbbef11087ba104ef31fa3 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 28 May 2024 00:56:45 -0300 Subject: [PATCH 27/52] Don't retrigger useProfiles on init --- .../context_awareness/hooks/use_profiles.ts | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/plugins/discover/public/context_awareness/hooks/use_profiles.ts b/src/plugins/discover/public/context_awareness/hooks/use_profiles.ts index 0fda1398bc111..9bd86e4386150 100644 --- a/src/plugins/discover/public/context_awareness/hooks/use_profiles.ts +++ b/src/plugins/discover/public/context_awareness/hooks/use_profiles.ts @@ -6,23 +6,31 @@ * Side Public License, v 1. */ -import { useMemo } from 'react'; -import useObservable from 'react-use/lib/useObservable'; +import { useEffect, useMemo, useState } from 'react'; import { useDiscoverServices } from '../../hooks/use_discover_services'; -import { GetProfilesOptions } from '../profiles_manager'; +import type { GetProfilesOptions } from '../profiles_manager'; export const useProfiles = ({ record }: GetProfilesOptions = {}) => { const { profilesManager } = useDiscoverServices(); - + const [profiles, setProfiles] = useState(() => profilesManager.getProfiles({ record })); const profiles$ = useMemo( () => profilesManager.getProfiles$({ record }), [profilesManager, record] ); - const profiles = useMemo( - () => profilesManager.getProfiles({ record }), - [profilesManager, record] - ); + useEffect(() => { + const subscription = profiles$.subscribe((newProfiles) => { + setProfiles((currentProfiles) => { + return currentProfiles.every((profile, i) => profile === newProfiles[i]) + ? currentProfiles + : newProfiles; + }); + }); + + return () => { + subscription.unsubscribe(); + }; + }, [profiles$]); - return useObservable(profiles$, profiles); + return profiles; }; From a8060f47041c66e4e1cb0204ac502c3c90d76651 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 28 May 2024 01:02:19 -0300 Subject: [PATCH 28/52] Move data source resolution before data fetching --- .../discover_data_state_container.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts index bfbdeb0f1ac1b..de95388f022e3 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts @@ -252,6 +252,12 @@ export function getDataStateContainer({ return; } + await profilesManager.resolveDataSourceProfile({ + dataSource: getAppState().dataSource, + dataView: getSavedSearch().searchSource.getField('index'), + query: getAppState().query, + }); + abortController = new AbortController(); const prevAutoRefreshDone = autoRefreshDone; @@ -275,12 +281,6 @@ export function getDataStateContainer({ autoRefreshDone = undefined; } - await profilesManager.resolveDataSourceProfile({ - dataSource: getAppState().dataSource, - dataView: getSavedSearch().searchSource.getField('index'), - query: getAppState().query, - }); - const defaultColumns = getMergedAccessor( profilesManager.getProfiles(), 'getDefaultColumns', From ebfcfae64ebb17378751935aa78903c152b7f8f1 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 29 May 2024 22:48:38 -0300 Subject: [PATCH 29/52] Log and fallback to default on context resolution failure --- .../context_awareness/profiles_manager.ts | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/src/plugins/discover/public/context_awareness/profiles_manager.ts b/src/plugins/discover/public/context_awareness/profiles_manager.ts index f2db1a6a79ae6..bab08a8dec1a4 100644 --- a/src/plugins/discover/public/context_awareness/profiles_manager.ts +++ b/src/plugins/discover/public/context_awareness/profiles_manager.ts @@ -11,6 +11,7 @@ import { isOfAggregateQueryType } from '@kbn/es-query'; import { isEqual, memoize } from 'lodash'; import { BehaviorSubject, combineLatest, map } from 'rxjs'; import { DataSourceType, isDataSourceType } from '../../common/data_sources'; +import { addLog } from '../utils/add_log'; import type { RootProfileService, DataSourceProfileService, @@ -70,7 +71,13 @@ export class ProfilesManager { this.rootProfileAbortController?.abort(); this.rootProfileAbortController = abortController; - const context = await this.rootProfileService.resolve(params); + let context = this.rootProfileService.defaultContext; + + try { + context = await this.rootProfileService.resolve(params); + } catch (e) { + logResolutionError(ContextType.Root, serializedParams, e); + } if (abortController.signal.aborted) { return; @@ -91,7 +98,13 @@ export class ProfilesManager { this.dataSourceProfileAbortController?.abort(); this.dataSourceProfileAbortController = abortController; - const context = await this.dataSourceProfileService.resolve(params); + let context = this.dataSourceProfileService.defaultContext; + + try { + context = await this.dataSourceProfileService.resolve(params); + } catch (e) { + logResolutionError(ContextType.DataSource, serializedParams, e); + } if (abortController.signal.aborted) { return; @@ -103,7 +116,17 @@ export class ProfilesManager { public resolveDocumentProfile(params: DocumentProfileProviderParams) { Object.defineProperty(params.record, 'context', { - get: memoize(() => this.documentProfileService.resolve(params)), + get: memoize(() => { + let context = this.documentProfileService.defaultContext; + + try { + context = this.documentProfileService.resolve(params); + } catch (e) { + logResolutionError(ContextType.Document, { recordId: params.record.id }, e); + } + + return context; + }), }); } @@ -152,3 +175,24 @@ const recordHasContext = ( ): record is DataTableRecordWithContext => { return Boolean(record && 'context' in record); }; + +enum ContextType { + Root = 'root', + DataSource = 'data source', + Document = 'document', +} + +const logResolutionError = ( + profileType: ContextType, + params: TParams, + error: TError +) => { + addLog( + `[ProfilesManager] ${profileType} context resolution failed with params: ${JSON.stringify( + params, + null, + 2 + )}`, + error + ); +}; From fb050caad2e1119d56e148a8a7c1e8454cb29ec2 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 29 May 2024 22:54:14 -0300 Subject: [PATCH 30/52] Remove profile provider ordering --- .../discover/public/context_awareness/profile_service.ts | 2 -- .../public/context_awareness/profiles/example_profiles.tsx | 3 --- 2 files changed, 5 deletions(-) diff --git a/src/plugins/discover/public/context_awareness/profile_service.ts b/src/plugins/discover/public/context_awareness/profile_service.ts index d86935d4629ae..2b43595761d19 100644 --- a/src/plugins/discover/public/context_awareness/profile_service.ts +++ b/src/plugins/discover/public/context_awareness/profile_service.ts @@ -23,7 +23,6 @@ export interface ProfileProvider< TContext, TMode extends ProfileProviderMode > { - order: number; profileId: string; profile: ComposableProfile; resolve: ( @@ -49,7 +48,6 @@ abstract class BaseProfileService< public registerProvider(provider: ProfileProvider) { this.providers.push(provider); - this.providers.sort((a, b) => a.order - b.order); } public getProfile(context: ContextWithProfileId): ComposableProfile { diff --git a/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx index 586475aa2cccd..6425223af8c45 100644 --- a/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx +++ b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx @@ -23,7 +23,6 @@ import { DocumentProfileProvider, DocumentType } from './document_profile'; import { RootProfileProvider, SolutionType } from './root_profile'; export const o11yRootProfileProvider: RootProfileProvider = { - order: 0, profileId: 'o11y-root-profile', profile: { getTopNavItems: (prev) => () => @@ -53,7 +52,6 @@ export const o11yRootProfileProvider: RootProfileProvider = { }; export const logsDataSourceProfileProvider: DataSourceProfileProvider = { - order: 0, profileId: 'logs-data-source-profile', profile: { getTopNavItems: (prev) => () => @@ -139,7 +137,6 @@ export const logsDataSourceProfileProvider: DataSourceProfileProvider = { }; export const logDocumentProfileProvider: DocumentProfileProvider = { - order: 0, profileId: 'log-document-profile', profile: { getDocViewsRegistry: (prev) => (registry) => { From ff97785b3eac44837600ed244c6acba135f46f74 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 29 May 2024 23:21:33 -0300 Subject: [PATCH 31/52] Add profiles support to saved search embeddable --- .../embeddable/saved_search_embeddable.tsx | 19 ++++++++++++++++++- .../public/embeddable/saved_search_grid.tsx | 8 ++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx index 906dba1935180..4ab408aa8fb7c 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { lastValueFrom, Subscription } from 'rxjs'; +import { firstValueFrom, lastValueFrom, Subscription } from 'rxjs'; import { onlyDisabledFiltersChanged, Filter, @@ -71,6 +71,7 @@ import { fetchEsql } from '../application/main/data_fetching/fetch_esql'; import { getValidViewMode } from '../application/main/utils/get_valid_view_mode'; import { ADHOC_DATA_VIEW_RENDER_EVENT } from '../constants'; import { getDiscoverLocatorParams } from './get_discover_locator_params'; +import { createDataViewDataSource, createEsqlDataSource } from '../../common/data_sources'; export interface SearchEmbeddableConfig { editable: boolean; @@ -163,6 +164,12 @@ export class SavedSearchEmbeddable await this.initializeOutput(); + const solutionNavId = await firstValueFrom( + this.services.core.chrome.getActiveSolutionNavId$() + ); + + this.services.profilesManager.resolveRootProfile({ solutionNavId }); + // deferred loading of this embeddable is complete this.setInitializationFinished(); @@ -305,6 +312,16 @@ export class SavedSearchEmbeddable const isEsqlMode = this.isEsqlMode(savedSearch); try { + await this.services.profilesManager.resolveDataSourceProfile({ + dataSource: isOfAggregateQueryType(query) + ? createEsqlDataSource() + : dataView.id + ? createDataViewDataSource({ dataViewId: dataView.id }) + : undefined, + dataView, + query, + }); + // Request ES|QL data if (isEsqlMode && query) { const result = await fetchEsql( diff --git a/src/plugins/discover/public/embeddable/saved_search_grid.tsx b/src/plugins/discover/public/embeddable/saved_search_grid.tsx index 8bf43fa5b3e3b..39a6dc1307c04 100644 --- a/src/plugins/discover/public/embeddable/saved_search_grid.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_grid.tsx @@ -21,6 +21,7 @@ import './saved_search_grid.scss'; import { DiscoverGridFlyout } from '../components/discover_grid_flyout'; import { SavedSearchEmbeddableBase } from './saved_search_embeddable_base'; import { TotalDocuments } from '../application/main/components/total_documents/total_documents'; +import { useProfileAccessor } from '../context_awareness'; export interface DiscoverGridEmbeddableProps extends Omit { @@ -88,6 +89,12 @@ export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) { [props.totalHitCount] ); + const getCellRenderersAccessor = useProfileAccessor('getCellRenderers'); + const cellRenderers = useMemo(() => { + const getCellRenderers = getCellRenderersAccessor(() => ({})); + return getCellRenderers(); + }, [getCellRenderersAccessor]); + return ( Date: Fri, 31 May 2024 00:43:18 -0300 Subject: [PATCH 32/52] Add Jest tests --- .../context_awareness/__mocks__/index.ts | 85 ++++++ .../composable_profile.test.ts | 67 +++++ .../hooks/use_profile_accessor.test.ts | 78 ++++++ .../hooks/use_profiles.test.tsx | 90 +++++++ .../hooks/use_root_profile.test.tsx | 45 ++++ .../context_awareness/profile_service.test.ts | 147 ++++++++++ .../profiles_manager.test.ts | 255 ++++++++++++++++++ .../context_awareness/profiles_manager.ts | 21 +- 8 files changed, 784 insertions(+), 4 deletions(-) create mode 100644 src/plugins/discover/public/context_awareness/__mocks__/index.ts create mode 100644 src/plugins/discover/public/context_awareness/composable_profile.test.ts create mode 100644 src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.test.ts create mode 100644 src/plugins/discover/public/context_awareness/hooks/use_profiles.test.tsx create mode 100644 src/plugins/discover/public/context_awareness/hooks/use_root_profile.test.tsx create mode 100644 src/plugins/discover/public/context_awareness/profile_service.test.ts create mode 100644 src/plugins/discover/public/context_awareness/profiles_manager.test.ts diff --git a/src/plugins/discover/public/context_awareness/__mocks__/index.ts b/src/plugins/discover/public/context_awareness/__mocks__/index.ts new file mode 100644 index 0000000000000..626bbf51831cf --- /dev/null +++ b/src/plugins/discover/public/context_awareness/__mocks__/index.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 { getDataTableRecords } from '../../__fixtures__/real_hits'; +import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield'; +import { + DataSourceCategory, + DataSourceProfileProvider, + DataSourceProfileService, + DocumentProfileProvider, + DocumentProfileService, + DocumentType, + RootProfileProvider, + RootProfileService, + SolutionType, +} from '../profiles'; +import { ProfilesManager } from '../profiles_manager'; + +export const createContextAwarenessMocks = () => { + const rootProfileProviderMock: RootProfileProvider = { + profileId: 'root-profile', + profile: { getCellRenderers: jest.fn() }, + resolve: jest.fn(() => ({ + isMatch: true, + context: { + solutionType: SolutionType.Observability, + }, + })), + }; + + const dataSourceProfileProviderMock: DataSourceProfileProvider = { + profileId: 'data-source-profile', + profile: { getCellRenderers: jest.fn() }, + resolve: jest.fn(() => ({ + isMatch: true, + context: { + category: DataSourceCategory.Logs, + }, + })), + }; + + const documentProfileProviderMock: DocumentProfileProvider = { + profileId: 'document-profile', + profile: { getCellRenderers: jest.fn() } as DocumentProfileProvider['profile'], + resolve: jest.fn(() => ({ + isMatch: true, + context: { + type: DocumentType.Log, + }, + })), + }; + + 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); + + const profilesManagerMock = new ProfilesManager( + rootProfileServiceMock, + dataSourceProfileServiceMock, + documentProfileServiceMock + ); + + return { + rootProfileProviderMock, + dataSourceProfileProviderMock, + documentProfileProviderMock, + contextRecordMock, + contextRecordMock2, + profilesManagerMock, + }; +}; diff --git a/src/plugins/discover/public/context_awareness/composable_profile.test.ts b/src/plugins/discover/public/context_awareness/composable_profile.test.ts new file mode 100644 index 0000000000000..251da37fa0126 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/composable_profile.test.ts @@ -0,0 +1,67 @@ +/* + * 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 { ComposableProfile, getMergedAccessor } from './composable_profile'; +import { Profile } from './types'; + +describe('getMergedAccessor', () => { + it('should return the base implementation if no profiles are provided', () => { + const baseImpl: Profile['getCellRenderers'] = jest.fn(() => ({ base: jest.fn() })); + const mergedAccessor = getMergedAccessor([], 'getCellRenderers', baseImpl); + const result = mergedAccessor(); + expect(baseImpl).toHaveBeenCalled(); + expect(result).toEqual({ base: expect.any(Function) }); + }); + + it('should merge the accessors in the correct order', () => { + const baseImpl: Profile['getCellRenderers'] = jest.fn(() => ({ base: jest.fn() })); + const profile1: ComposableProfile = { + getCellRenderers: jest.fn((prev) => () => ({ + ...prev(), + profile1: jest.fn(), + })), + }; + const profile2: ComposableProfile = { + getCellRenderers: jest.fn((prev) => () => ({ + ...prev(), + profile2: jest.fn(), + })), + }; + const mergedAccessor = getMergedAccessor([profile1, profile2], 'getCellRenderers', baseImpl); + const result = mergedAccessor(); + expect(baseImpl).toHaveBeenCalled(); + expect(profile1.getCellRenderers).toHaveBeenCalled(); + expect(profile2.getCellRenderers).toHaveBeenCalled(); + expect(result).toEqual({ + base: expect.any(Function), + profile1: expect.any(Function), + profile2: expect.any(Function), + }); + expect(Object.keys(result)).toEqual(['base', 'profile1', 'profile2']); + }); + + it('should allow overwriting previous accessors', () => { + const baseImpl: Profile['getCellRenderers'] = jest.fn(() => ({ base: jest.fn() })); + const profile1: ComposableProfile = { + getCellRenderers: jest.fn(() => () => ({ profile1: jest.fn() })), + }; + const profile2: ComposableProfile = { + getCellRenderers: jest.fn((prev) => () => ({ + ...prev(), + profile2: jest.fn(), + })), + }; + const mergedAccessor = getMergedAccessor([profile1, profile2], 'getCellRenderers', baseImpl); + const result = mergedAccessor(); + expect(baseImpl).not.toHaveBeenCalled(); + expect(profile1.getCellRenderers).toHaveBeenCalled(); + expect(profile2.getCellRenderers).toHaveBeenCalled(); + expect(result).toEqual({ profile1: expect.any(Function), profile2: expect.any(Function) }); + expect(Object.keys(result)).toEqual(['profile1', 'profile2']); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.test.ts b/src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.test.ts new file mode 100644 index 0000000000000..7f3cd816ae9e8 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.test.ts @@ -0,0 +1,78 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { ComposableProfile, getMergedAccessor } from '../composable_profile'; +import { useProfileAccessor } from './use_profile_accessor'; +import { getDataTableRecords } from '../../__fixtures__/real_hits'; +import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield'; +import { useProfiles } from './use_profiles'; + +let mockProfiles: ComposableProfile[] = []; + +jest.mock('./use_profiles', () => ({ + useProfiles: jest.fn(() => mockProfiles), +})); + +jest.mock('../composable_profile', () => { + const originalModule = jest.requireActual('../composable_profile'); + return { + ...originalModule, + getMergedAccessor: jest.fn(originalModule.getMergedAccessor), + }; +}); + +const record = getDataTableRecords(dataViewWithTimefieldMock)[0]; + +describe('useProfileAccessor', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockProfiles = [ + { getCellRenderers: (prev) => () => ({ ...prev(), profile1: jest.fn() }) }, + { getCellRenderers: (prev) => () => ({ ...prev(), profile2: jest.fn() }) }, + ]; + }); + + it('should return a function that merges accessors', () => { + const { result } = renderHook(() => useProfileAccessor('getCellRenderers', { record })); + expect(useProfiles).toHaveBeenCalledTimes(1); + expect(useProfiles).toHaveBeenCalledWith({ record }); + const base = () => ({ base: jest.fn() }); + const accessor = result.current(base); + expect(getMergedAccessor).toHaveBeenCalledTimes(1); + expect(getMergedAccessor).toHaveBeenCalledWith(mockProfiles, 'getCellRenderers', base); + const renderers = accessor(); + expect(renderers).toEqual({ + base: expect.any(Function), + profile1: expect.any(Function), + profile2: expect.any(Function), + }); + expect(Object.keys(renderers)).toEqual(['base', 'profile1', 'profile2']); + }); + + it('should recalculate the accessor when the key changes', () => { + const { rerender, result } = renderHook(({ key }) => useProfileAccessor(key, { record }), { + initialProps: { key: 'getCellRenderers' as const }, + }); + const prevResult = result.current; + rerender({ key: 'getCellRenderers' }); + expect(result.current).toBe(prevResult); + rerender({ key: 'otherKey' as unknown as 'getCellRenderers' }); + expect(result.current).not.toBe(prevResult); + }); + + it('should recalculate the accessor when the profiles change', () => { + const { rerender, result } = renderHook(() => + useProfileAccessor('getCellRenderers', { record }) + ); + const prevResult = result.current; + mockProfiles = [{ getCellRenderers: (prev) => () => ({ ...prev(), profile3: jest.fn() }) }]; + rerender(); + expect(result.current).not.toBe(prevResult); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/hooks/use_profiles.test.tsx b/src/plugins/discover/public/context_awareness/hooks/use_profiles.test.tsx new file mode 100644 index 0000000000000..715672903462b --- /dev/null +++ b/src/plugins/discover/public/context_awareness/hooks/use_profiles.test.tsx @@ -0,0 +1,90 @@ +/* + * 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 { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { renderHook } from '@testing-library/react-hooks'; +import React from 'react'; +import { discoverServiceMock } from '../../__mocks__/services'; +import { GetProfilesOptions } from '../profiles_manager'; +import { createContextAwarenessMocks } from '../__mocks__'; +import { useProfiles } from './use_profiles'; + +const { + rootProfileProviderMock, + dataSourceProfileProviderMock, + documentProfileProviderMock, + contextRecordMock, + contextRecordMock2, + profilesManagerMock, +} = createContextAwarenessMocks(); + +profilesManagerMock.resolveRootProfile({}); +profilesManagerMock.resolveDataSourceProfile({}); +profilesManagerMock.resolveDocumentProfile({ record: contextRecordMock }); +profilesManagerMock.resolveDocumentProfile({ record: contextRecordMock2 }); + +discoverServiceMock.profilesManager = profilesManagerMock; + +const getProfilesSpy = jest.spyOn(discoverServiceMock.profilesManager, 'getProfiles'); +const getProfiles$Spy = jest.spyOn(discoverServiceMock.profilesManager, 'getProfiles$'); + +const render = () => { + return renderHook((props) => useProfiles(props), { + initialProps: { record: contextRecordMock } as GetProfilesOptions, + wrapper: ({ children }) => ( + {children} + ), + }); +}; + +describe('useProfiles', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return profiles', () => { + const { result } = render(); + expect(getProfilesSpy).toHaveBeenCalledTimes(2); + expect(getProfiles$Spy).toHaveBeenCalledTimes(1); + expect(result.current).toEqual([ + rootProfileProviderMock.profile, + dataSourceProfileProviderMock.profile, + documentProfileProviderMock.profile, + ]); + }); + + it('should return the same array reference if profiles do not change', () => { + const { result, rerender } = render(); + expect(getProfilesSpy).toHaveBeenCalledTimes(2); + expect(getProfiles$Spy).toHaveBeenCalledTimes(1); + const prevResult = result.current; + rerender({ record: contextRecordMock }); + expect(getProfilesSpy).toHaveBeenCalledTimes(2); + expect(getProfiles$Spy).toHaveBeenCalledTimes(1); + expect(result.current).toBe(prevResult); + rerender({ record: contextRecordMock2 }); + expect(getProfilesSpy).toHaveBeenCalledTimes(3); + expect(getProfiles$Spy).toHaveBeenCalledTimes(2); + expect(result.current).toBe(prevResult); + }); + + it('should return a different array reference if profiles change', () => { + const { result, rerender } = render(); + expect(getProfilesSpy).toHaveBeenCalledTimes(2); + expect(getProfiles$Spy).toHaveBeenCalledTimes(1); + const prevResult = result.current; + rerender({ record: contextRecordMock }); + expect(getProfilesSpy).toHaveBeenCalledTimes(2); + expect(getProfiles$Spy).toHaveBeenCalledTimes(1); + expect(result.current).toBe(prevResult); + rerender({ record: undefined }); + expect(getProfilesSpy).toHaveBeenCalledTimes(3); + expect(getProfiles$Spy).toHaveBeenCalledTimes(2); + expect(result.current).not.toBe(prevResult); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/hooks/use_root_profile.test.tsx b/src/plugins/discover/public/context_awareness/hooks/use_root_profile.test.tsx new file mode 100644 index 0000000000000..a41ec7c23cf88 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/hooks/use_root_profile.test.tsx @@ -0,0 +1,45 @@ +/* + * 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 { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { renderHook } from '@testing-library/react-hooks'; +import React from 'react'; +import { discoverServiceMock } from '../../__mocks__/services'; +import { useRootProfile } from './use_root_profile'; + +const render = () => { + return renderHook((props) => useRootProfile(props), { + initialProps: { solutionNavId: 'solutionNavId' }, + wrapper: ({ children }) => ( + {children} + ), + }); +}; + +describe('useRootProfile', () => { + it('should return rootProfileLoading as true', () => { + const { result } = render(); + expect(result.current.rootProfileLoading).toBe(true); + }); + + it('should return rootProfileLoading as false', async () => { + const { result, waitForNextUpdate } = render(); + await waitForNextUpdate(); + expect(result.current.rootProfileLoading).toBe(false); + }); + + it('should return rootProfileLoading as true when solutionNavId changes', async () => { + const { result, rerender, waitForNextUpdate } = render(); + await waitForNextUpdate(); + expect(result.current.rootProfileLoading).toBe(false); + rerender({ solutionNavId: 'newSolutionNavId' }); + expect(result.current.rootProfileLoading).toBe(true); + await waitForNextUpdate(); + expect(result.current.rootProfileLoading).toBe(false); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/profile_service.test.ts b/src/plugins/discover/public/context_awareness/profile_service.test.ts new file mode 100644 index 0000000000000..e306ee149f52f --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_service.test.ts @@ -0,0 +1,147 @@ +/* + * 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. + */ + +/* eslint-disable max-classes-per-file */ + +import { AsyncProfileService, ContextWithProfileId, ProfileService } from './profile_service'; +import { Profile } from './types'; + +interface TestParams { + myParam: string; +} + +interface TestContext { + myContext: string; +} + +const defaultContext: ContextWithProfileId = { + profileId: 'test-profile', + myContext: 'test', +}; + +class TestProfileService extends ProfileService { + constructor() { + super(defaultContext); + } +} + +type TestProfileProvider = Parameters[0]; + +class TestAsyncProfileService extends AsyncProfileService { + constructor() { + super(defaultContext); + } +} + +type TestAsyncProfileProvider = Parameters[0]; + +const provider: TestProfileProvider = { + profileId: 'test-profile-1', + profile: { getCellRenderers: jest.fn() }, + resolve: jest.fn(() => ({ isMatch: false })), +}; + +const provider2: TestProfileProvider = { + profileId: 'test-profile-2', + profile: { getCellRenderers: jest.fn() }, + resolve: jest.fn(({ myParam }) => ({ isMatch: true, context: { myContext: myParam } })), +}; + +const provider3: TestProfileProvider = { + profileId: 'test-profile-3', + profile: { getCellRenderers: jest.fn() }, + resolve: jest.fn(({ myParam }) => ({ isMatch: true, context: { myContext: myParam } })), +}; + +const asyncProvider2: TestAsyncProfileProvider = { + profileId: 'test-profile-2', + profile: { getCellRenderers: jest.fn() }, + resolve: jest.fn(async ({ myParam }) => ({ isMatch: true, context: { myContext: myParam } })), +}; + +describe('ProfileService', () => { + let service: TestProfileService; + + beforeEach(() => { + jest.clearAllMocks(); + service = new TestProfileService(); + }); + + it('should expose defaultContext', () => { + expect(service.defaultContext).toBe(defaultContext); + }); + + it('should allow registering providers and getting profiles', () => { + service.registerProvider(provider); + service.registerProvider(provider2); + expect(service.getProfile({ profileId: 'test-profile-1', myContext: 'test' })).toBe( + provider.profile + ); + expect(service.getProfile({ profileId: 'test-profile-2', myContext: 'test' })).toBe( + provider2.profile + ); + }); + + it('should return empty profile if no provider is found', () => { + service.registerProvider(provider); + expect(service.getProfile({ profileId: 'test-profile-2', myContext: 'test' })).toEqual({}); + }); + + it('should resolve to first matching context', () => { + service.registerProvider(provider); + service.registerProvider(provider2); + service.registerProvider(provider3); + expect(service.resolve({ myParam: 'test' })).toEqual({ + profileId: 'test-profile-2', + myContext: 'test', + }); + expect(provider.resolve).toHaveBeenCalledTimes(1); + expect(provider.resolve).toHaveBeenCalledWith({ myParam: 'test' }); + expect(provider2.resolve).toHaveBeenCalledTimes(1); + expect(provider2.resolve).toHaveBeenCalledWith({ myParam: 'test' }); + expect(provider3.resolve).not.toHaveBeenCalled(); + }); + + it('should resolve to default context if no matching context is found', () => { + service.registerProvider(provider); + expect(service.resolve({ myParam: 'test' })).toEqual(defaultContext); + expect(provider.resolve).toHaveBeenCalledTimes(1); + expect(provider.resolve).toHaveBeenCalledWith({ myParam: 'test' }); + }); +}); + +describe('AsyncProfileService', () => { + let service: TestAsyncProfileService; + + beforeEach(() => { + jest.clearAllMocks(); + service = new TestAsyncProfileService(); + }); + + it('should resolve to first matching context', async () => { + service.registerProvider(provider); + service.registerProvider(asyncProvider2); + service.registerProvider(provider3); + await expect(service.resolve({ myParam: 'test' })).resolves.toEqual({ + profileId: 'test-profile-2', + myContext: 'test', + }); + expect(provider.resolve).toHaveBeenCalledTimes(1); + expect(provider.resolve).toHaveBeenCalledWith({ myParam: 'test' }); + expect(asyncProvider2.resolve).toHaveBeenCalledTimes(1); + expect(asyncProvider2.resolve).toHaveBeenCalledWith({ myParam: 'test' }); + expect(provider3.resolve).not.toHaveBeenCalled(); + }); + + it('should resolve to default context if no matching context is found', async () => { + service.registerProvider(provider); + await expect(service.resolve({ myParam: 'test' })).resolves.toEqual(defaultContext); + expect(provider.resolve).toHaveBeenCalledTimes(1); + expect(provider.resolve).toHaveBeenCalledWith({ myParam: 'test' }); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/profiles_manager.test.ts b/src/plugins/discover/public/context_awareness/profiles_manager.test.ts new file mode 100644 index 0000000000000..b778aaac323c6 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profiles_manager.test.ts @@ -0,0 +1,255 @@ +/* + * 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 { firstValueFrom, Subject } from 'rxjs'; +import { createEsqlDataSource } from '../../common/data_sources'; +import { addLog } from '../utils/add_log'; +import { createContextAwarenessMocks } from './__mocks__'; + +jest.mock('../utils/add_log'); + +let mocks = createContextAwarenessMocks(); + +describe('ProfilesManager', () => { + beforeEach(() => { + jest.clearAllMocks(); + mocks = createContextAwarenessMocks(); + }); + + it('should return default profiles', () => { + const profiles = mocks.profilesManagerMock.getProfiles(); + expect(profiles).toEqual([{}, {}, {}]); + }); + + it('should resolve root profile', async () => { + await mocks.profilesManagerMock.resolveRootProfile({}); + const profiles = mocks.profilesManagerMock.getProfiles(); + expect(profiles).toEqual([mocks.rootProfileProviderMock.profile, {}, {}]); + }); + + it('should resolve data source profile', async () => { + await mocks.profilesManagerMock.resolveDataSourceProfile({}); + const profiles = mocks.profilesManagerMock.getProfiles(); + expect(profiles).toEqual([{}, mocks.dataSourceProfileProviderMock.profile, {}]); + }); + + it('should resolve document profile', async () => { + mocks.profilesManagerMock.resolveDocumentProfile({ record: mocks.contextRecordMock }); + const profiles = mocks.profilesManagerMock.getProfiles({ record: mocks.contextRecordMock }); + expect(profiles).toEqual([{}, {}, mocks.documentProfileProviderMock.profile]); + }); + + it('should resolve multiple profiles', async () => { + await mocks.profilesManagerMock.resolveRootProfile({}); + await mocks.profilesManagerMock.resolveDataSourceProfile({}); + mocks.profilesManagerMock.resolveDocumentProfile({ record: mocks.contextRecordMock }); + const profiles = mocks.profilesManagerMock.getProfiles({ record: mocks.contextRecordMock }); + expect(profiles).toEqual([ + mocks.rootProfileProviderMock.profile, + mocks.dataSourceProfileProviderMock.profile, + mocks.documentProfileProviderMock.profile, + ]); + }); + + it('should expose profiles as an observable', async () => { + const getProfilesSpy = jest.spyOn(mocks.profilesManagerMock, 'getProfiles'); + const profiles$ = mocks.profilesManagerMock.getProfiles$({ record: mocks.contextRecordMock }); + const next = jest.fn(); + profiles$.subscribe(next); + expect(getProfilesSpy).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith([{}, {}, {}]); + await mocks.profilesManagerMock.resolveRootProfile({}); + expect(getProfilesSpy).toHaveBeenCalledTimes(2); + expect(next).toHaveBeenCalledWith([mocks.rootProfileProviderMock.profile, {}, {}]); + await mocks.profilesManagerMock.resolveDataSourceProfile({}); + expect(getProfilesSpy).toHaveBeenCalledTimes(3); + expect(next).toHaveBeenCalledWith([ + mocks.rootProfileProviderMock.profile, + mocks.dataSourceProfileProviderMock.profile, + {}, + ]); + mocks.profilesManagerMock.resolveDocumentProfile({ record: mocks.contextRecordMock }); + expect(getProfilesSpy).toHaveBeenCalledTimes(4); + expect(next).toHaveBeenCalledWith([ + mocks.rootProfileProviderMock.profile, + mocks.dataSourceProfileProviderMock.profile, + mocks.documentProfileProviderMock.profile, + ]); + }); + + it("should not resolve root profile again if params haven't changed", async () => { + await mocks.profilesManagerMock.resolveRootProfile({ solutionNavId: 'solutionNavId' }); + await mocks.profilesManagerMock.resolveRootProfile({ solutionNavId: 'solutionNavId' }); + expect(mocks.rootProfileProviderMock.resolve).toHaveBeenCalledTimes(1); + }); + + it('should resolve root profile again if params have changed', async () => { + await mocks.profilesManagerMock.resolveRootProfile({ solutionNavId: 'solutionNavId' }); + await mocks.profilesManagerMock.resolveRootProfile({ solutionNavId: 'newSolutionNavId' }); + expect(mocks.rootProfileProviderMock.resolve).toHaveBeenCalledTimes(2); + }); + + it('should not resolve data source profile again if params have not changed', async () => { + await mocks.profilesManagerMock.resolveDataSourceProfile({ + dataSource: createEsqlDataSource(), + query: { esql: 'from *' }, + }); + expect(mocks.dataSourceProfileProviderMock.resolve).toHaveBeenCalledTimes(1); + await mocks.profilesManagerMock.resolveDataSourceProfile({ + dataSource: createEsqlDataSource(), + query: { esql: 'from *' }, + }); + expect(mocks.dataSourceProfileProviderMock.resolve).toHaveBeenCalledTimes(1); + }); + + it('should resolve data source profile again if params have changed', async () => { + await mocks.profilesManagerMock.resolveDataSourceProfile({ + dataSource: createEsqlDataSource(), + query: { esql: 'from *' }, + }); + expect(mocks.dataSourceProfileProviderMock.resolve).toHaveBeenCalledTimes(1); + await mocks.profilesManagerMock.resolveDataSourceProfile({ + dataSource: createEsqlDataSource(), + query: { esql: 'from logs-*' }, + }); + expect(mocks.dataSourceProfileProviderMock.resolve).toHaveBeenCalledTimes(2); + }); + + it('should log an error and fall back to the default profile if root profile resolution fails', async () => { + await mocks.profilesManagerMock.resolveRootProfile({ solutionNavId: 'solutionNavId' }); + let profiles = mocks.profilesManagerMock.getProfiles(); + expect(profiles).toEqual([mocks.rootProfileProviderMock.profile, {}, {}]); + const resolveSpy = jest.spyOn(mocks.rootProfileProviderMock, 'resolve'); + resolveSpy.mockRejectedValue(new Error('Failed to resolve')); + await mocks.profilesManagerMock.resolveRootProfile({ solutionNavId: 'newSolutionNavId' }); + expect(addLog).toHaveBeenCalledWith( + '[ProfilesManager] root context resolution failed with params: {\n "solutionNavId": "newSolutionNavId"\n}', + new Error('Failed to resolve') + ); + profiles = mocks.profilesManagerMock.getProfiles(); + expect(profiles).toEqual([{}, {}, {}]); + }); + + it('should log an error and fall back to the default profile if data source profile resolution fails', async () => { + await mocks.profilesManagerMock.resolveDataSourceProfile({ + dataSource: createEsqlDataSource(), + query: { esql: 'from *' }, + }); + let profiles = mocks.profilesManagerMock.getProfiles(); + expect(profiles).toEqual([{}, mocks.dataSourceProfileProviderMock.profile, {}]); + const resolveSpy = jest.spyOn(mocks.dataSourceProfileProviderMock, 'resolve'); + resolveSpy.mockRejectedValue(new Error('Failed to resolve')); + await mocks.profilesManagerMock.resolveDataSourceProfile({ + dataSource: createEsqlDataSource(), + query: { esql: 'from logs-*' }, + }); + expect(addLog).toHaveBeenCalledWith( + '[ProfilesManager] data source context resolution failed with params: {\n "esqlQuery": "from logs-*"\n}', + new Error('Failed to resolve') + ); + profiles = mocks.profilesManagerMock.getProfiles(); + expect(profiles).toEqual([{}, {}, {}]); + }); + + it('should log an error and fall back to the default profile if document profile resolution fails', () => { + mocks.profilesManagerMock.resolveDocumentProfile({ record: mocks.contextRecordMock }); + let profiles = mocks.profilesManagerMock.getProfiles({ record: mocks.contextRecordMock }); + expect(profiles).toEqual([{}, {}, mocks.documentProfileProviderMock.profile]); + const resolveSpy = jest.spyOn(mocks.documentProfileProviderMock, 'resolve'); + resolveSpy.mockImplementation(() => { + throw new Error('Failed to resolve'); + }); + mocks.profilesManagerMock.resolveDocumentProfile({ record: mocks.contextRecordMock2 }); + profiles = mocks.profilesManagerMock.getProfiles({ record: mocks.contextRecordMock2 }); + expect(addLog).toHaveBeenCalledWith( + '[ProfilesManager] document context resolution failed with params: {\n "recordId": "logstash-2014.09.09::388::"\n}', + new Error('Failed to resolve') + ); + expect(profiles).toEqual([{}, {}, {}]); + }); + + it('should cancel existing root profile resolution when another is triggered', async () => { + const context = await mocks.rootProfileProviderMock.resolve({ solutionNavId: 'solutionNavId' }); + const newContext = await mocks.rootProfileProviderMock.resolve({ + solutionNavId: 'newSolutionNavId', + }); + const resolveSpy = jest.spyOn(mocks.rootProfileProviderMock, 'resolve'); + resolveSpy.mockClear(); + const resolvedDeferredResult$ = new Subject(); + const deferredResult = firstValueFrom(resolvedDeferredResult$).then(() => context); + resolveSpy.mockResolvedValueOnce(deferredResult); + const promise1 = mocks.profilesManagerMock.resolveRootProfile({ + solutionNavId: 'solutionNavId', + }); + expect(resolveSpy).toHaveReturnedTimes(1); + expect(resolveSpy).toHaveLastReturnedWith(deferredResult); + expect(mocks.profilesManagerMock.getProfiles()).toEqual([{}, {}, {}]); + const resolvedDeferredResult2$ = new Subject(); + const deferredResult2 = firstValueFrom(resolvedDeferredResult2$).then(() => newContext); + resolveSpy.mockResolvedValueOnce(deferredResult2); + const promise2 = mocks.profilesManagerMock.resolveRootProfile({ + solutionNavId: 'newSolutionNavId', + }); + expect(resolveSpy).toHaveReturnedTimes(2); + expect(resolveSpy).toHaveLastReturnedWith(deferredResult2); + expect(mocks.profilesManagerMock.getProfiles()).toEqual([{}, {}, {}]); + resolvedDeferredResult$.next(undefined); + await promise1; + expect(mocks.profilesManagerMock.getProfiles()).toEqual([{}, {}, {}]); + resolvedDeferredResult2$.next(undefined); + await promise2; + expect(mocks.profilesManagerMock.getProfiles()).toEqual([ + mocks.rootProfileProviderMock.profile, + {}, + {}, + ]); + }); + + it('should cancel existing data source profile resolution when another is triggered', async () => { + const context = await mocks.dataSourceProfileProviderMock.resolve({ + dataSource: createEsqlDataSource(), + query: { esql: 'from *' }, + }); + const newContext = await mocks.dataSourceProfileProviderMock.resolve({ + dataSource: createEsqlDataSource(), + query: { esql: 'from logs-*' }, + }); + const resolveSpy = jest.spyOn(mocks.dataSourceProfileProviderMock, 'resolve'); + resolveSpy.mockClear(); + const resolvedDeferredResult$ = new Subject(); + const deferredResult = firstValueFrom(resolvedDeferredResult$).then(() => context); + resolveSpy.mockResolvedValueOnce(deferredResult); + const promise1 = mocks.profilesManagerMock.resolveDataSourceProfile({ + dataSource: createEsqlDataSource(), + query: { esql: 'from *' }, + }); + expect(resolveSpy).toHaveReturnedTimes(1); + expect(resolveSpy).toHaveLastReturnedWith(deferredResult); + expect(mocks.profilesManagerMock.getProfiles()).toEqual([{}, {}, {}]); + const resolvedDeferredResult2$ = new Subject(); + const deferredResult2 = firstValueFrom(resolvedDeferredResult2$).then(() => newContext); + resolveSpy.mockResolvedValueOnce(deferredResult2); + const promise2 = mocks.profilesManagerMock.resolveDataSourceProfile({ + dataSource: createEsqlDataSource(), + query: { esql: 'from logs-*' }, + }); + expect(resolveSpy).toHaveReturnedTimes(2); + expect(resolveSpy).toHaveLastReturnedWith(deferredResult2); + expect(mocks.profilesManagerMock.getProfiles()).toEqual([{}, {}, {}]); + resolvedDeferredResult$.next(undefined); + await promise1; + expect(mocks.profilesManagerMock.getProfiles()).toEqual([{}, {}, {}]); + resolvedDeferredResult2$.next(undefined); + await promise2; + expect(mocks.profilesManagerMock.getProfiles()).toEqual([ + {}, + mocks.dataSourceProfileProviderMock.profile, + {}, + ]); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/profiles_manager.ts b/src/plugins/discover/public/context_awareness/profiles_manager.ts index bab08a8dec1a4..33fb5e30ba48e 100644 --- a/src/plugins/discover/public/context_awareness/profiles_manager.ts +++ b/src/plugins/discover/public/context_awareness/profiles_manager.ts @@ -9,7 +9,7 @@ import type { DataTableRecord } from '@kbn/discover-utils'; import { isOfAggregateQueryType } from '@kbn/es-query'; import { isEqual, memoize } from 'lodash'; -import { BehaviorSubject, combineLatest, map } from 'rxjs'; +import { BehaviorSubject, combineLatest, filter, map, Observable, startWith } from 'rxjs'; import { DataSourceType, isDataSourceType } from '../../common/data_sources'; import { addLog } from '../utils/add_log'; import type { @@ -45,6 +45,7 @@ export interface GetProfilesOptions { export class ProfilesManager { private readonly rootContext$: BehaviorSubject>; private readonly dataSourceContext$: BehaviorSubject>; + private readonly recordId$ = new BehaviorSubject(undefined); private prevRootProfileParams?: SerializedRootProfileParams; private prevDataSourceProfileParams?: SerializedDataSourceProfileParams; @@ -128,6 +129,8 @@ export class ProfilesManager { return context; }), }); + + this.recordId$.next(params.record.id); } public getProfiles({ record }: GetProfilesOptions = {}) { @@ -141,9 +144,19 @@ export class ProfilesManager { } public getProfiles$(options: GetProfilesOptions = {}) { - return combineLatest([this.rootContext$, this.dataSourceContext$]).pipe( - map(() => this.getProfiles(options)) - ); + const observables: Array> = [this.rootContext$, this.dataSourceContext$]; + const record = options.record; + + if (record) { + observables.push( + this.recordId$.pipe( + startWith(record.id), + filter((recordId) => recordId === record.id) + ) + ); + } + + return combineLatest(observables).pipe(map(() => this.getProfiles(options))); } private serializeRootProfileParams( From 0217bae97aa0ba5cce535192cf097cab832943ad Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Fri, 31 May 2024 00:51:48 -0300 Subject: [PATCH 33/52] Clean up ProfilesManager --- .../context_awareness/profiles_manager.ts | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/plugins/discover/public/context_awareness/profiles_manager.ts b/src/plugins/discover/public/context_awareness/profiles_manager.ts index 33fb5e30ba48e..26761a4e6b75c 100644 --- a/src/plugins/discover/public/context_awareness/profiles_manager.ts +++ b/src/plugins/discover/public/context_awareness/profiles_manager.ts @@ -62,7 +62,7 @@ export class ProfilesManager { } public async resolveRootProfile(params: RootProfileProviderParams) { - const serializedParams = this.serializeRootProfileParams(params); + const serializedParams = serializeRootProfileParams(params); if (isEqual(this.prevRootProfileParams, serializedParams)) { return; @@ -89,7 +89,7 @@ export class ProfilesManager { } public async resolveDataSourceProfile(params: DataSourceProfileProviderParams) { - const serializedParams = this.serializeDataSourceProfileParams(params); + const serializedParams = serializeDataSourceProfileParams(params); if (isEqual(this.prevDataSourceProfileParams, serializedParams)) { return; @@ -158,30 +158,30 @@ export class ProfilesManager { return combineLatest(observables).pipe(map(() => this.getProfiles(options))); } +} - private serializeRootProfileParams( - params: RootProfileProviderParams - ): SerializedRootProfileParams { - return { - solutionNavId: params.solutionNavId, - }; - } +const serializeRootProfileParams = ( + params: RootProfileProviderParams +): SerializedRootProfileParams => { + return { + solutionNavId: params.solutionNavId, + }; +}; - private serializeDataSourceProfileParams( - params: DataSourceProfileProviderParams - ): SerializedDataSourceProfileParams { - return { - dataViewId: isDataSourceType(params.dataSource, DataSourceType.DataView) - ? params.dataSource.dataViewId +const serializeDataSourceProfileParams = ( + params: DataSourceProfileProviderParams +): SerializedDataSourceProfileParams => { + return { + dataViewId: isDataSourceType(params.dataSource, DataSourceType.DataView) + ? params.dataSource.dataViewId + : undefined, + esqlQuery: + isDataSourceType(params.dataSource, DataSourceType.Esql) && + isOfAggregateQueryType(params.query) + ? params.query.esql : undefined, - esqlQuery: - isDataSourceType(params.dataSource, DataSourceType.Esql) && - isOfAggregateQueryType(params.query) - ? params.query.esql - : undefined, - }; - } -} + }; +}; const recordHasContext = ( record: DataTableRecord | undefined From b80e57f324859465cf71763d23af157481b57063 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Sat, 1 Jun 2024 00:55:50 -0300 Subject: [PATCH 34/52] More Jest tests --- .../src/utils/build_data_record.test.ts | 12 +++++ .../layout/discover_documents.test.tsx | 26 ++++++++++ .../data_fetching/fetch_documents.test.ts | 11 +++++ .../main/discover_main_route.test.tsx | 19 +++++++- .../discover_data_state_container.test.ts | 25 +++++++++- .../context_awareness/__mocks__/index.ts | 21 +++++++-- .../saved_search_embeddable.test.ts | 47 ++++++++++++++++++- 7 files changed, 154 insertions(+), 7 deletions(-) diff --git a/packages/kbn-discover-utils/src/utils/build_data_record.test.ts b/packages/kbn-discover-utils/src/utils/build_data_record.test.ts index ad486aba543a1..e6046d4f5977f 100644 --- a/packages/kbn-discover-utils/src/utils/build_data_record.test.ts +++ b/packages/kbn-discover-utils/src/utils/build_data_record.test.ts @@ -30,5 +30,17 @@ describe('Data table record utils', () => { expect(doc).toHaveProperty('isAnchor'); }); }); + + test('should support processing each record', () => { + const result = buildDataTableRecordList(esHitsMock, dataViewMock, { + processRecord: (record) => ({ ...record, id: 'custom-id' }), + }); + result.forEach((doc) => { + expect(doc).toHaveProperty('id', 'custom-id'); + expect(doc).toHaveProperty('raw'); + expect(doc).toHaveProperty('flattened'); + expect(doc).toHaveProperty('isAnchor'); + }); + }); }); }); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx index ae4b05f495cfa..fecdf31c339d5 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx @@ -26,11 +26,15 @@ import { DiscoverCustomization, DiscoverCustomizationProvider } from '../../../. import { createCustomizationService } from '../../../../customizations/customization_service'; import { DiscoverGrid } from '../../../../components/discover_grid'; import { createDataViewDataSource } from '../../../../../common/data_sources'; +import { createContextAwarenessMocks } from '../../../../context_awareness/__mocks__'; const customisationService = createCustomizationService(); +const { profilesManagerMock, rootProfileProviderMock } = createContextAwarenessMocks(); async function mountComponent(fetchStatus: FetchStatus, hits: EsHitRecord[]) { const services = discoverServiceMock; + + services.profilesManager = profilesManagerMock; services.data.query.timefilter.timefilter.getTime = () => { return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; }; @@ -69,6 +73,10 @@ async function mountComponent(fetchStatus: FetchStatus, hits: EsHitRecord[]) { } describe('Discover documents layout', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + test('render loading when loading and no documents', async () => { const component = await mountComponent(FetchStatus.LOADING, []); expect(component.find('.dscDocuments__loading').exists()).toBeTruthy(); @@ -131,4 +139,22 @@ describe('Discover documents layout', () => { expect(discoverGridComponent.prop('externalCustomRenderers')).toBeDefined(); expect(discoverGridComponent.prop('customGridColumnsConfiguration')).toBeDefined(); }); + + describe('context awareness', () => { + it('should pass cell renders from profile', async () => { + customisationService.set({ + id: 'data_table', + logsEnabled: true, + }); + await profilesManagerMock.resolveRootProfile({ solutionNavId: 'test' }); + const component = await mountComponent(FetchStatus.COMPLETE, esHitsMock); + const discoverGridComponent = component.find(DiscoverGrid); + expect(discoverGridComponent.exists()).toBeTruthy(); + expect(Object.keys(discoverGridComponent.prop('externalCustomRenderers')!)).toEqual([ + 'content', + 'resource', + 'rootProfile', + ]); + }); + }); }); diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.test.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.test.ts index 7abc2d2744a60..be1fddf64e87f 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.test.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.test.ts @@ -30,6 +30,10 @@ const getDeps = () => } as unknown as FetchDeps); describe('test fetchDocuments', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + test('resolves with returned documents', async () => { const hits = [ { _id: '1', foo: 'bar' }, @@ -38,10 +42,17 @@ describe('test fetchDocuments', () => { const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock)); savedSearchMock.searchSource.fetch$ = () => of({ rawResponse: { hits: { hits } } } as IKibanaSearchResponse>); + const resolveDocumentProfileSpy = jest.spyOn( + discoverServiceMock.profilesManager, + 'resolveDocumentProfile' + ); expect(await fetchDocuments(savedSearchMock.searchSource, getDeps())).toEqual({ interceptedWarnings: [], records: documents, }); + expect(resolveDocumentProfileSpy).toHaveBeenCalledTimes(2); + expect(resolveDocumentProfileSpy).toHaveBeenCalledWith({ record: documents[0] }); + expect(resolveDocumentProfileSpy).toHaveBeenCalledWith({ record: documents[1] }); }); test('rejects on query failure', async () => { diff --git a/src/plugins/discover/public/application/main/discover_main_route.test.tsx b/src/plugins/discover/public/application/main/discover_main_route.test.tsx index 07d87efce1d59..b49abb2fe6685 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.test.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.test.tsx @@ -40,12 +40,14 @@ jest.mock('./discover_main_app', () => { }; }); +let mockRootProfileLoading = false; + jest.mock('../../context_awareness', () => { const originalModule = jest.requireActual('../../context_awareness'); return { ...originalModule, useRootProfile: () => ({ - rootProfileLoading: false, + rootProfileLoading: mockRootProfileLoading, }), }; }); @@ -53,6 +55,7 @@ jest.mock('../../context_awareness', () => { describe('DiscoverMainRoute', () => { beforeEach(() => { mockCustomizationService = createCustomizationService(); + mockRootProfileLoading = false; }); test('renders the main app when hasESData=true & hasUserDataView=true ', async () => { @@ -107,6 +110,20 @@ describe('DiscoverMainRoute', () => { }); }); + test('renders LoadingIndicator while root profile is loading', async () => { + mockRootProfileLoading = true; + const component = mountComponent(true, true); + await waitFor(() => { + component.update(); + expect(component.find(DiscoverMainApp).exists()).toBe(false); + }); + mockRootProfileLoading = false; + await waitFor(() => { + component.setProps({}).update(); + expect(component.find(DiscoverMainApp).exists()).toBe(true); + }); + }); + test('should pass hideNavMenuItems=true to DiscoverTopNavInline while loading', async () => { const component = mountComponent(true, true); expect(component.find(DiscoverTopNavInline).prop('hideNavMenuItems')).toBe(true); diff --git a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.test.ts b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.test.ts index 67586670c01c4..05668e0406f9c 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.test.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.test.ts @@ -26,6 +26,10 @@ jest.mock('@kbn/ebt-tools', () => ({ const mockFetchDocuments = fetchDocuments as unknown as jest.MockedFunction; describe('test getDataStateContainer', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + test('return is valid', async () => { const stateContainer = getDiscoverStateMock({ isTimeBased: true }); const dataState = stateContainer.dataState; @@ -35,6 +39,7 @@ describe('test getDataStateContainer', () => { expect(dataState.data$.documents$.getValue().fetchStatus).toBe(FetchStatus.LOADING); expect(dataState.data$.totalHits$.getValue().fetchStatus).toBe(FetchStatus.LOADING); }); + test('refetch$ triggers a search', async () => { const stateContainer = getDiscoverStateMock({ isTimeBased: true }); jest.spyOn(stateContainer.searchSessionManager, 'getNextSearchSessionId'); @@ -46,10 +51,15 @@ describe('test getDataStateContainer', () => { discoverServiceMock.data.query.timefilter.timefilter.getTime = jest.fn(() => { return { from: '2021-05-01T20:00:00Z', to: '2021-05-02T20:00:00Z' }; }); - const dataState = stateContainer.dataState; + const dataState = stateContainer.dataState; const unsubscribe = dataState.subscribe(); + const resolveDataSourceProfileSpy = jest.spyOn( + discoverServiceMock.profilesManager, + 'resolveDataSourceProfile' + ); + expect(resolveDataSourceProfileSpy).not.toHaveBeenCalled(); expect(dataState.data$.totalHits$.value.result).toBe(undefined); expect(dataState.data$.documents$.value.result).toEqual(undefined); @@ -58,6 +68,12 @@ describe('test getDataStateContainer', () => { expect(dataState.data$.main$.value.fetchStatus).toBe('complete'); }); + expect(resolveDataSourceProfileSpy).toHaveBeenCalledTimes(1); + expect(resolveDataSourceProfileSpy).toHaveBeenCalledWith({ + dataSource: stateContainer.appState.get().dataSource, + dataView: stateContainer.savedSearchState.getState().searchSource.getField('index'), + query: stateContainer.appState.get().query, + }); expect(dataState.data$.totalHits$.value.result).toBe(0); expect(dataState.data$.documents$.value.result).toEqual([]); @@ -117,9 +133,13 @@ describe('test getDataStateContainer', () => { ).not.toHaveBeenCalled(); const dataState = stateContainer.dataState; - const unsubscribe = dataState.subscribe(); + const resolveDataSourceProfileSpy = jest.spyOn( + discoverServiceMock.profilesManager, + 'resolveDataSourceProfile' + ); + expect(resolveDataSourceProfileSpy).not.toHaveBeenCalled(); expect(dataState.data$.documents$.value.result).toEqual(initialRecords); let hasLoadingMoreStarted = false; @@ -131,6 +151,7 @@ describe('test getDataStateContainer', () => { } if (hasLoadingMoreStarted && value.fetchStatus === FetchStatus.COMPLETE) { + expect(resolveDataSourceProfileSpy).not.toHaveBeenCalled(); expect(value.result).toEqual([...initialRecords, ...moreRecords]); // it uses the same current search session id expect( diff --git a/src/plugins/discover/public/context_awareness/__mocks__/index.ts b/src/plugins/discover/public/context_awareness/__mocks__/index.ts index 626bbf51831cf..0f8beed5d955f 100644 --- a/src/plugins/discover/public/context_awareness/__mocks__/index.ts +++ b/src/plugins/discover/public/context_awareness/__mocks__/index.ts @@ -24,7 +24,12 @@ import { ProfilesManager } from '../profiles_manager'; export const createContextAwarenessMocks = () => { const rootProfileProviderMock: RootProfileProvider = { profileId: 'root-profile', - profile: { getCellRenderers: jest.fn() }, + profile: { + getCellRenderers: jest.fn((prev) => () => ({ + ...prev(), + rootProfile: () => 'root-profile', + })), + }, resolve: jest.fn(() => ({ isMatch: true, context: { @@ -35,7 +40,12 @@ export const createContextAwarenessMocks = () => { const dataSourceProfileProviderMock: DataSourceProfileProvider = { profileId: 'data-source-profile', - profile: { getCellRenderers: jest.fn() }, + profile: { + getCellRenderers: jest.fn((prev) => () => ({ + ...prev(), + rootProfile: () => 'data-source-profile', + })), + }, resolve: jest.fn(() => ({ isMatch: true, context: { @@ -46,7 +56,12 @@ export const createContextAwarenessMocks = () => { const documentProfileProviderMock: DocumentProfileProvider = { profileId: 'document-profile', - profile: { getCellRenderers: jest.fn() } as DocumentProfileProvider['profile'], + profile: { + getCellRenderers: jest.fn((prev) => () => ({ + ...prev(), + rootProfile: () => 'document-profile', + })), + } as DocumentProfileProvider['profile'], resolve: jest.fn(() => ({ isMatch: true, context: { diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts b/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts index 35e336df52325..bc80882204421 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts @@ -9,6 +9,7 @@ import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; import { createSearchSourceMock } from '@kbn/data-plugin/public/mocks'; import type { DataView } from '@kbn/data-views-plugin/common'; +import { createDataViewDataSource } from '../../common/data_sources'; import { SHOW_FIELD_STATISTICS } from '@kbn/discover-utils'; import { buildDataViewMock, deepMockedFields } from '@kbn/discover-utils/src/__mocks__'; import { ViewMode } from '@kbn/embeddable-plugin/public'; @@ -17,7 +18,7 @@ import { ReactWrapper } from 'enzyme'; import { ReactElement } from 'react'; import { render } from 'react-dom'; import { act } from 'react-dom/test-utils'; -import { Observable, throwError } from 'rxjs'; +import { BehaviorSubject, Observable, throwError } from 'rxjs'; import { SearchInput } from '..'; import { VIEW_MODE } from '../../common/constants'; import { DiscoverServices } from '../build_services'; @@ -140,6 +141,7 @@ describe('saved search embeddable', () => { }; beforeEach(() => { + jest.clearAllMocks(); mountpoint = document.createElement('div'); showFieldStatisticsMockValue = false; @@ -152,6 +154,8 @@ describe('saved search embeddable', () => { if (key === SHOW_FIELD_STATISTICS) return showFieldStatisticsMockValue; } ); + + servicesMock.core.chrome.getActiveSolutionNavId$ = () => new BehaviorSubject('test'); }); afterEach(() => { @@ -475,4 +479,45 @@ describe('saved search embeddable', () => { expect(editUrl).toBe('/base/mock-url'); }); }); + + describe('context awareness', () => { + it('should resolve root profile on init', async () => { + const resolveRootProfileSpy = jest.spyOn( + discoverServiceMock.profilesManager, + 'resolveRootProfile' + ); + const { embeddable } = createEmbeddable(); + expect(resolveRootProfileSpy).not.toHaveBeenCalled(); + await waitOneTick(); + expect(resolveRootProfileSpy).toHaveBeenCalledWith({ solutionNavId: 'test' }); + resolveRootProfileSpy.mockReset(); + expect(resolveRootProfileSpy).not.toHaveBeenCalled(); + embeddable.reload(); + await waitOneTick(); + expect(resolveRootProfileSpy).not.toHaveBeenCalled(); + }); + + it('should resolve data source profile when fetching', async () => { + const resolveDataSourceProfileSpy = jest.spyOn( + discoverServiceMock.profilesManager, + 'resolveDataSourceProfile' + ); + const { embeddable } = createEmbeddable(); + expect(resolveDataSourceProfileSpy).not.toHaveBeenCalled(); + await waitOneTick(); + expect(resolveDataSourceProfileSpy).toHaveBeenCalledWith({ + dataSource: createDataViewDataSource({ dataViewId: dataViewMock.id! }), + dataView: dataViewMock, + query: embeddable.getInput().query, + }); + resolveDataSourceProfileSpy.mockReset(); + expect(resolveDataSourceProfileSpy).not.toHaveBeenCalled(); + embeddable.reload(); + expect(resolveDataSourceProfileSpy).toHaveBeenCalledWith({ + dataSource: createDataViewDataSource({ dataViewId: dataViewMock.id! }), + dataView: dataViewMock, + query: embeddable.getInput().query, + }); + }); + }); }); From f16a506a3a1e17a1519e0013c06d1357a7d6d842 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 3 Jun 2024 00:30:08 -0300 Subject: [PATCH 35/52] await resolveRootProfile in saved search embeddable --- .../discover/public/embeddable/saved_search_embeddable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx index 4ab408aa8fb7c..0985720cb1f91 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx @@ -168,7 +168,7 @@ export class SavedSearchEmbeddable this.services.core.chrome.getActiveSolutionNavId$() ); - this.services.profilesManager.resolveRootProfile({ solutionNavId }); + await this.services.profilesManager.resolveRootProfile({ solutionNavId }); // deferred loading of this embeddable is complete this.setInitializationFinished(); From c992dfeaf44656321873550a9f781f686a7cf348 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 3 Jun 2024 01:15:21 -0300 Subject: [PATCH 36/52] Fix broken Jest tests --- .../public/context_awareness/profiles_manager.ts | 11 ++++++++++- .../embeddable/view_saved_search_action.test.ts | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/plugins/discover/public/context_awareness/profiles_manager.ts b/src/plugins/discover/public/context_awareness/profiles_manager.ts index 26761a4e6b75c..c461a18e9b952 100644 --- a/src/plugins/discover/public/context_awareness/profiles_manager.ts +++ b/src/plugins/discover/public/context_awareness/profiles_manager.ts @@ -9,7 +9,15 @@ import type { DataTableRecord } from '@kbn/discover-utils'; import { isOfAggregateQueryType } from '@kbn/es-query'; import { isEqual, memoize } from 'lodash'; -import { BehaviorSubject, combineLatest, filter, map, Observable, startWith } from 'rxjs'; +import { + BehaviorSubject, + combineLatest, + distinctUntilChanged, + filter, + map, + Observable, + startWith, +} from 'rxjs'; import { DataSourceType, isDataSourceType } from '../../common/data_sources'; import { addLog } from '../utils/add_log'; import type { @@ -151,6 +159,7 @@ export class ProfilesManager { observables.push( this.recordId$.pipe( startWith(record.id), + distinctUntilChanged(), filter((recordId) => recordId === record.id) ) ); diff --git a/src/plugins/discover/public/embeddable/view_saved_search_action.test.ts b/src/plugins/discover/public/embeddable/view_saved_search_action.test.ts index 29c72391fbf12..3a1106c7c7c87 100644 --- a/src/plugins/discover/public/embeddable/view_saved_search_action.test.ts +++ b/src/plugins/discover/public/embeddable/view_saved_search_action.test.ts @@ -13,6 +13,7 @@ import { createStartContractMock } from '../__mocks__/start_contract'; import { discoverServiceMock } from '../__mocks__/services'; import { ViewMode } from '@kbn/embeddable-plugin/public'; import { getDiscoverLocatorParams } from './get_discover_locator_params'; +import { BehaviorSubject } from 'rxjs'; const applicationMock = createStartContractMock(); const services = discoverServiceMock; @@ -34,6 +35,8 @@ const embeddableConfig = { executeTriggerActions, }; +services.core.chrome.getActiveSolutionNavId$ = () => new BehaviorSubject('test'); + describe('view saved search action', () => { it('is compatible when embeddable is of type saved search, in view mode && appropriate permissions are set', async () => { const action = new ViewSavedSearchAction(applicationMock, services.locator); From b4477efaf23184514b5058d9906fc7e6352cad19 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 3 Jun 2024 01:20:23 -0300 Subject: [PATCH 37/52] Remove unused import --- .../main/components/layout/discover_documents.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx index fecdf31c339d5..990f19d88f9be 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx @@ -29,7 +29,7 @@ import { createDataViewDataSource } from '../../../../../common/data_sources'; import { createContextAwarenessMocks } from '../../../../context_awareness/__mocks__'; const customisationService = createCustomizationService(); -const { profilesManagerMock, rootProfileProviderMock } = createContextAwarenessMocks(); +const { profilesManagerMock } = createContextAwarenessMocks(); async function mountComponent(fetchStatus: FetchStatus, hits: EsHitRecord[]) { const services = discoverServiceMock; From 5752651f474d99dfbdecfe9d869377b9edaf7c62 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 3 Jun 2024 01:22:43 -0300 Subject: [PATCH 38/52] Remove extension points except for getCellRenderers --- .../components/top_nav/use_discover_topnav.ts | 33 +++++++-------- .../discover_data_state_container.ts | 24 +---------- .../main/state_management/discover_state.ts | 1 - .../discover_grid_flyout.tsx | 28 +++---------- .../profiles/document_profile.ts | 2 +- .../profiles/example_profiles.tsx | 40 +------------------ .../public/context_awareness/types.ts | 8 ---- 7 files changed, 24 insertions(+), 112 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/top_nav/use_discover_topnav.ts b/src/plugins/discover/public/application/main/components/top_nav/use_discover_topnav.ts index 5dbdfed75598d..06efd3205d1be 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/use_discover_topnav.ts +++ b/src/plugins/discover/public/application/main/components/top_nav/use_discover_topnav.ts @@ -8,7 +8,6 @@ import { useMemo } from 'react'; import useObservable from 'react-use/lib/useObservable'; -import { useProfileAccessor } from '../../../../context_awareness'; import { useDiscoverCustomization } from '../../../../customizations'; import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { useInspector } from '../../hooks/use_inspector'; @@ -49,10 +48,9 @@ export const useDiscoverTopNav = ({ stateContainer, }); - const getTopNavMenuAccessor = useProfileAccessor('getTopNavItems'); - const topNavMenu = useMemo(() => { - const getTopNavMenu = getTopNavMenuAccessor(() => { - return getTopNavLinks({ + const topNavMenu = useMemo( + () => + getTopNavLinks({ dataView, services, state: stateContainer, @@ -60,20 +58,17 @@ export const useDiscoverTopNav = ({ isEsqlMode, adHocDataViews, topNavCustomization, - }); - }); - - return getTopNavMenu(); - }, [ - adHocDataViews, - dataView, - getTopNavMenuAccessor, - isEsqlMode, - onOpenInspector, - services, - stateContainer, - topNavCustomization, - ]); + }), + [ + adHocDataViews, + dataView, + isEsqlMode, + onOpenInspector, + services, + stateContainer, + topNavCustomization, + ] + ); return { topNavMenu, diff --git a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts index de95388f022e3..aaa1f6c15c0f4 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts @@ -19,7 +19,7 @@ import type { SearchResponseWarning } from '@kbn/search-response-warnings'; import type { DataTableRecord } from '@kbn/discover-utils/types'; import { SEARCH_FIELDS_FROM_SOURCE, SEARCH_ON_PAGE_LOAD_SETTING } from '@kbn/discover-utils'; import { getEsqlDataView } from './utils/get_esql_data_view'; -import { DiscoverAppState, DiscoverAppStateContainer } from './discover_app_state_container'; +import { DiscoverAppState } from './discover_app_state_container'; import { DiscoverServices } from '../../../build_services'; import { DiscoverSearchSessionManager } from './discover_search_session'; import { FetchStatus } from '../../types'; @@ -28,7 +28,6 @@ import { fetchAll, fetchMoreDocuments } from '../data_fetching/fetch_all'; import { sendResetMsg } from '../hooks/use_saved_search_messages'; import { getFetch$ } from '../data_fetching/get_fetch_observable'; import { InternalState } from './discover_internal_state_container'; -import { getMergedAccessor } from '../../../context_awareness'; export interface SavedSearchData { main$: DataMain$; @@ -145,7 +144,6 @@ export function getDataStateContainer({ getInternalState, getSavedSearch, setDataView, - updateAppState, }: { services: DiscoverServices; searchSessionManager: DiscoverSearchSessionManager; @@ -153,7 +151,6 @@ export function getDataStateContainer({ getInternalState: () => InternalState; getSavedSearch: () => SavedSearch; setDataView: (dataView: DataView) => void; - updateAppState: DiscoverAppStateContainer['update']; }): DiscoverDataStateContainer { const { data, uiSettings, toastNotifications, profilesManager } = services; const { timefilter } = data.query.timefilter; @@ -280,25 +277,6 @@ export function getDataStateContainer({ autoRefreshDone?.(); autoRefreshDone = undefined; } - - const defaultColumns = getMergedAccessor( - profilesManager.getProfiles(), - 'getDefaultColumns', - () => undefined - )(); - - if (defaultColumns) { - updateAppState( - { - columns: defaultColumns.columns, - grid: { - ...getAppState().grid, - columns: defaultColumns.settings, - }, - }, - true - ); - } }) ) .subscribe(); diff --git a/src/plugins/discover/public/application/main/state_management/discover_state.ts b/src/plugins/discover/public/application/main/state_management/discover_state.ts index 4597fa49d8d11..169e596a2cf93 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_state.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_state.ts @@ -294,7 +294,6 @@ export function getDiscoverStateContainer({ getInternalState: internalStateContainer.getState, getSavedSearch: savedSearchContainer.getState, setDataView, - updateAppState: appStateContainer.update, }); const loadDataViewList = async () => { diff --git a/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.tsx b/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.tsx index 4b81a09721b59..d6273d7669391 100644 --- a/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.tsx +++ b/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.tsx @@ -33,12 +33,10 @@ import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; import type { DataTableColumnsMeta } from '@kbn/unified-data-table'; import { UnifiedDocViewer } from '@kbn/unified-doc-viewer-plugin/public'; import useLocalStorage from 'react-use/lib/useLocalStorage'; -import { DocViewsRegistry } from '@kbn/unified-doc-viewer'; import { useDiscoverServices } from '../../hooks/use_discover_services'; import { useFlyoutActions } from './use_flyout_actions'; import { useDiscoverCustomization } from '../../customizations'; import { DiscoverGridFlyoutActions } from './discover_grid_flyout_actions'; -import { useProfileAccessor } from '../../context_awareness'; export interface DiscoverGridFlyoutProps { savedSearchId?: string; @@ -163,20 +161,6 @@ export function DiscoverGridFlyout({ [onRemoveColumn, services.toastNotifications] ); - const getDocViewsRegistryAccessor = useProfileAccessor('getDocViewsRegistry', { - record: actualHit, - }); - - const docViewsRegistry = useMemo(() => { - const getDocViewsRegistry = getDocViewsRegistryAccessor((registry) => - typeof flyoutCustomization?.docViewsRegistry === 'function' - ? flyoutCustomization.docViewsRegistry(registry) - : registry - ); - - return (registry: DocViewsRegistry) => getDocViewsRegistry(registry); - }, [flyoutCustomization, getDocViewsRegistryAccessor]); - const renderDefaultContent = useCallback( () => ( ), [ + actualHit, + addColumn, columns, columnsMeta, dataView, + hits, + isEsqlQuery, onFilter, - actualHit, - addColumn, removeColumn, - isEsqlQuery, - hits, - docViewsRegistry, + flyoutCustomization?.docViewsRegistry, ] ); 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 0faa5c4c6f976..d39cdc2af6a5a 100644 --- a/src/plugins/discover/public/context_awareness/profiles/document_profile.ts +++ b/src/plugins/discover/public/context_awareness/profiles/document_profile.ts @@ -23,7 +23,7 @@ export interface DocumentContext { type: DocumentType; } -export type DocumentProfile = Pick; +export type DocumentProfile = Profile; export class DocumentProfileService extends ProfileService< DocumentProfile, diff --git a/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx index 6425223af8c45..6b250553ffcca 100644 --- a/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx +++ b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx @@ -24,19 +24,7 @@ import { RootProfileProvider, SolutionType } from './root_profile'; export const o11yRootProfileProvider: RootProfileProvider = { profileId: 'o11y-root-profile', - profile: { - getTopNavItems: (prev) => () => - [ - { - id: 'o11y-root-entry', - label: 'O11y project entry', - run: () => { - alert('HELLO WORLD'); - }, - }, - ...prev(), - ], - }, + profile: {}, resolve: (params) => { if (params.solutionNavId === 'oblt') { return { @@ -54,25 +42,6 @@ export const o11yRootProfileProvider: RootProfileProvider = { export const logsDataSourceProfileProvider: DataSourceProfileProvider = { profileId: 'logs-data-source-profile', profile: { - getTopNavItems: (prev) => () => - [ - { - id: 'logs-data-source-entry', - label: 'Logs data source entry', - run: () => { - alert('HELLO WORLD'); - }, - }, - ...prev(), - ], - getDefaultColumns: () => () => ({ - columns: ['@timestamp', 'log.level', 'message'], - settings: { - 'log.level': { - width: 120, - }, - }, - }), getCellRenderers: (prev) => () => ({ ...prev(), '@timestamp': (props) => { @@ -138,12 +107,7 @@ export const logsDataSourceProfileProvider: DataSourceProfileProvider = { export const logDocumentProfileProvider: DocumentProfileProvider = { profileId: 'log-document-profile', - profile: { - getDocViewsRegistry: (prev) => (registry) => { - registry.enableById('doc_view_logs_overview'); - return prev(registry); - }, - }, + profile: {}, resolve: (params) => { if (getFieldValue(params.record, 'data_stream.type') === 'logs') { return { diff --git a/src/plugins/discover/public/context_awareness/types.ts b/src/plugins/discover/public/context_awareness/types.ts index 99e34e2cd2603..b612b2ce29907 100644 --- a/src/plugins/discover/public/context_awareness/types.ts +++ b/src/plugins/discover/public/context_awareness/types.ts @@ -6,16 +6,8 @@ * Side Public License, v 1. */ -import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; -import type { DiscoverGridSettings } from '@kbn/saved-search-plugin/common'; import type { CustomCellRenderer } from '@kbn/unified-data-table'; -import type { DocViewsRegistry } from '@kbn/unified-doc-viewer'; export interface Profile { - getTopNavItems: () => TopNavMenuData[]; - getDefaultColumns: () => - | { columns: string[]; settings?: DiscoverGridSettings['columns'] } - | undefined; getCellRenderers: () => CustomCellRenderer; - getDocViewsRegistry: (prevRegistry: DocViewsRegistry) => DocViewsRegistry; } From 3814f23e588869df5908f60ca964d7856227fe6e Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 3 Jun 2024 01:25:00 -0300 Subject: [PATCH 39/52] Comment out example cell renderers --- .../profiles/example_profiles.tsx | 83 +++++++++---------- 1 file changed, 39 insertions(+), 44 deletions(-) diff --git a/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx index 6b250553ffcca..e2605cb7139c8 100644 --- a/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx +++ b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx @@ -6,17 +6,17 @@ * Side Public License, v 1. */ -import { EuiBadge } from '@elastic/eui'; +// import { EuiBadge } from '@elastic/eui'; import { DataTableRecord, - getMessageFieldWithFallbacks, - LogDocumentOverview, + // 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 { 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'; @@ -42,44 +42,39 @@ export const o11yRootProfileProvider: RootProfileProvider = { 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); - }, - }), + // 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[] = []; From 411d736c35f0feb252b0160af9cdeb9e619bbccd Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 3 Jun 2024 14:41:44 -0300 Subject: [PATCH 40/52] Add remaining Jest tests --- .../layout/discover_documents.test.tsx | 2 +- .../main/data_fetching/fetch_esql.test.ts | 66 +++++++++++++++++++ .../saved_search_embeddable.test.ts | 16 +++++ 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 src/plugins/discover/public/application/main/data_fetching/fetch_esql.test.ts diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx index 990f19d88f9be..16e752234d6b9 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx @@ -141,7 +141,7 @@ describe('Discover documents layout', () => { }); describe('context awareness', () => { - it('should pass cell renders from profile', async () => { + it('should pass cell renderers from profile', async () => { customisationService.set({ id: 'data_table', logsEnabled: true, diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.test.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.test.ts new file mode 100644 index 0000000000000..5b35cf16e27c7 --- /dev/null +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.test.ts @@ -0,0 +1,66 @@ +/* + * 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 { EsHitRecord } from '@kbn/discover-utils'; +import type { ExecutionContract } from '@kbn/expressions-plugin/common'; +import { RequestAdapter } from '@kbn/inspector-plugin/common'; +import { of } from 'rxjs'; +import { dataViewWithTimefieldMock } from '../../../__mocks__/data_view_with_timefield'; +import { discoverServiceMock } from '../../../__mocks__/services'; +import { fetchEsql } from './fetch_esql'; + +describe('fetchEsql', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('resolves with returned records', async () => { + const hits = [ + { _id: '1', foo: 'bar' }, + { _id: '2', foo: 'baz' }, + ] as unknown as EsHitRecord[]; + const records = hits.map((hit, i) => ({ + id: String(i), + raw: hit, + flattened: hit, + })); + const expressionsExecuteSpy = jest.spyOn(discoverServiceMock.expressions, 'execute'); + expressionsExecuteSpy.mockReturnValueOnce({ + cancel: jest.fn(), + getData: jest.fn(() => + of({ + result: { + columns: ['_id', 'foo'], + rows: hits, + }, + }) + ), + } as unknown as ExecutionContract); + const resolveDocumentProfileSpy = jest.spyOn( + discoverServiceMock.profilesManager, + 'resolveDocumentProfile' + ); + expect( + await fetchEsql( + { esql: 'from *' }, + dataViewWithTimefieldMock, + discoverServiceMock.data, + discoverServiceMock.expressions, + { requests: new RequestAdapter() }, + discoverServiceMock.profilesManager + ) + ).toEqual({ + records, + esqlQueryColumns: ['_id', 'foo'], + esqlHeaderWarning: undefined, + }); + expect(resolveDocumentProfileSpy).toHaveBeenCalledTimes(2); + expect(resolveDocumentProfileSpy).toHaveBeenCalledWith({ record: records[0] }); + expect(resolveDocumentProfileSpy).toHaveBeenCalledWith({ record: records[1] }); + }); +}); diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts b/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts index bc80882204421..0f9d631ca5609 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts @@ -27,6 +27,8 @@ import { discoverServiceMock } from '../__mocks__/services'; import { getDiscoverLocatorParams } from './get_discover_locator_params'; import { SavedSearchEmbeddable, SearchEmbeddableConfig } from './saved_search_embeddable'; import { SavedSearchEmbeddableComponent } from './saved_search_embeddable_component'; +import { DiscoverGrid } from '../components/discover_grid'; +import { createContextAwarenessMocks } from '../context_awareness/__mocks__'; jest.mock('./get_discover_locator_params', () => { const actual = jest.requireActual('./get_discover_locator_params'); @@ -155,6 +157,9 @@ describe('saved search embeddable', () => { } ); + const { profilesManagerMock } = createContextAwarenessMocks(); + + servicesMock.profilesManager = profilesManagerMock; servicesMock.core.chrome.getActiveSolutionNavId$ = () => new BehaviorSubject('test'); }); @@ -519,5 +524,16 @@ describe('saved search embeddable', () => { query: embeddable.getInput().query, }); }); + + it('should pass cell renderers from profile', async () => { + const { embeddable } = createEmbeddable(); + await waitOneTick(); + embeddable.render(mountpoint); + const discoverGridComponent = discoverComponent.find(DiscoverGrid); + expect(discoverGridComponent.exists()).toBeTruthy(); + expect(Object.keys(discoverGridComponent.prop('externalCustomRenderers')!)).toEqual([ + 'rootProfile', + ]); + }); }); }); From 9ff0c830e56c8ccbc2f3619c6d431130a2ae9e3e Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 3 Jun 2024 14:50:00 -0300 Subject: [PATCH 41/52] Comment out example profile registrations --- .../profiles/example_profiles.tsx | 78 +++++++++---------- src/plugins/discover/public/plugin.tsx | 12 +-- 2 files changed, 43 insertions(+), 47 deletions(-) diff --git a/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx index e2605cb7139c8..3835337b25304 100644 --- a/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx +++ b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx @@ -6,17 +6,17 @@ * Side Public License, v 1. */ -// import { EuiBadge } from '@elastic/eui'; +import { EuiBadge } from '@elastic/eui'; import { DataTableRecord, - // getMessageFieldWithFallbacks, - // LogDocumentOverview, + 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 { 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'; @@ -42,39 +42,39 @@ export const o11yRootProfileProvider: RootProfileProvider = { 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); - // }, - // }), + 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[] = []; diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 97286c91a313e..62fa1553a1a33 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -88,11 +88,6 @@ import { ProfilesManager, RootProfileService, } from './context_awareness'; -import { - logDocumentProfileProvider, - logsDataSourceProfileProvider, - o11yRootProfileProvider, -} from './context_awareness/profiles/example_profiles'; /** * @public @@ -461,9 +456,10 @@ export class DiscoverPlugin } private registerProfiles() { - this.rootProfileService.registerProvider(o11yRootProfileProvider); - this.dataSourceProfileService.registerProvider(logsDataSourceProfileProvider); - this.documentProfileService.registerProvider(logDocumentProfileProvider); + // TODO: Conditionally register example profiles for functional testing in a follow up PR + // this.rootProfileService.registerProvider(o11yRootProfileProvider); + // this.dataSourceProfileService.registerProvider(logsDataSourceProfileProvider); + // this.documentProfileService.registerProvider(logDocumentProfileProvider); } private createProfilesManager() { From ce85a6a35fa3623bfdfac7dae41df2d840394154 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 3 Jun 2024 18:57:04 +0000 Subject: [PATCH 42/52] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../application/main/components/layout/discover_documents.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index c90cced254a87..caba229e9137a 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -434,7 +434,7 @@ function DiscoverDocumentsComponent({ externalCustomRenderers={cellRenderers} customGridColumnsConfiguration={customGridColumnsConfiguration} customControlColumnsConfiguration={customControlColumnsConfiguration} - additionalFieldGroups={additionalFieldGroups} + additionalFieldGroups={additionalFieldGroups} />
From 113f66faa15b604b33fdc979e658d2e99cdedcd9 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 5 Jun 2024 20:41:27 -0300 Subject: [PATCH 43/52] Update resolveDocumentProfile to return a Proxy for attaching context --- .../main/data_fetching/fetch_documents.ts | 5 +- .../main/data_fetching/fetch_esql.ts | 4 +- .../hooks/use_profiles.test.tsx | 13 +++-- .../profiles_manager.test.ts | 40 ++++++++----- .../context_awareness/profiles_manager.ts | 58 +++++++------------ 5 files changed, 55 insertions(+), 65 deletions(-) diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts index 18e6cd2bc0a30..4ffdd211c0e5e 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts @@ -68,10 +68,7 @@ export const fetchDocuments = ( filter((res) => !isRunningResponse(res)), map((res) => { return buildDataTableRecordList(res.rawResponse.hits.hits as EsHitRecord[], dataView, { - processRecord: (record) => { - services.profilesManager.resolveDocumentProfile({ record }); - return record; - }, + processRecord: (record) => services.profilesManager.resolveDocumentProfile({ record }), }); }) ); diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts index c50f8e045aa09..0e532e600e5a4 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts @@ -78,9 +78,7 @@ export function fetchEsql( flattened: row, }; - profilesManager.resolveDocumentProfile({ record }); - - return record; + return profilesManager.resolveDocumentProfile({ record }); }); } }); diff --git a/src/plugins/discover/public/context_awareness/hooks/use_profiles.test.tsx b/src/plugins/discover/public/context_awareness/hooks/use_profiles.test.tsx index 715672903462b..f8613e4fea380 100644 --- a/src/plugins/discover/public/context_awareness/hooks/use_profiles.test.tsx +++ b/src/plugins/discover/public/context_awareness/hooks/use_profiles.test.tsx @@ -25,8 +25,9 @@ const { profilesManagerMock.resolveRootProfile({}); profilesManagerMock.resolveDataSourceProfile({}); -profilesManagerMock.resolveDocumentProfile({ record: contextRecordMock }); -profilesManagerMock.resolveDocumentProfile({ record: contextRecordMock2 }); + +const record = profilesManagerMock.resolveDocumentProfile({ record: contextRecordMock }); +const record2 = profilesManagerMock.resolveDocumentProfile({ record: contextRecordMock2 }); discoverServiceMock.profilesManager = profilesManagerMock; @@ -35,7 +36,7 @@ const getProfiles$Spy = jest.spyOn(discoverServiceMock.profilesManager, 'getProf const render = () => { return renderHook((props) => useProfiles(props), { - initialProps: { record: contextRecordMock } as GetProfilesOptions, + initialProps: { record } as GetProfilesOptions, wrapper: ({ children }) => ( {children} ), @@ -63,11 +64,11 @@ describe('useProfiles', () => { expect(getProfilesSpy).toHaveBeenCalledTimes(2); expect(getProfiles$Spy).toHaveBeenCalledTimes(1); const prevResult = result.current; - rerender({ record: contextRecordMock }); + rerender({ record }); expect(getProfilesSpy).toHaveBeenCalledTimes(2); expect(getProfiles$Spy).toHaveBeenCalledTimes(1); expect(result.current).toBe(prevResult); - rerender({ record: contextRecordMock2 }); + rerender({ record: record2 }); expect(getProfilesSpy).toHaveBeenCalledTimes(3); expect(getProfiles$Spy).toHaveBeenCalledTimes(2); expect(result.current).toBe(prevResult); @@ -78,7 +79,7 @@ describe('useProfiles', () => { expect(getProfilesSpy).toHaveBeenCalledTimes(2); expect(getProfiles$Spy).toHaveBeenCalledTimes(1); const prevResult = result.current; - rerender({ record: contextRecordMock }); + rerender({ record }); expect(getProfilesSpy).toHaveBeenCalledTimes(2); expect(getProfiles$Spy).toHaveBeenCalledTimes(1); expect(result.current).toBe(prevResult); diff --git a/src/plugins/discover/public/context_awareness/profiles_manager.test.ts b/src/plugins/discover/public/context_awareness/profiles_manager.test.ts index b778aaac323c6..153ef979aabba 100644 --- a/src/plugins/discover/public/context_awareness/profiles_manager.test.ts +++ b/src/plugins/discover/public/context_awareness/profiles_manager.test.ts @@ -39,16 +39,20 @@ describe('ProfilesManager', () => { }); it('should resolve document profile', async () => { - mocks.profilesManagerMock.resolveDocumentProfile({ record: mocks.contextRecordMock }); - const profiles = mocks.profilesManagerMock.getProfiles({ record: mocks.contextRecordMock }); + const record = mocks.profilesManagerMock.resolveDocumentProfile({ + record: mocks.contextRecordMock, + }); + const profiles = mocks.profilesManagerMock.getProfiles({ record }); expect(profiles).toEqual([{}, {}, mocks.documentProfileProviderMock.profile]); }); it('should resolve multiple profiles', async () => { await mocks.profilesManagerMock.resolveRootProfile({}); await mocks.profilesManagerMock.resolveDataSourceProfile({}); - mocks.profilesManagerMock.resolveDocumentProfile({ record: mocks.contextRecordMock }); - const profiles = mocks.profilesManagerMock.getProfiles({ record: mocks.contextRecordMock }); + const record = mocks.profilesManagerMock.resolveDocumentProfile({ + record: mocks.contextRecordMock, + }); + const profiles = mocks.profilesManagerMock.getProfiles({ record }); expect(profiles).toEqual([ mocks.rootProfileProviderMock.profile, mocks.dataSourceProfileProviderMock.profile, @@ -58,23 +62,23 @@ describe('ProfilesManager', () => { it('should expose profiles as an observable', async () => { const getProfilesSpy = jest.spyOn(mocks.profilesManagerMock, 'getProfiles'); - const profiles$ = mocks.profilesManagerMock.getProfiles$({ record: mocks.contextRecordMock }); + const record = mocks.profilesManagerMock.resolveDocumentProfile({ + record: mocks.contextRecordMock, + }); + const profiles$ = mocks.profilesManagerMock.getProfiles$({ record }); const next = jest.fn(); profiles$.subscribe(next); expect(getProfilesSpy).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith([{}, {}, {}]); + expect(next).toHaveBeenCalledWith([{}, {}, mocks.documentProfileProviderMock.profile]); await mocks.profilesManagerMock.resolveRootProfile({}); expect(getProfilesSpy).toHaveBeenCalledTimes(2); - expect(next).toHaveBeenCalledWith([mocks.rootProfileProviderMock.profile, {}, {}]); - await mocks.profilesManagerMock.resolveDataSourceProfile({}); - expect(getProfilesSpy).toHaveBeenCalledTimes(3); expect(next).toHaveBeenCalledWith([ mocks.rootProfileProviderMock.profile, - mocks.dataSourceProfileProviderMock.profile, {}, + mocks.documentProfileProviderMock.profile, ]); - mocks.profilesManagerMock.resolveDocumentProfile({ record: mocks.contextRecordMock }); - expect(getProfilesSpy).toHaveBeenCalledTimes(4); + await mocks.profilesManagerMock.resolveDataSourceProfile({}); + expect(getProfilesSpy).toHaveBeenCalledTimes(3); expect(next).toHaveBeenCalledWith([ mocks.rootProfileProviderMock.profile, mocks.dataSourceProfileProviderMock.profile, @@ -157,15 +161,19 @@ describe('ProfilesManager', () => { }); it('should log an error and fall back to the default profile if document profile resolution fails', () => { - mocks.profilesManagerMock.resolveDocumentProfile({ record: mocks.contextRecordMock }); - let profiles = mocks.profilesManagerMock.getProfiles({ record: mocks.contextRecordMock }); + const record = mocks.profilesManagerMock.resolveDocumentProfile({ + record: mocks.contextRecordMock, + }); + let profiles = mocks.profilesManagerMock.getProfiles({ record }); expect(profiles).toEqual([{}, {}, mocks.documentProfileProviderMock.profile]); const resolveSpy = jest.spyOn(mocks.documentProfileProviderMock, 'resolve'); resolveSpy.mockImplementation(() => { throw new Error('Failed to resolve'); }); - mocks.profilesManagerMock.resolveDocumentProfile({ record: mocks.contextRecordMock2 }); - profiles = mocks.profilesManagerMock.getProfiles({ record: mocks.contextRecordMock2 }); + const record2 = mocks.profilesManagerMock.resolveDocumentProfile({ + record: mocks.contextRecordMock2, + }); + profiles = mocks.profilesManagerMock.getProfiles({ record: record2 }); expect(addLog).toHaveBeenCalledWith( '[ProfilesManager] document context resolution failed with params: {\n "recordId": "logstash-2014.09.09::388::"\n}', new Error('Failed to resolve') diff --git a/src/plugins/discover/public/context_awareness/profiles_manager.ts b/src/plugins/discover/public/context_awareness/profiles_manager.ts index c461a18e9b952..316419d2a7d3f 100644 --- a/src/plugins/discover/public/context_awareness/profiles_manager.ts +++ b/src/plugins/discover/public/context_awareness/profiles_manager.ts @@ -8,16 +8,8 @@ import type { DataTableRecord } from '@kbn/discover-utils'; import { isOfAggregateQueryType } from '@kbn/es-query'; -import { isEqual, memoize } from 'lodash'; -import { - BehaviorSubject, - combineLatest, - distinctUntilChanged, - filter, - map, - Observable, - startWith, -} from 'rxjs'; +import { isEqual } from 'lodash'; +import { BehaviorSubject, combineLatest, map } from 'rxjs'; import { DataSourceType, isDataSourceType } from '../../common/data_sources'; import { addLog } from '../utils/add_log'; import type { @@ -53,7 +45,6 @@ export interface GetProfilesOptions { export class ProfilesManager { private readonly rootContext$: BehaviorSubject>; private readonly dataSourceContext$: BehaviorSubject>; - private readonly recordId$ = new BehaviorSubject(undefined); private prevRootProfileParams?: SerializedRootProfileParams; private prevDataSourceProfileParams?: SerializedDataSourceProfileParams; @@ -124,21 +115,27 @@ export class ProfilesManager { } public resolveDocumentProfile(params: DocumentProfileProviderParams) { - Object.defineProperty(params.record, 'context', { - get: memoize(() => { - let context = this.documentProfileService.defaultContext; - - try { - context = this.documentProfileService.resolve(params); - } catch (e) { - logResolutionError(ContextType.Document, { recordId: params.record.id }, e); + let context: ContextWithProfileId | undefined; + + return new Proxy(params.record, { + has: (target, prop) => prop === 'context' || Reflect.has(target, prop), + get: (target, prop, receiver) => { + if (prop !== 'context') { + return Reflect.get(target, prop, receiver); + } + + if (!context) { + try { + context = this.documentProfileService.resolve(params); + } catch (e) { + logResolutionError(ContextType.Document, { recordId: params.record.id }, e); + context = this.documentProfileService.defaultContext; + } } return context; - }), + }, }); - - this.recordId$.next(params.record.id); } public getProfiles({ record }: GetProfilesOptions = {}) { @@ -152,20 +149,9 @@ export class ProfilesManager { } public getProfiles$(options: GetProfilesOptions = {}) { - const observables: Array> = [this.rootContext$, this.dataSourceContext$]; - const record = options.record; - - if (record) { - observables.push( - this.recordId$.pipe( - startWith(record.id), - distinctUntilChanged(), - filter((recordId) => recordId === record.id) - ) - ); - } - - return combineLatest(observables).pipe(map(() => this.getProfiles(options))); + return combineLatest([this.rootContext$, this.dataSourceContext$]).pipe( + map(() => this.getProfiles(options)) + ); } } From 66075243a52d778bcfbf575a88231f4a3acef0e9 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 5 Jun 2024 21:06:20 -0300 Subject: [PATCH 44/52] Ensure _id and _index are defined in EsHitRecord --- .../public/application/main/data_fetching/fetch_esql.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts index 0e532e600e5a4..ba5b34bcc4327 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts @@ -72,6 +72,9 @@ export function fetchEsql( esqlQueryColumns = table?.columns ?? undefined; esqlHeaderWarning = table.warning ?? undefined; finalData = rows.map((row, idx) => { + row._id = row._id ?? ''; + row._index = row._index ?? ''; + const record: DataTableRecord = { id: String(idx), raw: row as EsHitRecord, From 0bfbe8a928022a91c05094a4f0a0249463d641e9 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 5 Jun 2024 21:16:46 -0300 Subject: [PATCH 45/52] Fix mappedDoc in createLogsAIAssistantRenderer and revert change to LogAIAssistant --- .../public/components/log_ai_assistant/index.tsx | 6 +++--- .../public/components/log_ai_assistant/log_ai_assistant.tsx | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/log_ai_assistant/index.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/log_ai_assistant/index.tsx index 465f1f3f66eff..484af6e4a0809 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/components/log_ai_assistant/index.tsx +++ b/x-pack/plugins/observability_solution/logs_shared/public/components/log_ai_assistant/index.tsx @@ -22,12 +22,12 @@ export function createLogAIAssistant({ export const createLogsAIAssistantRenderer = (LogAIAssistantRender: ReturnType) => ({ doc }: ObservabilityLogsAIAssistantFeatureRenderDeps) => { - const mappedDoc = useMemo( + const mappedDoc = useMemo( () => ({ fields: Object.entries(doc.flattened).map(([field, value]) => ({ field, - value, - })) as LogAIAssistantDocument['fields'], + value: Array.isArray(value) ? value : [value], + })), }), [doc] ); diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/log_ai_assistant/log_ai_assistant.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/log_ai_assistant/log_ai_assistant.tsx index 49d4fbc01c2e4..3e1b6fced3337 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/components/log_ai_assistant/log_ai_assistant.tsx +++ b/x-pack/plugins/observability_solution/logs_shared/public/components/log_ai_assistant/log_ai_assistant.tsx @@ -51,8 +51,7 @@ export const LogAIAssistant = ({ return undefined; } - const messageValue = doc.fields.find((field) => field.field === 'message')?.value; - const message = Array.isArray(messageValue) ? messageValue[0] : messageValue; + const message = doc.fields.find((field) => field.field === 'message')?.value[0]; return getContextualInsightMessages({ message: `I'm looking at a log entry. Can you construct a Kibana KQL query that I can enter in the search bar that gives me similar log entries, based on the message field?`, From f5fd8429230ff03a347f48ff5396644eb28e8d69 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 5 Jun 2024 21:31:04 -0300 Subject: [PATCH 46/52] Use createContextAwarenessMocks in mock Discover services --- src/plugins/discover/public/__mocks__/services.ts | 14 +++----------- .../components/layout/discover_documents.test.tsx | 5 +---- .../embeddable/saved_search_embeddable.test.ts | 4 ---- 3 files changed, 4 insertions(+), 19 deletions(-) diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts index f59cb2392f3f8..f75755319a112 100644 --- a/src/plugins/discover/public/__mocks__/services.ts +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -41,12 +41,7 @@ import { SearchSourceDependencies } from '@kbn/data-plugin/common'; import { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { urlTrackerMock } from './url_tracker.mock'; import { createElement } from 'react'; -import { - DataSourceProfileService, - DocumentProfileService, - ProfilesManager, - RootProfileService, -} from '../context_awareness'; +import { createContextAwarenessMocks } from '../context_awareness/__mocks__'; export function createDiscoverServicesMock(): DiscoverServices { const dataPlugin = dataPluginMock.createStartContract(); @@ -143,6 +138,7 @@ export function createDiscoverServicesMock(): DiscoverServices { ...uiSettingsMock, }; + const { profilesManagerMock } = createContextAwarenessMocks(); const theme = themeServiceMock.createSetupContract({ darkMode: false }); corePluginMock.theme = theme; @@ -242,11 +238,7 @@ export function createDiscoverServicesMock(): DiscoverServices { contextLocator: { getRedirectUrl: jest.fn(() => '') }, singleDocLocator: { getRedirectUrl: jest.fn(() => '') }, urlTracker: urlTrackerMock, - profilesManager: new ProfilesManager( - new RootProfileService(), - new DataSourceProfileService(), - new DocumentProfileService() - ), + profilesManager: profilesManagerMock, setHeaderActionMenu: jest.fn(), } as unknown as DiscoverServices; } diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx index 16e752234d6b9..85c2dd581eecb 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx @@ -26,15 +26,12 @@ import { DiscoverCustomization, DiscoverCustomizationProvider } from '../../../. import { createCustomizationService } from '../../../../customizations/customization_service'; import { DiscoverGrid } from '../../../../components/discover_grid'; import { createDataViewDataSource } from '../../../../../common/data_sources'; -import { createContextAwarenessMocks } from '../../../../context_awareness/__mocks__'; const customisationService = createCustomizationService(); -const { profilesManagerMock } = createContextAwarenessMocks(); async function mountComponent(fetchStatus: FetchStatus, hits: EsHitRecord[]) { const services = discoverServiceMock; - services.profilesManager = profilesManagerMock; services.data.query.timefilter.timefilter.getTime = () => { return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; }; @@ -146,7 +143,7 @@ describe('Discover documents layout', () => { id: 'data_table', logsEnabled: true, }); - await profilesManagerMock.resolveRootProfile({ solutionNavId: 'test' }); + await discoverServiceMock.profilesManager.resolveRootProfile({ solutionNavId: 'test' }); const component = await mountComponent(FetchStatus.COMPLETE, esHitsMock); const discoverGridComponent = component.find(DiscoverGrid); expect(discoverGridComponent.exists()).toBeTruthy(); diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts b/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts index 0f9d631ca5609..2fc968a37d2ff 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts @@ -28,7 +28,6 @@ import { getDiscoverLocatorParams } from './get_discover_locator_params'; import { SavedSearchEmbeddable, SearchEmbeddableConfig } from './saved_search_embeddable'; import { SavedSearchEmbeddableComponent } from './saved_search_embeddable_component'; import { DiscoverGrid } from '../components/discover_grid'; -import { createContextAwarenessMocks } from '../context_awareness/__mocks__'; jest.mock('./get_discover_locator_params', () => { const actual = jest.requireActual('./get_discover_locator_params'); @@ -157,9 +156,6 @@ describe('saved search embeddable', () => { } ); - const { profilesManagerMock } = createContextAwarenessMocks(); - - servicesMock.profilesManager = profilesManagerMock; servicesMock.core.chrome.getActiveSolutionNavId$ = () => new BehaviorSubject('test'); }); From 3f1ba48af0d970ebf5063c5441382abe7bf60405 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 5 Jun 2024 21:46:18 -0300 Subject: [PATCH 47/52] Updated fetchEsql params --- .../main/data_fetching/fetch_all.ts | 14 ++++---- .../main/data_fetching/fetch_esql.test.ts | 16 +++++----- .../main/data_fetching/fetch_esql.ts | 32 ++++++++++++------- .../embeddable/saved_search_embeddable.tsx | 20 ++++++------ 4 files changed, 46 insertions(+), 36 deletions(-) diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts index 268adb87afc42..aed3e6f9a0222 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts @@ -64,7 +64,7 @@ export function fetchAll( savedSearch, abortController, } = fetchDeps; - const { data } = services; + const { data, expressions, profilesManager } = services; const searchSource = savedSearch.searchSource.createChild(); try { @@ -100,15 +100,15 @@ export function fetchAll( // Start fetching all required requests const response = isEsqlQuery - ? fetchEsql( + ? fetchEsql({ query, dataView, - data, - services.expressions, + abortSignal: abortController.signal, inspectorAdapters, - services.profilesManager, - abortController.signal - ) + data, + expressions, + profilesManager, + }) : fetchDocuments(searchSource, fetchDeps); const fetchType = isEsqlQuery ? 'fetchTextBased' : 'fetchDocuments'; const startTime = window.performance.now(); diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.test.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.test.ts index 5b35cf16e27c7..6546ae8ffaf2d 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.test.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.test.ts @@ -46,14 +46,14 @@ describe('fetchEsql', () => { 'resolveDocumentProfile' ); expect( - await fetchEsql( - { esql: 'from *' }, - dataViewWithTimefieldMock, - discoverServiceMock.data, - discoverServiceMock.expressions, - { requests: new RequestAdapter() }, - discoverServiceMock.profilesManager - ) + await fetchEsql({ + query: { esql: 'from *' }, + dataView: dataViewWithTimefieldMock, + inspectorAdapters: { requests: new RequestAdapter() }, + data: discoverServiceMock.data, + expressions: discoverServiceMock.expressions, + profilesManager: discoverServiceMock.profilesManager, + }) ).toEqual({ records, esqlQueryColumns: ['_id', 'foo'], diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts index ba5b34bcc4327..e7f31f538cf13 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts @@ -26,17 +26,27 @@ interface EsqlErrorResponse { type: 'error'; } -export function fetchEsql( - query: Query | AggregateQuery, - dataView: DataView, - data: DataPublicPluginStart, - expressions: ExpressionsStart, - inspectorAdapters: Adapters, - profilesManager: ProfilesManager, - abortSignal?: AbortSignal, - filters?: Filter[], - inputQuery?: Query -): Promise { +export function fetchEsql({ + query, + inputQuery, + filters, + dataView, + abortSignal, + inspectorAdapters, + data, + expressions, + profilesManager, +}: { + query: Query | AggregateQuery; + inputQuery?: Query; + filters?: Filter[]; + dataView: DataView; + abortSignal?: AbortSignal; + inspectorAdapters: Adapters; + data: DataPublicPluginStart; + expressions: ExpressionsStart; + profilesManager: ProfilesManager; +}): Promise { const timeRange = data.query.timefilter.timefilter.getTime(); return textBasedQueryStateToAstWithValidation({ filters, diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx index 0985720cb1f91..f1d1c512b933b 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx @@ -324,17 +324,17 @@ export class SavedSearchEmbeddable // Request ES|QL data if (isEsqlMode && query) { - const result = await fetchEsql( - savedSearch.searchSource.getField('query')!, + const result = await fetchEsql({ + query: savedSearch.searchSource.getField('query')!, + inputQuery: this.input.query, + filters: this.input.filters, dataView, - this.services.data, - this.services.expressions, - this.services.inspector, - this.services.profilesManager, - this.abortController.signal, - this.input.filters, - this.input.query - ); + abortSignal: this.abortController.signal, + inspectorAdapters: this.services.inspector, + data: this.services.data, + expressions: this.services.expressions, + profilesManager: this.services.profilesManager, + }); this.updateOutput({ ...this.getOutput(), From 2b8f6bf123a4ba3b22a3967c0c03175bd123e498 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 5 Jun 2024 22:29:54 -0300 Subject: [PATCH 48/52] Use mockReturnValue instead of overwriting getActiveSolutionNavId$ --- .../public/embeddable/saved_search_embeddable.test.ts | 4 +++- .../public/embeddable/view_saved_search_action.test.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts b/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts index 2fc968a37d2ff..53ce8c798f251 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts @@ -156,7 +156,9 @@ describe('saved search embeddable', () => { } ); - servicesMock.core.chrome.getActiveSolutionNavId$ = () => new BehaviorSubject('test'); + jest + .spyOn(servicesMock.core.chrome, 'getActiveSolutionNavId$') + .mockReturnValue(new BehaviorSubject('test')); }); afterEach(() => { diff --git a/src/plugins/discover/public/embeddable/view_saved_search_action.test.ts b/src/plugins/discover/public/embeddable/view_saved_search_action.test.ts index 3a1106c7c7c87..6063e0903ffe5 100644 --- a/src/plugins/discover/public/embeddable/view_saved_search_action.test.ts +++ b/src/plugins/discover/public/embeddable/view_saved_search_action.test.ts @@ -35,7 +35,9 @@ const embeddableConfig = { executeTriggerActions, }; -services.core.chrome.getActiveSolutionNavId$ = () => new BehaviorSubject('test'); +jest + .spyOn(services.core.chrome, 'getActiveSolutionNavId$') + .mockReturnValue(new BehaviorSubject('test')); describe('view saved search action', () => { it('is compatible when embeddable is of type saved search, in view mode && appropriate permissions are set', async () => { From 0c1d0232d3920cdc0c131e82ad3064ed943c4f22 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Thu, 6 Jun 2024 16:40:18 -0300 Subject: [PATCH 49/52] Revert "Ensure _id and _index are defined in EsHitRecord" This reverts commit 66075243a52d778bcfbf575a88231f4a3acef0e9. --- .../public/application/main/data_fetching/fetch_esql.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts index e7f31f538cf13..3f54984ae3d3f 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts @@ -82,9 +82,6 @@ export function fetchEsql({ esqlQueryColumns = table?.columns ?? undefined; esqlHeaderWarning = table.warning ?? undefined; finalData = rows.map((row, idx) => { - row._id = row._id ?? ''; - row._index = row._index ?? ''; - const record: DataTableRecord = { id: String(idx), raw: row as EsHitRecord, From 8bab5825002b79475178e64839410d7b53436099 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Thu, 6 Jun 2024 17:36:36 -0300 Subject: [PATCH 50/52] Omit getCellRenderers in DocumentProfile --- .../public/context_awareness/profiles/document_profile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d39cdc2af6a5a..70b134da452e4 100644 --- a/src/plugins/discover/public/context_awareness/profiles/document_profile.ts +++ b/src/plugins/discover/public/context_awareness/profiles/document_profile.ts @@ -23,7 +23,7 @@ export interface DocumentContext { type: DocumentType; } -export type DocumentProfile = Profile; +export type DocumentProfile = Omit; export class DocumentProfileService extends ProfileService< DocumentProfile, From f2e678802fc5001c87113ef2bb68ebec1a8a47a5 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Thu, 6 Jun 2024 21:23:46 -0300 Subject: [PATCH 51/52] export hooks from index.ts --- .../discover/public/context_awareness/hooks/index.ts | 10 ++++++++++ src/plugins/discover/public/context_awareness/index.ts | 3 +-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 src/plugins/discover/public/context_awareness/hooks/index.ts diff --git a/src/plugins/discover/public/context_awareness/hooks/index.ts b/src/plugins/discover/public/context_awareness/hooks/index.ts new file mode 100644 index 0000000000000..3235844de4fc5 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/hooks/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { useProfileAccessor } from './use_profile_accessor'; +export { useRootProfile } from './use_root_profile'; diff --git a/src/plugins/discover/public/context_awareness/index.ts b/src/plugins/discover/public/context_awareness/index.ts index 27245ccbdf123..6106d9d154e49 100644 --- a/src/plugins/discover/public/context_awareness/index.ts +++ b/src/plugins/discover/public/context_awareness/index.ts @@ -10,5 +10,4 @@ export * from './types'; export * from './profiles'; export { getMergedAccessor } from './composable_profile'; export { ProfilesManager } from './profiles_manager'; -export { useProfileAccessor } from './hooks/use_profile_accessor'; -export { useRootProfile } from './hooks/use_root_profile'; +export { useProfileAccessor, useRootProfile } from './hooks'; From 643e98d96bcdd6f5f6dca97646ea10dbbf2569fa Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Thu, 6 Jun 2024 21:33:13 -0300 Subject: [PATCH 52/52] Pass empty ProfilesManager to DiscoverContainer --- src/plugins/discover/public/plugin.tsx | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 62fa1553a1a33..7228070fe2d2c 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -429,7 +429,6 @@ export class DiscoverPlugin plugins.uiActions.registerTrigger(SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER); injectTruncateStyles(core.uiSettings.get(TRUNCATE_MAX_HEIGHT)); - const getDiscoverServicesInternal = () => this.getDiscoverServices(core, plugins); const isEsqlEnabled = core.uiSettings.get(ENABLE_ESQL); if (plugins.share && this.locator && isEsqlEnabled) { @@ -441,6 +440,10 @@ export class DiscoverPlugin ); } + const getDiscoverServicesInternal = () => { + return this.getDiscoverServices(core, plugins, this.createEmptyProfilesManager()); + }; + return { locator: this.locator, DiscoverContainer: (props: DiscoverContainerProps) => ( @@ -470,7 +473,19 @@ export class DiscoverPlugin ); } - private getDiscoverServices = (core: CoreStart, plugins: DiscoverStartPlugins) => { + private createEmptyProfilesManager() { + return new ProfilesManager( + new RootProfileService(), + new DataSourceProfileService(), + new DocumentProfileService() + ); + } + + private getDiscoverServices = ( + core: CoreStart, + plugins: DiscoverStartPlugins, + profilesManager = this.createProfilesManager() + ) => { return buildServices({ core, plugins, @@ -480,7 +495,7 @@ export class DiscoverPlugin singleDocLocator: this.singleDocLocator!, history: this.historyService.getHistory(), urlTracker: this.urlTracker!, - profilesManager: this.createProfilesManager(), + profilesManager, }); };