kibana/packages/shared-ux
Clint Andrew Hall 477505a2dd
[context] Unify Contexts, deprecate others (#161914)
> Pre-req for https://github.com/elastic/kibana/issues/56406

## Summary

We've had a long-standing problem in Kibana around our use of React
context, particularly with EUI and i18n. There hasn't existed an
idempotent context structure, and that has lead to a lot of unexpected
results, (e.g. missing translations, inconsistent dark mode, excess
context providers, etc).

The biggest change coming from this PR is knowing exactly which provider
to use in a particular use case. This means, for example,
`ReactDOM.render` calls won't be missing `i18n` or `theme` due to a
missing context. It also allows consumers to use `darkMode` without
having to read the `uiSetting` themselves, instead allowing the context
to do it for them.

We also haven't been honoring the intended [`EuiProvider`
API](https://eui.elastic.co/#/utilities/provider#theming-and-global-styles)...
in some cases we've been creating and re-creating the Emotion caches,
often by copy/paste of the cache code. We've also been nesting
`EuiThemeProvider` contexts unnecessarily-- thinking we need to render a
theme provider in an isolated component-- which renders an additional
`span` element into the DOM.

This PR attempts to address this inconsistency by creating a set of
context providers divided by use case:


![diagram](e01c6296-1b7a-4639-ae96-946866950efe)

### `KibanaRootContextProvider`
A root context provider for Kibana. This is the top level context
provider that wraps the entire application. It is responsible for
initializing all of the other contexts and providing them to the
application. It's provided as a package for specific use cases, (e.g.
the `RenderingService`, cases where we replace the entire page content,
Storybook, testing, etc), but not intended for plugins.

### `KibanaRenderContextProvider`
A render context provider for Kibana. This context is designed to be
used with ad-hoc renders of React components, (usually with
`ReactDOM.render`).

### `KibanaThemeContextProvider`
A theme context provider for Kibana. A corollary to EUI's
`EuiThemeProvider`, it uses Kibana services to ensure the EUI Theme is
customized correctly.

### (deprecated) `KibanaStyledComponentsThemeProvider`
A styled components theme provider for Kibana. This package is supplied
for compatibility with legacy code, but should not be used in new code.

## Deprecation strategy
This PR does *not* change any use of context by consumers. It maps the
existing contexts in `kibanaReact` to the new contexts, (along with the
loose API). This means that we won't have completely fixed all of our
dark mode issues yet. But this is necessary to keep this PR focused on
the change, rather than drawing in a lot of teams to review individual
uses.

We should, however, see an immediate performance improvement in the UI
from the reduction in `EuiProvider` calls.

## Open questions
- [ ] Does it make sense to expose a `useTheme` hook from
`@kbn/react-kibana-context-theme` to replace `useEuiTheme`?

## Next steps
- [ ] Update deprecated uses to new contexts.
- [ ] Audit and update calls to `ReactDOM.render`.
- [ ] Add ESLint rule to warn for use of EUI contexts.
- [ ] Delete code from `kibanaReact`.
2023-07-28 09:30:08 -07:00
..
avatar [Cases] Fix bug where the Cases users API will thrown an error when imageUrl is set to null (#158815) 2023-06-02 01:09:16 -07:00
button/exit_full_screen [SharedUX] Add custom branding to ExitFullScreenButton (#150620) 2023-02-14 10:27:35 +01:00
button_toolbar Upgrade EUI to 83.0.0 (#160813) 2023-07-06 12:46:55 -07:00
card/no_data Upgrade EUI to 83.0.0 (#160813) 2023-07-06 12:46:55 -07:00
chrome [Serverless navigation] Add documentation (#162318) 2023-07-26 08:24:04 -07:00
code_editor [context] Unify Contexts, deprecate others (#161914) 2023-07-28 09:30:08 -07:00
file [Files] Mime-type and extension filtering on client side (#159588) 2023-06-15 13:18:56 +02:00
link/redirect_app [codeowners] rename global experience to @elastic/appex-sharedux 2023-01-18 10:02:49 -07:00
markdown Upgrade EUI to 83.0.0 (#160813) 2023-07-06 12:46:55 -07:00
page Initial e2e tests for serverless plugins (#157166) 2023-05-22 12:57:38 +02:00
prompt [Fix] fix broken story in Prompt Custom Actions (#149992) 2023-02-06 09:21:48 -07:00
router [shared-ux-router] Add Router and Routes components (#159834) 2023-06-23 10:02:06 -05:00
storybook [codeowners] rename global experience to @elastic/appex-sharedux 2023-01-18 10:02:49 -07:00
README.mdx [Shared UX] Adopt multi-package strategy - ExitFullScreenButton (#130355) 2022-04-15 19:12:46 -05:00

# Shared UX Packages

This directory contains directories of packages of shared components and other code for use in Kibana solutions.

## How to use these components

Each package exports one or more components that can be consumed.

### Lazy by default

All components are exported to be lazily-loaded with a default `React.Suspense` default most appropriate to its nature.

If a solution needs to alter the `React.Suspense` behavior, (e.g. a different "loading" component), one can import the `Lazy[ComponentName]` version and surround it with a custom `React.Suspense` component.

### "Pure" and "Connected" components

If a package contains a component with functionality that relies on a Kibana core or plugin dependency, there are two components exported: a `pure` component and a `connected` component.

__Pure__ components:
- are focused on how a component looks and behaves;
- have their props and handlers exposed as simple types;
- have no logic specific to Kibana.

__Connected__ components, by contrast:
- *compose* their pure counterparts;
- rely on Kibana core and plugin dependencies to provide Kibana-specific logic;
- require a `ContextProvider` packaged with the component to provide stateful services from a start contract.

For example, the `ExitFullScreenButton` "pure" component is a button that is styled with the appropriate translated text.  It is simple and without dependency.

The "connected" component *composes* that pure component and:
- applies EUI theme state;
- uses the `coreStart.chrome.setIsVisible` API to change the full screen state `onClick`;
- applies `emotion` styles to position the button in the window.

### Connected component providers

Connected components are accompanied by a `Provider` which is intended to provide their external services.  We typically provide two: one that abstracts away the dependency into a simplified set of functions, and one that maps to the intended dependency directly.

For example, the `ExitFullScreenButton` relies on the `coreStart.chrome.setIsVisible` API to interact with the full screen state.  The package contains two providers.

The `ExitFullScreenButtonProvider` simply expects a single function, `setIsFullScreen`.  This pattern is useful for more complicated components that may rely on a number of dependencies:

```
<ExitFullScreenButtonProvider setIsFullScreen={...}>
  <ExitFullScreenButton />
</ExitFullScreenButtonProvider>
```

The `ExitFullScreenButtonKibanaProvider` creates a facsimile of the `coreStart` contract type, containing only the portions it expects to use.  This is a kind of "syntactic-sugar-workaround" to the fact plugin start contracts are not typically available to packages:

```
<ExitFullScreenButtonKibanaProvider coreStart={...}>
  <ExitFullScreenButton />
</ExitFullScreenButtonProvider>
```

Plugins can use either of these providers in their plugin at either the root of their plugin application or at any level of their React tree, wherever it makes sense.  Component compositions can do the same.  Either Provider can be used, depending on the situation.

## How can I contribute a component?

*__Yes, please!__ :elasticheart:*

The easiest way to contribute a shared component to Kibana is to follow our pattern and create a single package containing that contribution.  You can use the `generate` script to create a new boilerplate package.

> More detail on this is coming soon.  Contact the Shared UX team for more information.

## How this collection is organized

Typically, the `/packages` directory contains a flat list of packages, where each directory matches the name of the package.  Given that we expect to create a large number of packages, we're going to organize them into a loose tree structure:

```
- packages
  - shared-ux
    - button
      - exit_full_screen
    - [component type]
      - baz
      - qux
    - [subject matter]
      - foo
      - bar
```

This structure should then map to the name of the package:

```
- @kbn/shared-ux-button-exit-full-screen
- @kbn/shared-ux-[subject matter]-[foo]
- @kbn/shared-ux-[subject matter]-[bar]
- @kbn/shared-ux-[component-type]-[baz]
- @kbn/shared-ux-[component-type]-[qux]
```

## Why?

When we started exploring how to effectively share code between Kibana solutions, we realized-- admittedly through some trial and error-- that the usual ways in which we share code between plugins wasn't going to work.

### Why not a plugin?

First, with each component that we create, those components inevitably begin to depend on other plugins.  Once our plugin depends on another, that plugin then becomes unable to use Shared UX components without creating a circular dependency.

Second, the components, while useful to a plugin, are not actually dependent on the *plugin lifecycle*.  They are stateless.  Containing-- restricting-- them to a plugin lifecycle adds unnecessary complexity.

So we opted to organize our code in packages.

### Why not a single package of components?

We started that way, and quickly ran into the "`lodash` bundle problem": containing all of our components in one package (and all of our services in another) meant that any plugin using even one component would inherit *all* of them... even if they weren't used.  Bundle sizes would increase dramatically, as well as build times: any minor change would cascade through the entire monorepo.

Therefore we've opted to create a package for each component.

### How do your components share code?

At present, *they don't*.  Some utility code is shared, but this is code that should change very rarely, at most.

But that doesn't mean they cannot be *composed* together.  `ComponentA` can certainly compose `ComponentB` and `ComponentC`.  What's great is the dependencies become very clear, top-down, and reflect the granular nature of each component.