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

[State Management] State syncing helpers for query service I #56128

Merged
merged 44 commits into from
Feb 27, 2020
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
e16961d
wip
Dosant Jan 27, 2020
8201cbd
wip
Dosant Jan 28, 2020
e855db8
improve example
Dosant Jan 28, 2020
0ddbfe5
Merge branch 'master' of github.com:elastic/kibana into dev/experimen…
Dosant Jan 28, 2020
56895b7
improve
Dosant Jan 28, 2020
224841b
fix
Dosant Jan 28, 2020
98703dc
improve
Dosant Jan 28, 2020
606bfc4
Merge branch 'master' of github.com:elastic/kibana into dev/experimen…
Dosant Feb 6, 2020
bc07de4
merge
Dosant Feb 6, 2020
8be16b3
top nav ts arg support
Feb 6, 2020
1c7160e
Merge branch 'newplatform/data/stateful-top-nav-ts' of github.com:liz…
Dosant Feb 6, 2020
8b71f90
Merge branch 'newplatform/data/stateful-top-nav-ts' of github.com:liz…
Dosant Feb 6, 2020
04fc4d5
wip
Dosant Feb 6, 2020
ac55f33
snapshots
Dosant Feb 6, 2020
ce59461
use-callback
Dosant Feb 6, 2020
f4fb6b8
wip
Dosant Feb 7, 2020
e656578
wip
Dosant Feb 7, 2020
962f84f
wip
Dosant Feb 7, 2020
f0b04da
Merge branch 'dev/experiment-with-state-setup' of github.com:Dosant/k…
Dosant Feb 14, 2020
c70e8ac
Merge branch 'master' of github.com:elastic/kibana into dev/experimen…
Dosant Feb 16, 2020
29516bd
wip
Dosant Feb 16, 2020
72bdaf5
Merge branch 'master' of github.com:elastic/kibana into dev/experimen…
Dosant Feb 16, 2020
9b70221
fix
Dosant Feb 16, 2020
7be0613
jest -u
Dosant Feb 16, 2020
5bfd1e9
Merge branch 'master' of github.com:elastic/kibana into dev/experimen…
Dosant Feb 18, 2020
19770c2
improve discover and visualise sub url tracking
Dosant Feb 18, 2020
63f7c8c
update np karma mock
Dosant Feb 18, 2020
3e57279
fix mock
Dosant Feb 18, 2020
d39d491
wip
Dosant Feb 18, 2020
1f2c5d1
Merge branch 'master' of github.com:elastic/kibana into dev/experimen…
Dosant Feb 18, 2020
36f3036
Merge branch 'master' of github.com:elastic/kibana into dev/experimen…
Dosant Feb 18, 2020
3add348
improve
Dosant Feb 18, 2020
d376122
jset -u
Dosant Feb 18, 2020
1b53f05
Merge branch 'master' of github.com:elastic/kibana into dev/experimen…
Dosant Feb 20, 2020
3f24114
@lizozom review
Dosant Feb 20, 2020
cc1aaff
Merge branch 'master' into dev/experiment-with-state-setup
elasticmachine Feb 22, 2020
fa61db9
Merge branch 'master' into dev/experiment-with-state-setup
elasticmachine Feb 25, 2020
15bd129
fix query state in create search bar
Feb 26, 2020
c816281
removed useCallback
Feb 26, 2020
83e16c0
Enabled saved queries
Feb 26, 2020
5b96209
Merge branch 'master' into dev/experiment-with-state-setup
elasticmachine Feb 26, 2020
e3513e0
fix setting saved search
Dosant Feb 27, 2020
a08ec7e
Merge branch 'master' into dev/experiment-with-state-setup
elasticmachine Feb 27, 2020
cc91afb
jest -u
Dosant Feb 27, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions examples/state_containers_examples/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

