Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[docs] High-level Routing, Navigation and URL overview #76888

Merged
merged 16 commits into from
Sep 15, 2020
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions docs/developer/best-practices/index.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,10 @@ Review:
* <<development-unit-tests>>
* <<stability>>
* <<security-best-practices>>
* <<kibana-navigation>>

include::stability.asciidoc[leveloffset=+1]

include::security.asciidoc[leveloffset=+1]

include::navigation.asciidoc[leveloffset=+1]
203 changes: 203 additions & 0 deletions docs/developer/best-practices/navigation.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
[[kibana-navigation]]
== Routing, Navigation and URL

{kib} platform provides a set of tools to help developers built consistent experience around routing and browser navigation.
Dosant marked this conversation as resolved.
Show resolved Hide resolved
Some of that tooling is inside `core`, some is available as part of various plugins.

The purpose of this guide is to give you (a {kib} contributor}) a high-level overview of available tools and to explain common approaches for handling routing,
Dosant marked this conversation as resolved.
Show resolved Hide resolved
Dosant marked this conversation as resolved.
Show resolved Hide resolved
and browser navigation.

This guide covers following topics:

* <<navigating-between-kibana-apps>>
* <<deep-linking>>
* <<routing>>
* <<history-and-location>>
* <<state-sync>>
* <<preserve-state>>


