[Docs] Clean up state management examples (#88980) (#89005)

This commit is contained in:
Anton Dosov 2021-01-22 10:22:13 +01:00 committed by GitHub
parent 71a4715cc3
commit 104fdb8f63
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 305 additions and 547 deletions

View file

@ -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`.

View file

@ -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';

View file

@ -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"]
}

View file

@ -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>
);
};

View file

@ -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',
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -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
);

View file

@ -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>
);
};

View file

@ -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(() => {

View file

@ -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
);

View file

@ -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';

View file

@ -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 };

View file

@ -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(),
},
});
}
);
}

View file

@ -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 {}

View file

@ -2,6 +2,5 @@
"id": "kibanaReact",
"version": "kibana",
"ui": true,
"server": false,
"requiredBundles": ["kibanaUtils"]
"server": false
}

View file

@ -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';

View file

@ -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';

View file

@ -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('/');
});
});

View file

@ -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]);
}