mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
## Summary This PR removes all imports of Route from react-router-dom and '@kbn/kibana-react-plugin/public' and instead imports Route from @kbn/shared-ux-router. ### Context Based on https://github.com/elastic/kibana/issues/132629#issue-1243243678 This PR executes steps 2 - 4: > 2. To make the transition easier, we want to re-export other react-router-dom exports alongside the modified' Route'. > 3. Solutions should start using that Route component in place of the one from react-router-dom. I.e. replace all occurrences of import { ... } from 'react-router-dom' with import { ... } from '@kbn/shared-ux-router'. > 4. All manual calls to useExecutionContext are not needed anymore and should be removed. ### Future PR Looks like this might be getting worked on in: https://github.com/elastic/kibana/pull/145863 (thanks!) > Introduce an ESlint rule that ensures that react-router-dom is not used directly in Kibana and that imports go through the new @kbn/shared-ux-router package. This is tangentially accomplished through https://github.com/elastic/kibana/pull/150340 but only addresses using Route through @kbn/kibana-react-plugin/public' ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Tiago Costa <tiagoffcc@hotmail.com>
257 lines
8.5 KiB
TypeScript
257 lines
8.5 KiB
TypeScript
/*
|
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
* or more contributor license agreements. Licensed under the Elastic License
|
|
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
|
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
|
* Side Public License, v 1.
|
|
*/
|
|
|
|
import React, { useEffect, useMemo } from 'react';
|
|
import { Link, Router, Switch, useLocation } from 'react-router-dom';
|
|
import { Route } from '@kbn/shared-ux-router';
|
|
import { History } from 'history';
|
|
import {
|
|
EuiButton,
|
|
EuiCheckbox,
|
|
EuiFieldText,
|
|
EuiPageBody,
|
|
EuiPageContent_Deprecated as EuiPageContent,
|
|
EuiPageContentBody_Deprecated as EuiPageContentBody,
|
|
EuiPageHeader,
|
|
EuiPageHeaderSection,
|
|
EuiSpacer,
|
|
EuiText,
|
|
EuiTitle,
|
|
} from '@elastic/eui';
|
|
import {
|
|
BaseState,
|
|
BaseStateContainer,
|
|
createKbnUrlStateStorage,
|
|
createStateContainer,
|
|
getStateFromKbnUrl,
|
|
INullableBaseStateContainer,
|
|
StateContainer,
|
|
syncState,
|
|
useContainerSelector,
|
|
} from '@kbn/kibana-utils-plugin/public';
|
|
import {
|
|
defaultState,
|
|
pureTransitions,
|
|
TodoActions,
|
|
TodoState,
|
|
} from '@kbn/kibana-utils-plugin/demos/state_containers/todomvc';
|
|
|
|
interface TodoAppProps {
|
|
filter: 'completed' | 'not-completed' | null;
|
|
stateContainer: StateContainer<TodoState, TodoActions>;
|
|
}
|
|
|
|
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 (
|
|
<>
|
|
<div>
|
|
<Link to={{ ...location, pathname: '/' }} data-test-subj={'filterLinkAll'}>
|
|
<EuiButton size={'s'} color={!filter ? 'primary' : 'success'}>
|
|
All
|
|
</EuiButton>
|
|
</Link>
|
|
<Link to={{ ...location, pathname: '/completed' }} data-test-subj={'filterLinkCompleted'}>
|
|
<EuiButton size={'s'} color={filter === 'completed' ? 'primary' : 'success'}>
|
|
Completed
|
|
</EuiButton>
|
|
</Link>
|
|
<Link
|
|
to={{ ...location, pathname: '/not-completed' }}
|
|
data-test-subj={'filterLinkNotCompleted'}
|
|
>
|
|
<EuiButton size={'s'} color={filter === 'not-completed' ? 'primary' : 'success'}>
|
|
Not Completed
|
|
</EuiButton>
|
|
</Link>
|
|
</div>
|
|
<ul>
|
|
{filteredTodos.map((todo) => (
|
|
<li key={todo.id} style={{ display: 'flex', alignItems: 'center', margin: '16px 0px' }}>
|
|
<EuiCheckbox
|
|
id={todo.id + ''}
|
|
key={todo.id}
|
|
checked={todo.completed}
|
|
onChange={(e) => {
|
|
editTodo({
|
|
...todo,
|
|
completed: e.target.checked,
|
|
});
|
|
}}
|
|
label={todo.text}
|
|
data-test-subj={`todoCheckbox-${todo.id}`}
|
|
/>
|
|
<EuiButton
|
|
style={{ marginLeft: '8px' }}
|
|
size={'s'}
|
|
onClick={() => {
|
|
deleteTodo(todo.id);
|
|
}}
|
|
>
|
|
Delete
|
|
</EuiButton>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<form
|
|
onSubmit={(e) => {
|
|
const inputRef = (e.target as HTMLFormElement).elements.namedItem(
|
|
'newTodo'
|
|
) as HTMLInputElement;
|
|
if (!inputRef || !inputRef.value) return;
|
|
addTodo({
|
|
text: inputRef.value,
|
|
completed: false,
|
|
id: todos.map((todo) => todo.id).reduce((a, b) => Math.max(a, b), 0) + 1,
|
|
});
|
|
inputRef.value = '';
|
|
e.preventDefault();
|
|
}}
|
|
>
|
|
<EuiFieldText placeholder="Type your todo and press enter to submit" name="newTodo" />
|
|
</form>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const TodoAppPage: React.FC<{
|
|
history: History;
|
|
appTitle: string;
|
|
appBasePath: string;
|
|
}> = (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);
|
|
|
|
useEffect(() => {
|
|
// 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,
|
|
});
|
|
|
|
// key to store state in the storage. In this case in the key of the query param in the URL
|
|
const appStateKey = `_todo`;
|
|
|
|
// 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;
|
|
stateContainer.set(initialAppState);
|
|
kbnUrlStateStorage.set(appStateKey, initialAppState, { replace: true });
|
|
|
|
// start syncing state between state container and the URL
|
|
const { stop, start } = syncState({
|
|
stateContainer: withDefaultState(stateContainer, defaultState),
|
|
storageKey: appStateKey,
|
|
stateStorage: kbnUrlStateStorage,
|
|
});
|
|
|
|
start();
|
|
|
|
return () => {
|
|
stop();
|
|
};
|
|
}, [stateContainer, props.history, useHashedUrl]);
|
|
|
|
return (
|
|
<Router history={props.history}>
|
|
<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>
|
|
);
|
|
};
|
|
|
|
function withDefaultState<State extends BaseState>(
|
|
stateContainer: BaseStateContainer<State>,
|
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
defaultState: State
|
|
): INullableBaseStateContainer<State> {
|
|
return {
|
|
...stateContainer,
|
|
set: (state: State | null) => {
|
|
stateContainer.set({
|
|
...defaultState,
|
|
...state,
|
|
});
|
|
},
|
|
};
|
|
}
|