diff --git a/packages/react/kibana_context/render/render_provider.tsx b/packages/react/kibana_context/render/render_provider.tsx index 233abb42834b9..345a0993bb9e4 100644 --- a/packages/react/kibana_context/render/render_provider.tsx +++ b/packages/react/kibana_context/render/render_provider.tsx @@ -25,11 +25,11 @@ export type KibanaRenderContextProviderProps = Omit > = ({ children, ...props }) => { - const { analytics, i18n, theme, userProfile, colorMode, modify } = props; + const { analytics, executionContext, i18n, theme, userProfile, colorMode, modify } = props; return ( {children} diff --git a/packages/react/kibana_context/root/root_provider.test.tsx b/packages/react/kibana_context/root/root_provider.test.tsx index 8c8dbf38acfad..fc800708a8441 100644 --- a/packages/react/kibana_context/root/root_provider.test.tsx +++ b/packages/react/kibana_context/root/root_provider.test.tsx @@ -15,6 +15,8 @@ import { useEuiTheme } from '@elastic/eui'; import type { UseEuiTheme } from '@elastic/eui'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import type { KibanaTheme } from '@kbn/react-kibana-context-common'; +import type { ExecutionContextStart } from '@kbn/core-execution-context-browser'; +import { executionContextServiceMock } from '@kbn/core-execution-context-browser-mocks'; import { i18nServiceMock } from '@kbn/core-i18n-browser-mocks'; import { I18nStart } from '@kbn/core-i18n-browser'; import type { UserProfileService } from '@kbn/core-user-profile-browser'; @@ -25,11 +27,13 @@ describe('KibanaRootContextProvider', () => { let euiTheme: UseEuiTheme | undefined; let i18nMock: I18nStart; let userProfile: UserProfileService; + let executionContext: ExecutionContextStart; beforeEach(() => { euiTheme = undefined; i18nMock = i18nServiceMock.createStartContract(); userProfile = userProfileServiceMock.createStart(); + executionContext = executionContextServiceMock.createStartContract(); }); const flushPromises = async () => { @@ -64,6 +68,7 @@ describe('KibanaRootContextProvider', () => { @@ -82,6 +87,7 @@ describe('KibanaRootContextProvider', () => { diff --git a/packages/react/kibana_context/root/root_provider.tsx b/packages/react/kibana_context/root/root_provider.tsx index 386a67fb005e3..be8ddfa3f95ae 100644 --- a/packages/react/kibana_context/root/root_provider.tsx +++ b/packages/react/kibana_context/root/root_provider.tsx @@ -11,6 +11,8 @@ import React, { FC, PropsWithChildren } from 'react'; import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser'; import type { I18nStart } from '@kbn/core-i18n-browser'; +import type { ExecutionContextStart } from '@kbn/core-execution-context-browser'; +import { SharedUXRouterContext } from '@kbn/shared-ux-router'; // @ts-expect-error EUI exports this component internally, but Kibana isn't picking it up its types import { useIsNestedEuiProvider } from '@elastic/eui/lib/components/provider/nested'; @@ -25,6 +27,8 @@ export interface KibanaRootContextProviderProps extends KibanaEuiProviderProps { i18n: I18nStart; /** The `AnalyticsServiceStart` API from `CoreStart`. */ analytics?: Pick; + /** The `ExecutionContextStart` API from `CoreStart`. */ + executionContext?: ExecutionContextStart; } /** @@ -44,20 +48,26 @@ export interface KibanaRootContextProviderProps extends KibanaEuiProviderProps { export const KibanaRootContextProvider: FC> = ({ children, i18n, + executionContext, ...props }) => { const hasEuiProvider = useIsNestedEuiProvider(); + const rootContextProvider = ( + + {children} + + ); if (hasEuiProvider) { emitEuiProviderWarning( 'KibanaRootContextProvider has likely been nested in this React tree, either by direct reference or by KibanaRenderContextProvider. The result of this nesting is a nesting of EuiProvider, which has negative effects. Check your React tree for nested Kibana context providers.' ); - return {children}; + return rootContextProvider; } else { const { theme, userProfile, globalStyles, colorMode, modify } = props; return ( - {children} + {rootContextProvider} ); } diff --git a/packages/react/kibana_context/root/tsconfig.json b/packages/react/kibana_context/root/tsconfig.json index a7606025552b8..8035f8379e390 100644 --- a/packages/react/kibana_context/root/tsconfig.json +++ b/packages/react/kibana_context/root/tsconfig.json @@ -24,5 +24,8 @@ "@kbn/core-analytics-browser", "@kbn/core-user-profile-browser", "@kbn/core-user-profile-browser-mocks", + "@kbn/core-execution-context-browser", + "@kbn/core-execution-context-browser-mocks", + "@kbn/shared-ux-router" ] } diff --git a/packages/shared-ux/router/impl/BUILD.bazel b/packages/shared-ux/router/impl/BUILD.bazel new file mode 100644 index 0000000000000..224bebcf72e4a --- /dev/null +++ b/packages/shared-ux/router/impl/BUILD.bazel @@ -0,0 +1,34 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") + +SRCS = glob( + [ + "**/*.ts", + "**/*.tsx", + ], + exclude = [ + "**/test_helpers.ts", + "**/*.config.js", + "**/*.mock.*", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__/**", + "**/integration_tests/**", + "**/mocks/**", + "**/scripts/**", + "**/storybook/**", + "**/test_fixtures/**", + "**/test_helpers/**", + ], +) + +DEPS = [ + +] + +js_library( + name = "shared-ux-router", + package_name = "@kbn/shared-ux-router", + srcs = ["package.json"] + SRCS, + deps = DEPS, + visibility = ["//visibility:public"], +) diff --git a/packages/shared-ux/router/impl/__snapshots__/route.test.tsx.snap b/packages/shared-ux/router/impl/__snapshots__/route.test.tsx.snap index 418aa60b7c1f4..b7ef595289233 100644 --- a/packages/shared-ux/router/impl/__snapshots__/route.test.tsx.snap +++ b/packages/shared-ux/router/impl/__snapshots__/route.test.tsx.snap @@ -33,3 +33,5 @@ exports[`Route renders 1`] = ` `; + +exports[`Route renders with enableExecutionContextTracking as false 1`] = ``; diff --git a/packages/shared-ux/router/impl/index.ts b/packages/shared-ux/router/impl/index.ts index a8bef1b8499df..253d42beaa501 100644 --- a/packages/shared-ux/router/impl/index.ts +++ b/packages/shared-ux/router/impl/index.ts @@ -10,3 +10,5 @@ export { Route } from './route'; export { HashRouter, BrowserRouter, MemoryRouter, Router } from './router'; export { Routes } from './routes'; + +export { SharedUXRouterContext } from './services'; diff --git a/packages/shared-ux/router/impl/route.test.tsx b/packages/shared-ux/router/impl/route.test.tsx index 96bb6130387af..9d21690cdcf4f 100644 --- a/packages/shared-ux/router/impl/route.test.tsx +++ b/packages/shared-ux/router/impl/route.test.tsx @@ -9,15 +9,34 @@ import React, { Component, FC } from 'react'; import { shallow } from 'enzyme'; +import { useSharedUXRoutesContext } from './routes_context'; import { Route } from './route'; import { createMemoryHistory } from 'history'; +jest.mock('./routes_context', () => ({ + useSharedUXRoutesContext: jest.fn().mockImplementation(() => ({ + enableExecutionContextTracking: true, + })), +})); + describe('Route', () => { + beforeEach(() => { + jest.restoreAllMocks(); + }); + test('renders', () => { const example = shallow(); expect(example).toMatchSnapshot(); }); + test('renders with enableExecutionContextTracking as false', () => { + (useSharedUXRoutesContext as jest.Mock).mockImplementationOnce(() => ({ + enableExecutionContextTracking: false, + })); + const example = shallow(); + expect(example).toMatchSnapshot(); + }); + test('location renders as expected', () => { // create a history const historyLocation = createMemoryHistory(); diff --git a/packages/shared-ux/router/impl/route.tsx b/packages/shared-ux/router/impl/route.tsx index 3535ff7aecec8..5041f872b71b1 100644 --- a/packages/shared-ux/router/impl/route.tsx +++ b/packages/shared-ux/router/impl/route.tsx @@ -15,6 +15,7 @@ import { RouteProps, useRouteMatch, } from 'react-router-dom'; +import { useSharedUXRoutesContext } from './routes_context'; import { useKibanaSharedUX } from './services'; import { useSharedUXExecutionContext } from './use_execution_context'; @@ -30,17 +31,18 @@ export const Route = ({ render, ...rest }: RouteProps) => { + const { enableExecutionContextTracking } = useSharedUXRoutesContext(); const component = useMemo(() => { if (!Component) { return undefined; } return (props: RouteComponentProps) => ( <> - + {enableExecutionContextTracking && } ); - }, [Component]); + }, [Component, enableExecutionContextTracking]); if (component) { return ; @@ -52,7 +54,7 @@ export const Route = ({ {...rest} render={(props) => ( <> - + {enableExecutionContextTracking && } {/* @ts-ignore else condition exists if renderFunction is undefined*/} {renderFunction(props)} @@ -62,7 +64,7 @@ export const Route = ({ } return ( - + {enableExecutionContextTracking && } {children} ); @@ -75,6 +77,12 @@ export const MatchPropagator = () => { const { executionContext } = useKibanaSharedUX().services; const match = useRouteMatch(); + if (!executionContext && process.env.NODE_ENV !== 'production') { + throw new Error( + 'Default execution context tracking is enabled but the executionContext service is not available' + ); + } + useSharedUXExecutionContext(executionContext, { type: 'application', page: match.path, diff --git a/packages/shared-ux/router/impl/routes.tsx b/packages/shared-ux/router/impl/routes.tsx index 9c1a97de65830..9377562246276 100644 --- a/packages/shared-ux/router/impl/routes.tsx +++ b/packages/shared-ux/router/impl/routes.tsx @@ -13,6 +13,7 @@ import React, { Children } from 'react'; import { Switch, useRouteMatch } from 'react-router-dom'; import { Routes as ReactRouterRoutes, Route } from 'react-router-dom-v5-compat'; import { Route as LegacyRoute, MatchPropagator } from './route'; +import { SharedUXRoutesContext } from './routes_context'; type RouterElementChildren = Array< React.ReactElement< @@ -28,42 +29,47 @@ type RouterElementChildren = Array< export const Routes = ({ legacySwitch = true, + enableExecutionContextTracking = false, children, }: { legacySwitch?: boolean; + enableExecutionContextTracking?: boolean; children: React.ReactNode; }) => { const match = useRouteMatch(); return legacySwitch ? ( - {children} + + {children} + ) : ( - - {Children.map(children as RouterElementChildren, (child) => { - if (React.isValidElement(child) && child.type === LegacyRoute) { - const path = replace(child?.props.path, match.url + '/', ''); - const renderFunction = - typeof child?.props.children === 'function' - ? child?.props.children - : child?.props.render; - - return ( - - - {(child?.props?.component && ) || - (renderFunction && renderFunction()) || - children} - - } - /> - ); - } else { - return child; - } - })} - + + + {Children.map(children as RouterElementChildren, (child) => { + if (React.isValidElement(child) && child.type === LegacyRoute) { + const path = replace(child?.props.path, match.url + '/', ''); + const renderFunction = + typeof child?.props.children === 'function' + ? child?.props.children + : child?.props.render; + return ( + + {enableExecutionContextTracking && } + {(child?.props?.component && ) || + (renderFunction && renderFunction()) || + children} + + } + /> + ); + } else { + return child; + } + })} + + ); }; diff --git a/packages/shared-ux/router/impl/routes_context.ts b/packages/shared-ux/router/impl/routes_context.ts new file mode 100644 index 0000000000000..115a5df7780d5 --- /dev/null +++ b/packages/shared-ux/router/impl/routes_context.ts @@ -0,0 +1,18 @@ +/* + * 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'; +import { SharedUXRoutesContextType } from './types'; + +const defaultContextValue = {}; + +export const SharedUXRoutesContext = createContext(defaultContextValue); + +export const useSharedUXRoutesContext = (): SharedUXRoutesContextType => + useContext(SharedUXRoutesContext as unknown as React.Context); diff --git a/packages/shared-ux/router/impl/services.ts b/packages/shared-ux/router/impl/services.ts index c3ad2ab06e06d..7cea72f03bd82 100644 --- a/packages/shared-ux/router/impl/services.ts +++ b/packages/shared-ux/router/impl/services.ts @@ -50,7 +50,7 @@ export interface SharedUXExecutionContextSetup { */ export interface SharedUXExecutionContextSetup { /** {@link SharedUXExecutionContextSetup} */ - executionContext: SharedUXExecutionContextStart; + executionContext?: SharedUXExecutionContextStart; } export type KibanaServices = Partial; @@ -63,12 +63,14 @@ const defaultContextValue = { services: {}, }; -export const sharedUXContext = +export const SharedUXRouterContext = createContext>(defaultContextValue); export const useKibanaSharedUX = (): SharedUXRouterContextValue< KibanaServices & Extra > => useContext( - sharedUXContext as unknown as React.Context> + SharedUXRouterContext as unknown as React.Context< + SharedUXRouterContextValue + > ); diff --git a/packages/shared-ux/router/impl/types.ts b/packages/shared-ux/router/impl/types.ts index 833c5bdd0c816..067677d152cae 100644 --- a/packages/shared-ux/router/impl/types.ts +++ b/packages/shared-ux/router/impl/types.ts @@ -33,3 +33,11 @@ export declare interface SharedUXExecutionContext { /** an inner context spawned from the current context. */ child?: SharedUXExecutionContext; } + +export declare interface SharedUXRoutesContextType { + /** + * This flag is used to enable the default execution context tracking for a specific router. + * Enable this flag in case you don't have a custom implementation for execution context tracking. + * */ + readonly enableExecutionContextTracking?: boolean; +} diff --git a/packages/shared-ux/router/impl/use_execution_context.ts b/packages/shared-ux/router/impl/use_execution_context.ts index d460d4df30805..5e088fc3eac77 100644 --- a/packages/shared-ux/router/impl/use_execution_context.ts +++ b/packages/shared-ux/router/impl/use_execution_context.ts @@ -8,7 +8,7 @@ */ import useDeepCompareEffect from 'react-use/lib/useDeepCompareEffect'; -import { SharedUXExecutionContextSetup } from './services'; +import type { SharedUXExecutionContextSetup } from './services'; import { SharedUXExecutionContext } from './types'; /** diff --git a/x-pack/solutions/observability/plugins/observability/public/application/index.tsx b/x-pack/solutions/observability/plugins/observability/public/application/index.tsx index 3ae624d2f8ea1..54b8b4044e64e 100644 --- a/x-pack/solutions/observability/plugins/observability/public/application/index.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/application/index.tsx @@ -28,7 +28,7 @@ import { HideableReactQueryDevTools } from './hideable_react_query_dev_tools'; function App() { return ( <> - + {Object.keys(routes).map((key) => { const path = key as keyof typeof routes; const { handler, exact } = routes[path];