[EuiProvider] Fix Gis-Presentation-Visualization code (#183875)

## Summary

Fixes needed for getting CI to pass when EUI throws an error if
attempting to render a component without the EuiProvider in the render
tree.

## Detailed description
In https://github.com/elastic/kibana/pull/180819, I will deliver a
change that will cause EUI components to throw an error if the
EuiProvider context is missing. This PR comes in as part of the final
work to get all functional tests passing in an environment where EUI
will throw the error. The tied to the ["Fix 'dark mode' inconsistencies
in Kibana" Epic](https://github.com/elastic/kibana-team/issues/805) has
so far been in preparation for this.

**Reviewers: Please interact with critical paths through the UI
components touched in this PR, ESPECIALLY in terms of testing dark mode
and i18n.**

<img width="1107" alt="image"
src="c0d2ce08-ac35-45a7-8192-0b2256fceb0e">

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
This commit is contained in:
Tim Sullivan 2024-05-22 13:28:36 -07:00 committed by GitHub
parent ccefc645b3
commit c0696a8c58
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 194 additions and 126 deletions

View file

@ -18,11 +18,17 @@ import {
EuiText,
EuiLink,
} from '@elastic/eui';
import { AppMountParameters, IUiSettingsClient, ThemeServiceStart } from '@kbn/core/public';
import {
AppMountParameters,
I18nStart,
IUiSettingsClient,
ThemeServiceStart,
} from '@kbn/core/public';
import { ExpressionsStart } from '@kbn/expressions-plugin/public';
import { Start as InspectorStart } from '@kbn/inspector-plugin/public';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { SettingsStart } from '@kbn/core-ui-settings-browser';
import { RunExpressionsExample } from './run_expressions';
import { RenderExpressionsExample } from './render_expressions';
@ -36,6 +42,7 @@ interface Props {
uiSettings: IUiSettingsClient;
settings: SettingsStart;
theme: ThemeServiceStart;
i18n: I18nStart;
}
const ExpressionsExplorer = ({
@ -44,6 +51,7 @@ const ExpressionsExplorer = ({
actions,
uiSettings,
settings,
i18n,
theme,
}: Props) => {
const { Provider: KibanaReactContextProvider } = createKibanaReactContext({
@ -51,49 +59,52 @@ const ExpressionsExplorer = ({
settings,
theme,
});
const startServices = { i18n, theme };
return (
<KibanaReactContextProvider>
<EuiPage>
<EuiPageBody>
<EuiPageSection>
<EuiPageHeader pageTitle="Expressions Explorer" />
</EuiPageSection>
<EuiPageTemplate.Section>
<KibanaRenderContextProvider {...startServices}>
<KibanaReactContextProvider>
<EuiPage>
<EuiPageBody>
<EuiPageSection>
<EuiText>
<p>
There are a couple of ways to run the expressions. Below some of the options are
demonstrated. You can read more about it{' '}
<EuiLink
href={
'https://github.com/elastic/kibana/blob/main/src/plugins/expressions/README.asciidoc'
}
>
here
</EuiLink>
</p>
</EuiText>
<EuiSpacer />
<RunExpressionsExample expressions={expressions} inspector={inspector} />
<EuiSpacer />
<RenderExpressionsExample expressions={expressions} inspector={inspector} />
<EuiSpacer />
<ActionsExpressionsExample expressions={expressions} actions={actions} />
<EuiSpacer />
<ActionsExpressionsExample2 expressions={expressions} actions={actions} />
<EuiPageHeader pageTitle="Expressions Explorer" />
</EuiPageSection>
</EuiPageTemplate.Section>
</EuiPageBody>
</EuiPage>
</KibanaReactContextProvider>
<EuiPageTemplate.Section>
<EuiPageSection>
<EuiText>
<p>
There are a couple of ways to run the expressions. Below some of the options are
demonstrated. You can read more about it{' '}
<EuiLink
href={
'https://github.com/elastic/kibana/blob/main/src/plugins/expressions/README.asciidoc'
}
>
here
</EuiLink>
</p>
</EuiText>
<EuiSpacer />
<RunExpressionsExample expressions={expressions} inspector={inspector} />
<EuiSpacer />
<RenderExpressionsExample expressions={expressions} inspector={inspector} />
<EuiSpacer />
<ActionsExpressionsExample expressions={expressions} actions={actions} />
<EuiSpacer />
<ActionsExpressionsExample2 expressions={expressions} actions={actions} />
</EuiPageSection>
</EuiPageTemplate.Section>
</EuiPageBody>
</EuiPage>
</KibanaReactContextProvider>
</KibanaRenderContextProvider>
);
};

View file

@ -14,7 +14,7 @@ import { UiActionsStart, UiActionsSetup } from '@kbn/ui-actions-plugin/public';
import { getExpressionsInspectorViewDescription } from './inspector';
import { NAVIGATE_TRIGGER_ID, navigateTrigger } from './actions/navigate_trigger';
import { ACTION_NAVIGATE, createNavigateAction } from './actions/navigate_action';
import { buttonRenderer } from './renderers/button';
import { getButtonRenderer } from './renderers/button';
import { buttonFn } from './functions/button';
interface StartDeps {
@ -41,7 +41,7 @@ export class ExpressionsExplorerPlugin implements Plugin<void, void, SetupDeps,
deps.uiActions.attachAction(NAVIGATE_TRIGGER_ID, ACTION_NAVIGATE);
// register custom functions and renderers
deps.expressions.registerRenderer(buttonRenderer);
deps.expressions.registerRenderer(getButtonRenderer(core));
deps.expressions.registerFunction(buttonFn);
core.application.register({
@ -59,6 +59,7 @@ export class ExpressionsExplorerPlugin implements Plugin<void, void, SetupDeps,
uiSettings: core.uiSettings,
settings: core.settings,
theme: coreStart.theme,
i18n: coreStart.i18n,
},
params
);

View file

@ -9,41 +9,50 @@
import ReactDOM from 'react-dom';
import React from 'react';
import { EuiButton } from '@elastic/eui';
import { ExpressionRenderDefinition } from '@kbn/expressions-plugin/common/expression_renderers';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import type { ExpressionRenderDefinition } from '@kbn/expressions-plugin/common/expression_renderers';
import type { CoreSetup } from '@kbn/core-lifecycle-browser';
export const buttonRenderer: ExpressionRenderDefinition<any> = {
name: 'button',
displayName: 'Button',
reuseDomNode: true,
render(domNode, config, handlers) {
const buttonClick = () => {
handlers.event({
name: 'NAVIGATE',
data: {
href: config.href,
},
});
};
export const getButtonRenderer = (core: CoreSetup) => {
const buttonRenderer: ExpressionRenderDefinition<any> = {
name: 'button',
displayName: 'Button',
reuseDomNode: true,
async render(domNode, config, handlers) {
const [startServices] = await core.getStartServices();
const buttonClick = () => {
handlers.event({
name: 'NAVIGATE',
data: {
href: config.href,
},
});
};
const renderDebug = () => (
<div
style={{
width: domNode.offsetWidth,
height: domNode.offsetHeight,
}}
>
<EuiButton
data-test-subj="testExpressionButton"
onClick={buttonClick}
style={{ backgroundColor: config.color || 'white' }}
>
{config.name}
</EuiButton>
</div>
);
const renderDebug = () => (
<KibanaRenderContextProvider {...startServices}>
<div
style={{
width: domNode.offsetWidth,
height: domNode.offsetHeight,
}}
>
<EuiButton
data-test-subj="testExpressionButton"
onClick={buttonClick}
style={{ backgroundColor: config.color || 'white' }}
>
{config.name}
</EuiButton>
</div>
</KibanaRenderContextProvider>
);
ReactDOM.render(renderDebug(), domNode, () => handlers.done());
ReactDOM.render(renderDebug(), domNode, () => handlers.done());
handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode));
},
handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode));
},
};
return buttonRenderer;
};

View file

@ -23,5 +23,7 @@
"@kbn/i18n-react",
"@kbn/core-ui-settings-browser",
"@kbn/code-editor",
"@kbn/react-kibana-context-render",
"@kbn/core-lifecycle-browser",
]
}

View file

@ -19,12 +19,13 @@ export const getInputControlVisRenderer: (
name: 'input_control_vis',
reuseDomNode: true,
render: async (domNode, { visConfig }, handlers) => {
const [coreStart] = await deps.core.getStartServices();
let registeredController = inputControlVisRegistry.get(domNode);
if (!registeredController) {
const { createInputControlVisController } = await import('./vis_controller');
registeredController = createInputControlVisController(deps, handlers, domNode);
registeredController = createInputControlVisController(coreStart, deps, handlers, domNode);
inputControlVisRegistry.set(domNode, registeredController);
handlers.onDestroy(() => {

View file

@ -11,11 +11,12 @@ import { isEqual } from 'lodash';
import { render, unmountComponentAtNode } from 'react-dom';
import { Subscription } from 'rxjs';
import { I18nStart } from '@kbn/core/public';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { IInterpreterRenderHandlers } from '@kbn/expressions-plugin/common';
import { Filter } from '@kbn/es-query';
import { VisualizationContainer } from '@kbn/visualizations-plugin/public';
import { FilterManager } from '@kbn/data-plugin/public';
import { CoreStart } from '@kbn/core/public';
import { InputControlVis } from './components/vis/input_control_vis';
import { getControlFactory } from './control/control_factory';
@ -28,11 +29,11 @@ import { InputControlVisParams } from './types';
export type InputControlVisControllerType = ReturnType<typeof createInputControlVisController>;
export const createInputControlVisController = (
coreStart: CoreStart,
deps: InputControlVisDependencies,
handlers: IInterpreterRenderHandlers,
el: Element
) => {
let I18nContext: I18nStart['Context'] | undefined;
let isLoaded = false;
return new (class InputControlVisController {
@ -65,10 +66,6 @@ export const createInputControlVisController = (
}
async render(visParams: InputControlVisParams) {
if (!I18nContext) {
const [{ i18n }] = await deps.core.getStartServices();
I18nContext = i18n.Context;
}
if (!isLoaded || !isEqual(visParams, this.visParams)) {
this.visParams = visParams;
this.controls = [];
@ -86,12 +83,8 @@ export const createInputControlVisController = (
}
drawVis = () => {
if (!I18nContext) {
throw new Error('no i18n context found');
}
render(
<I18nContext>
<KibanaRenderContextProvider {...coreStart}>
<VisualizationContainer handlers={handlers}>
<InputControlVis
updateFiltersOnChange={this.visParams?.updateFiltersOnChange}
@ -106,7 +99,7 @@ export const createInputControlVisController = (
isDarkMode={this.isDarkMode}
/>
</VisualizationContainer>
</I18nContext>,
</KibanaRenderContextProvider>,
el
);
};

View file

@ -26,6 +26,7 @@
"@kbn/ui-actions-plugin",
"@kbn/embeddable-plugin",
"@kbn/presentation-publishing",
"@kbn/react-kibana-context-render",
],
"exclude": [
"target/**/*",

View file

@ -8,27 +8,43 @@
import React, { lazy } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { VisualizationContainer } from '@kbn/visualizations-plugin/public';
import { ExpressionRenderDefinition } from '@kbn/expressions-plugin/common/expression_renderers';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { StartServicesAccessor } from '@kbn/core-lifecycle-browser';
import { MarkdownVisRenderValue } from './markdown_fn';
/** @internal **/
export interface MarkdownVisRendererDependencies {
getStartDeps: StartServicesAccessor;
}
// @ts-ignore
const MarkdownVisComponent = lazy(() => import('./markdown_vis_controller'));
export const markdownVisRenderer: ExpressionRenderDefinition<MarkdownVisRenderValue> = {
export const getMarkdownVisRenderer: ({
getStartDeps,
}: MarkdownVisRendererDependencies) => ExpressionRenderDefinition<MarkdownVisRenderValue> = ({
getStartDeps,
}) => ({
name: 'markdown_vis',
displayName: 'markdown visualization',
reuseDomNode: true,
render: async (domNode, { visParams }, handlers) => {
const [core] = await getStartDeps();
handlers.onDestroy(() => {
unmountComponentAtNode(domNode);
});
render(
<VisualizationContainer className="markdownVis" handlers={handlers}>
<MarkdownVisComponent {...visParams} renderComplete={handlers.done} />
</VisualizationContainer>,
<KibanaRenderContextProvider {...core}>
<VisualizationContainer className="markdownVis" handlers={handlers}>
<MarkdownVisComponent {...visParams} renderComplete={handlers.done} />
</VisualizationContainer>
</KibanaRenderContextProvider>,
domNode
);
},
};
});

View file

@ -13,7 +13,7 @@ import { VisualizationsSetup } from '@kbn/visualizations-plugin/public';
import { markdownVisDefinition } from './markdown_vis';
import { createMarkdownVisFn } from './markdown_fn';
import { ConfigSchema } from '../config';
import { markdownVisRenderer } from './markdown_renderer';
import { getMarkdownVisRenderer } from './markdown_renderer';
/** @internal */
export interface MarkdownPluginSetupDependencies {
@ -31,7 +31,7 @@ export class MarkdownPlugin implements Plugin<void, void> {
public setup(core: CoreSetup, { expressions, visualizations }: MarkdownPluginSetupDependencies) {
visualizations.createBaseVisualization(markdownVisDefinition);
expressions.registerRenderer(markdownVisRenderer);
expressions.registerRenderer(getMarkdownVisRenderer({ getStartDeps: core.getStartServices }));
expressions.registerFunction(createMarkdownVisFn);
}

View file

@ -17,6 +17,8 @@
"@kbn/i18n-react",
"@kbn/config-schema",
"@kbn/kibana-react-plugin",
"@kbn/react-kibana-context-render",
"@kbn/core-lifecycle-browser",
],
"exclude": [
"target/**/*",

View file

@ -85,12 +85,12 @@ export const getTimelionVisRenderer: (
};
render(
<VisualizationContainer
renderComplete={renderComplete}
handlers={handlers}
showNoResult={showNoResult}
>
<KibanaRenderContextProvider {...getCoreStart()}>
<KibanaRenderContextProvider {...getCoreStart()}>
<VisualizationContainer
renderComplete={renderComplete}
handlers={handlers}
showNoResult={showNoResult}
>
<KibanaContextProvider services={{ ...deps }}>
{seriesList && (
<LazyTimelionVisComponent
@ -104,8 +104,9 @@ export const getTimelionVisRenderer: (
/>
)}
</KibanaContextProvider>
</KibanaRenderContextProvider>
</VisualizationContainer>,
</VisualizationContainer>
</KibanaRenderContextProvider>,
domNode
);
},

View file

@ -356,7 +356,13 @@ export class VisualizeEmbeddable
}
if (this.warningDomNode) {
render(<Warnings warnings={warnings || []} />, this.warningDomNode);
const { core } = this.deps.start();
render(
<KibanaRenderContextProvider {...core}>
<Warnings warnings={warnings || []} />
</KibanaRenderContextProvider>,
this.warningDomNode
);
}
}

View file

@ -12,7 +12,7 @@ import { Plugin as ExpressionsPlugin } from '@kbn/expressions-plugin/public';
import { SelfChangingEditor } from './self_changing_vis/self_changing_editor';
import { selfChangingVisFn, SelfChangingVisParams } from './self_changing_vis_fn';
import { selfChangingVisRenderer } from './self_changing_vis_renderer';
import { getSelfChangingVisRenderer } from './self_changing_vis_renderer';
import { toExpressionAst } from './to_ast';
export interface SetupDependencies {
@ -32,7 +32,7 @@ export class CustomVisualizationsPublicPlugin
/**
* Register a renderer for your visualization
*/
expressions.registerRenderer(selfChangingVisRenderer);
expressions.registerRenderer(getSelfChangingVisRenderer(core));
/**
* Create the visualization type with definition

View file

@ -9,18 +9,30 @@
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { ExpressionRenderDefinition } from '@kbn/expressions-plugin/common';
import { CoreSetup } from '@kbn/core-lifecycle-browser';
import { SelfChangingComponent } from './self_changing_vis/self_changing_components';
import { SelfChangingVisRenderValue } from './self_changing_vis_fn';
export const selfChangingVisRenderer: ExpressionRenderDefinition<SelfChangingVisRenderValue> = {
name: 'self_changing_vis',
reuseDomNode: true,
render: (domNode, { visParams }, handlers) => {
handlers.onDestroy(() => {
unmountComponentAtNode(domNode);
});
export const getSelfChangingVisRenderer = (core: CoreSetup) => {
const selfChangingVisRenderer: ExpressionRenderDefinition<SelfChangingVisRenderValue> = {
name: 'self_changing_vis',
reuseDomNode: true,
render: async (domNode, { visParams }, handlers) => {
const [coreSetup] = await core.getStartServices();
handlers.onDestroy(() => {
unmountComponentAtNode(domNode);
});
render(<SelfChangingComponent renderComplete={handlers.done} visParams={visParams} />, domNode);
},
render(
<KibanaRenderContextProvider {...coreSetup}>
<SelfChangingComponent renderComplete={handlers.done} visParams={visParams} />
</KibanaRenderContextProvider>,
domNode
);
},
};
return selfChangingVisRenderer;
};

View file

@ -17,5 +17,7 @@
"@kbn/data-plugin",
"@kbn/visualizations-plugin",
"@kbn/expressions-plugin",
"@kbn/core-lifecycle-browser",
"@kbn/react-kibana-context-render",
]
}

View file

@ -10,6 +10,7 @@ import { render, unmountComponentAtNode } from 'react-dom';
import { EuiCallOut } from '@elastic/eui';
import type { CoreSetup, AppMountParameters } from '@kbn/core/public';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import type { StartDependencies } from './plugin';
export const mount =
@ -21,10 +22,11 @@ export const mount =
const defaultDataView = await plugins.data.indexPatterns.getDefault();
const { formula } = await plugins.lens.stateHelperApi();
const i18nCore = core.i18n;
const { analytics, i18n, theme } = core;
const startServices = { analytics, i18n, theme };
const reactElement = (
<i18nCore.Context>
<KibanaRenderContextProvider {...startServices}>
{defaultDataView && defaultDataView.isTimeBased() ? (
<App core={core} plugins={plugins} defaultDataView={defaultDataView} formula={formula} />
) : (
@ -36,7 +38,7 @@ export const mount =
<p>This demo only works if your default index pattern is set and time based</p>
</EuiCallOut>
)}
</i18nCore.Context>
</KibanaRenderContextProvider>
);
render(reactElement, element);

View file

@ -21,5 +21,6 @@
"@kbn/developer-examples-plugin",
"@kbn/data-views-plugin",
"@kbn/ui-actions-plugin",
"@kbn/react-kibana-context-render",
]
}

View file

@ -13,7 +13,9 @@ import type { CoreSetup, CoreStart } from '@kbn/core/public';
import type { FileLayer } from '@elastic/ems-client';
import type { KibanaExecutionContext } from '@kbn/core-execution-context-common';
import { ChartSizeEvent } from '@kbn/chart-expressions-common';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import type { MapsPluginStartDependencies } from '../../plugin';
import { getAnalytics, getCoreI18n, getTheme } from '../../kibana_services';
import type { ChoroplethChartProps } from './types';
export const RENDERER_ID = 'lens_choropleth_chart_renderer';
@ -97,13 +99,19 @@ export function getExpressionRenderer(coreSetup: CoreSetup<MapsPluginStartDepend
handlers.event(chartSizeEvent);
ReactDOM.render(
<ChoroplethChart
{...config}
formatFactory={plugins.fieldFormats.deserialize}
uiSettings={coreStart.uiSettings}
emsFileLayers={emsFileLayers}
onRenderComplete={renderComplete}
/>,
<KibanaRenderContextProvider
analytics={getAnalytics()}
i18n={getCoreI18n()}
theme={getTheme()}
>
<ChoroplethChart
{...config}
formatFactory={plugins.fieldFormats.deserialize}
uiSettings={coreStart.uiSettings}
emsFileLayers={emsFileLayers}
onRenderComplete={renderComplete}
/>
</KibanaRenderContextProvider>,
domNode
);
handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode));