mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
This commit is contained in:
parent
334af0801d
commit
02a40a67bf
7 changed files with 283 additions and 47 deletions
BIN
docs/developer/best-practices/images/state_inside_the_link.png
Normal file
BIN
docs/developer/best-practices/images/state_inside_the_link.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 162 KiB |
|
@ -122,6 +122,14 @@ In addition, if users are relying on state stored in your app’s URL as
|
|||
part of your public contract, keep in mind that you may also need to
|
||||
provide backwards compatibility for bookmarked URLs.
|
||||
|
||||
[discrete]
|
||||
=== Routing, Navigation and URL
|
||||
|
||||
The {kib} platform provides a set of tools to help developers build consistent experience around routing and browser navigation.
|
||||
Some of that tooling is inside `core`, some is available as part of various plugins.
|
||||
|
||||
<<kibana-navigation, Follow this guide>> to get an idea of available tools and common approaches for handling routing and browser navigation.
|
||||
|
||||
[discrete]
|
||||
=== Testing & stability
|
||||
|
||||
|
@ -131,6 +139,8 @@ Review:
|
|||
* <<stability>>
|
||||
* <<security-best-practices>>
|
||||
|
||||
include::navigation.asciidoc[leveloffset=+1]
|
||||
|
||||
include::stability.asciidoc[leveloffset=+1]
|
||||
|
||||
include::security.asciidoc[leveloffset=+1]
|
||||
|
|
226
docs/developer/best-practices/navigation.asciidoc
Normal file
226
docs/developer/best-practices/navigation.asciidoc
Normal file
|
@ -0,0 +1,226 @@
|
|||
[[kibana-navigation]]
|
||||
== Routing, Navigation and URL
|
||||
|
||||
The {kib} platform provides a set of tools to help developers build consistent experience around routing and browser navigation.
|
||||
Some of that tooling is inside `core`, some is available as part of various plugins.
|
||||
|
||||
The purpose of this guide is to give a high-level overview of available tools and to explain common approaches for handling routing and browser navigation.
|
||||
|
||||
This guide covers following topics:
|
||||
|
||||
* <<deep-linking>>
|
||||
* <<navigating-between-kibana-apps>>
|
||||
* <<routing>>
|
||||
* <<history-and-location>>
|
||||
* <<state-sync>>
|
||||
* <<preserve-state>>
|
||||
|
||||
[[deep-linking]]
|
||||
=== Deep-linking into {kib} apps
|
||||
|
||||
Assuming you want to link from your app to *Discover*. When building such URL there are two things to consider:
|
||||
|
||||
1. Prepending a proper `basePath`.
|
||||
2. Specifying *Discover* state.
|
||||
|
||||
==== Prepending a proper `basePath`
|
||||
|
||||
To prepend {kib}'s `basePath` use {kib-repo}tree/{branch}/docs/development/core/public/kibana-plugin-core-public.ibasepath.prepend.md[core.http.basePath.prepend] helper:
|
||||
|
||||
[source,typescript jsx]
|
||||
----
|
||||
const discoverUrl = core.http.basePath.prepend(`/discover`);
|
||||
|
||||
console.log(discoverUrl); // http://localhost:5601/bpr/s/space/app/discover
|
||||
----
|
||||
|
||||
==== Specifying state
|
||||
|
||||
**Consider a {kib} app URL a part of app's plugin contract:**
|
||||
|
||||
. Avoid hardcoding other app's URL in your app's code.
|
||||
. Avoid generating other app's state and serializing it into URL query params.
|
||||
|
||||
[source,typescript jsx]
|
||||
----
|
||||
// Avoid relying on other app's state structure in your app's code:
|
||||
const discoverUrlWithSomeState = core.http.basePath.prepend(`/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:'2020-09-10T11:39:50.203Z',to:'2020-09-10T11:40:20.249Z'))&_a=(columns:!(_source),filters:!(),index:'90943e30-9a47-11e8-b64d-95841ca0b247',interval:auto,query:(language:kuery,query:''),sort:!())`);
|
||||
----
|
||||
|
||||
Instead, each app should expose {kib-repo}tree/{branch}/src/plugins/share/public/url_generators/README.md[a URL generator].
|
||||
Other apps should use those URL generators for creating URLs.
|
||||
|
||||
[source,typescript jsx]
|
||||
----
|
||||
// Properly generated URL to *Discover* app. Generator code is owned by *Discover* app and available on *Discover*'s plugin contract.
|
||||
const discoverUrl = discoverUrlGenerator.createUrl({filters, timeRange});
|
||||
----
|
||||
|
||||
To get a better idea, take a look at *Discover* URL generator {kib-repo}tree/{branch}/src/plugins/discover/public/url_generator.ts[implementation].
|
||||
It allows specifying various **Discover** app state pieces like: index pattern, filters, query, time range and more.
|
||||
|
||||
There are two ways to access other's app URL generator in your code:
|
||||
|
||||
1. From a plugin contract of a destination app *(preferred)*.
|
||||
2. Using URL generator service instance on `share` plugin contract (in case an explicit plugin dependency is not possible).
|
||||
|
||||
In case you want other apps to link to your app, then you should create a URL generator and expose it on your plugin's contract.
|
||||
|
||||
|
||||
[[navigating-between-kibana-apps]]
|
||||
=== Navigating between {kib} apps
|
||||
|
||||
{kib} is a single page application and there is a set of simple rules developers should follow
|
||||
to make sure there is no page reload when navigating from one place in {kib} to another.
|
||||
|
||||
For example, navigation using native browser APIs would cause a full page reload.
|
||||
|
||||
[source,js]
|
||||
----
|
||||
const urlToADashboard = core.http.basePath.prepend(`/dashboard/my-dashboard`);
|
||||
|
||||
// this would cause a full page reload:
|
||||
window.location.href = urlToADashboard;
|
||||
----
|
||||
|
||||
To navigate between different {kib} apps without a page reload there are APIs in `core`:
|
||||
|
||||
* {kib-repo}tree/{branch}/docs/development/core/public/kibana-plugin-core-public.applicationstart.navigatetoapp.md[core.application.navigateToApp]
|
||||
* {kib-repo}tree/{branch}/docs/development/core/public/kibana-plugin-core-public.applicationstart.navigatetourl.md[core.application.navigateToUrl]
|
||||
|
||||
*Rendering a link to a different {kib} app on its own would also cause a full page reload:*
|
||||
|
||||
[source,typescript jsx]
|
||||
----
|
||||
const myLink = () =>
|
||||
<a href={urlToADashboard}>Go to Dashboard</a>;
|
||||
----
|
||||
|
||||
A workaround could be to handle a click, prevent browser navigation and use `core.application.navigateToApp` API:
|
||||
|
||||
[source,typescript jsx]
|
||||
----
|
||||
const MySPALink = () =>
|
||||
<a
|
||||
href={urlToADashboard}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
core.application.navigateToApp('dashboard', { path: '/my-dashboard' });
|
||||
}}
|
||||
>
|
||||
Go to Dashboard
|
||||
</a>;
|
||||
----
|
||||
|
||||
As it would be too much boilerplate to do this for each {kib} link in your app, there is a handy wrapper that helps with it:
|
||||
{kib-repo}tree/{branch}/src/plugins/kibana_react/public/app_links/redirect_app_link.tsx#L49[RedirectAppLinks].
|
||||
|
||||
[source,typescript jsx]
|
||||
----
|
||||
const MyApp = () =>
|
||||
<RedirectAppLinks application={core.application}>
|
||||
{/*...*/}
|
||||
{/* navigations using this link will happen in SPA friendly way */}
|
||||
<a href={urlToADashboard}>Go to Dashboard</a>
|
||||
{/*...*/}
|
||||
</RedirectAppLinks>
|
||||
----
|
||||
|
||||
[[routing]]
|
||||
=== Setting up internal app routing
|
||||
|
||||
It is very common for {kib} apps to use React and React Router.
|
||||
Common rules to follow in this scenario:
|
||||
|
||||
* Set up `BrowserRouter` and not `HashRouter`.
|
||||
* *Initialize your router with `history` instance provided by the `core`.*
|
||||
|
||||
This is required to make sure `core` is aware of navigations triggered inside your app, so it could act accordingly when needed.
|
||||
|
||||
* `Core`'s {kib-repo}tree/{branch}/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md[ScopedHistory] instance.
|
||||
* {kib-repo}tree/{branch}/docs/development/core/public/kibana-plugin-core-public.appmountparameters.history.md[Example usage]
|
||||
* {kib-repo}tree/{branch}/test/plugin_functional/plugins/core_plugin_a/public/application.tsx#L120[Example plugin]
|
||||
|
||||
Relative links will be resolved relative to your app's route (e.g.: `http://localhost5601/app/{your-app-id}`)
|
||||
and setting up internal links in your app in SPA friendly way would look something like:
|
||||
|
||||
[source,typescript jsx]
|
||||
----
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
const MyInternalLink = () => <Link to="/my-other-page"></Link>
|
||||
----
|
||||
|
||||
[[history-and-location]]
|
||||
=== Using history and browser location
|
||||
|
||||
Try to avoid using `window.location` and `window.history` directly. +
|
||||
Instead, consider using {kib-repo}tree/{branch}/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md[ScopedHistory]
|
||||
instance provided by `core`.
|
||||
|
||||
* This way `core` will know about location changes triggered within your app, and it would act accordingly.
|
||||
* Some plugins are listening to location changes. Triggering location change manually could lead to unpredictable and hard-to-catch bugs.
|
||||
|
||||
Common use-case for using
|
||||
`core`'s {kib-repo}tree/{branch}/docs/development/core/public/kibana-plugin-core-public.scopedhistory.md[ScopedHistory] directly:
|
||||
|
||||
* Reading/writing query params or hash.
|
||||
* Imperatively triggering internal navigations within your app.
|
||||
* Listening to browser location changes.
|
||||
|
||||
|
||||
[[state-sync]]
|
||||
=== Syncing state with URL
|
||||
|
||||
Historically {kib} apps store _a lot_ of application state in the URL.
|
||||
The most common pattern that {kib} apps follow today is storing state in `_a` and `_g` query params in https://github.com/w33ble/rison-node#readme[rison] format.
|
||||
[[query-params]]
|
||||
Those query params follow the convention:
|
||||
|
||||
* `_g` (*global*) - global UI state that should be shared and synced across multiple apps. common example from Analyze group apps: time range, refresh interval, *pinned* filters.
|
||||
* `_a` (*application*) - UI state scoped to current app.
|
||||
|
||||
NOTE: After migrating to KP platform we got navigations without page reloads. Since then there is no real need to follow `_g` and `_a` separation anymore. It's up you to decide if you want to follow this pattern or if you prefer a single query param or something else. The need for this separation earlier is explained in <<preserve-state>>.
|
||||
|
||||
There are utils to help you to implement such kind of state syncing.
|
||||
|
||||
**When you should consider using state syncing utils:**
|
||||
|
||||
* You want to sync your application state with URL in similar manner Analyze group applications do.
|
||||
* You want to follow platform's <<history-and-location, working with browser history and location best practices>> out of the box.
|
||||
* You want to support `state:storeInSessionStore` escape hatch for URL overflowing out of the box.
|
||||
* You should also consider using them if you'd like to serialize state to different (not `rison`) format. Utils are composable, and you can implement your own `storage`.
|
||||
* In case you want to sync part of your state with URL, but other part of it with browser storage.
|
||||
|
||||
**When you shouldn't use state syncing utils:**
|
||||
|
||||
* Adding a query param flag or simple key/value to the URL.
|
||||
|
||||
Follow {kib-repo}tree/{branch}/src/plugins/kibana_utils/docs/state_sync#state-syncing-utilities[these] docs to learn more.
|
||||
|
||||
|
||||
[[preserve-state]]
|
||||
=== Preserving state between navigations
|
||||
|
||||
Consider the scenario:
|
||||
|
||||
1. You are in *Dashboard* app looking at a dashboard with some filters applied;
|
||||
2. Navigate to *Discover* using in-app navigation;
|
||||
3. Change the time filter'
|
||||
4. Navigate to *Dashboard* using in-app navigation.
|
||||
|
||||
You'd notice that you were navigated to *Dashboard* app with the *same state* that you left it with,
|
||||
except that the time filter has changed to the one you applied on *Discover* app.
|
||||
|
||||
Historically {kib} Analyze groups apps achieve that behavior relying on state in the URL.
|
||||
If you'd have a closer look on a link in the navigation,
|
||||
you'd notice that state is stored inside that link, and it also gets updated whenever relevant state changes happen:
|
||||
|
||||
[role="screenshot"]
|
||||
image:images/state_inside_the_link.png[State is stored inside the navigation link]
|
||||
|
||||
This is where <<query-params, separation>> into `_a` and `_g` query params comes into play. What is considered a *global* state gets constantly updated in those navigation links. In the example above it was a time filter.
|
||||
This is backed by {kib-repo}tree/{branch}/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts#L57[KbnUrlTracker] util. You can use it to achieve similar behavior.
|
||||
|
||||
NOTE: After migrating to KP navigation works without page reloads and all plugins are loaded simultaneously.
|
||||
Hence, likely there are simpler ways to preserve state of your application, unless you want to do it through URL.
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
Tools for building React applications in Kibana.
|
||||
|
||||
|
||||
## Context
|
||||
|
||||
You can create React context that holds Core or plugin services that your plugin depends on.
|
||||
|
@ -51,7 +50,6 @@ import { KibanaContextProvider } from 'kibana-react';
|
|||
</KibanaContextProvider>
|
||||
```
|
||||
|
||||
|
||||
## Accessing context
|
||||
|
||||
Using `useKibana` hook.
|
||||
|
@ -61,11 +59,7 @@ import { useKibana } from 'kibana-react';
|
|||
|
||||
const Demo = () => {
|
||||
const kibana = useKibana();
|
||||
return (
|
||||
<div>
|
||||
{kibana.services.uiSettings.get('theme:darkMode') ? 'dark' : 'light'}
|
||||
</div>
|
||||
);
|
||||
return <div>{kibana.services.uiSettings.get('theme:darkMode') ? 'dark' : 'light'}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
|
@ -75,11 +69,7 @@ Using `withKibana()` higher order component.
|
|||
import { withKibana } from 'kibana-react';
|
||||
|
||||
const Demo = ({ kibana }) => {
|
||||
return (
|
||||
<div>
|
||||
{kibana.services.uiSettings.get('theme:darkMode') ? 'dark' : 'light'}
|
||||
</div>
|
||||
);
|
||||
return <div>{kibana.services.uiSettings.get('theme:darkMode') ? 'dark' : 'light'}</div>;
|
||||
};
|
||||
|
||||
export default withKibana(Demo);
|
||||
|
@ -92,21 +82,17 @@ import { UseKibana } from 'kibana-react';
|
|||
|
||||
const Demo = () => {
|
||||
return (
|
||||
<UseKibana>{kibana =>
|
||||
<div>
|
||||
{kibana.services.uiSettings.get('theme:darkMode') ? 'dark' : 'light'}
|
||||
</div>
|
||||
}</UseKibana>
|
||||
<UseKibana>
|
||||
{(kibana) => <div>{kibana.services.uiSettings.get('theme:darkMode') ? 'dark' : 'light'}</div>}
|
||||
</UseKibana>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
## `uiSettings` service
|
||||
|
||||
Wrappers around Core's `uiSettings` service.
|
||||
|
||||
|
||||
### `useUiSetting` hook
|
||||
|
||||
`useUiSetting` synchronously returns the latest setting from `CoreStart['uiSettings']` service.
|
||||
|
@ -116,11 +102,7 @@ import { useUiSetting } from 'kibana-react';
|
|||
|
||||
const Demo = () => {
|
||||
const darkMode = useUiSetting<boolean>('theme:darkMode');
|
||||
return (
|
||||
<div>
|
||||
{darkMode ? 'dark' : 'light'}
|
||||
</div>
|
||||
);
|
||||
return <div>{darkMode ? 'dark' : 'light'}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
|
@ -130,7 +112,6 @@ const Demo = () => {
|
|||
useUiSetting<T>(key: string, defaultValue: T): T;
|
||||
```
|
||||
|
||||
|
||||
### `useUiSetting$` hook
|
||||
|
||||
`useUiSetting$` synchronously returns the latest setting from `CoreStart['uiSettings']` service and
|
||||
|
@ -141,11 +122,7 @@ import { useUiSetting$ } from 'kibana-react';
|
|||
|
||||
const Demo = () => {
|
||||
const [darkMode] = useUiSetting$<boolean>('theme:darkMode');
|
||||
return (
|
||||
<div>
|
||||
{darkMode ? 'dark' : 'light'}
|
||||
</div>
|
||||
);
|
||||
return <div>{darkMode ? 'dark' : 'light'}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
|
@ -155,7 +132,6 @@ const Demo = () => {
|
|||
useUiSetting$<T>(key: string, defaultValue: T): [T, (newValue: T) => void];
|
||||
```
|
||||
|
||||
|
||||
## `overlays` service
|
||||
|
||||
Wrapper around Core's `overlays` service, allows you to display React modals and flyouts
|
||||
|
@ -166,13 +142,11 @@ import { createKibanaReactContext } from 'kibana-react';
|
|||
|
||||
class MyPlugin {
|
||||
start(core) {
|
||||
const { value: { overlays } } = createKibanaReactContext(core);
|
||||
const {
|
||||
value: { overlays },
|
||||
} = createKibanaReactContext(core);
|
||||
|
||||
overlays.openModal(
|
||||
<div>
|
||||
Hello world!
|
||||
</div>
|
||||
);
|
||||
overlays.openModal(<div>Hello world!</div>);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -186,16 +160,11 @@ You can access `overlays` service through React context.
|
|||
const Demo = () => {
|
||||
const { overlays } = useKibana();
|
||||
useEffect(() => {
|
||||
overlays.openModal(
|
||||
<div>
|
||||
Oooops! {errorMessage}
|
||||
</div>
|
||||
);
|
||||
overlays.openModal(<div>Oooops! {errorMessage}</div>);
|
||||
}, [errorMessage]);
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
## `notifications` service
|
||||
|
||||
Wrapper around Core's `notifications` service, allows you to render React elements
|
||||
|
@ -206,11 +175,13 @@ import { createKibanaReactContext } from 'kibana-react';
|
|||
|
||||
class MyPlugin {
|
||||
start(core) {
|
||||
const { value: { notifications } } = createKibanaReactContext(core);
|
||||
const {
|
||||
value: { notifications },
|
||||
} = createKibanaReactContext(core);
|
||||
|
||||
notifications.toasts.show({
|
||||
title: <div>Hello</div>,
|
||||
body: <div>world!</div>
|
||||
body: <div>world!</div>,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -234,3 +205,15 @@ const Demo = () => {
|
|||
}, [errorMessage]);
|
||||
};
|
||||
```
|
||||
|
||||
## RedirectAppLinks
|
||||
|
||||
Utility component that will intercept click events on children anchor (`<a>`) elements to call
|
||||
`application.navigateToUrl` with the link's href. This will trigger SPA friendly navigation
|
||||
when the link points to a valid Kibana app.
|
||||
|
||||
```tsx
|
||||
<RedirectAppLinks application={application}>
|
||||
<a href="/base-path/app/another-app/some-path">Go to another-app</a>
|
||||
</RedirectAppLinks>
|
||||
```
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
* [docs](../../docs/state_containers)
|
||||
* [api reference](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers)
|
|
@ -3,6 +3,18 @@
|
|||
State syncing utilities are a set of helpers for syncing your application state
|
||||
with URL or browser storage.
|
||||
|
||||
**When you should consider using state syncing utils:**
|
||||
|
||||
- You want to sync your application state with URL in similar manner analyze applications do that.
|
||||
- You want to follow platform's <<history-and-location, working with browser history and location best practices>> out of the box.
|
||||
- You want to support `state:storeInSessionStore` escape hatch for URL overflowing out of the box.
|
||||
- You should also consider using them if you'd like to serialize state to different (not `rison`) format. Utils are composable, and you can implement your own `storage`.
|
||||
- In case you want to sync part of your state with URL, but other part of it with browser storage.
|
||||
|
||||
**When you shouldn't look into using state syncing utils:**
|
||||
|
||||
- Adding a query param flag or simple key/value to URL
|
||||
|
||||
They are designed to work together with [state containers](../state_containers). But state containers are not required.
|
||||
|
||||
State syncing utilities include:
|
||||
|
@ -42,9 +54,9 @@ stateContainer.set({ count: 2 });
|
|||
stop();
|
||||
```
|
||||
|
||||
## Demos Plugins
|
||||
## Demo Plugins
|
||||
|
||||
See demos plugins [here](../../../../../examples/state_containers_examples).
|
||||
See demo plugins [here](../../../../../examples/state_containers_examples).
|
||||
|
||||
To run them, start kibana with `--run-examples` flag.
|
||||
|
||||
|
|
3
src/plugins/kibana_utils/public/state_sync/README.md
Normal file
3
src/plugins/kibana_utils/public/state_sync/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
- [docs](../../docs/state_sync)
|
||||
- [demo plugins](../../../../../examples/state_containers_examples): run Kibana with `--run-examples` flag.
|
||||
- [api reference](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync)
|
Loading…
Add table
Add a link
Reference in a new issue