[[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:
Dosant marked this conversation as resolved.
Show resolved Hide resolved

[source,js]
----
window.location.href = core.http.basePath.prepend(`/dashboard/my-dashboard`); // (try to avoid this)
Copy link
Contributor

@streamich streamich Sep 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason this code block has almost no syntax highlighting:

image

I wonder if it is how our website is set up or does it has to do with [source,js].

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great example, but I was initially confused because you say "using native browser apis" but part of the example is using core apis.

What if we had the "Deep linking into Kibana Apps" section first, so the user is introduced to the core api core.http.basePath.prepend and what it does and that it should only be used if there isn't a url generator available?

Then you can follow up with this section and I think the user may understand that you are focusing on the window.location.href part of the line.

Could then even write it as:

const discoverUrl = discoverUrlGenerator.createUrl({filters, timeRange});
// Avoid this, as it will cause a full page reload.
window.location.href = discoverUrl;

Which focuses in on which is the part to avoid.

----

would cause a full page reload.
Dosant marked this conversation as resolved.
Show resolved Hide resolved

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={core.http.basePath.prepend(`/dashboard/my-dashboard`)}> Go to Dashboard </a>;
Dosant marked this conversation as resolved.
Show resolved Hide resolved
----

A workaround could be to handle a click, prevent browser navigation and use `core.application.navigateToApp` api:

[source,typescript jsx]
----
const MySPALink = () =>
<a
href={core.http.basePath.prepend(`/dashboard/my-dashboard`)}
onClick={(e) => {
e.preventDefault();
core.application.navigateToApp('dashboard', {path: '/my-dashboard'});
}}>
Go to Dashboard
</a>;
Dosant marked this conversation as resolved.
Show resolved Hide resolved
----


As it would be too much boilerplate to do this for each {kib} link in your app, there is a handy HOC that helps with it:
Dosant marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should not mention RedirectAppLinks in this doc as it is a hack to easily make legacy Kibana URLs work. Or mention that it is a hack and you should not be using it.

It seems we are missing a good way to navigate across Kibana apps, we need a component where one specifies an app and path:

<KbnLink app={'discover'} to={'/foo/bar'}>
  Click me!
</KbnLink>

Copy link
Contributor

@streamich streamich Sep 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe in kibana_react we could put a component that can do both, navigate within Kibana app:

import { Link } from 'src/plugins/kibana_react/public';

<Link to={'/foo/bar'} />

and across apps if app prop is specified:

<Link app={'discover'} to={'/foo/bar'} />

Copy link
Contributor Author

@Dosant Dosant Sep 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that component would makes sense.

RedirectAppLinks still worth a mention. Some places don't have React wired up and you need to make sure your non React links are nested into that component.

Copy link
Contributor

@pgayvallet pgayvallet Sep 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should not mention RedirectAppLinks in this doc as it is a hack to easily make legacy Kibana URLs work. Or mention that it is a hack and you should not be using it.

Late reply, but RedirectAppLinks is, AFAIK, not a hack, and the current preferred approach. See #58751 for more context and the whole discussion about the various options. The issue is still open for proposals for possible alternative to match other uses cases (the KbnLink was one of them, so I'm totally in favor of adding that though, see #58751 (comment))

{kib-repo}tree/{branch}/src/plugins/kibana_react/public/app_links/redirect_app_link.tsx#L49[RedirectAppLinks].
Dosant marked this conversation as resolved.
Show resolved Hide resolved

[source,typescript jsx]
----
const MyApp = () =>
<RedirectAppLinks application={core.application}>
{/*...*/}
{/* navigations using this link will happen in SPA friendly way */}
<a href={core.http.basePath.prepend(`/dashboard/my-dashboard`)}>Go to Dashboard</a>
Dosant marked this conversation as resolved.
Show resolved Hide resolved
{/*...*/}
</RedirectAppLinks>
----


[[deep-linking]]
=== Deep-linking into {kib} apps

**Consider a {kib} app URL a part of app's plugin contract.** +
Try to avoid hardcoding other app's URL in your app's code:

[source,typescript jsx]
----
const urlToDashboarWithComplexState = core.http.basePath.prepend(`/dashboards#/view/0289f5d0-e61c-11ea-8c31-8bd3114ed023?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!(),fullScreenMode:!f,options:(hidePanelTitles:!f,useMargins:!t),query:(language:kuery,query:''),timeRestore:!f,title:'URL%20Drilldown%20Demo',viewMode:view)`)
----

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 deep-linking.
To get a better idea, take a look at {kib-repo}tree/{branch}/src/plugins/dashboard/public/url_generator.ts#L38[dashboard's app URL generator].
Dosant marked this conversation as resolved.
Show resolved Hide resolved
It allows specifying various dashboard app state pieces like: dashboardId, filters, query, time range and more.
And then others should use it to generate a link a dashboard with specified state:

[source,typescript jsx]
----
const dashboardUrl = dashboardUrlGenerator.createUrl({dashboardId, filters, timeRange});
----

There are two ways to access other's app URL generator in your code:

1. *(preferred)* From a plugin contract of a destination app.
2. (in case an explicit plugin dependency is not possible) Using {kib-repo}tree/{branch}/src/plugins/share/public/url_generators/README.md[url generator service] from `share` plugin.
Dosant marked this conversation as resolved.
Show resolved Hide resolved


[[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`.*
Dosant marked this conversation as resolved.
Show resolved Hide resolved

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 (`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]]
=== Working with history and browser location

Try to avoid using `window.location.href` 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 bypassing `core` could cause unpredicted and hard-to-catch bugs.
Dosant marked this conversation as resolved.
Show resolved Hide resolved

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 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 applications do that.
Dosant marked this conversation as resolved.
Show resolved Hide resolved
* You want to follow platform's <<history-and-location, working with browser history and location best practices>> out of the box.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow this one. I don't see State Syncing utils, or the _a and _g format used in any of the documentation links for ScopedHistory:

So seems like I can use ScopedHistory without using any state syncing utils directly? Or am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So seems like I can use ScopedHistory without using any state syncing utils directly?

Exactly!

ScopedHistory is a core's wrapper around browser's navigation and history apis.

  1. Whenever you want to use window.location or window.history, you should consider using core's ScopeHistory instead. (for example, reading / writing query params, updating path, etc..)
  2. You can use it for listening to location changes. You can't do it with native apis. (Native api's are limited and support only popState or hashChange)
  3. You should initialize your app's react router with it.
  4. And you should initialize state state syncing utils with it! So inside state syncing utils are using core's history instance, instead of browser's api's directly

* 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:**
Dosant marked this conversation as resolved.
Show resolved Hide resolved

* Adding a query param flag or simple key/value to 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 a 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 are navigated to a dashboard app with the *same state* that you left it,
except that the time filter has changed to the one you applied on discover app.
Dosant marked this conversation as resolved.
Show resolved Hide resolved

Historically {kib} analyze apps achieve that behavior relying on state in the URL.
Dosant marked this conversation as resolved.
Show resolved Hide resolved
If you'd have a closer look on a link in the navigation,
you'd notice that state is stored inside those links, and it also gets update whenever relevant state change happens:
Dosant marked this conversation as resolved.
Show resolved Hide resolved

[role="screenshot"]
image:images/state_inside_the_link.png[State is stored inside the navigation link]

This is where <<query-params, separation>> on `_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.
Dosant marked this conversation as resolved.
Show resolved Hide resolved
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 platform we got navigations without page reloads. Because of that there are, probably, simpler ways to preserve state.
Dosant marked this conversation as resolved.
Show resolved Hide resolved
For example, you could just try keep the reference to your application state handy and reuse it when your app re-mounts.
2 changes: 2 additions & 0 deletions src/plugins/kibana_utils/common/state_containers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* [docs](../../docs/state_containers)
* [api reference](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers)
16 changes: 14 additions & 2 deletions src/plugins/kibana_utils/docs/state_sync/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So state:storeInSessionStore relies on _a and _g parameters in the URL?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, not really.

When using state syncing utils (with kbnUrlStateStorage to be precise) you can configure your state to be synced to any query key. You can also set it up to sync only hash into URL. This is covered in docs of stateSync, which is linked from this general guide: https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md#setting-url-format-option

- 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:
Expand Down Expand Up @@ -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.

Expand Down
3 changes: 3 additions & 0 deletions src/plugins/kibana_utils/public/state_sync/README.md
Original file line number Diff line number Diff line change
@@ -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)