From f544816d6df962491fe401b1237077e50ef197db Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 23 Oct 2024 21:06:23 -0300 Subject: [PATCH 01/11] Add getRenderAppWrapper extension --- .../application/context/context_app_route.tsx | 16 +- .../application/context/services/anchor.ts | 10 +- .../public/application/doc/components/doc.tsx | 182 ++++++++++-------- .../application/main/discover_main_route.tsx | 14 +- .../discover_container/discover_container.tsx | 1 - .../hooks/use_root_profile.test.tsx | 17 +- .../hooks/use_root_profile.ts | 6 +- .../public/context_awareness/types.ts | 9 + .../public/customizations/defaults.ts | 1 - .../discover/public/customizations/types.ts | 4 - src/plugins/discover/public/plugin.tsx | 1 - 11 files changed, 150 insertions(+), 111 deletions(-) diff --git a/src/plugins/discover/public/application/context/context_app_route.tsx b/src/plugins/discover/public/application/context/context_app_route.tsx index a272a032bbe35..7f056f85fe8e9 100644 --- a/src/plugins/discover/public/application/context/context_app_route.tsx +++ b/src/plugins/discover/public/application/context/context_app_route.tsx @@ -16,6 +16,7 @@ import { LoadingIndicator } from '../../components/common/loading_indicator'; import { useDataView } from '../../hooks/use_data_view'; import type { ContextHistoryLocationState } from './services/locator'; import { useDiscoverServices } from '../../hooks/use_discover_services'; +import { useProfileAccessor, useRootProfile } from '../../context_awareness'; export interface ContextUrlParams { dataViewId: string; @@ -47,8 +48,13 @@ export function ContextAppRoute() { const { dataViewId: encodedDataViewId, id } = useParams(); const dataViewId = decodeURIComponent(encodedDataViewId); const anchorId = decodeURIComponent(id); - const { dataView, error } = useDataView({ index: locationState?.dataViewSpec || dataViewId }); + const { rootProfileLoading } = useRootProfile(); + const getRenderAppWrapperAccessor = useProfileAccessor('getRenderAppWrapper'); + const AppWrapper = useMemo( + () => getRenderAppWrapperAccessor(({ children }) => <>{children}), + [getRenderAppWrapperAccessor] + ); if (error) { return ( @@ -72,9 +78,13 @@ export function ContextAppRoute() { ); } - if (!dataView) { + if (!dataView || rootProfileLoading) { return ; } - return ; + return ( + + + + ); } diff --git a/src/plugins/discover/public/application/context/services/anchor.ts b/src/plugins/discover/public/application/context/services/anchor.ts index 350c292772d87..ee5198a8b4100 100644 --- a/src/plugins/discover/public/application/context/services/anchor.ts +++ b/src/plugins/discover/public/application/context/services/anchor.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { firstValueFrom, lastValueFrom } from 'rxjs'; +import { lastValueFrom } from 'rxjs'; import { i18n } from '@kbn/i18n'; import { ISearchSource, EsQuerySortValue } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; @@ -29,11 +29,7 @@ export async function fetchAnchor( anchorRow: DataTableRecord; interceptedWarnings: SearchResponseWarning[]; }> { - const { core, profilesManager } = services; - - const solutionNavId = await firstValueFrom(core.chrome.getActiveSolutionNavId$()); - await profilesManager.resolveRootProfile({ solutionNavId }); - await profilesManager.resolveDataSourceProfile({ + await services.profilesManager.resolveDataSourceProfile({ dataSource: createDataSource({ dataView, query: undefined }), dataView, query: { query: '', language: 'kuery' }, @@ -68,7 +64,7 @@ export async function fetchAnchor( }); return { - anchorRow: profilesManager.resolveDocumentProfile({ + anchorRow: services.profilesManager.resolveDocumentProfile({ record: buildDataTableRecord(doc, dataView, true), }), interceptedWarnings, diff --git a/src/plugins/discover/public/application/doc/components/doc.tsx b/src/plugins/discover/public/application/doc/components/doc.tsx index 432687fdca5e6..16d27857b7ff2 100644 --- a/src/plugins/discover/public/application/doc/components/doc.tsx +++ b/src/plugins/discover/public/application/doc/components/doc.tsx @@ -7,9 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { firstValueFrom } from 'rxjs'; import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPage, EuiPageBody } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ElasticRequestState } from '@kbn/unified-doc-viewer'; @@ -20,6 +19,8 @@ import { setBreadcrumbs } from '../../../utils/breadcrumbs'; import { useDiscoverServices } from '../../../hooks/use_discover_services'; import { SingleDocViewer } from './single_doc_viewer'; import { createDataViewDataSource } from '../../../../common/data_sources'; +import { useProfileAccessor, useRootProfile } from '../../../context_awareness'; +import { LoadingIndicator } from '../../../components/common/loading_indicator'; export interface DocProps extends EsDocSearchProps { /** @@ -31,18 +32,16 @@ export interface DocProps extends EsDocSearchProps { export function Doc(props: DocProps) { const { dataView } = props; const services = useDiscoverServices(); - const { locator, chrome, docLinks, core, profilesManager } = services; + const { locator, chrome, docLinks, profilesManager } = services; const indexExistsLink = docLinks.links.apis.indexExists; const onBeforeFetch = useCallback(async () => { - const solutionNavId = await firstValueFrom(core.chrome.getActiveSolutionNavId$()); - await profilesManager.resolveRootProfile({ solutionNavId }); await profilesManager.resolveDataSourceProfile({ dataSource: dataView?.id ? createDataViewDataSource({ dataViewId: dataView.id }) : undefined, dataView, query: { query: '', language: 'kuery' }, }); - }, [profilesManager, core, dataView]); + }, [profilesManager, dataView]); const onProcessRecord = useCallback( (record: DataTableRecord) => { @@ -65,91 +64,104 @@ export function Doc(props: DocProps) { }); }, [chrome, props.referrer, props.index, props.id, dataView, locator, services]); + const { rootProfileLoading } = useRootProfile(); + const getRenderAppWrapperAccessor = useProfileAccessor('getRenderAppWrapper'); + const AppWrapper = useMemo( + () => getRenderAppWrapperAccessor(({ children }) => <>{children}), + [getRenderAppWrapperAccessor] + ); + + if (rootProfileLoading) { + ; + } + return ( - -

- {i18n.translate('discover.doc.pageTitle', { - defaultMessage: 'Single document - #{id}', - values: { id: props.id }, - })} -

- - {reqState === ElasticRequestState.NotFoundDataView && ( - - } - /> - )} - {reqState === ElasticRequestState.NotFound && ( - - } - > - + +

+ {i18n.translate('discover.doc.pageTitle', { + defaultMessage: 'Single document - #{id}', + values: { id: props.id }, + })} +

+ + {reqState === ElasticRequestState.NotFoundDataView && ( + + } /> - - )} - - {reqState === ElasticRequestState.Error && ( - + } + > - } - > - {' '} - + + )} + + {reqState === ElasticRequestState.Error && ( + + } + > - - - )} + id="discover.doc.somethingWentWrongDescription" + defaultMessage="{indexName} is missing." + values={{ indexName: props.index }} + />{' '} + + + +
+ )} - {reqState === ElasticRequestState.Loading && ( - - {' '} - - - )} + {reqState === ElasticRequestState.Loading && ( + + {' '} + + + )} - {reqState === ElasticRequestState.Found && record !== null && dataView && ( -
- -
- )} -
-
+ {reqState === ElasticRequestState.Found && record !== null && dataView && ( +
+ +
+ )} + + + ); } 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 6991b1c30d9b2..0cc3a6a171f20 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.tsx @@ -41,7 +41,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 { useRootProfile } from '../../context_awareness'; +import { useProfileAccessor, useRootProfile } from '../../context_awareness'; const DiscoverMainAppMemoized = memo(DiscoverMainApp); @@ -345,8 +345,12 @@ export function DiscoverMainRoute({ stateContainer, ]); - const { solutionNavId } = customizationContext; - const { rootProfileLoading } = useRootProfile({ solutionNavId }); + const { rootProfileLoading } = useRootProfile(); + const getRenderAppWrapperAccessor = useProfileAccessor('getRenderAppWrapper'); + const AppWrapper = useMemo( + () => getRenderAppWrapperAccessor(({ children }) => <>{children}), + [getRenderAppWrapperAccessor] + ); if (error) { return ; @@ -359,13 +363,13 @@ export function DiscoverMainRoute({ return ( - <> + {mainContent} - + ); 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 4210fd86144b0..92ae1b7c0f4cb 100644 --- a/src/plugins/discover/public/components/discover_container/discover_container.tsx +++ b/src/plugins/discover/public/components/discover_container/discover_container.tsx @@ -45,7 +45,6 @@ const discoverContainerWrapperCss = css` `; const customizationContext: DiscoverCustomizationContext = { - solutionNavId: null, displayMode: 'embedded', inlineTopNav: { enabled: false, 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 index 8edbc35ab11a1..8ce4a937bfae7 100644 --- 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 @@ -12,9 +12,17 @@ import { renderHook } from '@testing-library/react-hooks'; import React from 'react'; import { discoverServiceMock } from '../../__mocks__/services'; import { useRootProfile } from './use_root_profile'; +import { BehaviorSubject } from 'rxjs'; +import { act } from '@testing-library/react'; + +const mockSolutionNavId$ = new BehaviorSubject('solutionNavId'); + +jest + .spyOn(discoverServiceMock.core.chrome, 'getActiveSolutionNavId$') + .mockReturnValue(mockSolutionNavId$); const render = () => { - return renderHook((props) => useRootProfile(props), { + return renderHook(() => useRootProfile(), { initialProps: { solutionNavId: 'solutionNavId' } as React.PropsWithChildren<{ solutionNavId: string; }>, @@ -25,6 +33,10 @@ const render = () => { }; describe('useRootProfile', () => { + beforeEach(() => { + mockSolutionNavId$.next('solutionNavId'); + }); + it('should return rootProfileLoading as true', () => { const { result } = render(); expect(result.current.rootProfileLoading).toBe(true); @@ -40,7 +52,8 @@ describe('useRootProfile', () => { const { result, rerender, waitForNextUpdate } = render(); await waitForNextUpdate(); expect(result.current.rootProfileLoading).toBe(false); - rerender({ solutionNavId: 'newSolutionNavId' }); + act(() => mockSolutionNavId$.next('newSolutionNavId')); + rerender(); expect(result.current.rootProfileLoading).toBe(true); await waitForNextUpdate(); expect(result.current.rootProfileLoading).toBe(false); diff --git a/src/plugins/discover/public/context_awareness/hooks/use_root_profile.ts b/src/plugins/discover/public/context_awareness/hooks/use_root_profile.ts index 2ffccc6d786b2..4aa099fa0c2dd 100644 --- a/src/plugins/discover/public/context_awareness/hooks/use_root_profile.ts +++ b/src/plugins/discover/public/context_awareness/hooks/use_root_profile.ts @@ -8,6 +8,7 @@ */ import { useEffect, useState } from 'react'; +import useObservable from 'react-use/lib/useObservable'; import { useDiscoverServices } from '../../hooks/use_discover_services'; /** @@ -15,8 +16,9 @@ import { useDiscoverServices } from '../../hooks/use_discover_services'; * @param options Options object * @returns If the root profile is loading */ -export const useRootProfile = ({ solutionNavId }: { solutionNavId: string | null }) => { - const { profilesManager } = useDiscoverServices(); +export const useRootProfile = () => { + const { profilesManager, core } = useDiscoverServices(); + const solutionNavId = useObservable(core.chrome.getActiveSolutionNavId$()); const [rootProfileLoading, setRootProfileLoading] = useState(true); useEffect(() => { diff --git a/src/plugins/discover/public/context_awareness/types.ts b/src/plugins/discover/public/context_awareness/types.ts index 5797a9023f93e..f03e3773c7d87 100644 --- a/src/plugins/discover/public/context_awareness/types.ts +++ b/src/plugins/discover/public/context_awareness/types.ts @@ -20,6 +20,7 @@ import type { EuiIconType } from '@elastic/eui/src/components/icon/icon'; import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; import type { OmitIndexSignature } from 'type-fest'; import type { Trigger } from '@kbn/ui-actions-plugin/public'; +import type { PropsWithChildren, ReactElement } from 'react'; import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; import type { DiscoverDataSource } from '../../common/data_sources'; import type { DiscoverAppState } from '../application/main/state_management/discover_app_state_container'; @@ -235,6 +236,14 @@ export interface Profile { * Lifecycle */ + /** + * Render a custom wrapper component around the Discover application, + * e.g. to allow using profile specific context providers + * @param props The app wrapper props + * @returns The custom app wrapper component + */ + getRenderAppWrapper: (props: PropsWithChildren<{}>) => ReactElement; + /** * Gets default Discover app state that should be used when the profile is resolved * @param params The default app state extension parameters diff --git a/src/plugins/discover/public/customizations/defaults.ts b/src/plugins/discover/public/customizations/defaults.ts index 600f1501a1d41..d44b6527b3909 100644 --- a/src/plugins/discover/public/customizations/defaults.ts +++ b/src/plugins/discover/public/customizations/defaults.ts @@ -10,7 +10,6 @@ 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 e72426b00d8a2..bf71fa80148ec 100644 --- a/src/plugins/discover/public/customizations/types.ts +++ b/src/plugins/discover/public/customizations/types.ts @@ -22,10 +22,6 @@ 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 dbbcc90a7d451..0ee80da03a7d1 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -213,7 +213,6 @@ export class DiscoverPlugin .pipe( map((solutionNavId) => ({ ...defaultCustomizationContext, - solutionNavId, inlineTopNav: this.inlineTopNav.get(solutionNavId) ?? this.inlineTopNav.get(null) ?? From 96279fd1196c9a17f0bff08fde50ba48b7683df5 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Wed, 23 Oct 2024 23:00:39 -0300 Subject: [PATCH 02/11] Add functional tests --- .../public/context_awareness/README.md | 190 +++++++++++++++++- .../example/example_context.ts | 22 ++ .../example_data_source_profile/profile.tsx | 17 +- .../example/example_root_pofile/profile.tsx | 39 ---- .../index.ts | 5 +- .../example/example_root_profile/profile.tsx | 91 +++++++++ .../register_profile_providers.test.ts | 2 +- .../register_profile_providers.ts | 6 +- .../apps/discover/context_awareness/config.ts | 7 +- .../extensions/_get_render_app_wrapper.ts | 97 +++++++++ .../apps/discover/context_awareness/index.ts | 1 + .../extensions/_get_render_app_wrapper.ts | 100 +++++++++ .../discover/context_awareness/index.ts | 1 + .../observability/config.context_awareness.ts | 7 +- .../search/config.context_awareness.ts | 7 +- .../security/config.context_awareness.ts | 7 +- 16 files changed, 551 insertions(+), 48 deletions(-) create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/example/example_context.ts delete mode 100644 src/plugins/discover/public/context_awareness/profile_providers/example/example_root_pofile/profile.tsx rename src/plugins/discover/public/context_awareness/profile_providers/example/{example_root_pofile => example_root_profile}/index.ts (81%) create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/example/example_root_profile/profile.tsx create mode 100644 test/functional/apps/discover/context_awareness/extensions/_get_render_app_wrapper.ts create mode 100644 x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_render_app_wrapper.ts diff --git a/src/plugins/discover/public/context_awareness/README.md b/src/plugins/discover/public/context_awareness/README.md index 3bb70dbb93e73..ab927a4523377 100644 --- a/src/plugins/discover/public/context_awareness/README.md +++ b/src/plugins/discover/public/context_awareness/README.md @@ -102,7 +102,7 @@ Existing providers can be extended using the [`extendProfileProvider`](./profile Example profile provider implementations are located in [`profile_providers/example`](./profile_providers/example). -## Example implementation +### Example implementation ```ts /** @@ -191,3 +191,191 @@ const createDataSourceProfileProviders = (providerServices: ProfileProviderServi * to resolve the profile: `FROM my-example-logs` */ ``` + +## React context and state management + +In the Discover context awareness framework, pieces of Discover’s state are passed down explicitly to extension points as needed. This avoids leaking Discover internals – which may change – to consumer extension point implementations and allows us to be intentional about which pieces of state extension points have access to. This approach generally works well when extension points need access to things like the current ES|QL query or data view, time range, columns, etc. However, this does not provide a solution for consumers to manage custom shared state between their extension point implementations. + +In cases where the state for an extension point implementation is local to that implementation, consumers can simply manage the state within the corresponding profile method or returned React component: + +```tsx +// Extension point implementation definition +const getCellRenderers = (prev) => (params) => { + // Declare shared state within the profile method closure + const blueOrRed$ = new BehaviorSubject<'blue' | 'red'>('blue'); + + return { + ...prev(params), + foo: function FooComponent() { + // It's still in scope and can be easily accessed... + const blueOrRed = useObservable(blueOrRed$, blueOrRed$.getValue()); + + return ( + // ...and modified... + + ); + }, + bar: function BarComponent() { + const blueOrRed = useObservable(blueOrRed$, blueOrRed$.getValue()); + + // ...and we can react to the changes + return Look ma, I'm {blueOrRed}!; + }, + }; +}; +``` + +For more advanced use cases, such as when state needs to be shared across extension point implementations, we provide an extension point called `getRenderAppWrapper`. The app wrapper extension point allows consumers to wrap the Discover root in a custom wrapper component, such as a React context provider. With this approach consumers can handle things like integrating with a state management library, accessing custom services from within their extension point implementations, managing shared components such as flyouts, etc. in a React-friendly way and without needing to work around the context awareness framework: + +```tsx +// The app wrapper extension point supports common patterns like React context +const flyoutContext = createContext({ setFlyoutOpen: (open: boolean) => {} }); + +// App wrapper implementations can exist at the root or data source level -- when implemented at the root, their lifecycle will match the Discover lifecycle +export const createSecurityRootProfileProvider = (): RootProfileProvider => ({ + profileId: 'security-root-profile', + profile: { + // The app wrapper extension point implementation + getRenderAppWrapper: (PrevWrapper) => + function AppWrapper({ children }) { + // Now we can declare state high up in the React tree + const [flyoutOpen, setFlyoutOpen] = useState(false); + + return ( + // Be sure to render previous wrappers as well since other implementations may depend on them + + // This is our wrapper -- it uses React context to give extension point implementations + access to the shared state + + // Make sure to render `children`, which is the Discover app + {children} + // Now extension point implementations can interact with shared state managed higher + up in the tree + {flyoutOpen && ( + setFlyoutOpen(false)}> + Check it out, I'm a flyout! + + )} + + + ); + }, + // Some other extension point implementation that depends on the shared state + getCellRenderers: (prev) => (params) => ({ + ...prev(params), + foo: function FooComponent() { + // Since the app wrapper implementation wrapped Discover with a React context provider, we can now access its values from within our extension point implementations + const { setFlyoutOpen } = useContext(flyoutContext); + + return ; + }, + }), + }, + resolve: (params) => { + if (params.solutionNavId === SolutionType.Security) { + return { + isMatch: true, + context: { solutionType: SolutionType.Security }, + }; + } + + return { isMatch: false }; + }, +}); +``` + +## Overriding defaults + +Discover ships with a set of common contextual profiles, shared across Solutions in Kibana (e.g. the current logs data source profile). The goal of these profiles is to provide Solution agnostic contextual features to help improve the default data exploration experience for various data types. They should be generally useful across user types and not be tailored to specific Solution workflows – for example, viewing logs should be a delightful experience regardless of whether it’s done within the Observability Solution, the Search Solution, or the classic on-prem experience. + +We’re aiming to make these profiles generic enough that they don’t obstruct Solution workflows or create confusion, but there will always be some complexity around juggling the various Discover use cases. For situations where Solution teams are confident some common profile feature will not be helpful to their users or will create confusion, there is an option to override these defaults while keeping the remainder of the functionality for the target profile intact. To do so a Solution team would follow these steps: + +- Create and register a Solution specific root profile provider, e.g. `SecurityRootProfileProvider`. +- Identify the contextual feature you want to override and the common profile provider it belongs to, e.g. the `getDocViewer` implementation in the common `LogsDataSourceProfileProvider`. +- Implement a Solution specific version of the profile provider that extends the common provider as its base (using the `extendProfileProvider` utility), and excludes the extension point implementations you don’t want, e.g. `SecurityLogsDataSourceProfileProvider`. Other than the excluded extension point implementations, the only required change is to update its `resolve` method to first check the `rootContext.solutionType` for the target solution type before executing the base provider’s `resolve` method. This will ensure the override profile only resolves for the specific Solution, and will fall back to the common profile in other Solutions. +- Register the Solution specific version of the profile provider in Discover, ensuring it precedes the common provider in the registration array. The ordering here is important since the Solution specific profile should attempt to resolve first, otherwise the common profile would be resolved instead. + +This is how an example implementation would work in code: + +```tsx +/** + * profile_providers/security/security_root_profile/profile.tsx + */ + +// Create a solution specific root profile provider +export const createSecurityRootProfileProvider = (): RootProfileProvider => ({ + profileId: 'security-root-profile', + profile: {}, + resolve: (params) => { + if (params.solutionNavId === SolutionType.Security) { + return { + isMatch: true, + context: { solutionType: SolutionType.Security }, + }; + } + + return { isMatch: false }; + }, +}); + +/** + * profile_providers/security/security_logs_data_source_profile/profile.tsx + */ + +// Create a solution specific data source profile provider that extends a target base provider +export const createSecurityLogsDataSourceProfileProivder = ( + logsDataSourceProfileProvider: DataSourceProfileProvider +): DataSourceProfileProvider => + // Extend the base profile provider with `extendProfileProvider` + extendProfileProvider(logsDataSourceProfileProvider, { + profileId: 'security-logs-data-source-profile', + profile: { + // Completely remove a specific extension point implementation + getDocViewer: undefined, + // Modify the result of an existing extension point implementation + getCellRenderers: (prev) => (params) => { + // Retrieve and execute the base implementation + const baseImpl = logsDataSourceProfileProvider.profile.getCellRenderers?.(prev); + const baseRenderers = baseImpl?.(params); + + // Return the modified result + return omit(baseRenderers, 'log.level'); + }, + }, + // Customize the `resolve` implementation + resolve: (params) => { + // Only match this profile when in the target solution context + if (params.rootContext.solutionType !== SolutionType.Security) { + return { isMatch: false }; + } + + // Delegate to the base implementation + return logsDataSourceProfileProvider.resolve(params); + }, + }); + +/** + * profile_providers/register_profile_providers.ts + */ + +// Register root profile providers +const createRootProfileProviders = (providerServices: ProfileProviderServices) => [ + // Register the solution specific root profile provider + createSecurityRootProfileProvider(), +]; + +// Register data source profile providers +const createDataSourceProfileProviders = (providerServices: ProfileProviderServices) => { + // Instantiate the data source profile provider base implementation + const logsDataSourceProfileProvider = createLogsDataSourceProfileProvider(providerServices); + + return [ + // Ensure the solution specific override is registered and resolved first + createSecurityLogsDataSourceProfileProivder(logsDataSourceProfileProvider), + // Then register the base implementation + logsDataSourceProfileProvider, + ]; +}; +``` diff --git a/src/plugins/discover/public/context_awareness/profile_providers/example/example_context.ts b/src/plugins/discover/public/context_awareness/profile_providers/example/example_context.ts new file mode 100644 index 0000000000000..e9475d61f1425 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/example/example_context.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { createContext, useContext } from 'react'; + +const exampleContext = createContext<{ + currentMessage: string | undefined; + setCurrentMessage: (message: string | undefined) => void; +}>({ + currentMessage: undefined, + setCurrentMessage: () => {}, +}); + +export const ExampleContextProvider = exampleContext.Provider; + +export const useExampleContext = () => useContext(exampleContext); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/example/example_data_source_profile/profile.tsx b/src/plugins/discover/public/context_awareness/profile_providers/example/example_data_source_profile/profile.tsx index c82cf1a893c8d..980e15ba7c4cb 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/example/example_data_source_profile/profile.tsx +++ b/src/plugins/discover/public/context_awareness/profile_providers/example/example_data_source_profile/profile.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { EuiBadge } from '@elastic/eui'; +import { EuiBadge, EuiLink } from '@elastic/eui'; import { getFieldValue, RowControlColumn } from '@kbn/discover-utils'; import { isOfAggregateQueryType } from '@kbn/es-query'; import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; @@ -16,6 +16,7 @@ import { capitalize } from 'lodash'; import React from 'react'; import { DataSourceType, isDataSourceType } from '../../../../../common/data_sources'; import { DataSourceCategory, DataSourceProfileProvider } from '../../../profiles'; +import { useExampleContext } from '../example_context'; export const createExampleDataSourceProfileProvider = (): DataSourceProfileProvider => ({ profileId: 'example-data-source-profile', @@ -53,6 +54,20 @@ export const createExampleDataSourceProfileProvider = (): DataSourceProfileProvi ); }, + message: function Message(props) { + const { currentMessage, setCurrentMessage } = useExampleContext(); + const message = getFieldValue(props.row, 'message') as string; + + return ( + setCurrentMessage(message)} + css={{ fontWeight: currentMessage === message ? 'bold' : undefined }} + data-test-subj="exampleDataSourceProfileMessage" + > + {message} + + ); + }, }), getDocViewer: (prev) => (params) => { const recordId = params.record.id; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/example/example_root_pofile/profile.tsx b/src/plugins/discover/public/context_awareness/profile_providers/example/example_root_pofile/profile.tsx deleted file mode 100644 index 125cb609fb849..0000000000000 --- a/src/plugins/discover/public/context_awareness/profile_providers/example/example_root_pofile/profile.tsx +++ /dev/null @@ -1,39 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { EuiBadge } from '@elastic/eui'; -import { getFieldValue } from '@kbn/discover-utils'; -import React from 'react'; -import { RootProfileProvider, SolutionType } from '../../../profiles'; - -export const createExampleRootProfileProvider = (): RootProfileProvider => ({ - profileId: 'example-root-profile', - isExperimental: true, - profile: { - getCellRenderers: (prev) => (params) => ({ - ...prev(params), - '@timestamp': (props) => { - const timestamp = getFieldValue(props.row, '@timestamp') as string; - - return ( - - {timestamp} - - ); - }, - }), - }, - resolve: (params) => { - if (params.solutionNavId != null) { - return { isMatch: false }; - } - - return { isMatch: true, context: { solutionType: SolutionType.Default } }; - }, -}); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/example/example_root_pofile/index.ts b/src/plugins/discover/public/context_awareness/profile_providers/example/example_root_profile/index.ts similarity index 81% rename from src/plugins/discover/public/context_awareness/profile_providers/example/example_root_pofile/index.ts rename to src/plugins/discover/public/context_awareness/profile_providers/example/example_root_profile/index.ts index 0c13a49d17d7a..8b7d05d00daae 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/example/example_root_pofile/index.ts +++ b/src/plugins/discover/public/context_awareness/profile_providers/example/example_root_profile/index.ts @@ -7,4 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { createExampleRootProfileProvider } from './profile'; +export { + createExampleRootProfileProvider, + createExampleFallbackRootProfileProvider, +} from './profile'; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/example/example_root_profile/profile.tsx b/src/plugins/discover/public/context_awareness/profile_providers/example/example_root_profile/profile.tsx new file mode 100644 index 0000000000000..0730fe9c4b447 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/example/example_root_profile/profile.tsx @@ -0,0 +1,91 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { + EuiBadge, + EuiCodeBlock, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, +} from '@elastic/eui'; +import { getFieldValue } from '@kbn/discover-utils'; +import React, { useState } from 'react'; +import { RootProfileProvider, SolutionType } from '../../../profiles'; +import { ExampleContextProvider } from '../example_context'; + +export const createExampleRootProfileProvider = (): RootProfileProvider => ({ + profileId: 'example-root-profile', + isExperimental: true, + profile: { + getRenderAppWrapper, + getCellRenderers: (prev) => (params) => ({ + ...prev(params), + '@timestamp': (props) => { + const timestamp = getFieldValue(props.row, '@timestamp') as string; + + return ( + + {timestamp} + + ); + }, + }), + }, + resolve: (params) => { + if (params.solutionNavId != null) { + return { isMatch: false }; + } + + return { isMatch: true, context: { solutionType: SolutionType.Default } }; + }, +}); + +export const createExampleFallbackRootProfileProvider = (): RootProfileProvider => ({ + profileId: 'example-fallback-root-profile', + isExperimental: true, + profile: { getRenderAppWrapper }, + resolve: (params) => ({ + isMatch: true, + context: { solutionType: params.solutionNavId as SolutionType }, + }), +}); + +const getRenderAppWrapper: RootProfileProvider['profile']['getRenderAppWrapper'] = + (PrevWrapper) => + ({ children }) => { + const [currentMessage, setCurrentMessage] = useState(undefined); + + return ( + + + {children} + {currentMessage && ( + setCurrentMessage(undefined)} + data-test-subj="exampleRootProfileFlyout" + > + + +

Inspect message

+
+
+ + + {currentMessage} + + +
+ )} +
+
+ ); + }; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.test.ts b/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.test.ts index 940eb6b67e591..ddff243b5117a 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.test.ts +++ b/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.test.ts @@ -9,7 +9,7 @@ import { createEsqlDataSource } from '../../../common/data_sources'; import { createContextAwarenessMocks } from '../__mocks__'; -import { createExampleRootProfileProvider } from './example/example_root_pofile'; +import { createExampleRootProfileProvider } from './example/example_root_profile'; import { createExampleDataSourceProfileProvider } from './example/example_data_source_profile/profile'; import { createExampleDocumentProfileProvider } from './example/example_document_profile'; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.ts b/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.ts index 58ff63ca35c19..e426672f42d52 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.ts +++ b/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.ts @@ -15,7 +15,10 @@ import type { import type { BaseProfileProvider, BaseProfileService } from '../profile_service'; import { createExampleDataSourceProfileProvider } from './example/example_data_source_profile/profile'; import { createExampleDocumentProfileProvider } from './example/example_document_profile'; -import { createExampleRootProfileProvider } from './example/example_root_pofile'; +import { + createExampleFallbackRootProfileProvider, + createExampleRootProfileProvider, +} from './example/example_root_profile'; import { createLogsDataSourceProfileProviders } from './common/logs_data_source_profile'; import { createLogDocumentProfileProvider } from './common/log_document_profile'; import { createSecurityRootProfileProvider } from './security/security_root_profile'; @@ -117,6 +120,7 @@ export const registerEnabledProfileProviders = < */ const createRootProfileProviders = (providerServices: ProfileProviderServices) => [ createExampleRootProfileProvider(), + createExampleFallbackRootProfileProvider(), createSecurityRootProfileProvider(providerServices), ]; diff --git a/test/functional/apps/discover/context_awareness/config.ts b/test/functional/apps/discover/context_awareness/config.ts index 9261cef450adb..c707060531e7c 100644 --- a/test/functional/apps/discover/context_awareness/config.ts +++ b/test/functional/apps/discover/context_awareness/config.ts @@ -25,7 +25,12 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...baseConfig.kbnTestServer, serverArgs: [ ...baseConfig.kbnTestServer.serverArgs, - '--discover.experimental.enabledProfiles=["example-root-profile","example-data-source-profile","example-document-profile"]', + `--discover.experimental.enabledProfiles=${JSON.stringify([ + 'example-root-profile', + 'example-fallback-root-profile', + 'example-data-source-profile', + 'example-document-profile', + ])}`, `--plugin-path=${path.resolve( __dirname, '../../../../analytics/plugins/analytics_ftr_helpers' diff --git a/test/functional/apps/discover/context_awareness/extensions/_get_render_app_wrapper.ts b/test/functional/apps/discover/context_awareness/extensions/_get_render_app_wrapper.ts new file mode 100644 index 0000000000000..31ccae26a78ab --- /dev/null +++ b/test/functional/apps/discover/context_awareness/extensions/_get_render_app_wrapper.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import kbnRison from '@kbn/rison'; +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const { common, discover, header, unifiedFieldList } = getPageObjects([ + 'common', + 'discover', + 'header', + 'unifiedFieldList', + ]); + const testSubjects = getService('testSubjects'); + const dataViews = getService('dataViews'); + const dataGrid = getService('dataGrid'); + const browser = getService('browser'); + + describe('extension getRenderAppWrapper', () => { + describe('ES|QL mode', () => { + it('should allow clicking message cells to inspect the message', async () => { + const state = kbnRison.encode({ + dataSource: { type: 'esql' }, + query: { esql: 'from my-example-logs | sort @timestamp desc' }, + }); + await common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, + }); + await discover.waitUntilSearchingHasFinished(); + await unifiedFieldList.clickFieldListItemAdd('message'); + let messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 0); + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + let message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is a debug log'); + messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 0); + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is an error log'); + await testSubjects.click('euiFlyoutCloseButton'); + await testSubjects.missingOrFail('exampleRootProfileFlyout'); + }); + }); + + describe('data view mode', () => { + it('should allow clicking message cells to inspect the message', async () => { + await common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); + await dataViews.switchTo('my-example-logs'); + await discover.waitUntilSearchingHasFinished(); + await unifiedFieldList.clickFieldListItemAdd('message'); + let messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 1); + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + let message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is a debug log'); + messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 1); + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is an error log'); + await testSubjects.click('euiFlyoutCloseButton'); + await testSubjects.missingOrFail('exampleRootProfileFlyout'); + + // check Surrounding docs page + await dataGrid.clickRowToggle(); + const [, surroundingActionEl] = await dataGrid.getRowActions(); + await surroundingActionEl.click(); + await header.waitUntilLoadingHasFinished(); + await browser.refresh(); + await header.waitUntilLoadingHasFinished(); + + messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 1); + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is a debug log'); + messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 1); + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is an error log'); + await testSubjects.click('euiFlyoutCloseButton'); + await testSubjects.missingOrFail('exampleRootProfileFlyout'); + }); + }); + }); +} diff --git a/test/functional/apps/discover/context_awareness/index.ts b/test/functional/apps/discover/context_awareness/index.ts index f937f38c741f9..d2e145baf8e6a 100644 --- a/test/functional/apps/discover/context_awareness/index.ts +++ b/test/functional/apps/discover/context_awareness/index.ts @@ -45,5 +45,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid loadTestFile(require.resolve('./extensions/_get_cell_renderers')); loadTestFile(require.resolve('./extensions/_get_default_app_state')); loadTestFile(require.resolve('./extensions/_get_additional_cell_actions')); + loadTestFile(require.resolve('./extensions/_get_render_app_wrapper')); }); } diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_render_app_wrapper.ts b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_render_app_wrapper.ts new file mode 100644 index 0000000000000..8b6e5fe3ad43a --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_render_app_wrapper.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import kbnRison from '@kbn/rison'; +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const { common, discover, header, unifiedFieldList, svlCommonPage } = getPageObjects([ + 'common', + 'discover', + 'header', + 'unifiedFieldList', + 'svlCommonPage', + ]); + const testSubjects = getService('testSubjects'); + const dataViews = getService('dataViews'); + const dataGrid = getService('dataGrid'); + const browser = getService('browser'); + + describe('extension getRenderAppWrapper', () => { + before(async () => { + await svlCommonPage.loginAsAdmin(); + }); + + describe('ES|QL mode', () => { + it('should allow clicking message cells to inspect the message', async () => { + const state = kbnRison.encode({ + dataSource: { type: 'esql' }, + query: { esql: 'from my-example-logs | sort @timestamp desc' }, + }); + await common.navigateToActualUrl('discover', `?_a=${state}`, { + ensureCurrentUrl: false, + }); + await discover.waitUntilSearchingHasFinished(); + await unifiedFieldList.clickFieldListItemAdd('message'); + let messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 0); + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + let message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is a debug log'); + messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 0); + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is an error log'); + await testSubjects.click('euiFlyoutCloseButton'); + await testSubjects.missingOrFail('exampleRootProfileFlyout'); + }); + }); + + describe('data view mode', () => { + it('should allow clicking message cells to inspect the message', async () => { + await common.navigateToActualUrl('discover', undefined, { + ensureCurrentUrl: false, + }); + await dataViews.switchTo('my-example-logs'); + await discover.waitUntilSearchingHasFinished(); + await unifiedFieldList.clickFieldListItemAdd('message'); + let messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 1); + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + let message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is a debug log'); + messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 1); + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is an error log'); + await testSubjects.click('euiFlyoutCloseButton'); + await testSubjects.missingOrFail('exampleRootProfileFlyout'); + + // check Surrounding docs page + await dataGrid.clickRowToggle(); + const [, surroundingActionEl] = await dataGrid.getRowActions(); + await surroundingActionEl.click(); + await header.waitUntilLoadingHasFinished(); + await browser.refresh(); + await header.waitUntilLoadingHasFinished(); + + messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 1); + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is a debug log'); + messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 1); + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is an error log'); + await testSubjects.click('euiFlyoutCloseButton'); + await testSubjects.missingOrFail('exampleRootProfileFlyout'); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/index.ts b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/index.ts index d7ce3bdc0434b..f37bf7a731146 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/index.ts @@ -43,5 +43,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid loadTestFile(require.resolve('./extensions/_get_cell_renderers')); loadTestFile(require.resolve('./extensions/_get_default_app_state')); loadTestFile(require.resolve('./extensions/_get_additional_cell_actions')); + loadTestFile(require.resolve('./extensions/_get_render_app_wrapper')); }); } diff --git a/x-pack/test_serverless/functional/test_suites/observability/config.context_awareness.ts b/x-pack/test_serverless/functional/test_suites/observability/config.context_awareness.ts index 76362cc111e6f..e0af5773a315b 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/config.context_awareness.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/config.context_awareness.ts @@ -14,7 +14,12 @@ export default createTestConfig({ reportName: 'Serverless Observability Discover Context Awareness Functional Tests', }, kbnServerArgs: [ - '--discover.experimental.enabledProfiles=["example-root-profile","example-data-source-profile","example-document-profile"]', + `--discover.experimental.enabledProfiles=${JSON.stringify([ + 'example-root-profile', + 'example-fallback-root-profile', + 'example-data-source-profile', + 'example-document-profile', + ])}`, ], // include settings from project controller // https://github.com/elastic/project-controller/blob/main/internal/project/observability/config/elasticsearch.yml diff --git a/x-pack/test_serverless/functional/test_suites/search/config.context_awareness.ts b/x-pack/test_serverless/functional/test_suites/search/config.context_awareness.ts index 7b608c29c9f3a..1d0fc596d73ad 100644 --- a/x-pack/test_serverless/functional/test_suites/search/config.context_awareness.ts +++ b/x-pack/test_serverless/functional/test_suites/search/config.context_awareness.ts @@ -14,7 +14,12 @@ export default createTestConfig({ reportName: 'Serverless Search Discover Context Awareness Functional Tests', }, kbnServerArgs: [ - '--discover.experimental.enabledProfiles=["example-root-profile","example-data-source-profile","example-document-profile"]', + `--discover.experimental.enabledProfiles=${JSON.stringify([ + 'example-root-profile', + 'example-fallback-root-profile', + 'example-data-source-profile', + 'example-document-profile', + ])}`, ], // include settings from project controller // https://github.com/elastic/project-controller/blob/main/internal/project/observability/config/elasticsearch.yml diff --git a/x-pack/test_serverless/functional/test_suites/security/config.context_awareness.ts b/x-pack/test_serverless/functional/test_suites/security/config.context_awareness.ts index 6276922df83f4..aa888eedec8f0 100644 --- a/x-pack/test_serverless/functional/test_suites/security/config.context_awareness.ts +++ b/x-pack/test_serverless/functional/test_suites/security/config.context_awareness.ts @@ -14,7 +14,12 @@ export default createTestConfig({ reportName: 'Serverless Security Discover Context Awareness Functional Tests', }, kbnServerArgs: [ - '--discover.experimental.enabledProfiles=["example-root-profile","example-data-source-profile","example-document-profile"]', + `--discover.experimental.enabledProfiles=${JSON.stringify([ + 'example-root-profile', + 'example-fallback-root-profile', + 'example-data-source-profile', + 'example-document-profile', + ])}`, ], // include settings from project controller // https://github.com/elastic/project-controller/blob/main/internal/project/observability/config/elasticsearch.yml From 3cd9fbd1e2383d21c926c9f421d68ec974860a22 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Thu, 24 Oct 2024 21:54:49 -0300 Subject: [PATCH 03/11] Fix useRootProfile retrieving solutionNavId --- .../hooks/use_root_profile.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/plugins/discover/public/context_awareness/hooks/use_root_profile.ts b/src/plugins/discover/public/context_awareness/hooks/use_root_profile.ts index 4aa099fa0c2dd..ecfefb72e0e93 100644 --- a/src/plugins/discover/public/context_awareness/hooks/use_root_profile.ts +++ b/src/plugins/discover/public/context_awareness/hooks/use_root_profile.ts @@ -8,7 +8,7 @@ */ import { useEffect, useState } from 'react'; -import useObservable from 'react-use/lib/useObservable'; +import { distinctUntilChanged, filter, switchMap, tap } from 'rxjs'; import { useDiscoverServices } from '../../hooks/use_discover_services'; /** @@ -18,24 +18,24 @@ import { useDiscoverServices } from '../../hooks/use_discover_services'; */ export const useRootProfile = () => { const { profilesManager, core } = useDiscoverServices(); - const solutionNavId = useObservable(core.chrome.getActiveSolutionNavId$()); const [rootProfileLoading, setRootProfileLoading] = useState(true); useEffect(() => { - let aborted = false; - - setRootProfileLoading(true); - - profilesManager.resolveRootProfile({ solutionNavId }).then(() => { - if (!aborted) { - setRootProfileLoading(false); - } - }); + const subscription = core.chrome + .getActiveSolutionNavId$() + .pipe( + distinctUntilChanged(), + filter((id) => id !== undefined), + tap(() => setRootProfileLoading(true)), + switchMap((id) => profilesManager.resolveRootProfile({ solutionNavId: id })), + tap(() => setRootProfileLoading(false)) + ) + .subscribe(); return () => { - aborted = true; + subscription.unsubscribe(); }; - }, [profilesManager, solutionNavId]); + }, [core.chrome, profilesManager]); return { rootProfileLoading }; }; From 651a31c29c9258e924fc13adf4be86a8a4fd7dc9 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 28 Oct 2024 01:16:42 -0300 Subject: [PATCH 04/11] Fix issue where AppWrapper was remounting entire Discover app, only allow at root profile level --- .../application/context/context_app_route.tsx | 15 +- .../public/application/doc/components/doc.tsx | 175 ++++++++---------- .../application/doc/single_doc_route.tsx | 11 +- .../main/discover_main_route.test.tsx | 3 +- .../application/main/discover_main_route.tsx | 15 +- .../public/context_awareness/README.md | 4 +- .../hooks/use_root_profile.test.tsx | 14 +- ...e_root_profile.ts => use_root_profile.tsx} | 20 +- .../context_awareness/profiles_manager.ts | 13 +- .../public/context_awareness/types.ts | 4 +- 10 files changed, 141 insertions(+), 133 deletions(-) rename src/plugins/discover/public/context_awareness/hooks/{use_root_profile.ts => use_root_profile.tsx} (64%) diff --git a/src/plugins/discover/public/application/context/context_app_route.tsx b/src/plugins/discover/public/application/context/context_app_route.tsx index 7f056f85fe8e9..dad0dd2eb7b93 100644 --- a/src/plugins/discover/public/application/context/context_app_route.tsx +++ b/src/plugins/discover/public/application/context/context_app_route.tsx @@ -16,7 +16,7 @@ import { LoadingIndicator } from '../../components/common/loading_indicator'; import { useDataView } from '../../hooks/use_data_view'; import type { ContextHistoryLocationState } from './services/locator'; import { useDiscoverServices } from '../../hooks/use_discover_services'; -import { useProfileAccessor, useRootProfile } from '../../context_awareness'; +import { useRootProfile } from '../../context_awareness'; export interface ContextUrlParams { dataViewId: string; @@ -49,12 +49,7 @@ export function ContextAppRoute() { const dataViewId = decodeURIComponent(encodedDataViewId); const anchorId = decodeURIComponent(id); const { dataView, error } = useDataView({ index: locationState?.dataViewSpec || dataViewId }); - const { rootProfileLoading } = useRootProfile(); - const getRenderAppWrapperAccessor = useProfileAccessor('getRenderAppWrapper'); - const AppWrapper = useMemo( - () => getRenderAppWrapperAccessor(({ children }) => <>{children}), - [getRenderAppWrapperAccessor] - ); + const rootProfileState = useRootProfile(); if (error) { return ( @@ -78,13 +73,13 @@ export function ContextAppRoute() { ); } - if (!dataView || rootProfileLoading) { + if (!dataView || rootProfileState.rootProfileLoading) { return ; } return ( - + - + ); } diff --git a/src/plugins/discover/public/application/doc/components/doc.tsx b/src/plugins/discover/public/application/doc/components/doc.tsx index 16d27857b7ff2..8609968f838de 100644 --- a/src/plugins/discover/public/application/doc/components/doc.tsx +++ b/src/plugins/discover/public/application/doc/components/doc.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPage, EuiPageBody } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -19,8 +19,6 @@ import { setBreadcrumbs } from '../../../utils/breadcrumbs'; import { useDiscoverServices } from '../../../hooks/use_discover_services'; import { SingleDocViewer } from './single_doc_viewer'; import { createDataViewDataSource } from '../../../../common/data_sources'; -import { useProfileAccessor, useRootProfile } from '../../../context_awareness'; -import { LoadingIndicator } from '../../../components/common/loading_indicator'; export interface DocProps extends EsDocSearchProps { /** @@ -64,104 +62,91 @@ export function Doc(props: DocProps) { }); }, [chrome, props.referrer, props.index, props.id, dataView, locator, services]); - const { rootProfileLoading } = useRootProfile(); - const getRenderAppWrapperAccessor = useProfileAccessor('getRenderAppWrapper'); - const AppWrapper = useMemo( - () => getRenderAppWrapperAccessor(({ children }) => <>{children}), - [getRenderAppWrapperAccessor] - ); - - if (rootProfileLoading) { - ; - } - return ( - - -

- {i18n.translate('discover.doc.pageTitle', { - defaultMessage: 'Single document - #{id}', - values: { id: props.id }, - })} -

- - {reqState === ElasticRequestState.NotFoundDataView && ( - - } - /> - )} - {reqState === ElasticRequestState.NotFound && ( - - } - > + +

+ {i18n.translate('discover.doc.pageTitle', { + defaultMessage: 'Single document - #{id}', + values: { id: props.id }, + })} +

+ + {reqState === ElasticRequestState.NotFoundDataView && ( + + } + /> + )} + {reqState === ElasticRequestState.NotFound && ( + - - )} + } + > + + + )} - {reqState === ElasticRequestState.Error && ( - - } - > + {reqState === ElasticRequestState.Error && ( + {' '} - - - - - )} + id="discover.doc.failedToExecuteQueryDescription" + defaultMessage="Cannot run search" + /> + } + > + {' '} + + + + + )} - {reqState === ElasticRequestState.Loading && ( - - {' '} - - - )} + {reqState === ElasticRequestState.Loading && ( + + {' '} + + + )} - {reqState === ElasticRequestState.Found && record !== null && dataView && ( -
- -
- )} -
-
-
+ {reqState === ElasticRequestState.Found && record !== null && dataView && ( +
+ +
+ )} + + ); } diff --git a/src/plugins/discover/public/application/doc/single_doc_route.tsx b/src/plugins/discover/public/application/doc/single_doc_route.tsx index 8091e637e8beb..3eedac7be1644 100644 --- a/src/plugins/discover/public/application/doc/single_doc_route.tsx +++ b/src/plugins/discover/public/application/doc/single_doc_route.tsx @@ -19,6 +19,7 @@ import { useDiscoverServices } from '../../hooks/use_discover_services'; import { DiscoverError } from '../../components/common/error_alert'; import { useDataView } from '../../hooks/use_data_view'; import { DocHistoryLocationState } from './locator'; +import { useRootProfile } from '../../context_awareness'; export interface DocUrlParams { dataViewId: string; @@ -53,6 +54,8 @@ export const SingleDocRoute = () => { index: locationState?.dataViewSpec || decodeURIComponent(dataViewId), }); + const rootProfileState = useRootProfile(); + if (error) { return ( { ); } - if (!dataView) { + if (!dataView || rootProfileState.rootProfileLoading) { return ; } @@ -94,5 +97,9 @@ export const SingleDocRoute = () => { ); } - return ; + return ( + + + + ); }; 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 d2e074720bb0b..df787f5756ae7 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 @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React from 'react'; +import React, { ReactNode } from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { waitFor } from '@testing-library/react'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; @@ -50,6 +50,7 @@ jest.mock('../../context_awareness', () => { ...originalModule, useRootProfile: () => ({ rootProfileLoading: mockRootProfileLoading, + AppWrapper: ({ children }: { children: ReactNode }) => <>{children}, }), }; }); 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 0cc3a6a171f20..d86788172386f 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.tsx @@ -41,7 +41,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 { useProfileAccessor, useRootProfile } from '../../context_awareness'; +import { useRootProfile } from '../../context_awareness'; const DiscoverMainAppMemoized = memo(DiscoverMainApp); @@ -345,31 +345,26 @@ export function DiscoverMainRoute({ stateContainer, ]); - const { rootProfileLoading } = useRootProfile(); - const getRenderAppWrapperAccessor = useProfileAccessor('getRenderAppWrapper'); - const AppWrapper = useMemo( - () => getRenderAppWrapperAccessor(({ children }) => <>{children}), - [getRenderAppWrapperAccessor] - ); + const rootProfileState = useRootProfile(); if (error) { return ; } - if (!customizationService || rootProfileLoading) { + if (!customizationService || rootProfileState.rootProfileLoading) { return loadingIndicator; } return ( - + {mainContent} - + ); diff --git a/src/plugins/discover/public/context_awareness/README.md b/src/plugins/discover/public/context_awareness/README.md index ab927a4523377..a6e7e3e24a585 100644 --- a/src/plugins/discover/public/context_awareness/README.md +++ b/src/plugins/discover/public/context_awareness/README.md @@ -233,7 +233,7 @@ For more advanced use cases, such as when state needs to be shared across extens // The app wrapper extension point supports common patterns like React context const flyoutContext = createContext({ setFlyoutOpen: (open: boolean) => {} }); -// App wrapper implementations can exist at the root or data source level -- when implemented at the root, their lifecycle will match the Discover lifecycle +// App wrapper implementations can only exist at the root level, and their lifecycle will match the Discover lifecycle export const createSecurityRootProfileProvider = (): RootProfileProvider => ({ profileId: 'security-root-profile', profile: { @@ -244,7 +244,7 @@ export const createSecurityRootProfileProvider = (): RootProfileProvider => ({ const [flyoutOpen, setFlyoutOpen] = useState(false); return ( - // Be sure to render previous wrappers as well since other implementations may depend on them + // Be sure to render the previous wrapper as well // This is our wrapper -- it uses React context to give extension point implementations access to the shared state 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 index 8ce4a937bfae7..26c3aa2df3f15 100644 --- 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 @@ -8,12 +8,11 @@ */ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { renderHook } from '@testing-library/react-hooks'; +import { act, renderHook } from '@testing-library/react-hooks'; import React from 'react'; import { discoverServiceMock } from '../../__mocks__/services'; import { useRootProfile } from './use_root_profile'; import { BehaviorSubject } from 'rxjs'; -import { act } from '@testing-library/react'; const mockSolutionNavId$ = new BehaviorSubject('solutionNavId'); @@ -37,25 +36,32 @@ describe('useRootProfile', () => { mockSolutionNavId$.next('solutionNavId'); }); - it('should return rootProfileLoading as true', () => { - const { result } = render(); + it('should return rootProfileLoading as true', async () => { + const { result, waitForNextUpdate } = render(); expect(result.current.rootProfileLoading).toBe(true); + expect((result.current as Record).AppWrapper).toBeUndefined(); + // avoid act warning + await waitForNextUpdate(); }); it('should return rootProfileLoading as false', async () => { const { result, waitForNextUpdate } = render(); await waitForNextUpdate(); expect(result.current.rootProfileLoading).toBe(false); + expect((result.current as Record).AppWrapper).toBeDefined(); }); it('should return rootProfileLoading as true when solutionNavId changes', async () => { const { result, rerender, waitForNextUpdate } = render(); await waitForNextUpdate(); expect(result.current.rootProfileLoading).toBe(false); + expect((result.current as Record).AppWrapper).toBeDefined(); act(() => mockSolutionNavId$.next('newSolutionNavId')); rerender(); expect(result.current.rootProfileLoading).toBe(true); + expect((result.current as Record).AppWrapper).toBeUndefined(); await waitForNextUpdate(); expect(result.current.rootProfileLoading).toBe(false); + expect((result.current as Record).AppWrapper).toBeDefined(); }); }); diff --git a/src/plugins/discover/public/context_awareness/hooks/use_root_profile.ts b/src/plugins/discover/public/context_awareness/hooks/use_root_profile.tsx similarity index 64% rename from src/plugins/discover/public/context_awareness/hooks/use_root_profile.ts rename to src/plugins/discover/public/context_awareness/hooks/use_root_profile.tsx index ecfefb72e0e93..7d8bf83d4bd8e 100644 --- a/src/plugins/discover/public/context_awareness/hooks/use_root_profile.ts +++ b/src/plugins/discover/public/context_awareness/hooks/use_root_profile.tsx @@ -9,7 +9,9 @@ import { useEffect, useState } from 'react'; import { distinctUntilChanged, filter, switchMap, tap } from 'rxjs'; +import React from 'react'; import { useDiscoverServices } from '../../hooks/use_discover_services'; +import type { Profile } from '../types'; /** * Hook to trigger and wait for root profile resolution @@ -18,7 +20,10 @@ import { useDiscoverServices } from '../../hooks/use_discover_services'; */ export const useRootProfile = () => { const { profilesManager, core } = useDiscoverServices(); - const [rootProfileLoading, setRootProfileLoading] = useState(true); + const [rootProfileState, setRootProfileState] = useState< + | { rootProfileLoading: true } + | { rootProfileLoading: false; AppWrapper: Profile['getRenderAppWrapper'] } + >({ rootProfileLoading: true }); useEffect(() => { const subscription = core.chrome @@ -26,9 +31,16 @@ export const useRootProfile = () => { .pipe( distinctUntilChanged(), filter((id) => id !== undefined), - tap(() => setRootProfileLoading(true)), + tap(() => setRootProfileState({ rootProfileLoading: true })), switchMap((id) => profilesManager.resolveRootProfile({ solutionNavId: id })), - tap(() => setRootProfileLoading(false)) + tap(({ getRenderAppWrapper }) => { + const BaseAppWrapper: Profile['getRenderAppWrapper'] = ({ children }) => <>{children}; + + setRootProfileState({ + rootProfileLoading: false, + AppWrapper: getRenderAppWrapper?.(BaseAppWrapper) ?? BaseAppWrapper, + }); + }) ) .subscribe(); @@ -37,5 +49,5 @@ export const useRootProfile = () => { }; }, [core.chrome, profilesManager]); - return { rootProfileLoading }; + return rootProfileState; }; diff --git a/src/plugins/discover/public/context_awareness/profiles_manager.ts b/src/plugins/discover/public/context_awareness/profiles_manager.ts index 6b7bef5e02294..a209e5dfc9f7c 100644 --- a/src/plugins/discover/public/context_awareness/profiles_manager.ts +++ b/src/plugins/discover/public/context_awareness/profiles_manager.ts @@ -25,7 +25,7 @@ import type { DocumentContext, } from './profiles'; import type { ContextWithProfileId } from './profile_service'; -import { DiscoverEBTManager } from '../services/discover_ebt_manager'; +import type { DiscoverEBTManager } from '../services/discover_ebt_manager'; interface SerializedRootProfileParams { solutionNavId: RootProfileProviderParams['solutionNavId']; @@ -79,7 +79,7 @@ export class ProfilesManager { const serializedParams = serializeRootProfileParams(params); if (isEqual(this.prevRootProfileParams, serializedParams)) { - return; + return { getRenderAppWrapper: this.getRootRenderAppWrapper() }; } const abortController = new AbortController(); @@ -95,11 +95,13 @@ export class ProfilesManager { } if (abortController.signal.aborted) { - return; + return { getRenderAppWrapper: this.getRootRenderAppWrapper() }; } this.rootContext$.next(context); this.prevRootProfileParams = serializedParams; + + return { getRenderAppWrapper: this.getRootRenderAppWrapper() }; } /** @@ -208,6 +210,11 @@ export class ProfilesManager { this.ebtManager.updateProfilesContextWith(dscProfiles); } + + private getRootRenderAppWrapper() { + const rootProfile = this.rootProfileService.getProfile(this.rootContext$.getValue()); + return rootProfile.getRenderAppWrapper; + } } const serializeRootProfileParams = ( diff --git a/src/plugins/discover/public/context_awareness/types.ts b/src/plugins/discover/public/context_awareness/types.ts index f03e3773c7d87..1266584d94d81 100644 --- a/src/plugins/discover/public/context_awareness/types.ts +++ b/src/plugins/discover/public/context_awareness/types.ts @@ -21,10 +21,10 @@ import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; import type { OmitIndexSignature } from 'type-fest'; import type { Trigger } from '@kbn/ui-actions-plugin/public'; import type { PropsWithChildren, ReactElement } from 'react'; -import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; +import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; import type { DiscoverDataSource } from '../../common/data_sources'; import type { DiscoverAppState } from '../application/main/state_management/discover_app_state_container'; -import { DiscoverStateContainer } from '../application/main/state_management/discover_state'; +import type { DiscoverStateContainer } from '../application/main/state_management/discover_state'; /** * Supports customizing the Discover document viewer flyout From 23611721dab371c6b83058763504454b766e9e37 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 28 Oct 2024 01:19:58 -0300 Subject: [PATCH 05/11] Omit getRenderAppWrapper from data source profile --- .../public/context_awareness/profiles/data_source_profile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 807072d777a93..c4d06e0a502cb 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 @@ -25,7 +25,7 @@ export enum DataSourceCategory { /** * The data source profile interface */ -export type DataSourceProfile = Profile; +export type DataSourceProfile = Omit; /** * Parameters for the data source profile provider `resolve` method From 16c33cdb2cfae3e3ffd2696f388f188bf2a9c6db Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 28 Oct 2024 12:28:47 -0300 Subject: [PATCH 06/11] Fixing functional tests --- .../extensions/_get_render_app_wrapper.ts | 12 ++++++------ .../extensions/_get_render_app_wrapper.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/test/functional/apps/discover/context_awareness/extensions/_get_render_app_wrapper.ts b/test/functional/apps/discover/context_awareness/extensions/_get_render_app_wrapper.ts index 31ccae26a78ab..0e852c90de3a4 100644 --- a/test/functional/apps/discover/context_awareness/extensions/_get_render_app_wrapper.ts +++ b/test/functional/apps/discover/context_awareness/extensions/_get_render_app_wrapper.ts @@ -35,12 +35,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); await discover.waitUntilSearchingHasFinished(); await unifiedFieldList.clickFieldListItemAdd('message'); - let messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 0); + let messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 2); await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); await testSubjects.existOrFail('exampleRootProfileFlyout'); let message = await testSubjects.find('exampleRootProfileCurrentMessage'); expect(await message.getVisibleText()).to.be('This is a debug log'); - messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 0); + messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 2); await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); await testSubjects.existOrFail('exampleRootProfileFlyout'); message = await testSubjects.find('exampleRootProfileCurrentMessage'); @@ -58,12 +58,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dataViews.switchTo('my-example-logs'); await discover.waitUntilSearchingHasFinished(); await unifiedFieldList.clickFieldListItemAdd('message'); - let messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 1); + let messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 2); await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); await testSubjects.existOrFail('exampleRootProfileFlyout'); let message = await testSubjects.find('exampleRootProfileCurrentMessage'); expect(await message.getVisibleText()).to.be('This is a debug log'); - messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 1); + messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 2); await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); await testSubjects.existOrFail('exampleRootProfileFlyout'); message = await testSubjects.find('exampleRootProfileCurrentMessage'); @@ -79,12 +79,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.refresh(); await header.waitUntilLoadingHasFinished(); - messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 1); + messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 2); await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); await testSubjects.existOrFail('exampleRootProfileFlyout'); message = await testSubjects.find('exampleRootProfileCurrentMessage'); expect(await message.getVisibleText()).to.be('This is a debug log'); - messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 1); + messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 2); await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); await testSubjects.existOrFail('exampleRootProfileFlyout'); message = await testSubjects.find('exampleRootProfileCurrentMessage'); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_render_app_wrapper.ts b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_render_app_wrapper.ts index 8b6e5fe3ad43a..12029c2177413 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_render_app_wrapper.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_render_app_wrapper.ts @@ -38,12 +38,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); await discover.waitUntilSearchingHasFinished(); await unifiedFieldList.clickFieldListItemAdd('message'); - let messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 0); + let messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 2); await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); await testSubjects.existOrFail('exampleRootProfileFlyout'); let message = await testSubjects.find('exampleRootProfileCurrentMessage'); expect(await message.getVisibleText()).to.be('This is a debug log'); - messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 0); + messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 2); await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); await testSubjects.existOrFail('exampleRootProfileFlyout'); message = await testSubjects.find('exampleRootProfileCurrentMessage'); @@ -61,12 +61,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dataViews.switchTo('my-example-logs'); await discover.waitUntilSearchingHasFinished(); await unifiedFieldList.clickFieldListItemAdd('message'); - let messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 1); + let messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 2); await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); await testSubjects.existOrFail('exampleRootProfileFlyout'); let message = await testSubjects.find('exampleRootProfileCurrentMessage'); expect(await message.getVisibleText()).to.be('This is a debug log'); - messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 1); + messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 2); await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); await testSubjects.existOrFail('exampleRootProfileFlyout'); message = await testSubjects.find('exampleRootProfileCurrentMessage'); @@ -82,12 +82,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.refresh(); await header.waitUntilLoadingHasFinished(); - messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 1); + messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 2); await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); await testSubjects.existOrFail('exampleRootProfileFlyout'); message = await testSubjects.find('exampleRootProfileCurrentMessage'); expect(await message.getVisibleText()).to.be('This is a debug log'); - messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 1); + messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 2); await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); await testSubjects.existOrFail('exampleRootProfileFlyout'); message = await testSubjects.find('exampleRootProfileCurrentMessage'); From 0c23d427766c972171113a040049d1f8480e55e9 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 28 Oct 2024 21:34:15 -0300 Subject: [PATCH 07/11] Fixing functionals --- .../extensions/_get_render_app_wrapper.ts | 37 +++++++++++++------ .../extensions/_get_render_app_wrapper.ts | 37 +++++++++++++------ 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/test/functional/apps/discover/context_awareness/extensions/_get_render_app_wrapper.ts b/test/functional/apps/discover/context_awareness/extensions/_get_render_app_wrapper.ts index 0e852c90de3a4..d6da4b81c8283 100644 --- a/test/functional/apps/discover/context_awareness/extensions/_get_render_app_wrapper.ts +++ b/test/functional/apps/discover/context_awareness/extensions/_get_render_app_wrapper.ts @@ -22,6 +22,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dataViews = getService('dataViews'); const dataGrid = getService('dataGrid'); const browser = getService('browser'); + const retry = getService('retry'); describe('extension getRenderAppWrapper', () => { describe('ES|QL mode', () => { @@ -36,13 +37,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await discover.waitUntilSearchingHasFinished(); await unifiedFieldList.clickFieldListItemAdd('message'); let messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 2); - await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); - await testSubjects.existOrFail('exampleRootProfileFlyout'); + await retry.try(async () => { + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + }); let message = await testSubjects.find('exampleRootProfileCurrentMessage'); expect(await message.getVisibleText()).to.be('This is a debug log'); messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 2); - await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); - await testSubjects.existOrFail('exampleRootProfileFlyout'); + await retry.try(async () => { + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + }); message = await testSubjects.find('exampleRootProfileCurrentMessage'); expect(await message.getVisibleText()).to.be('This is an error log'); await testSubjects.click('euiFlyoutCloseButton'); @@ -59,13 +64,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await discover.waitUntilSearchingHasFinished(); await unifiedFieldList.clickFieldListItemAdd('message'); let messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 2); - await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); - await testSubjects.existOrFail('exampleRootProfileFlyout'); + await retry.try(async () => { + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + }); let message = await testSubjects.find('exampleRootProfileCurrentMessage'); expect(await message.getVisibleText()).to.be('This is a debug log'); messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 2); - await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); - await testSubjects.existOrFail('exampleRootProfileFlyout'); + await retry.try(async () => { + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + }); message = await testSubjects.find('exampleRootProfileCurrentMessage'); expect(await message.getVisibleText()).to.be('This is an error log'); await testSubjects.click('euiFlyoutCloseButton'); @@ -80,13 +89,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await header.waitUntilLoadingHasFinished(); messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 2); - await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); - await testSubjects.existOrFail('exampleRootProfileFlyout'); + await retry.try(async () => { + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + }); message = await testSubjects.find('exampleRootProfileCurrentMessage'); expect(await message.getVisibleText()).to.be('This is a debug log'); messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 2); - await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); - await testSubjects.existOrFail('exampleRootProfileFlyout'); + await retry.try(async () => { + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + }); message = await testSubjects.find('exampleRootProfileCurrentMessage'); expect(await message.getVisibleText()).to.be('This is an error log'); await testSubjects.click('euiFlyoutCloseButton'); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_render_app_wrapper.ts b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_render_app_wrapper.ts index 12029c2177413..a279c9e13c08b 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_render_app_wrapper.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_render_app_wrapper.ts @@ -21,6 +21,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dataViews = getService('dataViews'); const dataGrid = getService('dataGrid'); const browser = getService('browser'); + const retry = getService('retry'); describe('extension getRenderAppWrapper', () => { before(async () => { @@ -39,13 +40,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await discover.waitUntilSearchingHasFinished(); await unifiedFieldList.clickFieldListItemAdd('message'); let messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 2); - await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); - await testSubjects.existOrFail('exampleRootProfileFlyout'); + await retry.try(async () => { + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + }); let message = await testSubjects.find('exampleRootProfileCurrentMessage'); expect(await message.getVisibleText()).to.be('This is a debug log'); messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 2); - await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); - await testSubjects.existOrFail('exampleRootProfileFlyout'); + await retry.try(async () => { + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + }); message = await testSubjects.find('exampleRootProfileCurrentMessage'); expect(await message.getVisibleText()).to.be('This is an error log'); await testSubjects.click('euiFlyoutCloseButton'); @@ -62,13 +67,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await discover.waitUntilSearchingHasFinished(); await unifiedFieldList.clickFieldListItemAdd('message'); let messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 2); - await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); - await testSubjects.existOrFail('exampleRootProfileFlyout'); + await retry.try(async () => { + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + }); let message = await testSubjects.find('exampleRootProfileCurrentMessage'); expect(await message.getVisibleText()).to.be('This is a debug log'); messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 2); - await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); - await testSubjects.existOrFail('exampleRootProfileFlyout'); + await retry.try(async () => { + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + }); message = await testSubjects.find('exampleRootProfileCurrentMessage'); expect(await message.getVisibleText()).to.be('This is an error log'); await testSubjects.click('euiFlyoutCloseButton'); @@ -83,13 +92,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await header.waitUntilLoadingHasFinished(); messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 2); - await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); - await testSubjects.existOrFail('exampleRootProfileFlyout'); + await retry.try(async () => { + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + }); message = await testSubjects.find('exampleRootProfileCurrentMessage'); expect(await message.getVisibleText()).to.be('This is a debug log'); messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 2); - await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); - await testSubjects.existOrFail('exampleRootProfileFlyout'); + await retry.try(async () => { + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + }); message = await testSubjects.find('exampleRootProfileCurrentMessage'); expect(await message.getVisibleText()).to.be('This is an error log'); await testSubjects.click('euiFlyoutCloseButton'); From 21ae097c9e5e18619c41e4b6de77736a53332939 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 29 Oct 2024 16:01:27 -0300 Subject: [PATCH 08/11] Move BaseAppWrapper definition out of the useRootProfile hook --- .../context_awareness/hooks/use_root_profile.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/discover/public/context_awareness/hooks/use_root_profile.tsx b/src/plugins/discover/public/context_awareness/hooks/use_root_profile.tsx index 7d8bf83d4bd8e..4e22c3b9f4152 100644 --- a/src/plugins/discover/public/context_awareness/hooks/use_root_profile.tsx +++ b/src/plugins/discover/public/context_awareness/hooks/use_root_profile.tsx @@ -33,14 +33,12 @@ export const useRootProfile = () => { filter((id) => id !== undefined), tap(() => setRootProfileState({ rootProfileLoading: true })), switchMap((id) => profilesManager.resolveRootProfile({ solutionNavId: id })), - tap(({ getRenderAppWrapper }) => { - const BaseAppWrapper: Profile['getRenderAppWrapper'] = ({ children }) => <>{children}; - + tap(({ getRenderAppWrapper }) => setRootProfileState({ rootProfileLoading: false, AppWrapper: getRenderAppWrapper?.(BaseAppWrapper) ?? BaseAppWrapper, - }); - }) + }) + ) ) .subscribe(); @@ -51,3 +49,5 @@ export const useRootProfile = () => { return rootProfileState; }; + +const BaseAppWrapper: Profile['getRenderAppWrapper'] = ({ children }) => <>{children}; From 2eede18fd5bb01c995340b36f9d59fe2ba0fc21b Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 29 Oct 2024 19:31:32 -0300 Subject: [PATCH 09/11] Rename example-fallback-root-profile to example-solution-view-root-profile --- .../profile_providers/example/example_root_profile/index.ts | 2 +- .../example/example_root_profile/profile.tsx | 4 ++-- .../profile_providers/register_profile_providers.ts | 4 ++-- test/functional/apps/discover/context_awareness/config.ts | 2 +- .../test_suites/observability/config.context_awareness.ts | 2 +- .../functional/test_suites/search/config.context_awareness.ts | 2 +- .../test_suites/security/config.context_awareness.ts | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/plugins/discover/public/context_awareness/profile_providers/example/example_root_profile/index.ts b/src/plugins/discover/public/context_awareness/profile_providers/example/example_root_profile/index.ts index 8b7d05d00daae..b286a7d8cdce0 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/example/example_root_profile/index.ts +++ b/src/plugins/discover/public/context_awareness/profile_providers/example/example_root_profile/index.ts @@ -9,5 +9,5 @@ export { createExampleRootProfileProvider, - createExampleFallbackRootProfileProvider, + createExampleSolutionViewRootProfileProvider, } from './profile'; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/example/example_root_profile/profile.tsx b/src/plugins/discover/public/context_awareness/profile_providers/example/example_root_profile/profile.tsx index 0730fe9c4b447..24eb32f4336f6 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/example/example_root_profile/profile.tsx +++ b/src/plugins/discover/public/context_awareness/profile_providers/example/example_root_profile/profile.tsx @@ -47,8 +47,8 @@ export const createExampleRootProfileProvider = (): RootProfileProvider => ({ }, }); -export const createExampleFallbackRootProfileProvider = (): RootProfileProvider => ({ - profileId: 'example-fallback-root-profile', +export const createExampleSolutionViewRootProfileProvider = (): RootProfileProvider => ({ + profileId: 'example-solution-view-root-profile', isExperimental: true, profile: { getRenderAppWrapper }, resolve: (params) => ({ diff --git a/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.ts b/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.ts index e426672f42d52..5bac0d9cea483 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.ts +++ b/src/plugins/discover/public/context_awareness/profile_providers/register_profile_providers.ts @@ -16,7 +16,7 @@ import type { BaseProfileProvider, BaseProfileService } from '../profile_service import { createExampleDataSourceProfileProvider } from './example/example_data_source_profile/profile'; import { createExampleDocumentProfileProvider } from './example/example_document_profile'; import { - createExampleFallbackRootProfileProvider, + createExampleSolutionViewRootProfileProvider, createExampleRootProfileProvider, } from './example/example_root_profile'; import { createLogsDataSourceProfileProviders } from './common/logs_data_source_profile'; @@ -120,7 +120,7 @@ export const registerEnabledProfileProviders = < */ const createRootProfileProviders = (providerServices: ProfileProviderServices) => [ createExampleRootProfileProvider(), - createExampleFallbackRootProfileProvider(), + createExampleSolutionViewRootProfileProvider(), createSecurityRootProfileProvider(providerServices), ]; diff --git a/test/functional/apps/discover/context_awareness/config.ts b/test/functional/apps/discover/context_awareness/config.ts index c707060531e7c..ded4755a61f92 100644 --- a/test/functional/apps/discover/context_awareness/config.ts +++ b/test/functional/apps/discover/context_awareness/config.ts @@ -27,7 +27,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...baseConfig.kbnTestServer.serverArgs, `--discover.experimental.enabledProfiles=${JSON.stringify([ 'example-root-profile', - 'example-fallback-root-profile', + 'example-solution-view-root-profile', 'example-data-source-profile', 'example-document-profile', ])}`, diff --git a/x-pack/test_serverless/functional/test_suites/observability/config.context_awareness.ts b/x-pack/test_serverless/functional/test_suites/observability/config.context_awareness.ts index e0af5773a315b..283e4e7e10a2f 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/config.context_awareness.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/config.context_awareness.ts @@ -16,7 +16,7 @@ export default createTestConfig({ kbnServerArgs: [ `--discover.experimental.enabledProfiles=${JSON.stringify([ 'example-root-profile', - 'example-fallback-root-profile', + 'example-solution-view-root-profile', 'example-data-source-profile', 'example-document-profile', ])}`, diff --git a/x-pack/test_serverless/functional/test_suites/search/config.context_awareness.ts b/x-pack/test_serverless/functional/test_suites/search/config.context_awareness.ts index 1d0fc596d73ad..6be4a7e30e999 100644 --- a/x-pack/test_serverless/functional/test_suites/search/config.context_awareness.ts +++ b/x-pack/test_serverless/functional/test_suites/search/config.context_awareness.ts @@ -16,7 +16,7 @@ export default createTestConfig({ kbnServerArgs: [ `--discover.experimental.enabledProfiles=${JSON.stringify([ 'example-root-profile', - 'example-fallback-root-profile', + 'example-solution-view-root-profile', 'example-data-source-profile', 'example-document-profile', ])}`, diff --git a/x-pack/test_serverless/functional/test_suites/security/config.context_awareness.ts b/x-pack/test_serverless/functional/test_suites/security/config.context_awareness.ts index aa888eedec8f0..984ce1c904d80 100644 --- a/x-pack/test_serverless/functional/test_suites/security/config.context_awareness.ts +++ b/x-pack/test_serverless/functional/test_suites/security/config.context_awareness.ts @@ -16,7 +16,7 @@ export default createTestConfig({ kbnServerArgs: [ `--discover.experimental.enabledProfiles=${JSON.stringify([ 'example-root-profile', - 'example-fallback-root-profile', + 'example-solution-view-root-profile', 'example-data-source-profile', 'example-document-profile', ])}`, From 2f493e7b3e42269129fc160d10e25033c2555196 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Tue, 29 Oct 2024 23:27:55 -0300 Subject: [PATCH 10/11] More func test fixes --- .../extensions/_get_render_app_wrapper.ts | 12 ++++++------ .../extensions/_get_render_app_wrapper.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/test/functional/apps/discover/context_awareness/extensions/_get_render_app_wrapper.ts b/test/functional/apps/discover/context_awareness/extensions/_get_render_app_wrapper.ts index d6da4b81c8283..d040e8e9f3da6 100644 --- a/test/functional/apps/discover/context_awareness/extensions/_get_render_app_wrapper.ts +++ b/test/functional/apps/discover/context_awareness/extensions/_get_render_app_wrapper.ts @@ -47,9 +47,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async () => { await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); await testSubjects.existOrFail('exampleRootProfileFlyout'); + message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is an error log'); }); - message = await testSubjects.find('exampleRootProfileCurrentMessage'); - expect(await message.getVisibleText()).to.be('This is an error log'); await testSubjects.click('euiFlyoutCloseButton'); await testSubjects.missingOrFail('exampleRootProfileFlyout'); }); @@ -74,9 +74,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async () => { await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); await testSubjects.existOrFail('exampleRootProfileFlyout'); + message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is an error log'); }); - message = await testSubjects.find('exampleRootProfileCurrentMessage'); - expect(await message.getVisibleText()).to.be('This is an error log'); await testSubjects.click('euiFlyoutCloseButton'); await testSubjects.missingOrFail('exampleRootProfileFlyout'); @@ -99,9 +99,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async () => { await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); await testSubjects.existOrFail('exampleRootProfileFlyout'); + message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is an error log'); }); - message = await testSubjects.find('exampleRootProfileCurrentMessage'); - expect(await message.getVisibleText()).to.be('This is an error log'); await testSubjects.click('euiFlyoutCloseButton'); await testSubjects.missingOrFail('exampleRootProfileFlyout'); }); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_render_app_wrapper.ts b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_render_app_wrapper.ts index a279c9e13c08b..401006a65ba9d 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_render_app_wrapper.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_render_app_wrapper.ts @@ -50,9 +50,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async () => { await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); await testSubjects.existOrFail('exampleRootProfileFlyout'); + message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is an error log'); }); - message = await testSubjects.find('exampleRootProfileCurrentMessage'); - expect(await message.getVisibleText()).to.be('This is an error log'); await testSubjects.click('euiFlyoutCloseButton'); await testSubjects.missingOrFail('exampleRootProfileFlyout'); }); @@ -77,9 +77,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async () => { await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); await testSubjects.existOrFail('exampleRootProfileFlyout'); + message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is an error log'); }); - message = await testSubjects.find('exampleRootProfileCurrentMessage'); - expect(await message.getVisibleText()).to.be('This is an error log'); await testSubjects.click('euiFlyoutCloseButton'); await testSubjects.missingOrFail('exampleRootProfileFlyout'); @@ -102,9 +102,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async () => { await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); await testSubjects.existOrFail('exampleRootProfileFlyout'); + message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is an error log'); }); - message = await testSubjects.find('exampleRootProfileCurrentMessage'); - expect(await message.getVisibleText()).to.be('This is an error log'); await testSubjects.click('euiFlyoutCloseButton'); await testSubjects.missingOrFail('exampleRootProfileFlyout'); }); From 52aa6456152becf7c279bfe83421a47c2812f3af Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Fri, 1 Nov 2024 15:55:14 -0300 Subject: [PATCH 11/11] Support getRenderAppWrapper in dashboards --- .../public/context_awareness/hooks/index.ts | 2 +- .../hooks/use_root_profile.tsx | 2 +- .../public/context_awareness/index.ts | 7 ++- .../get_search_embeddable_factory.test.tsx | 2 +- .../get_search_embeddable_factory.tsx | 48 +++++++------- .../extensions/_get_render_app_wrapper.ts | 63 ++++++++++++++++++- .../extensions/_get_render_app_wrapper.ts | 63 ++++++++++++++++++- 7 files changed, 160 insertions(+), 27 deletions(-) diff --git a/src/plugins/discover/public/context_awareness/hooks/index.ts b/src/plugins/discover/public/context_awareness/hooks/index.ts index c509fd0119059..28a45be84de76 100644 --- a/src/plugins/discover/public/context_awareness/hooks/index.ts +++ b/src/plugins/discover/public/context_awareness/hooks/index.ts @@ -8,5 +8,5 @@ */ export { useProfileAccessor } from './use_profile_accessor'; -export { useRootProfile } from './use_root_profile'; +export { useRootProfile, BaseAppWrapper } from './use_root_profile'; export { useAdditionalCellActions } from './use_additional_cell_actions'; diff --git a/src/plugins/discover/public/context_awareness/hooks/use_root_profile.tsx b/src/plugins/discover/public/context_awareness/hooks/use_root_profile.tsx index 4e22c3b9f4152..bf20d6ba58a97 100644 --- a/src/plugins/discover/public/context_awareness/hooks/use_root_profile.tsx +++ b/src/plugins/discover/public/context_awareness/hooks/use_root_profile.tsx @@ -50,4 +50,4 @@ export const useRootProfile = () => { return rootProfileState; }; -const BaseAppWrapper: Profile['getRenderAppWrapper'] = ({ children }) => <>{children}; +export const BaseAppWrapper: Profile['getRenderAppWrapper'] = ({ children }) => <>{children}; diff --git a/src/plugins/discover/public/context_awareness/index.ts b/src/plugins/discover/public/context_awareness/index.ts index fcaec25c0f247..61d829d4e5c5c 100644 --- a/src/plugins/discover/public/context_awareness/index.ts +++ b/src/plugins/discover/public/context_awareness/index.ts @@ -11,4 +11,9 @@ export * from './types'; export * from './profiles'; export { getMergedAccessor } from './composable_profile'; export { ProfilesManager } from './profiles_manager'; -export { useProfileAccessor, useRootProfile, useAdditionalCellActions } from './hooks'; +export { + useProfileAccessor, + useRootProfile, + useAdditionalCellActions, + BaseAppWrapper, +} from './hooks'; diff --git a/src/plugins/discover/public/embeddable/get_search_embeddable_factory.test.tsx b/src/plugins/discover/public/embeddable/get_search_embeddable_factory.test.tsx index 1c8b77982fb24..b1c589f3e1d84 100644 --- a/src/plugins/discover/public/embeddable/get_search_embeddable_factory.test.tsx +++ b/src/plugins/discover/public/embeddable/get_search_embeddable_factory.test.tsx @@ -238,7 +238,7 @@ describe('saved search embeddable', () => { await waitOneTick(); // wait for build to complete expect(resolveRootProfileSpy).toHaveBeenCalledWith({ solutionNavId: 'test' }); - resolveRootProfileSpy.mockReset(); + resolveRootProfileSpy.mockClear(); expect(resolveRootProfileSpy).not.toHaveBeenCalled(); }); diff --git a/src/plugins/discover/public/embeddable/get_search_embeddable_factory.tsx b/src/plugins/discover/public/embeddable/get_search_embeddable_factory.tsx index 549b42c8a6cbe..37213b17c377d 100644 --- a/src/plugins/discover/public/embeddable/get_search_embeddable_factory.tsx +++ b/src/plugins/discover/public/embeddable/get_search_embeddable_factory.tsx @@ -42,6 +42,7 @@ import { SearchEmbeddableSerializedState, } from './types'; import { deserializeState, serializeState } from './utils/serialization_utils'; +import { BaseAppWrapper } from '../context_awareness'; export const getSearchEmbeddableFactory = ({ startServices, @@ -69,7 +70,10 @@ export const getSearchEmbeddableFactory = ({ const solutionNavId = await firstValueFrom( discoverServices.core.chrome.getActiveSolutionNavId$() ); - await discoverServices.profilesManager.resolveRootProfile({ solutionNavId }); + const { getRenderAppWrapper } = await discoverServices.profilesManager.resolveRootProfile({ + solutionNavId, + }); + const AppWrapper = getRenderAppWrapper?.(BaseAppWrapper) ?? BaseAppWrapper; /** Specific by-reference state */ const savedObjectId$ = new BehaviorSubject(initialState?.savedObjectId); @@ -280,30 +284,32 @@ export const getSearchEmbeddableFactory = ({ return ( - {renderAsFieldStatsTable ? ( - - ) : ( - - + {renderAsFieldStatsTable ? ( + - - )} + ) : ( + + + + )} + ); diff --git a/test/functional/apps/discover/context_awareness/extensions/_get_render_app_wrapper.ts b/test/functional/apps/discover/context_awareness/extensions/_get_render_app_wrapper.ts index d040e8e9f3da6..b30d16c215044 100644 --- a/test/functional/apps/discover/context_awareness/extensions/_get_render_app_wrapper.ts +++ b/test/functional/apps/discover/context_awareness/extensions/_get_render_app_wrapper.ts @@ -12,19 +12,26 @@ import expect from '@kbn/expect'; import type { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const { common, discover, header, unifiedFieldList } = getPageObjects([ + const { common, discover, header, unifiedFieldList, dashboard } = getPageObjects([ 'common', 'discover', 'header', 'unifiedFieldList', + 'dashboard', ]); const testSubjects = getService('testSubjects'); const dataViews = getService('dataViews'); const dataGrid = getService('dataGrid'); const browser = getService('browser'); const retry = getService('retry'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const kibanaServer = getService('kibanaServer'); describe('extension getRenderAppWrapper', () => { + after(async () => { + await kibanaServer.savedObjects.clean({ types: ['search'] }); + }); + describe('ES|QL mode', () => { it('should allow clicking message cells to inspect the message', async () => { const state = kbnRison.encode({ @@ -52,6 +59,32 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); await testSubjects.click('euiFlyoutCloseButton'); await testSubjects.missingOrFail('exampleRootProfileFlyout'); + + // check Dashboard page + await discover.saveSearch('ES|QL app wrapper test'); + await dashboard.navigateToApp(); + await dashboard.gotoDashboardLandingPage(); + await dashboard.clickNewDashboard(); + await dashboardAddPanel.addSavedSearch('ES|QL app wrapper test'); + await header.waitUntilLoadingHasFinished(); + await dashboard.waitForRenderComplete(); + + messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 2); + await retry.try(async () => { + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + }); + message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is a debug log'); + messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 2); + await retry.try(async () => { + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is an error log'); + }); + await testSubjects.click('euiFlyoutCloseButton'); + await testSubjects.missingOrFail('exampleRootProfileFlyout'); }); }); @@ -88,6 +121,34 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.refresh(); await header.waitUntilLoadingHasFinished(); + messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 2); + await retry.try(async () => { + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + }); + message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is a debug log'); + messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 2); + await retry.try(async () => { + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is an error log'); + }); + await testSubjects.click('euiFlyoutCloseButton'); + await testSubjects.missingOrFail('exampleRootProfileFlyout'); + await browser.goBack(); + await discover.waitUntilSearchingHasFinished(); + + // check Dashboard page + await discover.saveSearch('Data view app wrapper test'); + await dashboard.navigateToApp(); + await dashboard.gotoDashboardLandingPage(); + await dashboard.clickNewDashboard(); + await dashboardAddPanel.addSavedSearch('Data view app wrapper test'); + await header.waitUntilLoadingHasFinished(); + await dashboard.waitForRenderComplete(); + messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 2); await retry.try(async () => { await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_render_app_wrapper.ts b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_render_app_wrapper.ts index 401006a65ba9d..a2a1d4d9156ae 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_render_app_wrapper.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_render_app_wrapper.ts @@ -10,11 +10,12 @@ import expect from '@kbn/expect'; import type { FtrProviderContext } from '../../../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const { common, discover, header, unifiedFieldList, svlCommonPage } = getPageObjects([ + const { common, discover, header, unifiedFieldList, dashboard, svlCommonPage } = getPageObjects([ 'common', 'discover', 'header', 'unifiedFieldList', + 'dashboard', 'svlCommonPage', ]); const testSubjects = getService('testSubjects'); @@ -22,12 +23,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dataGrid = getService('dataGrid'); const browser = getService('browser'); const retry = getService('retry'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const kibanaServer = getService('kibanaServer'); describe('extension getRenderAppWrapper', () => { before(async () => { await svlCommonPage.loginAsAdmin(); }); + after(async () => { + await kibanaServer.savedObjects.clean({ types: ['search'] }); + }); + describe('ES|QL mode', () => { it('should allow clicking message cells to inspect the message', async () => { const state = kbnRison.encode({ @@ -55,6 +62,32 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); await testSubjects.click('euiFlyoutCloseButton'); await testSubjects.missingOrFail('exampleRootProfileFlyout'); + + // check Dashboard page + await discover.saveSearch('ES|QL app wrapper test'); + await dashboard.navigateToApp(); + await dashboard.gotoDashboardLandingPage(); + await dashboard.clickNewDashboard(); + await dashboardAddPanel.addSavedSearch('ES|QL app wrapper test'); + await header.waitUntilLoadingHasFinished(); + await dashboard.waitForRenderComplete(); + + messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 2); + await retry.try(async () => { + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + }); + message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is a debug log'); + messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 2); + await retry.try(async () => { + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is an error log'); + }); + await testSubjects.click('euiFlyoutCloseButton'); + await testSubjects.missingOrFail('exampleRootProfileFlyout'); }); }); @@ -91,6 +124,34 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.refresh(); await header.waitUntilLoadingHasFinished(); + messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 2); + await retry.try(async () => { + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + }); + message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is a debug log'); + messageCell = await dataGrid.getCellElementExcludingControlColumns(1, 2); + await retry.try(async () => { + await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click(); + await testSubjects.existOrFail('exampleRootProfileFlyout'); + message = await testSubjects.find('exampleRootProfileCurrentMessage'); + expect(await message.getVisibleText()).to.be('This is an error log'); + }); + await testSubjects.click('euiFlyoutCloseButton'); + await testSubjects.missingOrFail('exampleRootProfileFlyout'); + await browser.goBack(); + await discover.waitUntilSearchingHasFinished(); + + // check Dashboard page + await discover.saveSearch('Data view app wrapper test'); + await dashboard.navigateToApp(); + await dashboard.gotoDashboardLandingPage(); + await dashboard.clickNewDashboard(); + await dashboardAddPanel.addSavedSearch('Data view app wrapper test'); + await header.waitUntilLoadingHasFinished(); + await dashboard.waitForRenderComplete(); + messageCell = await dataGrid.getCellElementExcludingControlColumns(0, 2); await retry.try(async () => { await (await messageCell.findByTestSubject('exampleDataSourceProfileMessage')).click();