[Stack monitoring] Add global state context and routeInit component (#109790)

* Add global state to stack monitoring react app

* Add type for state

* Add some todos

* Add route_init migration

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ester Martí Vilaseca 2021-08-26 11:03:21 +02:00 committed by GitHub
parent 9e04d2c5c7
commit 9c922a078c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 249 additions and 27 deletions

View file

@ -0,0 +1,66 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { createContext } from 'react';
import { GlobalState } from '../url_state';
import { MonitoringStartPluginDependencies } from '../types';
interface GlobalStateProviderProps {
query: MonitoringStartPluginDependencies['data']['query'];
toasts: MonitoringStartPluginDependencies['core']['notifications']['toasts'];
children: React.ReactNode;
}
interface State {
cluster_uuid?: string;
}
export const GlobalStateContext = createContext({} as State);
export const GlobalStateProvider = ({ query, toasts, children }: GlobalStateProviderProps) => {
// TODO: remove fakeAngularRootScope and fakeAngularLocation when angular is removed
const fakeAngularRootScope: Partial<ng.IRootScopeService> = {
$on: (
name: string,
listener: (event: ng.IAngularEvent, ...args: any[]) => any
): (() => void) => () => {},
$applyAsync: () => {},
};
const fakeAngularLocation: Partial<ng.ILocationService> = {
search: () => {
return {} as any;
},
replace: () => {
return {} as any;
},
};
const localState: { [key: string]: unknown } = {};
const state = new GlobalState(
query,
toasts,
fakeAngularRootScope,
fakeAngularLocation,
localState
);
const initialState: any = state.getState();
for (const key in initialState) {
if (!initialState.hasOwnProperty(key)) {
continue;
}
localState[key] = initialState[key];
}
localState.save = () => {
const newState = { ...localState };
delete newState.save;
state.setState(newState);
};
return <GlobalStateContext.Provider value={localState}>{children}</GlobalStateContext.Provider>;
};

View file

@ -8,8 +8,7 @@ import { useState, useEffect } from 'react';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../common/constants';
export function useClusters(codePaths?: string[], fetchAllClusters?: boolean, ccs?: any) {
const clusterUuid = fetchAllClusters ? null : '';
export function useClusters(clusterUuid?: string | null, ccs?: any, codePaths?: string[]) {
const { services } = useKibana<{ data: any }>();
const bounds = services.data?.query.timefilter.timefilter.getBounds();
@ -43,7 +42,7 @@ export function useClusters(codePaths?: string[], fetchAllClusters?: boolean, cc
} catch (err) {
// TODO: handle errors
} finally {
setLoaded(null);
setLoaded(true);
}
};

View file

@ -8,10 +8,13 @@
import { CoreStart, AppMountParameters } from 'kibana/public';
import React from 'react';
import ReactDOM from 'react-dom';
import { Route, Switch, Redirect, HashRouter } from 'react-router-dom';
import { Route, Switch, Redirect, Router } from 'react-router-dom';
import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public';
import { LoadingPage } from './pages/loading_page';
import { MonitoringStartPluginDependencies } from '../types';
import { GlobalStateProvider } from './global_state_context';
import { createPreserveQueryHistory } from './preserve_query_history';
import { RouteInit } from './route_init';
export const renderApp = (
core: CoreStart,
@ -29,21 +32,37 @@ const MonitoringApp: React.FC<{
core: CoreStart;
plugins: MonitoringStartPluginDependencies;
}> = ({ core, plugins }) => {
const history = createPreserveQueryHistory();
return (
<KibanaContextProvider services={{ ...core, ...plugins }}>
<HashRouter>
<Switch>
<Route path="/loading" component={LoadingPage} />
<Route path="/no-data" component={NoData} />
<Route path="/home" component={Home} />
<Route path="/overview" component={ClusterOverview} />
<Redirect
to={{
pathname: '/loading',
}}
/>
</Switch>
</HashRouter>
<GlobalStateProvider query={plugins.data.query} toasts={core.notifications.toasts}>
<Router history={history}>
<Switch>
<Route path="/no-data" component={NoData} />
<Route path="/loading" component={LoadingPage} />
<RouteInit
path="/license"
component={License}
codePaths={['all']}
fetchAllClusters={false}
/>
<RouteInit path="/home" component={Home} codePaths={['all']} fetchAllClusters={false} />
<RouteInit
path="/overview"
component={ClusterOverview}
codePaths={['all']}
fetchAllClusters={false}
/>
<Redirect
to={{
pathname: '/loading',
search: history.location.search,
}}
/>
</Switch>
</Router>
</GlobalStateProvider>
</KibanaContextProvider>
);
};
@ -59,3 +78,7 @@ const Home: React.FC<{}> = () => {
const ClusterOverview: React.FC<{}> = () => {
return <div>Cluster overview page</div>;
};
const License: React.FC<{}> = () => {
return <div>License page</div>;
};

View file

@ -16,7 +16,7 @@ import { CODE_PATH_ELASTICSEARCH } from '../../../common/constants';
const CODE_PATHS = [CODE_PATH_ELASTICSEARCH];
export const LoadingPage = () => {
const { clusters, loaded } = useClusters(CODE_PATHS, true);
const { clusters, loaded } = useClusters(null, undefined, CODE_PATHS);
const title = i18n.translate('xpack.monitoring.loading.pageTitle', {
defaultMessage: 'Loading',
});

View file

@ -0,0 +1,38 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { History, createHashHistory, LocationDescriptor, LocationDescriptorObject } from 'history';
function preserveQueryParameters(
history: History,
location: LocationDescriptorObject
): LocationDescriptorObject {
location.search = history.location.search;
return location;
}
function createLocationDescriptorObject(
location: LocationDescriptor,
state?: History.LocationState
): LocationDescriptorObject {
return typeof location === 'string' ? { pathname: location, state } : location;
}
export function createPreserveQueryHistory() {
const history = createHashHistory({ hashType: 'slash' });
const oldPush = history.push;
const oldReplace = history.replace;
history.push = (path: LocationDescriptor, state?: History.LocationState) =>
oldPush.apply(history, [
preserveQueryParameters(history, createLocationDescriptorObject(path, state)),
]);
history.replace = (path: LocationDescriptor, state?: History.LocationState) =>
oldReplace.apply(history, [
preserveQueryParameters(history, createLocationDescriptorObject(path, state)),
]);
return history;
}

View file

@ -0,0 +1,71 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useContext } from 'react';
import { Route, Redirect, useLocation } from 'react-router-dom';
import { useClusters } from './hooks/use_clusters';
import { GlobalStateContext } from './global_state_context';
import { getClusterFromClusters } from '../lib/get_cluster_from_clusters';
interface RouteInitProps {
path: string;
component: React.ComponentType;
codePaths: string[];
fetchAllClusters: boolean;
unsetGlobalState?: boolean;
}
export const RouteInit: React.FC<RouteInitProps> = ({
path,
component,
codePaths,
fetchAllClusters,
unsetGlobalState = false,
}) => {
const globalState = useContext(GlobalStateContext);
const clusterUuid = fetchAllClusters ? null : globalState.cluster_uuid;
const location = useLocation();
const { clusters, loaded } = useClusters(clusterUuid, undefined, codePaths);
// TODO: we will need this when setup mode is migrated
// const inSetupMode = isInSetupMode();
const cluster = getClusterFromClusters(clusters, globalState, unsetGlobalState);
// TODO: check for setupMode too when the setup mode is migrated
if (loaded && !cluster) {
return <Redirect to="/no-data" />;
}
if (loaded && cluster) {
// check if we need to redirect because of license problems
if (
location.pathname !== 'license' &&
location.pathname !== 'home' &&
isExpired(cluster.license)
) {
return <Redirect to="/license" />;
}
// check if we need to redirect because of attempt at unsupported multi-cluster monitoring
const clusterSupported = cluster.isSupported || clusters.length === 1;
if (location.pathname !== 'home' && !clusterSupported) {
return <Redirect to="/home" />;
}
}
return loaded ? <Route path={path} component={component} /> : null;
};
const isExpired = (license: any): boolean => {
const { expiry_date_in_millis: expiryDateInMillis } = license;
if (expiryDateInMillis !== undefined) {
return new Date().getTime() >= expiryDateInMillis;
}
return false;
};

View file

@ -147,4 +147,8 @@ export class Legacy {
}
return Legacy._shims;
}
public static isInitializated(): boolean {
return Boolean(Legacy._shims);
}
}

View file

@ -0,0 +1,12 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const getClusterFromClusters: (
clusters: any,
globalState: State,
unsetGlobalState: boolean
) => any;

View file

@ -57,6 +57,7 @@ export interface MonitoringAppStateTransitions {
const GLOBAL_STATE_KEY = '_g';
const objectEquals = (objA: any, objB: any) => JSON.stringify(objA) === JSON.stringify(objB);
// TODO: clean all angular references after angular is removed
export class GlobalState {
private readonly stateSyncRef: ISyncStateRef;
private readonly stateContainer: StateContainer<
@ -74,8 +75,8 @@ export class GlobalState {
constructor(
queryService: MonitoringStartPluginDependencies['data']['query'],
toasts: MonitoringStartPluginDependencies['core']['notifications']['toasts'],
rootScope: ng.IRootScopeService,
ngLocation: ng.ILocationService,
rootScope: Partial<ng.IRootScopeService>,
ngLocation: Partial<ng.ILocationService>,
externalState: RawObject
) {
this.timefilterRef = queryService.timefilter.timefilter;
@ -102,11 +103,16 @@ export class GlobalState {
this.stateContainerChangeSub = this.stateContainer.state$.subscribe(() => {
this.lastAssignedState = this.getState();
if (!this.stateContainer.get() && this.lastKnownGlobalState) {
ngLocation.search(`${GLOBAL_STATE_KEY}=${this.lastKnownGlobalState}`).replace();
ngLocation.search?.(`${GLOBAL_STATE_KEY}=${this.lastKnownGlobalState}`).replace();
}
Legacy.shims.breadcrumbs.update();
// TODO: check if this is not needed after https://github.com/elastic/kibana/pull/109132 is merged
if (Legacy.isInitializated()) {
Legacy.shims.breadcrumbs.update();
}
this.syncExternalState(externalState);
rootScope.$applyAsync();
rootScope.$applyAsync?.();
});
this.syncQueryStateWithUrlManager = syncQueryStateWithUrl(queryService, this.stateStorage);
@ -114,7 +120,7 @@ export class GlobalState {
this.startHashSync(rootScope, ngLocation);
this.lastAssignedState = this.getState();
rootScope.$on('$destroy', () => this.destroy());
rootScope.$on?.('$destroy', () => this.destroy());
}
private syncExternalState(externalState: { [key: string]: unknown }) {
@ -131,15 +137,18 @@ export class GlobalState {
}
}
private startHashSync(rootScope: ng.IRootScopeService, ngLocation: ng.ILocationService) {
rootScope.$on(
private startHashSync(
rootScope: Partial<ng.IRootScopeService>,
ngLocation: Partial<ng.ILocationService>
) {
rootScope.$on?.(
'$routeChangeStart',
(_: { preventDefault: () => void }, newState: Route, oldState: Route) => {
const currentGlobalState = oldState?.params?._g;
const nextGlobalState = newState?.params?._g;
if (!nextGlobalState && currentGlobalState && typeof currentGlobalState === 'string') {
newState.params._g = currentGlobalState;
ngLocation.search(`${GLOBAL_STATE_KEY}=${currentGlobalState}`).replace();
ngLocation.search?.(`${GLOBAL_STATE_KEY}=${currentGlobalState}`).replace();
}
this.lastKnownGlobalState = (nextGlobalState || currentGlobalState) as string;
}