export const PLUGIN_ID = 'stateContainersExampleWithDataServices';
export const PLUGIN_NAME = 'State containers example - with data services';
4 changes: 2 additions & 2 deletions examples/state_containers_examples/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"version": "0.0.1",
"kibanaVersion": "kibana",
"configPath": ["state_containers_examples"],
"server": false,
"server": true,
"ui": true,
"requiredPlugins": [],
"requiredPlugins": ["navigation", "data"],
"optionalPlugins": []
}
23 changes: 19 additions & 4 deletions examples/state_containers_examples/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@
*/

import { AppMountParameters, CoreSetup, Plugin } from 'kibana/public';
import { AppPluginDependencies } from './with_data_services/types';
import { PLUGIN_ID, PLUGIN_NAME } from '../common';

export class StateContainersExamplesPlugin implements Plugin {
public setup(core: CoreSetup) {
core.application.register({
id: 'state-containers-example-browser-history',
id: 'stateContainersExampleBrowserHistory',
title: 'State containers example - browser history routing',
async mount(params: AppMountParameters) {
const { renderApp, History } = await import('./app');
const { renderApp, History } = await import('./todo/app');
return renderApp(params, {
appInstanceId: '1',
appTitle: 'Routing with browser history',
Expand All @@ -34,17 +36,30 @@ export class StateContainersExamplesPlugin implements Plugin {
},
});
core.application.register({
id: 'state-containers-example-hash-history',
id: 'stateContainersExampleHashHistory',
title: 'State containers example - hash history routing',
async mount(params: AppMountParameters) {
const { renderApp, History } = await import('./app');
const { renderApp, History } = await import('./todo/app');
return renderApp(params, {
appInstanceId: '2',
appTitle: 'Routing with hash history',
historyType: History.Hash,
});
},
});

core.application.register({
id: PLUGIN_ID,
title: PLUGIN_NAME,
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);
},
});
}

public start() {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ import {
syncStates,
getStateFromKbnUrl,
BaseState,
} from '../../../src/plugins/kibana_utils/public';
import { useUrlTracker } from '../../../src/plugins/kibana_react/public';
} from '../../../../src/plugins/kibana_utils/public';
import { useUrlTracker } from '../../../../src/plugins/kibana_react/public';
import {
defaultState,
pureTransitions,
TodoActions,
TodoState,
} from '../../../src/plugins/kibana_utils/demos/state_containers/todomvc';
} from '../../../../src/plugins/kibana_utils/demos/state_containers/todomvc';

interface GlobalState {
text: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import React from 'react';
import ReactDOM from 'react-dom';
import { createBrowserHistory } from 'history';
import { AppMountParameters, CoreStart } from '../../../../src/core/public';
import { AppPluginDependencies } from './types';
import { StateDemoApp } from './components/app';
import { createKbnUrlStateStorage } from '../../../../src/plugins/kibana_utils/public/';

export const renderApp = (
{ notifications, http }: CoreStart,
{ navigation, data }: AppPluginDependencies,
{ appBasePath, element }: AppMountParameters
) => {
const history = createBrowserHistory({ basename: appBasePath });
const kbnUrlStateStorage = createKbnUrlStateStorage({ useHash: false, history });

ReactDOM.render(
<StateDemoApp
notifications={notifications}
http={http}
navigation={navigation}
data={data}
history={history}
kbnUrlStateStorage={kbnUrlStateStorage}
/>,
element
);

return () => ReactDOM.unmountComponentAtNode(element);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import React, { useEffect, useRef, 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,
EuiTitle,
} from '@elastic/eui';

import { CoreStart } from '../../../../../src/core/public';
import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public';
import {
connectToQueryState,
syncQueryStateWithUrl,
DataPublicPluginStart,
IIndexPattern,
QueryState,
Filter,
esFilters,
} 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';

interface StateDemoAppDeps {
notifications: CoreStart['notifications'];
http: CoreStart['http'];
navigation: NavigationPublicPluginStart;
data: DataPublicPluginStart;
history: History;
kbnUrlStateStorage: IKbnUrlStateStorage;
}

interface AppState {
name: string;
filters: Filter[];
// TODO: https://github.com/elastic/kibana/issues/58111
// query?: Query;
}
const defaultAppState: AppState = {
name: '',
filters: [],
};
const {
Provider: AppStateContainerProvider,
useState: useAppState,
useContainer: useAppStateContainer,
} = createStateContainerReactHelpers<ReduxLikeStateContainer<AppState>>();

const App = ({
notifications,
http,
navigation,
data,
history,
kbnUrlStateStorage,
}: StateDemoAppDeps) => {
const appStateContainer = useAppStateContainer();
const appState = useAppState();

useGlobalStateSyncing(data.query, kbnUrlStateStorage);
useAppStateSyncing(appStateContainer, data.query, kbnUrlStateStorage);

// TODO: https://github.com/elastic/kibana/issues/58111
// useEffect indefinite cycle inside TopNavManu
// const onQuerySubmit = useCallback(
lizozom marked this conversation as resolved.
Show resolved Hide resolved
// ({ query }) => {
// appStateContainer.set({ ...appState, query });
// },
// [appStateContainer, appState]
// );

const indexPattern = useIndexPattern(data);
if (!indexPattern) return <div>Loading...</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>
<>
<navigation.ui.TopNavMenu
appName={PLUGIN_ID}
showSearchBar={true}
indexPatterns={[indexPattern]}
useDefaultBehaviors={true}
// TODO: https://github.com/elastic/kibana/issues/58111
// onQuerySubmit={onQuerySubmit}
// query={appState.query}
/>
<EuiPage restrictWidth="1000px">
<EuiPageBody>
<EuiPageHeader>
<EuiTitle size="l">
<h1>
<FormattedMessage
id="stateDemo.helloWorldText"
defaultMessage="{name}!"
values={{ name: PLUGIN_NAME }}
/>
</h1>
</EuiTitle>
</EuiPageHeader>
<EuiPageContent>
<EuiFieldText
Dosant marked this conversation as resolved.
Show resolved Hide resolved
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>
</>
</I18nProvider>
</Router>
);
};

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(() => {
const fetchIndexPattern = async () => {
const defaultIndexPattern = await data.indexPatterns.getDefault();
if (defaultIndexPattern) {
setIndexPattern(defaultIndexPattern);
}
};
fetchIndexPattern();
}, [data.indexPatterns]);

return indexPattern;
}

function useGlobalStateSyncing(
query: DataPublicPluginStart['query'],
kbnUrlStateStorage: IKbnUrlStateStorage
) {
// setup sync state utils
useEffect(() => {
// sync global filters, time filters, refresh interval from data.query to url '_g'
const { stop } = syncQueryStateWithUrl(query, kbnUrlStateStorage);
return () => {
stop();
};
}, [query, kbnUrlStateStorage]);
}

function useAppStateSyncing<AppState extends QueryState>(
appStateContainer: BaseStateContainer<AppState>,
query: DataPublicPluginStart['query'],
kbnUrlStateStorage: IKbnUrlStateStorage
) {
// setup sync state utils
useEffect(() => {
// sync app filters with app state container from data.query to state container
const stopSyncingQueryAppStateWithStateContainer = connectToQueryState(
query,
appStateContainer,
{ filters: esFilters.FilterStateStore.APP_STATE }
);

// sets up syncing app state container with url
const { start: startSyncingAppStateWithUrl, stop: stopSyncingAppStateWithUrl } = syncState({
storageKey: '_a',
stateStorage: kbnUrlStateStorage,
stateContainer: {
...appStateContainer,
// stateSync utils requires explicit handling of default state ("null")
set: state => state && appStateContainer.set(state),
},
});

// merge initial state from app state container and current state in url
const initialAppState: AppState = {
...appStateContainer.get(),
...kbnUrlStateStorage.get<AppState>('_a'),
};
// trigger state update. actually needed in case some data was in url
appStateContainer.set(initialAppState);

// set current url to whatever is in app state container
kbnUrlStateStorage.set<AppState>('_a', initialAppState);

// finally start syncing state containers with url
startSyncingAppStateWithUrl();

return () => {
stopSyncingQueryAppStateWithStateContainer();
stopSyncingAppStateWithUrl();
};
}, [query, kbnUrlStateStorage, appStateContainer]);
}
Loading