Skip to content

Commit

Permalink
[8.x] Fix context.pageName by fixing missing executionContext and add…
Browse files Browse the repository at this point in the history
… enableExecutionContextTracking flag (#204547) (#204798)

# Backport

This will backport the following commits from `main` to `8.x`:
- [Fix context.pageName by fixing missing executionContext and add
enableExecutionContextTracking flag
(#204547)](#204547)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Maryam
Saeidi","email":"[email protected]"},"sourceCommit":{"committedDate":"2024-12-18T12:59:23Z","message":"Fix
context.pageName by fixing missing executionContext and add
enableExecutionContextTracking flag (#204547)\n\nResolves
https://github.com/elastic/kibana/issues/195778\r\n\r\n## 🐞
Summary\r\nThis PR fixes missing executionContext in sharedux router by
adding\r\n`SharedUXContext` to the `KibanaRootContextProvider` (inside
of the\r\n`KibanaRenderContextProvider`). (More context provided in
this\r\nhttps://github.com//issues/195778#issuecomment-2426936142)\r\n\r\nIt
also introduces `enableExecutionContextTracking` to enable
tracking\r\nlogic to avoid creating a race condition for the existing
custom\r\nexecution context tracking implementations.\r\n\r\nI enabled
this flag for the observability plugin and here is
the\r\ndifference:\r\n\r\n|Item|Screenshot|\r\n|---|---|\r\n\r\n|Before|![image](https://github.com/user-attachments/assets/83283d23-3347-45be-95c1-120cdfabb9c5)|\r\n\r\n|After|![image](https://github.com/user-attachments/assets/9de51645-6bf1-4537-baeb-6878e7bb3590)|\r\n\r\n###
🧪 How to test\r\n- Go to the observability alerts page and check the
kibana-browser\r\nrequest as shown above\r\n\r\n### ✨ Possible future
improvements\r\n\r\nAllowing this logic to be provided by the consumer
so that we can get\r\nrid of custom implementations (Example: [APM
custom
execution\r\ncontext](https://github.com/elastic/kibana/blob/e9671937bacfaa911d32de0e8885e7f26425888a/x-pack/plugins/observability_solution/apm/public/components/routing/app_root/update_execution_context_on_route_change.ts#L21,L25))\r\n\r\n---------\r\n\r\nCo-authored-by:
Anton Dosov <[email protected]>\r\nCo-authored-by: Davis McPhee
<[email protected]>\r\nCo-authored-by: Marco Antonio Ghiani
<[email protected]>\r\nCo-authored-by: Elena Stoeva
<[email protected]>","sha":"53748fdefa1d59d58a4708258a1476dc140b1588","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:prev-minor","Team:obs-ux-management"],"number":204547,"url":"https://github.com/elastic/kibana/pull/204547","mergeCommit":{"message":"Fix
context.pageName by fixing missing executionContext and add
enableExecutionContextTracking flag (#204547)\n\nResolves
https://github.com/elastic/kibana/issues/195778\r\n\r\n## 🐞
Summary\r\nThis PR fixes missing executionContext in sharedux router by
adding\r\n`SharedUXContext` to the `KibanaRootContextProvider` (inside
of the\r\n`KibanaRenderContextProvider`). (More context provided in
this\r\nhttps://github.com//issues/195778#issuecomment-2426936142)\r\n\r\nIt
also introduces `enableExecutionContextTracking` to enable
tracking\r\nlogic to avoid creating a race condition for the existing
custom\r\nexecution context tracking implementations.\r\n\r\nI enabled
this flag for the observability plugin and here is
the\r\ndifference:\r\n\r\n|Item|Screenshot|\r\n|---|---|\r\n\r\n|Before|![image](https://github.com/user-attachments/assets/83283d23-3347-45be-95c1-120cdfabb9c5)|\r\n\r\n|After|![image](https://github.com/user-attachments/assets/9de51645-6bf1-4537-baeb-6878e7bb3590)|\r\n\r\n###
🧪 How to test\r\n- Go to the observability alerts page and check the
kibana-browser\r\nrequest as shown above\r\n\r\n### ✨ Possible future
improvements\r\n\r\nAllowing this logic to be provided by the consumer
so that we can get\r\nrid of custom implementations (Example: [APM
custom
execution\r\ncontext](https://github.com/elastic/kibana/blob/e9671937bacfaa911d32de0e8885e7f26425888a/x-pack/plugins/observability_solution/apm/public/components/routing/app_root/update_execution_context_on_route_change.ts#L21,L25))\r\n\r\n---------\r\n\r\nCo-authored-by:
Anton Dosov <[email protected]>\r\nCo-authored-by: Davis McPhee
<[email protected]>\r\nCo-authored-by: Marco Antonio Ghiani
<[email protected]>\r\nCo-authored-by: Elena Stoeva
<[email protected]>","sha":"53748fdefa1d59d58a4708258a1476dc140b1588"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/204547","number":204547,"mergeCommit":{"message":"Fix
context.pageName by fixing missing executionContext and add
enableExecutionContextTracking flag (#204547)\n\nResolves
https://github.com/elastic/kibana/issues/195778\r\n\r\n## 🐞
Summary\r\nThis PR fixes missing executionContext in sharedux router by
adding\r\n`SharedUXContext` to the `KibanaRootContextProvider` (inside
of the\r\n`KibanaRenderContextProvider`). (More context provided in
this\r\nhttps://github.com//issues/195778#issuecomment-2426936142)\r\n\r\nIt
also introduces `enableExecutionContextTracking` to enable
tracking\r\nlogic to avoid creating a race condition for the existing
custom\r\nexecution context tracking implementations.\r\n\r\nI enabled
this flag for the observability plugin and here is
the\r\ndifference:\r\n\r\n|Item|Screenshot|\r\n|---|---|\r\n\r\n|Before|![image](https://github.com/user-attachments/assets/83283d23-3347-45be-95c1-120cdfabb9c5)|\r\n\r\n|After|![image](https://github.com/user-attachments/assets/9de51645-6bf1-4537-baeb-6878e7bb3590)|\r\n\r\n###
🧪 How to test\r\n- Go to the observability alerts page and check the
kibana-browser\r\nrequest as shown above\r\n\r\n### ✨ Possible future
improvements\r\n\r\nAllowing this logic to be provided by the consumer
so that we can get\r\nrid of custom implementations (Example: [APM
custom
execution\r\ncontext](https://github.com/elastic/kibana/blob/e9671937bacfaa911d32de0e8885e7f26425888a/x-pack/plugins/observability_solution/apm/public/components/routing/app_root/update_execution_context_on_route_change.ts#L21,L25))\r\n\r\n---------\r\n\r\nCo-authored-by:
Anton Dosov <[email protected]>\r\nCo-authored-by: Davis McPhee
<[email protected]>\r\nCo-authored-by: Marco Antonio Ghiani
<[email protected]>\r\nCo-authored-by: Elena Stoeva
<[email protected]>","sha":"53748fdefa1d59d58a4708258a1476dc140b1588"}}]}]
BACKPORT-->
  • Loading branch information
maryam-saeidi authored Dec 19, 2024
1 parent 7dcfba5 commit 1665216
Show file tree
Hide file tree
Showing 15 changed files with 159 additions and 41 deletions.
4 changes: 2 additions & 2 deletions packages/react/kibana_context/render/render_provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ export type KibanaRenderContextProviderProps = Omit<KibanaRootContextProviderPro
export const KibanaRenderContextProvider: FC<
PropsWithChildren<KibanaRenderContextProviderProps>
> = ({ children, ...props }) => {
const { analytics, i18n, theme, userProfile, colorMode, modify } = props;
const { analytics, executionContext, i18n, theme, userProfile, colorMode, modify } = props;
return (
<KibanaRootContextProvider
globalStyles={false}
{...{ i18n, theme, userProfile, modify, colorMode }}
{...{ executionContext, i18n, theme, userProfile, modify, colorMode }}
>
<KibanaErrorBoundaryProvider analytics={analytics}>
<KibanaErrorBoundary>{children}</KibanaErrorBoundary>
Expand Down
6 changes: 6 additions & 0 deletions packages/react/kibana_context/root/root_provider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 () => {
Expand Down Expand Up @@ -64,6 +68,7 @@ describe('KibanaRootContextProvider', () => {
<KibanaRootContextProvider
i18n={i18nMock}
userProfile={userProfile}
executionContext={executionContext}
theme={{ theme$: of(coreTheme) }}
>
<InnerComponent />
Expand All @@ -82,6 +87,7 @@ describe('KibanaRootContextProvider', () => {
<KibanaRootContextProvider
i18n={i18nMock}
userProfile={userProfile}
executionContext={executionContext}
theme={{ theme$: coreTheme$ }}
>
<InnerComponent />
Expand Down
14 changes: 12 additions & 2 deletions packages/react/kibana_context/root/root_provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -25,6 +27,8 @@ export interface KibanaRootContextProviderProps extends KibanaEuiProviderProps {
i18n: I18nStart;
/** The `AnalyticsServiceStart` API from `CoreStart`. */
analytics?: Pick<AnalyticsServiceStart, 'reportEvent'>;
/** The `ExecutionContextStart` API from `CoreStart`. */
executionContext?: ExecutionContextStart;
}

/**
Expand All @@ -44,20 +48,26 @@ export interface KibanaRootContextProviderProps extends KibanaEuiProviderProps {
export const KibanaRootContextProvider: FC<PropsWithChildren<KibanaRootContextProviderProps>> = ({
children,
i18n,
executionContext,
...props
}) => {
const hasEuiProvider = useIsNestedEuiProvider();
const rootContextProvider = (
<SharedUXRouterContext.Provider value={{ services: { executionContext } }}>
<i18n.Context>{children}</i18n.Context>
</SharedUXRouterContext.Provider>
);

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 <i18n.Context>{children}</i18n.Context>;
return rootContextProvider;
} else {
const { theme, userProfile, globalStyles, colorMode, modify } = props;
return (
<KibanaEuiProvider {...{ theme, userProfile, globalStyles, colorMode, modify }}>
<i18n.Context>{children}</i18n.Context>
{rootContextProvider}
</KibanaEuiProvider>
);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/react/kibana_context/root/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
34 changes: 34 additions & 0 deletions packages/shared-ux/router/impl/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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"],
)

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/shared-ux/router/impl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@
export { Route } from './route';
export { HashRouter, BrowserRouter, MemoryRouter, Router } from './router';
export { Routes } from './routes';

export { SharedUXRouterContext } from './services';
19 changes: 19 additions & 0 deletions packages/shared-ux/router/impl/route.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Route />);
expect(example).toMatchSnapshot();
});

test('renders with enableExecutionContextTracking as false', () => {
(useSharedUXRoutesContext as jest.Mock).mockImplementationOnce(() => ({
enableExecutionContextTracking: false,
}));
const example = shallow(<Route />);
expect(example).toMatchSnapshot();
});

test('location renders as expected', () => {
// create a history
const historyLocation = createMemoryHistory();
Expand Down
16 changes: 12 additions & 4 deletions packages/shared-ux/router/impl/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -30,17 +31,18 @@ export const Route = <T extends {}>({
render,
...rest
}: RouteProps<string, { [K: string]: string } & T>) => {
const { enableExecutionContextTracking } = useSharedUXRoutesContext();
const component = useMemo(() => {
if (!Component) {
return undefined;
}
return (props: RouteComponentProps) => (
<>
<MatchPropagator />
{enableExecutionContextTracking && <MatchPropagator />}
<Component {...props} />
</>
);
}, [Component]);
}, [Component, enableExecutionContextTracking]);

if (component) {
return <ReactRouterRoute {...rest} component={component} />;
Expand All @@ -52,7 +54,7 @@ export const Route = <T extends {}>({
{...rest}
render={(props) => (
<>
<MatchPropagator />
{enableExecutionContextTracking && <MatchPropagator />}
{/* @ts-ignore else condition exists if renderFunction is undefined*/}
{renderFunction(props)}
</>
Expand All @@ -62,7 +64,7 @@ export const Route = <T extends {}>({
}
return (
<ReactRouterRoute {...rest}>
<MatchPropagator />
{enableExecutionContextTracking && <MatchPropagator />}
{children}
</ReactRouterRoute>
);
Expand All @@ -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,
Expand Down
62 changes: 34 additions & 28 deletions packages/shared-ux/router/impl/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand All @@ -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 ? (
<Switch>{children}</Switch>
<SharedUXRoutesContext.Provider value={{ enableExecutionContextTracking }}>
<Switch>{children}</Switch>
</SharedUXRoutesContext.Provider>
) : (
<ReactRouterRoutes>
{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 (
<Route
path={path}
element={
<>
<MatchPropagator />
{(child?.props?.component && <child.props.component />) ||
(renderFunction && renderFunction()) ||
children}
</>
}
/>
);
} else {
return child;
}
})}
</ReactRouterRoutes>
<SharedUXRoutesContext.Provider value={{ enableExecutionContextTracking }}>
<ReactRouterRoutes>
{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 (
<Route
path={path}
element={
<>
{enableExecutionContextTracking && <MatchPropagator />}
{(child?.props?.component && <child.props.component />) ||
(renderFunction && renderFunction()) ||
children}
</>
}
/>
);
} else {
return child;
}
})}
</ReactRouterRoutes>
</SharedUXRoutesContext.Provider>
);
};
18 changes: 18 additions & 0 deletions packages/shared-ux/router/impl/routes_context.ts
Original file line number Diff line number Diff line change
@@ -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<SharedUXRoutesContextType>(defaultContextValue);

export const useSharedUXRoutesContext = (): SharedUXRoutesContextType =>
useContext(SharedUXRoutesContext as unknown as React.Context<SharedUXRoutesContextType>);
8 changes: 5 additions & 3 deletions packages/shared-ux/router/impl/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export interface SharedUXExecutionContextSetup {
*/
export interface SharedUXExecutionContextSetup {
/** {@link SharedUXExecutionContextSetup} */
executionContext: SharedUXExecutionContextStart;
executionContext?: SharedUXExecutionContextStart;
}

export type KibanaServices = Partial<SharedUXExecutionContextSetup>;
Expand All @@ -63,12 +63,14 @@ const defaultContextValue = {
services: {},
};

export const sharedUXContext =
export const SharedUXRouterContext =
createContext<SharedUXRouterContextValue<KibanaServices>>(defaultContextValue);

export const useKibanaSharedUX = <Extra extends object = {}>(): SharedUXRouterContextValue<
KibanaServices & Extra
> =>
useContext(
sharedUXContext as unknown as React.Context<SharedUXRouterContextValue<KibanaServices & Extra>>
SharedUXRouterContext as unknown as React.Context<
SharedUXRouterContextValue<KibanaServices & Extra>
>
);
8 changes: 8 additions & 0 deletions packages/shared-ux/router/impl/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading

0 comments on commit 1665216

Please sign in to comment.