mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Docs] Clean up state management examples (#88980)
This commit is contained in:
parent
b5d2d89c14
commit
7727ab74d2
19 changed files with 305 additions and 547 deletions
|
@ -2,7 +2,7 @@
|
|||
|
||||
This example app shows how to:
|
||||
- Use state containers to manage your application state
|
||||
- Integrate with browser history and hash history routing
|
||||
- Integrate with browser history or hash history routing
|
||||
- Sync your state container with the URL
|
||||
|
||||
To run this example, use the command `yarn start --run-examples`.
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
export const PLUGIN_ID = 'stateContainersExampleWithDataServices';
|
||||
export const PLUGIN_NAME = 'State containers example - with data services';
|
|
@ -2,9 +2,9 @@
|
|||
"id": "stateContainersExamples",
|
||||
"version": "0.0.1",
|
||||
"kibanaVersion": "kibana",
|
||||
"server": true,
|
||||
"server": false,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["navigation", "data", "developerExamples"],
|
||||
"optionalPlugins": [],
|
||||
"requiredBundles": ["kibanaUtils", "kibanaReact"]
|
||||
"requiredBundles": ["kibanaUtils"]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { EuiPage, EuiPageSideBar, EuiSideNav } from '@elastic/eui';
|
||||
import { CoreStart } from '../../../../src/core/public';
|
||||
|
||||
export interface ExampleLink {
|
||||
title: string;
|
||||
appId: string;
|
||||
}
|
||||
|
||||
interface NavProps {
|
||||
navigateToApp: CoreStart['application']['navigateToApp'];
|
||||
exampleLinks: ExampleLink[];
|
||||
}
|
||||
|
||||
const SideNav: React.FC<NavProps> = ({ navigateToApp, exampleLinks }: NavProps) => {
|
||||
const navItems = exampleLinks.map((example) => ({
|
||||
id: example.appId,
|
||||
name: example.title,
|
||||
onClick: () => navigateToApp(example.appId),
|
||||
'data-test-subj': example.appId,
|
||||
}));
|
||||
|
||||
return (
|
||||
<EuiSideNav
|
||||
items={[
|
||||
{
|
||||
name: 'State management examples',
|
||||
id: 'home',
|
||||
items: [...navItems],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
navigateToApp: CoreStart['application']['navigateToApp'];
|
||||
exampleLinks: ExampleLink[];
|
||||
}
|
||||
|
||||
export const StateContainersExamplesPage: React.FC<Props> = ({
|
||||
navigateToApp,
|
||||
children,
|
||||
exampleLinks,
|
||||
}: PropsWithChildren<Props>) => {
|
||||
return (
|
||||
<EuiPage>
|
||||
<EuiPageSideBar>
|
||||
<SideNav navigateToApp={navigateToApp} exampleLinks={exampleLinks} />
|
||||
</EuiPageSideBar>
|
||||
{children}
|
||||
</EuiPage>
|
||||
);
|
||||
};
|
|
@ -8,8 +8,8 @@
|
|||
|
||||
import { AppMountParameters, CoreSetup, Plugin, AppNavLinkStatus } from '../../../src/core/public';
|
||||
import { AppPluginDependencies } from './with_data_services/types';
|
||||
import { PLUGIN_ID, PLUGIN_NAME } from '../common';
|
||||
import { DeveloperExamplesSetup } from '../../developer_examples/public';
|
||||
import image from './state_sync.png';
|
||||
|
||||
interface SetupDeps {
|
||||
developerExamples: DeveloperExamplesSetup;
|
||||
|
@ -17,97 +17,95 @@ interface SetupDeps {
|
|||
|
||||
export class StateContainersExamplesPlugin implements Plugin {
|
||||
public setup(core: CoreSetup, { developerExamples }: SetupDeps) {
|
||||
const examples = {
|
||||
stateContainersExampleBrowserHistory: {
|
||||
title: 'Todo App (browser history)',
|
||||
},
|
||||
stateContainersExampleHashHistory: {
|
||||
title: 'Todo App (hash history)',
|
||||
},
|
||||
stateContainersExampleWithDataServices: {
|
||||
title: 'Search bar integration',
|
||||
},
|
||||
};
|
||||
|
||||
const exampleLinks = Object.keys(examples).map((id: string) => ({
|
||||
appId: id,
|
||||
title: examples[id as keyof typeof examples].title,
|
||||
}));
|
||||
|
||||
core.application.register({
|
||||
id: 'stateContainersExampleBrowserHistory',
|
||||
title: 'State containers example - browser history routing',
|
||||
title: examples.stateContainersExampleBrowserHistory.title,
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
async mount(params: AppMountParameters) {
|
||||
const { renderApp, History } = await import('./todo/app');
|
||||
return renderApp(params, {
|
||||
appInstanceId: '1',
|
||||
appTitle: 'Routing with browser history',
|
||||
historyType: History.Browser,
|
||||
});
|
||||
const [coreStart] = await core.getStartServices();
|
||||
return renderApp(
|
||||
params,
|
||||
{
|
||||
appTitle: examples.stateContainersExampleBrowserHistory.title,
|
||||
historyType: History.Browser,
|
||||
},
|
||||
{ navigateToApp: coreStart.application.navigateToApp, exampleLinks }
|
||||
);
|
||||
},
|
||||
});
|
||||
core.application.register({
|
||||
id: 'stateContainersExampleHashHistory',
|
||||
title: 'State containers example - hash history routing',
|
||||
title: examples.stateContainersExampleHashHistory.title,
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
async mount(params: AppMountParameters) {
|
||||
const { renderApp, History } = await import('./todo/app');
|
||||
return renderApp(params, {
|
||||
appInstanceId: '2',
|
||||
appTitle: 'Routing with hash history',
|
||||
historyType: History.Hash,
|
||||
});
|
||||
const [coreStart] = await core.getStartServices();
|
||||
return renderApp(
|
||||
params,
|
||||
{
|
||||
appTitle: examples.stateContainersExampleHashHistory.title,
|
||||
historyType: History.Hash,
|
||||
},
|
||||
{ navigateToApp: coreStart.application.navigateToApp, exampleLinks }
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
core.application.register({
|
||||
id: PLUGIN_ID,
|
||||
title: PLUGIN_NAME,
|
||||
id: 'stateContainersExampleWithDataServices',
|
||||
title: examples.stateContainersExampleWithDataServices.title,
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
async mount(params: AppMountParameters) {
|
||||
// Load application bundle
|
||||
const { renderApp } = await import('./with_data_services/application');
|
||||
// Get start services as specified in kibana.json
|
||||
const [coreStart, depsStart] = await core.getStartServices();
|
||||
// Render the application
|
||||
return renderApp(coreStart, depsStart as AppPluginDependencies, params);
|
||||
return renderApp(coreStart, depsStart as AppPluginDependencies, params, { exampleLinks });
|
||||
},
|
||||
});
|
||||
|
||||
developerExamples.register({
|
||||
appId: 'stateContainersExampleBrowserHistory',
|
||||
title: 'State containers using browser history',
|
||||
description: `An example todo app that uses browser history and state container utilities like createStateContainerReactHelpers,
|
||||
createStateContainer, createKbnUrlStateStorage, createSessionStorageStateStorage,
|
||||
syncStates and getStateFromKbnUrl to keep state in sync with the URL. Change some parameters, navigate away and then back, and the
|
||||
state should be preserved.`,
|
||||
appId: exampleLinks[0].appId,
|
||||
title: 'State Management',
|
||||
description: 'Examples of using state containers and state syncing utils.',
|
||||
image,
|
||||
links: [
|
||||
{
|
||||
label: 'README',
|
||||
label: 'State containers README',
|
||||
href:
|
||||
'https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers/README.md',
|
||||
'https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers',
|
||||
iconType: 'logoGithub',
|
||||
size: 's',
|
||||
target: '_blank',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
developerExamples.register({
|
||||
appId: 'stateContainersExampleHashHistory',
|
||||
title: 'State containers using hash history',
|
||||
description: `An example todo app that uses hash history and state container utilities like createStateContainerReactHelpers,
|
||||
createStateContainer, createKbnUrlStateStorage, createSessionStorageStateStorage,
|
||||
syncStates and getStateFromKbnUrl to keep state in sync with the URL. Change some parameters, navigate away and then back, and the
|
||||
state should be preserved.`,
|
||||
links: [
|
||||
{
|
||||
label: 'README',
|
||||
label: 'State sync utils README',
|
||||
href:
|
||||
'https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers/README.md',
|
||||
'https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync',
|
||||
iconType: 'logoGithub',
|
||||
size: 's',
|
||||
target: '_blank',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
developerExamples.register({
|
||||
appId: PLUGIN_ID,
|
||||
title: 'Sync state from a query bar with the url',
|
||||
description: `Shows how to use data.syncQueryStateWitUrl in combination with state container utilities from kibana_utils to
|
||||
show a query bar that stores state in the url and is kept in sync.
|
||||
`,
|
||||
links: [
|
||||
{
|
||||
label: 'README',
|
||||
href:
|
||||
'https://github.com/elastic/kibana/blob/master/src/plugins/data/public/query/state_sync/README.md',
|
||||
iconType: 'logoGithub',
|
||||
label: 'Kibana navigation best practices',
|
||||
href: 'https://www.elastic.co/guide/en/kibana/master/kibana-navigation.html',
|
||||
iconType: 'logoKibana',
|
||||
size: 's',
|
||||
target: '_blank',
|
||||
},
|
||||
|
|
BIN
examples/state_containers_examples/public/state_sync.png
Normal file
BIN
examples/state_containers_examples/public/state_sync.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
|
@ -6,14 +6,14 @@
|
|||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { AppMountParameters } from 'kibana/public';
|
||||
import { AppMountParameters, CoreStart } from 'kibana/public';
|
||||
import ReactDOM from 'react-dom';
|
||||
import React from 'react';
|
||||
import { createHashHistory } from 'history';
|
||||
import { TodoAppPage } from './todo';
|
||||
import { StateContainersExamplesPage, ExampleLink } from '../common/example_page';
|
||||
|
||||
export interface AppOptions {
|
||||
appInstanceId: string;
|
||||
appTitle: string;
|
||||
historyType: History;
|
||||
}
|
||||
|
@ -23,30 +23,21 @@ export enum History {
|
|||
Hash,
|
||||
}
|
||||
|
||||
export interface Deps {
|
||||
navigateToApp: CoreStart['application']['navigateToApp'];
|
||||
exampleLinks: ExampleLink[];
|
||||
}
|
||||
|
||||
export const renderApp = (
|
||||
{ appBasePath, element, history: platformHistory }: AppMountParameters,
|
||||
{ appInstanceId, appTitle, historyType }: AppOptions
|
||||
{ appTitle, historyType }: AppOptions,
|
||||
{ navigateToApp, exampleLinks }: Deps
|
||||
) => {
|
||||
const history = historyType === History.Browser ? platformHistory : createHashHistory();
|
||||
ReactDOM.render(
|
||||
<TodoAppPage
|
||||
history={history}
|
||||
appInstanceId={appInstanceId}
|
||||
appTitle={appTitle}
|
||||
appBasePath={appBasePath}
|
||||
isInitialRoute={() => {
|
||||
const stripTrailingSlash = (path: string) =>
|
||||
path.charAt(path.length - 1) === '/' ? path.substr(0, path.length - 1) : path;
|
||||
const currentAppUrl = stripTrailingSlash(history.createHref(history.location));
|
||||
if (historyType === History.Browser) {
|
||||
// browser history
|
||||
return currentAppUrl === '' && !history.location.search && !history.location.hash;
|
||||
} else {
|
||||
// hashed history
|
||||
return currentAppUrl === '#' && !history.location.search;
|
||||
}
|
||||
}}
|
||||
/>,
|
||||
<StateContainersExamplesPage navigateToApp={navigateToApp} exampleLinks={exampleLinks}>
|
||||
<TodoAppPage history={history} appTitle={appTitle} appBasePath={appBasePath} />
|
||||
</StateContainersExamplesPage>,
|
||||
element
|
||||
);
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { Link, Route, Router, Switch, useLocation } from 'react-router-dom';
|
||||
import { History } from 'history';
|
||||
import {
|
||||
|
@ -18,21 +18,21 @@ import {
|
|||
EuiPageContentBody,
|
||||
EuiPageHeader,
|
||||
EuiPageHeaderSection,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
BaseStateContainer,
|
||||
INullableBaseStateContainer,
|
||||
createKbnUrlStateStorage,
|
||||
createSessionStorageStateStorage,
|
||||
createStateContainer,
|
||||
createStateContainerReactHelpers,
|
||||
PureTransition,
|
||||
syncStates,
|
||||
getStateFromKbnUrl,
|
||||
BaseState,
|
||||
BaseStateContainer,
|
||||
createKbnUrlStateStorage,
|
||||
createStateContainer,
|
||||
getStateFromKbnUrl,
|
||||
INullableBaseStateContainer,
|
||||
StateContainer,
|
||||
syncState,
|
||||
useContainerSelector,
|
||||
} from '../../../../src/plugins/kibana_utils/public';
|
||||
import { useUrlTracker } from '../../../../src/plugins/kibana_react/public';
|
||||
import {
|
||||
defaultState,
|
||||
pureTransitions,
|
||||
|
@ -40,42 +40,24 @@ import {
|
|||
TodoState,
|
||||
} from '../../../../src/plugins/kibana_utils/demos/state_containers/todomvc';
|
||||
|
||||
interface GlobalState {
|
||||
text: string;
|
||||
}
|
||||
interface GlobalStateAction {
|
||||
setText: PureTransition<GlobalState, [string]>;
|
||||
}
|
||||
const defaultGlobalState: GlobalState = { text: '' };
|
||||
const globalStateContainer = createStateContainer<GlobalState, GlobalStateAction>(
|
||||
defaultGlobalState,
|
||||
{
|
||||
setText: (state) => (text) => ({ ...state, text }),
|
||||
}
|
||||
);
|
||||
|
||||
const GlobalStateHelpers = createStateContainerReactHelpers<typeof globalStateContainer>();
|
||||
|
||||
const container = createStateContainer<TodoState, TodoActions>(defaultState, pureTransitions);
|
||||
const { Provider, connect, useTransitions, useState } = createStateContainerReactHelpers<
|
||||
typeof container
|
||||
>();
|
||||
|
||||
interface TodoAppProps {
|
||||
filter: 'completed' | 'not-completed' | null;
|
||||
stateContainer: StateContainer<TodoState, TodoActions>;
|
||||
}
|
||||
|
||||
const TodoApp: React.FC<TodoAppProps> = ({ filter }) => {
|
||||
const { setText } = GlobalStateHelpers.useTransitions();
|
||||
const { text } = GlobalStateHelpers.useState();
|
||||
const { edit: editTodo, delete: deleteTodo, add: addTodo } = useTransitions();
|
||||
const todos = useState().todos;
|
||||
const filteredTodos = todos.filter((todo) => {
|
||||
if (!filter) return true;
|
||||
if (filter === 'completed') return todo.completed;
|
||||
if (filter === 'not-completed') return !todo.completed;
|
||||
return true;
|
||||
});
|
||||
const TodoApp: React.FC<TodoAppProps> = ({ filter, stateContainer }) => {
|
||||
const { edit: editTodo, delete: deleteTodo, add: addTodo } = stateContainer.transitions;
|
||||
const todos = useContainerSelector(stateContainer, (state) => state.todos);
|
||||
const filteredTodos = useMemo(
|
||||
() =>
|
||||
todos.filter((todo) => {
|
||||
if (!filter) return true;
|
||||
if (filter === 'completed') return todo.completed;
|
||||
if (filter === 'not-completed') return !todo.completed;
|
||||
return true;
|
||||
}),
|
||||
[todos, filter]
|
||||
);
|
||||
const location = useLocation();
|
||||
return (
|
||||
<>
|
||||
|
@ -144,158 +126,115 @@ const TodoApp: React.FC<TodoAppProps> = ({ filter }) => {
|
|||
>
|
||||
<EuiFieldText placeholder="Type your todo and press enter to submit" name="newTodo" />
|
||||
</form>
|
||||
<div style={{ margin: '16px 0px' }}>
|
||||
<label htmlFor="globalInput">Global state piece: </label>
|
||||
<input name="globalInput" value={text} onChange={(e) => setText(e.target.value)} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const TodoAppConnected = GlobalStateHelpers.connect<TodoAppProps, never>(() => ({}))(
|
||||
connect<TodoAppProps, never>(() => ({}))(TodoApp)
|
||||
);
|
||||
|
||||
export const TodoAppPage: React.FC<{
|
||||
history: History;
|
||||
appInstanceId: string;
|
||||
appTitle: string;
|
||||
appBasePath: string;
|
||||
isInitialRoute: () => boolean;
|
||||
}> = (props) => {
|
||||
const initialAppUrl = React.useRef(window.location.href);
|
||||
const stateContainer = React.useMemo(
|
||||
() => createStateContainer<TodoState, TodoActions>(defaultState, pureTransitions),
|
||||
[]
|
||||
);
|
||||
|
||||
// Most of kibana apps persist state in the URL in two ways:
|
||||
// * Rison encoded.
|
||||
// * Hashed URL: In the URL only the hash from the state is stored. The state itself is stored in
|
||||
// the sessionStorage. See `state:storeInSessionStorage` advanced option for more context.
|
||||
// This example shows how to use both of them
|
||||
const [useHashedUrl, setUseHashedUrl] = React.useState(false);
|
||||
|
||||
/**
|
||||
* Replicates what src/legacy/ui/public/chrome/api/nav.ts did
|
||||
* Persists the url in sessionStorage and tries to restore it on "componentDidMount"
|
||||
*/
|
||||
useUrlTracker(`lastUrlTracker:${props.appInstanceId}`, props.history, (urlToRestore) => {
|
||||
// shouldRestoreUrl:
|
||||
// App decides if it should restore url or not
|
||||
// In this specific case, restore only if navigated to initial route
|
||||
if (props.isInitialRoute()) {
|
||||
// navigated to the base path, so should restore the url
|
||||
return true;
|
||||
} else {
|
||||
// navigated to specific route, so should not restore the url
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// have to sync with history passed to react-router
|
||||
// history v5 will be singleton and this will not be needed
|
||||
// storage to sync our app state with
|
||||
// in this case we want to sync state with query params in the URL serialised in rison format
|
||||
// similar like Discover or Dashboard apps do
|
||||
const kbnUrlStateStorage = createKbnUrlStateStorage({
|
||||
useHash: useHashedUrl,
|
||||
history: props.history,
|
||||
});
|
||||
|
||||
const sessionStorageStateStorage = createSessionStorageStateStorage();
|
||||
// key to store state in the storage. In this case in the key of the query param in the URL
|
||||
const appStateKey = `_todo`;
|
||||
|
||||
/**
|
||||
* Restoring global state:
|
||||
* State restoration similar to what GlobalState in legacy world did
|
||||
* It restores state both from url and from session storage
|
||||
*/
|
||||
const globalStateKey = `_g`;
|
||||
const globalStateFromInitialUrl = getStateFromKbnUrl<GlobalState>(
|
||||
globalStateKey,
|
||||
initialAppUrl.current
|
||||
);
|
||||
const globalStateFromCurrentUrl = kbnUrlStateStorage.get<GlobalState>(globalStateKey);
|
||||
const globalStateFromSessionStorage = sessionStorageStateStorage.get<GlobalState>(
|
||||
globalStateKey
|
||||
);
|
||||
|
||||
const initialGlobalState: GlobalState = {
|
||||
...defaultGlobalState,
|
||||
...globalStateFromCurrentUrl,
|
||||
...globalStateFromSessionStorage,
|
||||
...globalStateFromInitialUrl,
|
||||
};
|
||||
globalStateContainer.set(initialGlobalState);
|
||||
kbnUrlStateStorage.set(globalStateKey, initialGlobalState, { replace: true });
|
||||
sessionStorageStateStorage.set(globalStateKey, initialGlobalState);
|
||||
|
||||
/**
|
||||
* Restoring app local state:
|
||||
* State restoration similar to what AppState in legacy world did
|
||||
* It restores state both from url
|
||||
*/
|
||||
const appStateKey = `_todo-${props.appInstanceId}`;
|
||||
// take care of initial state. Make sure state in memory is the same as in the URL before starting any syncing
|
||||
const initialAppState: TodoState =
|
||||
getStateFromKbnUrl<TodoState>(appStateKey, initialAppUrl.current) ||
|
||||
kbnUrlStateStorage.get<TodoState>(appStateKey) ||
|
||||
defaultState;
|
||||
container.set(initialAppState);
|
||||
stateContainer.set(initialAppState);
|
||||
kbnUrlStateStorage.set(appStateKey, initialAppState, { replace: true });
|
||||
|
||||
// start syncing only when made sure, that state in synced
|
||||
const { stop, start } = syncStates([
|
||||
{
|
||||
stateContainer: withDefaultState(container, defaultState),
|
||||
storageKey: appStateKey,
|
||||
stateStorage: kbnUrlStateStorage,
|
||||
},
|
||||
{
|
||||
stateContainer: withDefaultState(globalStateContainer, defaultGlobalState),
|
||||
storageKey: globalStateKey,
|
||||
stateStorage: kbnUrlStateStorage,
|
||||
},
|
||||
{
|
||||
stateContainer: withDefaultState(globalStateContainer, defaultGlobalState),
|
||||
storageKey: globalStateKey,
|
||||
stateStorage: sessionStorageStateStorage,
|
||||
},
|
||||
]);
|
||||
// start syncing state between state container and the URL
|
||||
const { stop, start } = syncState({
|
||||
stateContainer: withDefaultState(stateContainer, defaultState),
|
||||
storageKey: appStateKey,
|
||||
stateStorage: kbnUrlStateStorage,
|
||||
});
|
||||
|
||||
start();
|
||||
|
||||
return () => {
|
||||
stop();
|
||||
|
||||
// reset state containers
|
||||
container.set(defaultState);
|
||||
globalStateContainer.set(defaultGlobalState);
|
||||
};
|
||||
}, [props.appInstanceId, props.history, useHashedUrl]);
|
||||
}, [stateContainer, props.history, useHashedUrl]);
|
||||
|
||||
return (
|
||||
<Router history={props.history}>
|
||||
<GlobalStateHelpers.Provider value={globalStateContainer}>
|
||||
<Provider value={container}>
|
||||
<EuiPageBody>
|
||||
<EuiPageHeader>
|
||||
<EuiPageHeaderSection>
|
||||
<EuiTitle size="l">
|
||||
<h1>
|
||||
State sync example. Instance: ${props.appInstanceId}. {props.appTitle}
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiButton onClick={() => setUseHashedUrl(!useHashedUrl)}>
|
||||
{useHashedUrl ? 'Use Expanded State' : 'Use Hashed State'}
|
||||
</EuiButton>
|
||||
</EuiPageHeaderSection>
|
||||
</EuiPageHeader>
|
||||
<EuiPageContent>
|
||||
<EuiPageContentBody>
|
||||
<Switch>
|
||||
<Route path={'/completed'}>
|
||||
<TodoAppConnected filter={'completed'} />
|
||||
</Route>
|
||||
<Route path={'/not-completed'}>
|
||||
<TodoAppConnected filter={'not-completed'} />
|
||||
</Route>
|
||||
<Route path={'/'}>
|
||||
<TodoAppConnected filter={null} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</EuiPageContentBody>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</Provider>
|
||||
</GlobalStateHelpers.Provider>
|
||||
<EuiPageBody>
|
||||
<EuiPageHeader>
|
||||
<EuiPageHeaderSection>
|
||||
<EuiTitle size="l">
|
||||
<h1>{props.appTitle}</h1>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<EuiText>
|
||||
<p>
|
||||
This is a simple TODO app that uses state containers and state syncing utils. It
|
||||
stores state in the URL similar like Discover or Dashboard apps do. <br />
|
||||
Play with the app and see how the state is persisted in the URL.
|
||||
<br /> Undo/Redo with browser history also works.
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiPageHeaderSection>
|
||||
</EuiPageHeader>
|
||||
<EuiPageContent>
|
||||
<EuiPageContentBody>
|
||||
<Switch>
|
||||
<Route path={'/completed'}>
|
||||
<TodoApp filter={'completed'} stateContainer={stateContainer} />
|
||||
</Route>
|
||||
<Route path={'/not-completed'}>
|
||||
<TodoApp filter={'not-completed'} stateContainer={stateContainer} />
|
||||
</Route>
|
||||
<Route path={'/'}>
|
||||
<TodoApp filter={null} stateContainer={stateContainer} />
|
||||
</Route>
|
||||
</Switch>
|
||||
<EuiSpacer size={'xxl'} />
|
||||
<EuiText size={'s'}>
|
||||
<p>Most of kibana apps persist state in the URL in two ways:</p>
|
||||
<ol>
|
||||
<li>Expanded state in rison format</li>
|
||||
<li>
|
||||
Just a state hash. <br />
|
||||
In the URL only the hash from the state is stored. The state itself is stored in
|
||||
the sessionStorage. See `state:storeInSessionStorage` advanced option for more
|
||||
context.
|
||||
</li>
|
||||
</ol>
|
||||
<p>You can switch between these two mods:</p>
|
||||
</EuiText>
|
||||
<EuiSpacer />
|
||||
<EuiButton onClick={() => setUseHashedUrl(!useHashedUrl)}>
|
||||
{useHashedUrl ? 'Use Expanded State' : 'Use Hashed State'}
|
||||
</EuiButton>
|
||||
</EuiPageContentBody>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,50 +6,47 @@
|
|||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { History } from 'history';
|
||||
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
EuiFieldText,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiPageHeader,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public';
|
||||
|
||||
import { CoreStart } from '../../../../../src/core/public';
|
||||
import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public';
|
||||
import {
|
||||
connectToQueryState,
|
||||
syncQueryStateWithUrl,
|
||||
DataPublicPluginStart,
|
||||
IIndexPattern,
|
||||
QueryState,
|
||||
Filter,
|
||||
esFilters,
|
||||
Filter,
|
||||
IIndexPattern,
|
||||
Query,
|
||||
} from '../../../../../src/plugins/data/public';
|
||||
QueryState,
|
||||
syncQueryStateWithUrl,
|
||||
} from '../../../../src/plugins/data/public';
|
||||
import {
|
||||
BaseState,
|
||||
BaseStateContainer,
|
||||
createStateContainer,
|
||||
createStateContainerReactHelpers,
|
||||
IKbnUrlStateStorage,
|
||||
ReduxLikeStateContainer,
|
||||
syncState,
|
||||
} from '../../../../../src/plugins/kibana_utils/public';
|
||||
import { PLUGIN_ID, PLUGIN_NAME } from '../../../common';
|
||||
useContainerState,
|
||||
} from '../../../../src/plugins/kibana_utils/public';
|
||||
import { ExampleLink, StateContainersExamplesPage } from '../common/example_page';
|
||||
|
||||
interface StateDemoAppDeps {
|
||||
notifications: CoreStart['notifications'];
|
||||
http: CoreStart['http'];
|
||||
navigateToApp: CoreStart['application']['navigateToApp'];
|
||||
navigation: NavigationPublicPluginStart;
|
||||
data: DataPublicPluginStart;
|
||||
history: History;
|
||||
kbnUrlStateStorage: IKbnUrlStateStorage;
|
||||
exampleLinks: ExampleLink[];
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
|
@ -61,85 +58,74 @@ const defaultAppState: AppState = {
|
|||
name: '',
|
||||
filters: [],
|
||||
};
|
||||
const {
|
||||
Provider: AppStateContainerProvider,
|
||||
useState: useAppState,
|
||||
useContainer: useAppStateContainer,
|
||||
} = createStateContainerReactHelpers<ReduxLikeStateContainer<AppState>>();
|
||||
|
||||
const App = ({ navigation, data, history, kbnUrlStateStorage }: StateDemoAppDeps) => {
|
||||
const appStateContainer = useAppStateContainer();
|
||||
const appState = useAppState();
|
||||
export const App = ({
|
||||
navigation,
|
||||
data,
|
||||
history,
|
||||
kbnUrlStateStorage,
|
||||
exampleLinks,
|
||||
navigateToApp,
|
||||
}: StateDemoAppDeps) => {
|
||||
const appStateContainer = useMemo(() => createStateContainer(defaultAppState), []);
|
||||
const appState = useContainerState(appStateContainer);
|
||||
|
||||
useGlobalStateSyncing(data.query, kbnUrlStateStorage);
|
||||
useAppStateSyncing(appStateContainer, data.query, kbnUrlStateStorage);
|
||||
|
||||
const indexPattern = useIndexPattern(data);
|
||||
if (!indexPattern)
|
||||
return <div>No index pattern found. Please create an index patter before loading...</div>;
|
||||
return (
|
||||
<div>
|
||||
No index pattern found. Please create an index pattern before trying this example...
|
||||
</div>
|
||||
);
|
||||
|
||||
// Render the application DOM.
|
||||
// Note that `navigation.ui.TopNavMenu` is a stateful component exported on the `navigation` plugin's start contract.
|
||||
return (
|
||||
<Router history={history}>
|
||||
<I18nProvider>
|
||||
<StateContainersExamplesPage navigateToApp={navigateToApp} exampleLinks={exampleLinks}>
|
||||
<Router history={history}>
|
||||
<>
|
||||
<navigation.ui.TopNavMenu
|
||||
appName={PLUGIN_ID}
|
||||
showSearchBar={true}
|
||||
indexPatterns={[indexPattern]}
|
||||
useDefaultBehaviors={true}
|
||||
showSaveQuery={true}
|
||||
/>
|
||||
<EuiPage restrictWidth="1000px">
|
||||
<EuiPageBody>
|
||||
<EuiPageHeader>
|
||||
<EuiTitle size="l">
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="stateDemo.helloWorldText"
|
||||
defaultMessage="{name}!"
|
||||
values={{ name: PLUGIN_NAME }}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiPageHeader>
|
||||
<EuiPageContent>
|
||||
<EuiFieldText
|
||||
placeholder="Additional application state: My name is..."
|
||||
value={appState.name}
|
||||
onChange={(e) => appStateContainer.set({ ...appState, name: e.target.value })}
|
||||
aria-label="My name"
|
||||
/>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
<EuiPageBody>
|
||||
<EuiPageHeader>
|
||||
<EuiTitle size="l">
|
||||
<h1>Integration with search bar</h1>
|
||||
</EuiTitle>
|
||||
</EuiPageHeader>
|
||||
<EuiText>
|
||||
<p>
|
||||
This examples shows how you can use state containers, state syncing utils and
|
||||
helpers from data plugin to sync your app state and search bar state with the URL.
|
||||
</p>
|
||||
</EuiText>
|
||||
|
||||
<navigation.ui.TopNavMenu
|
||||
appName={'Example'}
|
||||
showSearchBar={true}
|
||||
indexPatterns={[indexPattern]}
|
||||
useDefaultBehaviors={true}
|
||||
showSaveQuery={true}
|
||||
/>
|
||||
<EuiPageContent>
|
||||
<EuiText>
|
||||
<p>
|
||||
In addition to state from query bar also sync your arbitrary application state:
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiFieldText
|
||||
placeholder="Additional example applications state: My name is..."
|
||||
value={appState.name}
|
||||
onChange={(e) => appStateContainer.set({ ...appState, name: e.target.value })}
|
||||
aria-label="My name"
|
||||
/>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</>
|
||||
</I18nProvider>
|
||||
</Router>
|
||||
</Router>
|
||||
</StateContainersExamplesPage>
|
||||
);
|
||||
};
|
||||
|
||||
export const StateDemoApp = (props: StateDemoAppDeps) => {
|
||||
const appStateContainer = useCreateStateContainer(defaultAppState);
|
||||
|
||||
return (
|
||||
<AppStateContainerProvider value={appStateContainer}>
|
||||
<App {...props} />
|
||||
</AppStateContainerProvider>
|
||||
);
|
||||
};
|
||||
|
||||
function useCreateStateContainer<State extends BaseState>(
|
||||
defaultState: State
|
||||
): ReduxLikeStateContainer<State> {
|
||||
const stateContainerRef = useRef<ReduxLikeStateContainer<State> | null>(null);
|
||||
if (!stateContainerRef.current) {
|
||||
stateContainerRef.current = createStateContainer(defaultState);
|
||||
}
|
||||
return stateContainerRef.current;
|
||||
}
|
||||
|
||||
function useIndexPattern(data: DataPublicPluginStart) {
|
||||
const [indexPattern, setIndexPattern] = useState<IIndexPattern>();
|
||||
useEffect(() => {
|
|
@ -10,24 +10,26 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
import { AppMountParameters, CoreStart } from '../../../../src/core/public';
|
||||
import { AppPluginDependencies } from './types';
|
||||
import { StateDemoApp } from './components/app';
|
||||
import { App } from './app';
|
||||
import { createKbnUrlStateStorage } from '../../../../src/plugins/kibana_utils/public/';
|
||||
import { ExampleLink } from '../common/example_page';
|
||||
|
||||
export const renderApp = (
|
||||
{ notifications, http }: CoreStart,
|
||||
{ notifications, application }: CoreStart,
|
||||
{ navigation, data }: AppPluginDependencies,
|
||||
{ element, history }: AppMountParameters
|
||||
{ element, history }: AppMountParameters,
|
||||
{ exampleLinks }: { exampleLinks: ExampleLink[] }
|
||||
) => {
|
||||
const kbnUrlStateStorage = createKbnUrlStateStorage({ useHash: false, history });
|
||||
|
||||
ReactDOM.render(
|
||||
<StateDemoApp
|
||||
notifications={notifications}
|
||||
http={http}
|
||||
<App
|
||||
navigation={navigation}
|
||||
data={data}
|
||||
history={history}
|
||||
kbnUrlStateStorage={kbnUrlStateStorage}
|
||||
exampleLinks={exampleLinks}
|
||||
navigateToApp={application.navigateToApp}
|
||||
/>,
|
||||
element
|
||||
);
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { PluginInitializerContext } from '../../../src/core/server';
|
||||
import { StateDemoServerPlugin } from './plugin';
|
||||
|
||||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new StateDemoServerPlugin(initializerContext);
|
||||
}
|
||||
|
||||
export { StateDemoServerPlugin as Plugin };
|
||||
export * from '../common';
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
PluginInitializerContext,
|
||||
CoreSetup,
|
||||
CoreStart,
|
||||
Plugin,
|
||||
Logger,
|
||||
} from '../../../src/core/server';
|
||||
|
||||
import { StateDemoPluginSetup, StateDemoPluginStart } from './types';
|
||||
import { defineRoutes } from './routes';
|
||||
|
||||
export class StateDemoServerPlugin implements Plugin<StateDemoPluginSetup, StateDemoPluginStart> {
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
this.logger = initializerContext.logger.get();
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup) {
|
||||
this.logger.debug('State_demo: Ssetup');
|
||||
const router = core.http.createRouter();
|
||||
|
||||
// Register server side APIs
|
||||
defineRoutes(router);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
public start(core: CoreStart) {
|
||||
this.logger.debug('State_demo: Started');
|
||||
return {};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
}
|
||||
|
||||
export { StateDemoServerPlugin as Plugin };
|
|
@ -1,25 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { IRouter } from '../../../../src/core/server';
|
||||
|
||||
export function defineRoutes(router: IRouter) {
|
||||
router.get(
|
||||
{
|
||||
path: '/api/state_demo/example',
|
||||
validate: false,
|
||||
},
|
||||
async (context, request, response) => {
|
||||
return response.ok({
|
||||
body: {
|
||||
time: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface StateDemoPluginSetup {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface StateDemoPluginStart {}
|
|
@ -2,6 +2,5 @@
|
|||
"id": "kibanaReact",
|
||||
"version": "kibana",
|
||||
"ui": true,
|
||||
"server": false,
|
||||
"requiredBundles": ["kibanaUtils"]
|
||||
"server": false
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ export { ValidatedDualRange, Value } from './validated_range';
|
|||
export * from './notifications';
|
||||
export { Markdown, MarkdownSimple } from './markdown';
|
||||
export { reactToUiComponent, uiToReactComponent } from './adapters';
|
||||
export { useUrlTracker } from './use_url_tracker';
|
||||
export { toMountPoint, MountPointPortal } from './util';
|
||||
export { RedirectAppLinks } from './app_links';
|
||||
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
export { useUrlTracker } from './use_url_tracker';
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useUrlTracker } from './use_url_tracker';
|
||||
import { StubBrowserStorage } from '@kbn/test/jest';
|
||||
import { createMemoryHistory } from 'history';
|
||||
|
||||
describe('useUrlTracker', () => {
|
||||
const key = 'key';
|
||||
let storage = new StubBrowserStorage();
|
||||
let history = createMemoryHistory();
|
||||
beforeEach(() => {
|
||||
storage = new StubBrowserStorage();
|
||||
history = createMemoryHistory();
|
||||
});
|
||||
|
||||
it('should track history changes and save them to storage', () => {
|
||||
expect(storage.getItem(key)).toBeNull();
|
||||
const { unmount } = renderHook(() => {
|
||||
useUrlTracker(key, history, () => false, storage);
|
||||
});
|
||||
expect(storage.getItem(key)).toBe('/');
|
||||
history.push('/change');
|
||||
expect(storage.getItem(key)).toBe('/change');
|
||||
unmount();
|
||||
history.push('/other-change');
|
||||
expect(storage.getItem(key)).toBe('/change');
|
||||
});
|
||||
|
||||
it('by default should restore initial url', () => {
|
||||
storage.setItem(key, '/change');
|
||||
renderHook(() => {
|
||||
useUrlTracker(key, history, undefined, storage);
|
||||
});
|
||||
expect(history.location.pathname).toBe('/change');
|
||||
});
|
||||
|
||||
it('should restore initial url if shouldRestoreUrl cb returns true', () => {
|
||||
storage.setItem(key, '/change');
|
||||
renderHook(() => {
|
||||
useUrlTracker(key, history, () => true, storage);
|
||||
});
|
||||
expect(history.location.pathname).toBe('/change');
|
||||
});
|
||||
|
||||
it('should not restore initial url if shouldRestoreUrl cb returns false', () => {
|
||||
storage.setItem(key, '/change');
|
||||
renderHook(() => {
|
||||
useUrlTracker(key, history, () => false, storage);
|
||||
});
|
||||
expect(history.location.pathname).toBe('/');
|
||||
});
|
||||
});
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { History } from 'history';
|
||||
import { useLayoutEffect } from 'react';
|
||||
import { createUrlTracker } from '../../../kibana_utils/public/';
|
||||
|
||||
/**
|
||||
* State management url_tracker in react hook form
|
||||
*
|
||||
* Replicates what src/legacy/ui/public/chrome/api/nav.ts did
|
||||
* Persists the url in sessionStorage so it could be restored if navigated back to the app
|
||||
*
|
||||
* @param key - key to use in storage
|
||||
* @param history - history instance to use
|
||||
* @param shouldRestoreUrl - cb if url should be restored
|
||||
* @param storage - storage to use. window.sessionStorage is default
|
||||
*/
|
||||
export function useUrlTracker(
|
||||
key: string,
|
||||
history: History,
|
||||
shouldRestoreUrl: (urlToRestore: string) => boolean = () => true,
|
||||
storage: Storage = sessionStorage
|
||||
) {
|
||||
useLayoutEffect(() => {
|
||||
const urlTracker = createUrlTracker(key, storage);
|
||||
const urlToRestore = urlTracker.getTrackedUrl();
|
||||
if (urlToRestore && shouldRestoreUrl(urlToRestore)) {
|
||||
history.replace(urlToRestore);
|
||||
}
|
||||
const stopTrackingUrl = urlTracker.startTrackingUrl(history);
|
||||
return () => {
|
||||
stopTrackingUrl();
|
||||
};
|
||||
}, [key, history]);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue